一、基础概念
1、锁定义
如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字。
2、锁特性(线程3要素(除可重复性))
- 原子性:一个操作要么全部发生,要么全部不发生,i++不具备原子性。
- 可见性:加锁,释放锁之前会将对变量的修改刷新到主存当中,或直接操作主存。
- 有序性:有序性指程序执行的顺序按照代码先后执行。
- 可重复性:一个线程拥有了锁仍然还可以重复申请该锁。
3、锁类型
synchronized可以修饰方法、代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是类
类上锁:同步块、静态方法
对象上锁:同步块、成员方法
二、锁实现原理
1、测试类
/**
* 归根结底它上锁的资源只有两类:一个是对象,一个是类。
*/
public class SynchronizedTest {
public static int i=0;
SynchronizedTest test = new SynchronizedTest();
/**
* 对成员函数加锁,必须获得该类的实例对象的锁才能进入同步块
*/
public synchronized void test1() {
i++;
}
/**
* 对静态方法加锁,必须获得该类的锁才能进入同步块,本文暂不关注该方法
*/
public static synchronized void test2() {
System.out.println("Hello static test2");
}
public void test3() {
/**
* 必须获得类锁
*/
synchronized(SynchronizedTest.class) {
System.out.println("Hello test3");
}
/**
* 必须获得对象锁, 本文暂不关注该代码块
*/
synchronized(test) {
System.out.println("Hello test3-2");
}
}
}
2、查看字节码文件
// 编译生成字节码文件
javac SynchronizedTest.java
// 反编译查看字节码文件
javap -verbose SynchronizedTest.class
3、同步块的实现
同步块字节码
由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。
4、同步方法
同步方法
flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。
三、锁底层实现原理
锁存放在对象头的Mark Word区域中。
锁总共四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录。
每个对象都存在着一个monitor与之关联,当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态,Mark Word会记录锁定状态和线程id,monitor中的_owner标志会指向持有monitor的线程同时monitor中的计数器count加1。
总结:
对象头有Mark Word区域,当线程视图获取对象锁的时候,会生成一个monitor与对象关联。线程获取锁其实就是获取对象的monitor;当一个monitor被线程持有以后,monitor中的_owner标志会指向持有monitor的线程,同时monitor中的计数器count加1。Mark Word区域会记录锁定状态、锁的类型和线程id。
四、锁优化
锁升级方向:无锁——>偏向锁——>轻量级锁——>重量级锁,并且方向不可逆。
1、偏向锁:
在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
2、轻量级锁:
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。
3、重量级锁:
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
4、锁消除:
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如局部变量的加锁。
5、锁粗化:
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如把for循环内部的锁扩大到for循环外部。
6、自旋锁与自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
6.1、自旋锁
许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
6.2、自适应自旋锁
这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上自旋并成功获取锁的拥有者的自旋次数来决定,这就解决了自旋锁带来的缺点。
网友评论