一.synchronized的特性
1.原子性:一个操作要么都执行并且执行时不会被任何因素打断,要么都不执行。
其中synchronized和volatile最大的区别在于synchronized保证了原子性,而volatile不保证原子性。
2.可见性:当一个线程对主内存的共享变量进行了修改,其他线程也能立即看到修改后的最新值。
3.有序性:程序的执行顺序和代码的编写顺序一致。
4.可重入性:synchronized和ReentrantLock都是可重入锁。当一个线程再次请求自己持有对象锁的临界资源时,属于可重入。通俗说就是一个线程拥有了锁仍然还能重复申请锁。
二.synchronized的用法
1.修饰对象
Object object = new Object();
public void methos3() {
synchronized (object) {
System.out.println("被synchronized修饰的代码块,执行前要获得对象的锁");
}
}
2.修饰方法
对成员函数加锁,要获得该类的实例对象的锁才能进入同步代码块
对静态方法加锁,必须获得类的锁才能进入同步代码块
public synchronized void method1() {
System.out.println("对成员函数加锁");
}
public static synchronized void method2() {
System.out.println("对静态方法加锁");
}
从语法上,synchronized可以把任何非null的对象作为锁。在JVM中锁也叫对象监视器。
当synchronized作用在方法上,监视器锁就是对象实例;
当synchronized作用在静态方法上,监视器锁就是对象的Class实例;
当synchronized作用在同步代码块上,监视器锁就是括号括起来的对象实例。
三.synchronized锁的同步原理
当一个线程访问同步代码块时,要先获得对象的锁才能执行同步代码块,当执行完或者发生异常时释放锁。底层原理是,进入同步代码前先获取锁,获取到锁后锁的计数器+1,同步代码块执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。
每一个对象有一个监视器锁,当监视器锁被占用时会处于锁定状态,获取过程如下:
1.如果监视器锁的进入数为0,则该线程进入监视器锁,然后将进入数设为1,该线程为监视器锁的持有者。
2.如果该线程已经持有该监视器锁,只是重新进入,则监视器锁的进入数+1。
3.如果其他线程已经持有了监视器锁,则该线程进入阻塞状态,直到监视器锁的进入数为0,再重新尝试获取监视器锁的持有权。
四.锁的优化
1.自旋锁
线程的阻塞和唤醒会给系统的并发性带来压力,在对象锁持续很短的情况下频繁的阻塞和唤醒线程时不值得的。
自旋锁指当一个线程尝试获取某个锁时,如果该锁已经被其他线程占有,就一直循环检测锁是否被释放,而不是进入睡眠状态。
自旋锁适用于锁占用时间短的情况,自选等待不能代替阻塞,但是能避免线程切换带来的开销,不过会占用CPU的时间。自旋锁等待的时间必须要有一个限制,如果超过了限制的时间还没有获得锁就应该被挂起。
2.适应性自旋锁
适应性自旋锁指自旋的次数不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态决定。
如果当前自旋成功,那么下次自旋的次数会增加。如果对于某个锁,很少有自旋成功,那么在获取这个锁的时候自旋次数会减少甚至省略自旋过程,避免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控的完善,虚拟机对程序锁的状况越来越准确。
3.锁消除
为了保证数据完整性,在操作时对一部分操作进行同步控制。但是当不可能存在多线程访问共享数据时,JVM会对同步锁进行锁消除。锁消除可以节省毫无意义的请求锁的时间。
4.锁粗化
在使用同步锁时,需要让同步块的范围尽可能小,仅在共享数据的实际作用域才进行同步,这样在锁竞争时等待的线程能尽快拿到锁。但是在连续加锁解锁的操作中,可能会导致不必要的性能损耗,所以引入锁粗化。
锁粗化就是将多个连续加锁解锁的操作连在一起,合并为一个更大的锁。
五.锁升级
锁有四种状态:无锁-偏向锁-轻量锁-重量锁,并且不可逆。
1.偏向锁:减少线程获取锁的代价,如果锁不存在多线程竞争,总是由同一线程多次获得,此时就是偏向锁。
如果一个线程获得了锁,那么锁进入偏向模式,此时Mark Word的结构变为偏向锁结构,当线程再次请求时,无需做任何同步操作,获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可。节省了大量有关锁申请的操作。步骤如下;
(1)检测Mark Word是否为可偏向状态,锁标识位为01;
(2)若是可偏向状态,则对比线程ID是否是Mark Word中的ThredID。如果是,则执行同步代码块。
(3)如果当前线程ID不等于Mark Word中的ThreadID,则通过CAS操作竞争锁,竞争成功后将Mark Wod中的ThreadID设为当前线程的ID。
(4)通过CAS获取失败的话,说明当前存在多线程竞争,偏向锁升级为轻量锁。
引入偏向锁的目的:在没有多线程竞争时减少不必要的轻量锁执行。因为轻量锁的加锁解锁依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID时依赖一次CAS原子指令。
偏向锁的释放:只有竞争才会释放锁,线程不会主动释放偏向锁,需要其他线程来竞争。
(1)暂停拥有偏向锁的线程。
(2)如果对象没有被锁定则恢复到无锁状态,否则挂起持有锁的当前i线程,并将当前线程的锁记录地址的指针放入对象头Mark Word中,升级为轻量锁,再恢复持有锁的当前线程。
2.轻量锁:由偏向锁升级而来,当存在其他线程申请当前线程持有的对象锁时,偏向锁会立即升级为轻量锁。
(1)在线程进入同步块时,如果同步对象无锁状态,虚拟机会在当前线程的栈帧中建立锁记录空间,用于存储锁对象目前的Mark Word拷贝。
(2)拷贝对象头中的Mark Word到锁记录中。
(3)拷贝成功后,虚拟机用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
(4)执行成功后,线程拥有了该对象的锁,并且对象Mark Word的标志位设为00,表示处于轻量锁状态。
(5)如果执行失败,虚拟机会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,说明当前线程拥有了这个对象的锁,否则说明有多个线程竞争,进入自旋,若自旋结束后未获得锁,则升级为重量级锁,Mark Word中存储的指向重量锁的指针。
轻量锁适用于线程交替执行同步代码块的情况。如果存在同一时间访问同一锁的情况,轻量锁会升级为重量锁。
轻量锁就是当前线程栈帧会把对象头的Mark Word复制到锁记录中,把对象头的Mark Word更新为执行锁记录,把锁记录的指针指向对象的Mark Word。
3.重锁:重锁由轻量锁升级而来,当同一时间有多个线程竞争锁时,锁会被升级为重量锁,此时申请带来的开销大。synchronized是通过对象内部的监视器锁实现的,监视器锁本质依赖于底层的操作系统的Mutex Lock实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个过程开销大。这种依赖操作系统Mutex Lock实现的锁叫重量锁。
4.偏向锁,轻量锁,重量锁之间的转换
单线程下偏向锁开销最小,不用CAS操作,只需要比较对象头。
如果出现了其他线程竞争,则偏向锁升级为轻量锁。
如果其他线程通过一定次数的CAS操作没有尝试成功,锁升级为重量锁。