前言 在Java开发过程中,我们为了提高效率往往采用多线程的方式,然而多线程也伴随着一些问题,比如:存在多个线程操作共享资源导致的数据问题。
当然Java也为我们提供了此类问题的解决办法:synchronized关键字,今天我主要和大家分享一下关于synchronized的一些原理探寻。
一、同步锁的作用范围
既然是原理探寻,概念性的东西我就不再多说,大家都知道synchronized是一种同步锁机制,它保证同一时刻,只允许一个线程访问被synchronized修饰的方法或者代码块,那么它的作用范围又是怎样的呢?
其实在synchronized中,提供了两种锁:类锁与对象锁
- 类锁:全局性质,当多个线程调用不同实例的同步方法会互斥
public class Test{
public void method(){
synchronized(Test.class){
//代码
}
}
public static void main(String[] args){
Test test1 = new Test();
Test test2 = new Test();
new Thread(()->test1.method(),"thread1").start();
new Thread(()->test2.method(),"thread2").start();
}
}
- 对象锁:针对实例对象,当多个线程调用用一个对象实例的同步方法会互斥
public class Test{
static Object object = new Object();
public void method(){
synchronized(object){
//代码
}
}
public static void main(String[] args){
Test test1 = new Test();
Test test2 = new Test();
new Thread(()->test1.method(),"thread1").start();
new Thread(()->test2.method(),"thread2").start();
}
}
二、对象头是中如何记录锁的标记
synchronized既然保证了多线程访问同一资源的互斥性,那肯定会有一个标记来识别当前锁的状态,是直接拿锁执行还是等待锁的释放。这个状态就存在对象头中。 一个 对象是由对象头、实例数据以及对齐填充三部分组成的。
其中对象头里的Mark Word便记录了锁的相关信息。
| 锁状态 | 是否是偏向锁 | 锁标记位 |
|---|---|---|
| 无锁 | 0 | 01 |
| 偏向锁 | 1 | 01 |
| 轻量级锁 | 00 | |
| 重量级锁 | 10 |
2.1 锁升级
其实针对一个对象而言,也并非一开始就采用重量级锁,在这其中也会存在一个锁升级过程:
无锁-->偏向锁-->轻量级锁-->重量级锁
三、JDK1.6前后的synchronized
-
jdk1.6之前:
synchronized只有重量级锁机制,没有获取锁的线程会通过park方法进行阻塞等待。底层依赖于操作系统的Lock来实现,然而涉及到操作系统层面将线程阻塞,就得进行用户态与内核态的切换,对于性能来说是影响很大的。
-
jdk1.6以后:
synchronized进行了一定程度上的优化,新增了偏向锁与轻量级锁,让线程在不阻塞的情况下达到线程安全的目的。从而在性能与线程安全之间做好一个平衡。
3.1偏向锁原理
偏向锁其实就是在没有线程竞争(单线程情况下)的情况去访问synchronized修饰的代码块,先尝试通过偏向锁来获取访问资格的这么一个过程。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01” (默认标志位就是01),即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁 相关的同步块时,虚拟机都可以不再进行任何同步操作。
-XX:+UseBiasedLocking //启动参数
-XX:+BiasedLockingStartupDelay=0 //设置延迟启动时间
3.2轻量级锁原理
既然偏向锁是单线程情况下,那么多线程访问同步方法,那些没有抢占到锁资源的线程不能像重量级锁一样去阻塞等待,很影响性能。所以就有一种新的方案:轻量级锁,也就是让没有抢占到锁的线程进行一定次数的自旋
当然也不可能让其一直自旋,自旋的线程会浪费CPU资源,所以默认是10次,当到达10次后,还未能获取锁资源,则会升级为重量级锁;不过jdk1.6以后也出现了自适应自旋锁,自旋次数不是固定的,会根据上一次的自旋次数自己调整决定。
3.3重量级锁原理
自然自旋10次都未能获取锁资源,就不能一直再这样消耗CPU资源,此时就只能是升级为重量级锁,从操作系统层面将线程挂起,等待获取锁资源时再唤醒。
参考书籍《Java并发编程深度解析与实战》