一、Synchronized的使用
注意:
1、一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
2、每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁。
3、synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。
(一)对象锁
对象锁有2种:方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)
代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁
方法锁形式:synchronized修饰普通方法,锁对象默认为this
(二)类锁
类锁指synchronize修饰静态的方法或指定锁对象为Class对象,所以无论是哪个线程访问它,所有线程需要的锁都是同一把。
1、synchronize修饰静态方法
synchronized用在普通方法上,默认的锁就是this,当前实例。synchronized用在静态方法上,默认的锁就是当前所在的Class类。
2、synchronized指定锁对象为Class对象
二、Synchronized原理分析
1)加锁和释放锁的原理
Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。
一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
1、monitor计数器为0,意味着目前还没有被获得。这个线程获得锁之后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待。
2、如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
3、这把锁已经被别的线程获取了,等待锁释放
monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
2)、可重入原理:加锁次数计数器
在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
3)保证可见性的原理:内存模型和happens-before规则
Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。
三、JVM中锁的优化
JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。
不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
1、锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
2、锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
3、轻量级锁(Lightweight Locking):
在无锁竞争的情况(单线程)下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。
当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
4、偏向锁(Biased Locking):是为了在无锁竞争的情况下,避免在锁获取过程中,执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小,但还是存在非常可观的本地延迟。
5、适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,会进入与monitor相关联的操作系统重量级锁前,会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功,则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。
(一)锁的类型
在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率
锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的)
(二)自旋锁与自适应自旋锁
自旋锁
在如今多处理器环境下,完全可以让另一个没有获取到锁的线程在门外等待一会(自旋),但不放弃CPU的执行时间。这种忙循环(自旋),这便是自旋锁由来的原因。
自旋锁早在JDK1.4 中就引入了,只是当时默认时关闭的。在JDK 1.6后默认为开启状态。自旋锁本质上与阻塞并不相同,如果锁占用的时间非常的短,那么自旋锁的性能会非常的好;相反,其会带来更多的性能开销。因此自旋等待的时间必须要有一定的限度,如果自旋次数超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了。在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。
自适应自旋锁
在JDK 1.6中引入了自适应自旋锁。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取到锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,JVM对程序的锁的状态预测会越来越准备,JVM也会越来越聪明。
(三)锁消除
锁消除是指虚拟机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁会进行🔐消除。锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断在一段程序中的同步,明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据时线程独有的,不需要加同步。此时就会进行锁消除。
(四)锁粗化
原则上,在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围。但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要地性能操作。JVM会检测到这样一连串地操作都是对同一个对象加锁,那么JVM会将加锁同步地范围扩展(粗化)到整个一系列操作的 外部,使整个一连串地append()操作只需要加锁一次就可以了。
(五)轻量级锁
在JDK 1.6之后引入的轻量级锁,需要注意的是轻量级锁并不是替代重量级锁的,而是对在大多数情况下同步块并不会有竞争出现提出的一种优化。它可以减少重量级锁对线程的阻塞带来地线程开销。从而提高并发性能。
(六)轻量级锁加锁
在线程执行同步块之前,JVM会先在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝到锁记录中 。
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头中,如果成功,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁,两个线程同时争夺锁,
(七)偏向锁
HotSpot的作者在Java SE 1.6 中对Synchronized进行了优化,引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。
偏向锁的撤销
偏向锁使用了一种等待竞争出现才会释放锁的机制。所以当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放锁。但是偏向锁的撤销需要等到全局安全点(就是当前线程没有正在执行的字节码)。
首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着。如果线程不处于活动状态,直接将对象头设置为无锁状态。如果线程活着,JVM会遍历栈帧中的锁记录,栈帧中的锁记录和对象头要么偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁。
(八)锁的优缺点对比
四、Synchronized与Lock
synchronized的缺陷
1、效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程。相对而言,Lock可以中断和设置超时
2、不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
3、无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,.....
Lock解决相应问题
Lock类这里不做过多解释,主要看里面的4个方法:
1、lock(): 加锁
2、unlock(): 解锁
3、tryLock(): 尝试获取锁,返回一个boolean值
4、tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
ReentrantLock为常用类,它是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
五、再深入理解
synchronized是通过软件(JVM)实现的,简单易用,即使在JDK5之后有了Lock,仍然被广泛地使用。
使用Synchronized有哪些要注意的?
1、锁对象不能为空,因为锁的信息都保存在对象头里
2、作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
3、避免死锁
在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错。