目录
前言
前面我们学习了synchronized对应的锁策略,那么本篇我们就来深入学习一下synchronized原理。本篇后面也会讲解synchronized的一些相关面试题。
synchronized特性
synchronized有四个特性:原子性、可见性、可重入性、有序性。
- 原子性 :即对一个操作或者多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。
在java中,对基本数据类型的变量的读取和赋值操作都是原子性的,这些操作不会被打断。但像i++,i-=1这类操作,就不是原子性的,是由读取、计算、赋值这三个指令构成的,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的就是脏数据,无法保证原子性。
被synchronized修饰的类或者对象的的所有操作都是原子的。
注意:volatile关键字不具有原子性。
2.可见性 :指多个线程访问一个资源时,该资源的状态、信息等对于其他线程都是可见的。
synchronized和volatile都具有可见性 ,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到内存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。
而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新到内存,内存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。
3.可重入性:线程可对同一把锁加多次锁,可以再次获取锁而不会出现死锁。
synchronized和ReentrantLock都是可重入锁 。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
4.有序性:有序性值程序执行的顺序按照代码先后执行。
synchronized和volatile都具有有序性 ,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
总结一下synchronized的特点。
synchronized特点
- 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
- 开始时轻量级锁实现,如果锁被持有时间太长,就转换为重量级锁,
- 实现轻量级锁的时候大概率用的是自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
synchronize的加锁过程
JVM将synchronized锁分为无锁、偏向锁、轻量级锁、重量级锁。会根据情况,进行依次升级。
synchronized在jdk6后进行了优化,锁升级的方向是不可逆的。
1.无锁-->偏向锁
当我们使用synchronized进行加锁的时候,线程不会立即从无锁的状态转换为加锁的状态,而是会先处于一个偏向锁的状态。
什么是偏向锁?
**偏向锁不是真的"加锁",只是给对象头中做⼀个"偏向锁的标记",记录这个锁属于哪个线程.**如果后续没有其他线程来竞争该锁,那么就不⽤进⾏其他同步操作了(避免了加锁解锁的开销);如果后续有线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程),那就取消原来的偏向锁状态,进入一般的轻量级锁状态。
偏向锁本质上相当于"延迟加锁 "能不加锁就不加锁,类似于懒汉模式,尽量来避免不必要的开销。但该做的标记还是得做,否则无法区分何时需要真正加锁。
2.偏向锁->轻量级锁
当出现锁竞争后,持有偏向锁的线程就会转换成轻量级锁(自适应的自旋锁) 。此处的轻量级锁就是通过**CAS(简单理解就是一条指令就完成比较和交换)**来实现的。
- 通过CAS检查并更新一块内存(比如null=>该线程引用)
- 如果更新成功,则认为加锁成功
- 如果更新失败,则认为锁被占用,继续自旋式的等待(并不放弃CPU)
3.轻量级锁->重量级锁
如果锁竞争比较激烈,那么synchronized就会**从轻量级锁转换成重量级锁(挂起等待锁)**会使线程进入阻塞等待。
- 执行加锁操作,先进入内核态
- 在内核态判断当前锁是否被占用
- 如果锁没有被占用,则加锁,并切换为用户态
- 如果锁被占用,就会阻塞等待挂起,等待被唤醒。
- 经历了⼀系列的沧海桑⽥,这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒 这个线程,尝试重新获取锁.
锁的优化操作
除了上面提到的锁升级(锁膨胀)还有以下的优化技术。
1.锁消除
Java中锁消除是Java虚拟机(JVM)中的一种优化技术,用于消除不必要的同步锁,从而提高程序的性能。
synchronized关键字能够实现同步和互斥,确保多个线程对共享资源访问的一致性。但synchronized也会有一定的开销,频繁的加锁,也会降低程序的性能,包括获取锁、执行同步代码块、释放锁等操作所产生的时间成本,也可能导致线程阻塞和上下文切换的代价。
为了减少synchronized带来的开销,JVM对synchronized进行了锁消除的优化技术。
锁消除原理 :在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而提高程序性能的目的。例如在单线程的情况下,使用StringBuffer的append方法、Vector的add方法等,JVM会进行锁消除优化。
示例:
java
class Demotest{
// 使用静态变量count来记录累加的和,静态变量被所有实例共享
static int count=0;
/**
* 主函数执行累加操作
* @param args 命令行参数,本例中未使用
*/
public static void main(String[] args) {
// 循环100次进行累加操作
for(int i=0;i<100;i++){
// 使用类对象作为锁进行同步控制,确保线程安全
synchronized (Demotest.class){
count+=i;
}
};
// 输出最终的累加结果
System.out.println(count);
}
}
对于上述这段代码中,由于没有其他线程与主线程进行锁竞争,因此JVM可以安全地消除掉该代码块的同步锁操作,提高程序性能。
在高并发环境下,还是有必要使用synchronized来使线程对共享资源访问的正确性和一致性。锁消除只是一种优化技术,不能保证在所有情况下都能消除同步锁操作。但也不能无脑加锁。
注意:锁消除的优化是在编译阶段进行的操作,而偏向锁则是在代码运行过程中完成的。
2.锁粗化
锁的粒度:锁的粒度是指在并发编程中锁的作用范围或者说是锁保护的数据结构的大小。分为粗和细。锁的粒度越细,程序的并发性能就越好,但带来的锁竞争和开销就越多。
通常情况下,为了多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但在某些情况下,一个程序对同一个锁不间断的请求、同步和释放,那么就会消耗一定的系统资源。所以我们就可以通过锁粗化,将多个锁请求合并成一个,避免短时间内大量请求。锁粗化就是将多个锁请求合并成一个请求,以降低短时间内大量的锁请求、同步和释放带来的性能损耗。
锁粗化其实就是将多个"细粒度"的锁合并为一个"粗粒度"的锁
实际开发过程中,使**⽤细粒度锁,是期望释放锁的时候其他线程能使⽤锁** . 但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会⾃动把锁粗化,避免频繁申请释放锁。
示例:
java
class Demoks{
static int count=0;
public static void main(String[] args) {
for(int i=0;i<1000;i++){
synchronized (Demotest.class){
count+=i;
}
}
System.out.println(count);
}
}
对于上述代码中,我们可以看到,每次循环都需要进行加锁释放锁,这样带来的性能损耗就比较大,所以我们可以将锁粗化,将多个锁合并成一个。即:
java
class Demoks{
static int count=0;
public static void main(String[] args) {
synchronized (Demotest.class) {
for (int i = 0; i < 1000; i++) {
count += i;
}
}
System.out.println(count);
}
}
这样就可以有效地避免资源浪费。
当然,不是任何情况下都能进行锁粗化的优化操作,我们需要保证锁粗化的结执行果是正确情况下代码的执行结果。
3.自适应自旋锁
自旋锁在上一篇我们已经讲过,其实就是在获取不到锁的时候,不会释放CPU资源,而是会一直循环尝试获取到锁,直到获取到锁为止的策略。伪代码如下:
java
//尝试获取锁
while(!isLock()){
}
自旋锁的优点在于避免了一些线程的挂起和恢复操作,这两操作都是需要从用户态转入内核态的,过程较慢,但自旋锁是在用户态的,通过自旋的方式在一定程度上能避免线程挂起和恢复造成的性能开销。
但是长时间自旋还获取不到锁,就会造成一定的资源浪费,所以我们会给自旋设置一个固定的值来避免一直自旋的性能开销。对于synchronized来说,它的自旋锁是自适应自旋锁,在jdk1.6的优化后,就引入了自适应的自旋锁。
自适应自旋锁 :线程自旋的次数不再是固定的值,而是一个动态变化的值,这个值会根据你上一次自旋获取锁的状态来决定自旋的次数,例如,上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
相关面试题
1.什么是偏向锁?
偏向锁不是真的加锁,而是在锁的对象头中记录一个标记(记录该锁所属的线程),如果没有其他线程参与锁竞争,那么就不会真正执行加锁操作,从而降低程序开销。一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态。
2.synchronized的实现原理是什么?
本篇内容。
以上就是本篇所有内容,若有不足,欢迎指正~