Synchronized锁要点

Synchronized锁是JVM提供的一种悲观锁,即线程每次访问数据的时候都认为其它线程会修改数据,所以先获取锁,再访问数据;

1、作用

Synchronized锁很好的保证了多线程并发的安全性,Java多线程并发的三大特性都能通过Synchronized锁保证:

1.1 原子性

确保线程互斥的访问同步代码;

1.2 可见性

保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空其它线程工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;

1.3 有序性

有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

2、实现方式

从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor),任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。

2.1 Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。当且一个Monitor被持有后,它将处于锁定状态。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表,每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该线程占有了该锁。

2.2 锁的位置

    修饰实例方法:对当前实例对象加锁,进入同步代码前要获取对象实例的锁,作用于同一个对象,monitor是对象实例(this) 修饰静态方法:对当前类对象加锁,作用于整个类,monitor是对象的class实例 修饰代码块:可指定对象加锁

3、实现原理

当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。synchronized锁通过monitorenter&monitorexit或ACC_SYNCHRONIZED 标示符来实现同步。

3.1 同步代码块

通过反编译可以看到线程在获取锁和释放锁时,会执行monitorenter和monitorexit命令

monitorenter: 每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    如果monitor的进入数为0,则线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者; 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1; 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit: 执行monitorexit的线程必须是object所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

从以上也可以看出,Synchronized是可重入锁,个人觉得这里和ReentrantLock的可重入原理有异曲同工之妙,只是实现的方式不一样

3.2 同步方法

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

以上两种实现方法并无本质区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

3.3 三个队列

上面讨论了成功获取锁的情况,那么对于其它没有获得锁的线程,会进入一个cxq等待队列,在这个队列中尝试自旋获取锁,如果获取失败挂起。这里可以看到Synchronized锁的第二个“特性”:不公平锁。

Synchronized锁通常会与wait()、notify()方法一起使用;

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于 Object类的一部分,而不属于Thread类。

wait()和notify()方法必须出现在同步方法或同步代码块中,因为等待和唤醒是依赖于同步锁实现的,同步锁是对象持有,每个对象有且只有一个。所以wait要释放锁,因为只有他释放了该锁,其他对象才能获取该锁唤醒挂起的线程。所以这也就是wait和notify方法为什么出现在Object对象上

1)wait() 底层是调用了native方法,调用了ObjectMonitor的wait方法实现的,具体实现如下:

    将调用wait()的线程封装为ObjectWaiter类的对象node,ObjectWaiter是双向链表结构,保存了当前线程及当前状态; 通过ObjectMonitor的AddWaiter方法将node添加到WaitSet列表中,这是一个双向循环链表; 通过ObjectMonitor的exit方法释放当前的ObjectMonitor对象,这样其它线程就可以获取该monitor对象了; 最终通过底层的park()方法挂起线程;

可以看到这里出现了第二个队列:WaitSet,也就是说调用wait()方法的线程,并不会放到cxq队列中,二是进入了WaitSet;

同时这里提一点:注意到方法抛出了InterruptException,当处于wait状态的线程调用了interrupt()方法,这里就会抛出中断异常,从而中断线程。

2)notify() 调用notify会唤醒线程,具体的:

    若waitset为null,则没有需要唤醒的线程,直接退出 否则随机获取waitset列表中的一个节点; 将取出来的节点放到EntryList,则可以等待竞争锁;

注意这里提到,将WaitSet中的线程需要放到EntryList中去等待获取锁,这也就是第三个队列了。当一个线程要释放锁,先看EntryList中有没有线程,没有再去cxq队列中获取。

再次感叹,对比ReentrantLock和await()方法的实现原理,二者的思想仍然很相似,同时,这两种实现线程通信的方式也是经常需要辨析的地方。

4、锁的优化

前面说到,Synchronized通过monitor来实现线程同步,monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现线程同步。JDK6之前synchronized锁都是通过这种方式实现同步,这种依赖于操作系统Mutex Lock所实现的锁我们称为 “重量级锁”。重量级锁就涉及到线程状态的转换,可能会出现“状态转换消耗的时间有可能比用户代码执行的时间还要长”的情况。JDK6为了减少获得和释放锁带来的内存消耗,引入了 “偏向锁” 和 “轻量级锁”。

对象头 对象的头部保存Mark Word,有个tag bits,记录了锁的四种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁的状态只能升级,不能降级。

4.1 无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS操作即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

4.2 偏向锁

在大多数情况下,锁总是由同一个线程获取,不存在多线程竞争。引入偏向锁时为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换Thread ID时候依赖一次CAS指令(一旦出现多线程竞争情况,必须撤销偏向锁)。

当锁对象第一次被线程获取的时候,进入偏向状态,标记为01,同时通过CAS操作将线程ID存储到对象头的Mark Word中,如果成功,这个线程以后每次获取锁就不再需要进行同步操作,甚至CAS操作都不需要。线程不会主动释放偏向锁,只有当另一个线程尝试获取这个锁,偏向状态结束,恢复到未锁定状态或者轻量级状态。

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁就是为了在只有一个线程执行同步块时进一步提高性能。

4.3 轻量级锁

轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程通过自旋方式获取锁,不会阻塞,从而提高性能。

轻量级锁的加锁过程: (1)在代码进入同步块是,如果同步对象的锁状态为无锁,虚拟机首先在当前线程的帧栈建立一个名为Lock Record的空间,用于存储锁对象目前Mark Word的拷贝。 (2)拷贝对象头中的Mark Word复制到Lock Record中 (3)拷贝成功后,虚拟机使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的Owner指针指向Mark Word,如果更新成功,执行(4),否则(5); (4)如果更新成功,此时线程拥有了该对象的锁,将对象的锁标志位设为“00”,表示对象储于轻量级锁状态。 (5)如果更新失败,先检查Mark Record是否指向当前线程的栈帧,是的话说明当前线程已经获得锁,继续执行;否则说明多个锁竞争,轻量级锁就要膨胀为重量级锁,状态值变为“10”,Mark Word中存储的就是指向重量级锁的指针,后面等待的锁就要自旋来获取锁,如果获取不到,进入阻塞。 若当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定次数后,或者一个线程在持有锁,一个线程在等待,又有第三个线程来访时,轻量级锁升级为重量级锁。

4.4 重量级锁

升级为重量级锁时,锁标志位状态值变为“10”,此时MarkWord中存储的是指向重量级锁的指针,此时所有等待锁的线程都会进入阻塞状态。

综上,偏向锁通过对比MarkWord解决锁问题,避免执行CAS操作。而轻量级锁是通过CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是除了拥有锁的线程以外的线程都阻塞。

经过优化后的Synchronized锁性能上和ReenTrantLock已经相差不多。

经验分享 程序员 微信小程序 职场和发展