目录
前言
一、synchronized特性
1.1 原子性
1.2 可见性
1.3 有序性
1.4 可重入
二、synchronized的用法
2.1 修饰方法
修饰静态方法
2.2 修饰代码块
三、synchronized的特点
四、synchronized加锁的工作过程
4.1 偏向锁
4.2 轻量级锁(自旋锁)
4.3 重量级锁
五、其他synchronized的优化
5.1 锁消除
5.2 锁粗化
总结
如果某一个资源被多个线程共享,为了避免因为线程的抢占资源而出现线程不安全的因素,我们需要对线程进行同步,synchronized就是为了保证不会因为线程的抢占式执行而出现的bug。是并发编程中的一个重要的特性。那么接下来就看明白synchronized的底层原理。
所谓的原子性就是一段代码块或者一条语句,要么全部执行,并且在执行过程中不会被打断,直到这些操作全部执行完成,要么就全部不执行。
保证原子性是并发编程中非常必要的操作,在java中,一些简单的赋值语句本身就是原子的操作,比如 int a = 10; 这种类型的操作本身在底层就是CPU的一条指令,即使在多线程环境下,也不会出现原子性的问题。
但是比如i++; i+=1 ; 等操作就不是原子的了,
他们在操作系统底层分为 load(读取) add(计算) save (赋值)。
在多线程环境下,这个操作就会出现严重的问题。
如何解决呢? synchronized就出现了。
synchronized就可以保证上述问题不会出现bug,synchronized会在计算操作时先进行加锁操作,加锁完成后,直到执行完成之后,才会释放锁,在加锁完成之后,其他线程就不能再对synchronized加锁的代码块或者某一操作进行操作了。直到synchronized释放锁。
面试题:关于synchronized和volatile的区别
synchronized和volatile最大的区别就是原子性这里,synchronized能保证原子性,而volatile不能保证原子性,volatile能保证内存可见性。当前synchronized也能保证内存可见性。
可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。
synchronized和volatile都能保证内存可见性。
当synchronized针对某个资源获取锁之后,就对这个资源进行加锁操作,其他线程要想也针对这个资源进行操作,就得等待锁的释放,然后也进行加锁。
在synchronized获取到这个锁的时候,这个锁的状态对于其他线程都是可见的,而且在释放锁之前,会把当前修改的变量或者一些值写入到主内存中,保证资源的可见性。主内存是对个线程共享的。其他线程再进行操作的时候,就会读取主内存中的数据。
有序性指程序执行的顺序按照代码先后执行。
synchronized也能保证有序性,java中允许编译器或者处理器在不对整个程序的执行结果有影响的前提下对代码进行指令重排序。
指令重排序在单线程下不会有影响,但是在对线程环境下就会出现问题。
synchronized保证了某一个时刻只能有一个线程进行操作,这也就保证了有序性。
synchronized同时也有可重入的特性。
当一个线程试图操作其他线程持有的锁对象时,就会阻塞等待。
synchronized对一个对象在不解锁的前提下加锁两次,不会造成死锁。
这种情况属于重入锁。通俗一点讲就是说一个线程拥有了当前锁对象仍然还可以重复申请当前锁对象。
想了解更多关于线程安全问题的可以参考下这篇文章
https://blog.csdn.net/qq_63525426/article/details/129832560?spm=1001.2014.3001.5501
synchronized可以修饰静态方法、普通方法,同时还可以修饰定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是类。
class test{
int a = 100;
public synchronized() void add() {
a++;
}
}
修饰普通方法,就是synchronized就是针对当前test对象进行加锁。
针对当前类对象进行加锁
class test{
int a = 100;
static int b = 1;
public synchronized void add() {
a++;
}
public static synchronized void add1() {
b++;
}
}
class test{
int a = 100;
static int b = 1;
Object object = new Object();
public void add2() {
synchronized (object) {
a+=b;
}
}
}
上述代码就是针对object对象进行加锁,其他线程如果也想针对object进行加锁,就等阻塞等待。
多个线程针对同一个对象加锁,才会造成阻塞。
开始是乐观锁,如果锁冲突频繁,就编程悲观锁
开始是轻量级锁,如果锁被持有的时间较长,就转会为重量级锁
实现轻量级锁大概率会用到自旋锁策略
是不公平锁
是可重入锁
不是读写锁
synchronized分为 无锁,偏向锁,轻量级锁,重量级锁的状态,会根据实际情况进行升级。
程序在运行过程中,JVM做的优化。具体优化如下:
即保证了程序整体效率,又保证了线程安全问题。
此时越来越多的线程都加入锁竞争,比如有10个线程,但是只有1个锁对象,那么此时就意味着只有一个线程能获取锁对象,其他线程都只能等待,剩下的9个线程就自旋等待。
自旋锁是基于CAS实现的。
剩下的这9个线程会自旋等待。自旋的速度是非常快的,CPU的资源就消耗特别大。既然如此,就升级成为了重量级锁。
升级为重量级锁之后,线程就会在内核里面进行阻塞等待。
意味着线程要暂时放弃CPU,由内核进行后序的调度。
是编译阶段做的优化,具体是程序在编译的时候,会检查代码是否为对线程执行,是否有必要进行加锁操作,如果没有必要进行加锁,又把锁给加上了,那么此时就会在编译时期自动的把锁去掉。
非必要,不加锁。
注意:synchronized不应该滥用,应该是具体事物,具体分析。选择合适的加锁方案。
锁的规则只有一个:多个线程针对同一个对象进行加锁,才会产生锁竞争。
锁粗话也可以称为锁的粒度:就是synchronized代码块中包含代码的多少。
代码越多:粒度越粗。
代码越少:粒度越细。
我们一般在进行写代码时,会尽量的让锁的粒度更细些。(串行执行的代码越少,并发执行的代码越多)。
这样才能最大限度的保证程序的整体效率。
但是在某个场景中,需要频繁的加锁解锁,此时编译器就把这个操作优化为更粗粒度的锁。
因为每次加锁解锁,都是要有开销的,尤其是释放锁之后,重新加锁,还需要重新竞争。
每次锁竞争都有可能引入一定的等待开销,此时整体效率可能还会降低。
synchronized在Java并发编程中是非常重要的一个知识点。
希望通过上述的描述,对synchronized有个整体的认识。如有不妥,还请多多包涵。