上篇铺垫:Java中的锁机制?(体系梳理篇)-CSDN博客
一、sychronized的介绍
sychronized是Java原生的一个同步锁机制,可以保证在多线程并发的情况下,仅允许一个线程执行加锁的方法和代码块,保证了线程安全。
在Java1.5之前,sychronized关键字实现的同步锁,底层是一种悲观锁的理念,在1.6版本后,Java对该关键字进行了优化,使得该锁在某些情况下呈现轻量锁的作用(这个后面介绍,先有个印象)
二、sychronized的作用
- 原子性:保证各个线程间的互斥。
- 可见性:保证共享变量的可见性,就是在加锁的时候仅允许持有锁的线程修改变量,在修改结束后,该变量的值会同步到内存当中,保证其他变量可以访问这个变量的修改。
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
对于可见性,可能很多人有疑问,volatile关键字也是保证共享变量的可见性,它和sychronized有什么区别?
volatile关键字保证了你对当前变量的修改会直接同步到内存中,而sychronized关键字它是先加锁然后修改值,释放锁之后才会把值同步到内存中,sychronized是有一个加锁的过程,保证了原子性,而volatile关键字并没有加锁,因此它并不具备原子性。
那么很多人可能又想问volatile关键字如何保证线程并发问题?
很简单,通过CAS来保证在多线程并发情况下,仅有一个线程修改volatile修饰的变量。详情请看Java中的锁机制?(体系梳理篇)-CSDN博客
三、Java1.6之后对sychronized关键字的优化了什么?
在1.5版本之前,sychronized关键字底层是一个互斥锁,是一个重量级锁,怎么理解呢?只要我对当前代码块加锁,那么无论我持锁线程是读取值还是修改值,其他的线程都必须等待我当前线程释放锁,然而在很多情况下,大部分都是读操作,并不需要如此重量级的线程管控。
因此在Java1.6版本之后,Java对sychronized关键字进行了优化。在Java1.6中,sychronized并不会直接对锁进行重量级锁的实现,而是会根据实际情况一步步将锁升级为重量级锁。我们先了解一下锁的几种状态:
- 无锁
当前线程第一次访问加锁代码块,此时代码块还未加锁,成为无锁。如何辨别是第一次访问呢?在加锁的对象头中又有一个threadid字段,用于表示当前代码块是否被加锁。
- 偏向锁
当前线程第二次访问加锁代码块,此时代码块已经被当前线程加了一次锁,在读取threadid字段后,知道是当前线程在持有锁,锁升级为偏向锁。此时会对当前线程直接放行,不需要重复申请锁,通过这种机制,实现了可重入锁,也就是当前线程可以重复获取当前锁。
- 轻量锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。第二个线程会通过一定的自旋循环来获取锁。
- 重量锁
重量级锁是由轻量级锁进一步升级而来,当同一时间有多个线程竞争锁时,自旋线程过多,锁就会被升级成重量级锁,因为自旋线程过多会严重消耗cpu性能。
2.锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。
3. 锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。
四、sychronized的底层原理
synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
五、sychroized为什么是非公平锁?
要讨论这个问题,我们必须明白什么是公平锁?什么是非公平锁?
- 公平锁:公平锁是指,当锁被当前线程持有,其他线程会按照先后顺序排队,排在前面的会优先获取锁,这就称为公平锁,在Java中JUC包下的AQS就可以实现公平锁,至于原理我们下篇文章介绍AQS时展开讨论。
- 非公平锁:由上面结论我们举反例可以知道,非公平锁就是,我不管你是否先来,谁先抢到锁谁就执行,sychronized就是一个非公平锁。
但为什么呢?其实很简单,在sychronized中有两个池的概念,一个是EntryList,一个是WaitSet,EntryList是一个单链表结构,当线程获取锁失败后,线程就会被封装称Entry对象然后加入到EntryList中。当某个持有锁线程调用wait()方法,线程就会释放锁然后加入到WaitSet中,当该线程被唤醒后会加入到EntryList中等待JVM调用。
但JVM并不会按照顺序去调用EntryList中的线程,而是随机调用,这也就导致了sychronized是一个非公平锁。
至于锁对象头的内容大家请看这篇文章:synchronized详解-CSDN博客