(1)synchronized 俗称对象锁,采用互斥的方式让同一时刻至多有一个线程能持有对象锁。Synchronized 的互斥保证了临界区代码的原子性,不会上下文切换引起指令的交错。同步是保证了某一个条件不满足时让线程等待,待条件满足后继续向下运行。
(2)thread1 线程获得锁后,即使时间片用完进入就绪态,其他线程也无法拿到 thread1 锁住的对象,也就无法进入临界区,此时处于 BLOCKED 状态。
(3)当 thread1 调用 synchronized 而 thread2 未调用 synchronized,即使锁未被释放 thread2 仍能访问临界区中的资源。
(4)synchronized 加在成员方法上表示对当前调用的对象加锁(锁住 this对象),加在类方法上表示对当前类对象加锁,类的实例化对象没有影响(不同的对象)。
(1)如果它们没有共享,则线程安全。
(2)如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:如果只有读操作,则线程安全;如果有读写操作,则这段代码是临界区,需要考虑线程安全。
(1)局部变量是线程安全的,因为局部变量存在于方法独有栈帧的局部变量表中,其他线程无法访问。
(2)局部变量引用的对象则未必:如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,需要考虑线程安全。
普通 Java 对象头共 64 字节,Mark Word32 字节,Klassword(对象所属类型的指针)32 字节。数组对象头共 96 字节,比普通对象头多了 32 字节的 array length。
Mark Word 结构如下:
Monitor 被翻译为监视器或管程,即之前提到的锁。每个 Java 对象都可以关联一个 Monitor 对象,在调用 synchronized 尝试给对象上锁(重量级锁)之后,该对象头的 Mark Word 中就被设置指向Monitor 的指针。Monitor 是操作系统层面的,本身存在很多的 Monitor,初始
时 Monitor 的 Owner 指针都是 null。
给对象上锁时,若该对象尚未关联 Monitor,该对象的 MarkWord 会指向一个 Monitor 的地址并将 MarkWord 的标志位从 01 改为 10,State 由 Normal 状态变成 HeavyWeight Lock 状态,覆盖前面的 hashcode,分代年龄等信息用所指向的 Monitor 的地址覆盖(注意:Monitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash
code)。
线程层面,因为只有 Thread2 在申请锁,所以申请成功,Monitor 的 owner属性指向 Thread2。当 Thread1 使用 synchronized 获取一个对象的锁时先检查该对象是否已经关联某个 Monitor 锁,若已关联 Monitor 则先检查锁关联的Monitor 是否有主人(Owner 是否指向某一线程),若已有主人则将 Thread1 加入 Monitor 的 EntryList 阻塞队列(EntryList 底层是链表结构),进入 BLOCKED状态。
关联同一个对象才会有上述效果(关联同一个对象底层才会由同一个Monitor 处理),不加 synchronized 的对象不会关联监视器,不遵从以上规则。
Synchronized 是操作系统层面的锁,频繁使用对性能影响比较大,因此出现了轻量级锁和偏向锁。
(1)由于很多情况下没有发生资源的竞争(比如对共享资源的使用是错开的), 而使用重量级锁 Monitor 对性能影响较大,此时适合使用轻量级锁。即如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
(2)轻量级锁对使用者是透明的,语法仍然是 synchronized。
(3)加锁:创建锁记录对象,每个线程的栈帧结构中都包含一个锁记录的结构,内部可以存储锁定对象的 MarkWord。在创建轻量级锁时,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间。
锁记录中主要存储两部分内容,一是要加锁对象的指针,指向该加锁的对象,二是存储加锁对象的 MarkWord 信息,因为加锁之后对象的 MarkWord 中例如 HashCode,分代年龄等信息都要被覆盖为加锁对象的指针。
刚创建出来的锁记录对象中存储着锁记录的地址以及标志位 00(表示轻量级锁),创建出来后先将锁记录的 Object reference 指向加锁的对象,并尝试用cas 替换锁记录的地址和对象的 MarkWord。
如果 cas 替换成功,则对象头中此时存储了锁地址和状态 00,表示由该线程为对象加了锁,而对象 MarkWord 中的信息则存储在了锁记录中,等解锁的时候恢复给对象 MarkWord 中。
当其他线程尝试给同一对象加锁时,检查 MarkWord 的标志位,发现是 00就证明已经被其他线程加过锁了,此时会进入锁膨胀的阶段。
当自己又执行了一遍 synchroized 锁重入,那么虽然标志位是 00,但是锁地址是指向自己的,此时会再添加一条 LockRecord 作为重入的计数,称为锁重入。
解锁:解锁时如果发现有取值为 null 的锁记录,就表示有锁重入,此时重置该锁记录,表示解开了一把重入的锁。当对象指针不为 null 时,此时再尝试使用 cas 将 MarkWord 的值恢复给对象头,如果成功,则解锁成功;如果失败说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程。
(1)如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁,因为后进来的线程不能在干耗着,需要进入一个等待队列,而轻量级锁是没有等待队列的也就无法让后进的线程阻塞。
(2)当 Thread0 已经加上轻量级锁后,Thread1 还想对同一对象加轻量级锁,此时第一步先将锁记录的对象指针指向锁定的对象,之后发现要锁定Object 的 MarkWord 标志位为 00,即表明已被轻量级锁锁定,此时进入锁膨胀。
(3)先为 Object 申请一个 Monitor 锁,让 Object 对象头的 MarkWord 指向重量级锁的地址,并将其标志位置为 10。然后 Thread1 会进入 Monitor 的EntryList 中阻塞。
(4)当 Thread0 执行完临界区中代码准备解锁时,尝试使用 cas 将MarkWord 的值恢复给对象头,此时会失败,随即进入重量级锁的解锁流程。即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,并唤醒 EntryList中 Blocked 的线程。此时由于 Thread0 中锁记录还保存着 Object 的 HashCode、分代年龄等信息,还会将其交给 Monitor 存储。
(1)重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。因为阻塞会发生上下文切换影响性能。
(2)自旋优化要使用 CPU,适合多核 CPU。若单核 CPU 自旋没有意义。
(3)在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
(4)Java 7 之后不能控制是否开启自旋功能。
JIT 即时编译器,Java 是解释+编译的运行模式,对 Java 字节码解释执行,但是对其中一些热点代码(反复执行的),会使用 JIT 进行优化,其中一个手段就是去分析局部变量是否能优化。若局部变量经过分析不会逃离方法的作用范围,就代表这个变量不可能被共享,此时对其加锁就没有任何意义,所以 JIT 即使编译器就会将 synchorized 优化掉,真正执行的时候就不会有加锁操作了。
(1)轻量级锁缺点:轻量级锁在没有竞争时(只有自己线程在使用临界资源),每次锁重入依然要执行 cas 操作。
(2)只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
(3)偏向锁和正常状态的状态位都是 01,不同的是 biased_lock 置为 1。即偏向锁的后三位为 101,正常状态的后三位为 001。
(4)偏向锁默认是延迟的,不会在 Java 程序启动时立即生效,而是会延迟两三秒后生效,即程序一启动就打印某一刚创建的对象头后三位是 001,而等几秒后新建对象并打印对象头,其后三位就为 101 了。如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
(5)对于偏向的加锁状态,前 54 位为加锁线程的 ID,覆盖了正常MarkWord 的 unused 位和 hashcode 位。
(6)即使加了偏向锁的对象后来解锁了,对象的 MarkWord 也不会变化,因为目前为止并没有线程来竞争资源,偏向锁就默认该共享资源只被该线程使用。
(7)偏向锁适合的场景是冲突很少的场景,若项目的场景是高并发竞争很多就不适合使用偏向锁,可以添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁。
(1)在使用某一个对象的偏向锁之前先调用该对象的 hashCode 方法,就会禁用该对象的偏向锁。(hashCode 默认是 0,只有第一次调用这个对象的 hashCode 时才会产生并写入对象头中,此时MarkWord 的空间不够再存储偏向锁所偏向的线程信息了,所以偏向锁就会效)。
(2)其它线程使用该对象使偏向锁升级为轻量级锁(注意此时的条件是有多个线程使用共享资源但是没有产生竞争,即虽然多个线程访问了共享资源但是使用时间是叫错开的,如果没有交错开发生了竞争则会直接进入重量级锁阶段,因为竞争的线程需要队列阻塞),即Thread0 当给一个对象加了偏向锁后,Thread1 在其运行结束使用共享资源,就会将偏向状态改为不可偏向,MarkWord 后三位由 101 设为 001,前面的偏向线程信息置为 0。
(3)使用了 wait/notify,因为这个机制只有重量级锁才有。
如果对象虽然被多个线程访问,但没有竞争发生,即刚刚所述的第二种撤销情况,且撤销偏向对性能的损耗不小。所以当撤销偏向锁的阈值超过 20 次以后,jvm 会觉得之前偏向错了,于是在给这些对象加锁时重新偏向至新加锁的线程。
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,
新建的对象也是不可偏向的。注意作用范围是整个类。