部分内容来源:JavaGuide
Synchronized是什么?有什么用
Synchronized是同步的意思,主要解决多个线程之间访问资源的同步性,是一个同步锁
我们会真的把我们的资源给锁住
保证被他修饰的资源或代码块在任意时刻只能有一个线程来执行
如何使用Synchronized
1.修饰我们的示例方法
给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2.修饰静态方法
给当前类加锁,会作用于类的所有实例对象,进入同步代码前要获得当前Class锁
这是因为静态成员不属于任何一个实例对象,会归整个类所有,不依赖于类的特定实例被类的所有实例共享
静态的Synchronized方法和非静态的Synchronized方法之间的调用互斥吗?
不互斥
如果一个线程A调用实例对象的非静态Synchronized方法,而线程B需要调用这个实例对象所属类的Synchronized方法,这个是允许的,不会发生互斥现象
因为访问静态Synchronized方法占用的锁是当前类的锁,而访问非静态Synchronized方法占用的锁是当前实例的锁
3.修饰静态代码块
Synchronized(Object)表示进入静态代码块前需要获得指定的对象锁
Synchronized(XXX.Class)表示进入代码块前要获得给定Class的锁
Synchronized锁住实例对象和锁住类Class具体有什么区别呢?
其实用Synchronized可以理解成我们要排队使用,要阻塞一些其他线程
锁住实例对象
当 synchronized
锁住实例对象时,它会锁定该对象的实例。
这意味着在同一时间内,只有一个线程可以执行该对象的同步实例方法或同步代码块
锁住类Class对象
当 synchronized
锁住类的 Class
对象时,它会锁定该类的类对象。
这意味着在同一时间内,只有一个线程可以执行该类的同步静态方法或同步代码块
具体区别
锁定对象:
实例对象:锁定具体的实例对象(this
)。
类对象:锁定类的 Class
对象(MyClass.class
)。
影响范围:
实例对象:只影响该实例的同步方法或代码块,不同实例之间不互相影响。
类对象:影响整个类的同步静态方法或同步代码块,所有实例共享同一个类对象的锁。
使用场景:
实例对象:适用于需要对单个对象的同步方法或代码块进行并发控制的场景。
类对象:适用于需要对整个类的静态方法或代码块进行并发控制的场景
锁住对象的时候:我们就只影响当前这个实例对象里的同步方法和同步代码块
锁住类Class的时候:我们影响里面的静态方法,同步方法,同步代码块
说一下Synchronized的底层原理
分析一下同步代码块
同步代码块的实现使用的是monitor enter和monitor exit指令
monitor enter:
指向同步代码块开始的位置
monitor exit:
指向同步代码块结束的位置
上面的字节码Class中包含一个 monitorenter 指令以及两个 monitorexit 指令,
这是为了保证锁在同步代码块,代码正常执行以及出现异常的这两种情况下都能被正确释放。
当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor 对象。
另外,wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。
获取锁的时候+1
释放锁的时候置0
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
分析一下synchronized修饰方法
ACC_SYNCHRONIZED
标识
如果是实例方法,JVM会获取当前实例对象的锁
如果是静态方法,JVM会尝试获取当前class的锁
sychronized能保证内存可见性吗?为什么?怎么做到的?
synchronized
如何保证内存可见性:
进入同步代码块:线程在进入同步方法或同步代码块时,会清空本地内存缓存,强制从主内存读取共享变量的值。在JVM字节码中我们的指令标识的就是EnterMonitor
退出同步代码块:线程在退出同步代码块时,会将其对共享变量的修改刷新到主内存,这样其他线程能够看到更新后的值。在JVM字节码中我们的指令标识就是ExitMonitor
为什么 synchronized
保证原子性吗?
因为我们可以用sycnhronized锁住我们的方法和代码块
保证被synchronized修饰的方法和代码块只能有一个线程进入去执行方法
synchronized和volatile有什么区别
volatile和synchronized是互补的存在
一个是轻量级实现,性能好
一个是重量级实现,保证多个线程访问资源的同步性
volatile可以保证数据的可见性
synchroniez可以解决多个线程之间访问资源的同步性,保证资源只有一个线程访问
Synchronized和ReentranLock有什么区别
所属层面
synchronized是独占式悲观锁, 通过JVM实现的,只允许同一时刻只有一个线程占用资源
ReentranLock是 Lock 的默认实现方式之一,是基于AQS 实现的API级别的,默认是通过非公平锁实现的
synchronized 是基于JVM层面实现的内置锁,reentrantlock是基于AQS实现的API级别的锁
拓展功能
ReentranLock有更多的拓展功能
ReentranLock更加灵活,比如可以判断是否成功获取到锁
ReentranLock需要手动加锁和释放锁,如果忘记释放锁,就会导致资源被永久占用,sync无需手动释放
ReentranLock可设置为公平锁(默认为非公平锁),synch 不行
ReentranLock只能修饰代码块, sync可以修饰方法和代码块
Synchronized的底层是如何实现的
JVM 内置的 Monitor 监视器 的Monitorenter 和 Monitorexit 实现的
监视器Monitor是如何实现的
他是一个概念或者机制,是用c++的object monitor来实现的
每当有一个线程进入,就会有一个监视器技术 + 1
Synchronized支持重入吗?如何实现
synchronized是基于原子性的内部锁机制,是可重入的
synchronized是基于原子性的内部锁机制,是可重入的,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性
synchronized底层是利用计算机系统mutex Lock实现的。
每一个可重入锁都会关联一个线程ID和一个锁状态status
当一个线程请求方法时,会去检查锁状态
- 如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
- 如果锁状态不是0,代表有线程在访问该方法。 此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等待。
在释放锁时,
- 如果是可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
- 如果非可重入锁的,线程退出方法,直接就会释放该锁
syncronized锁升级的过程讲一下
具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁
4种锁状态
一开始没有线程执行,就根本无锁
无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置
只有一个线程执行就是偏向锁
偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向
当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作
多个线程竞争锁但是竞争不激烈,我们就用轻量级锁
轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。
也就是线程少的时候我们自旋和用CAS操作来获取锁
锁竞争非常激烈的时候
重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后线程会被操作系统调度然后挂起,这可以节约CPU资源。
也就是线程多的时候我们要用线程的休眠和唤醒机制
锁升级过程
线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。
但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁
后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞
Synchronized偏向锁为什么被废弃了
在 JDK15 中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking
启用偏向锁)
在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)
在官方声明中,主要原因有两个方面:
性能收益不明显
偏向锁是 HotSpot 虚拟机的一项优化技术,可以提升单线程对同步代码块的访问性能。
受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。
随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。
偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益
如果存在多线程竞争,就需要 撤销偏向锁 ,这个操作的性能开销是比较昂贵的
偏向锁的撤销需要等待进入到全局安全点(safe point),该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销
JVM 内部代码维护成本太高
偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性
偏向锁引入的复杂逻辑和对其他组件的侵入,使得 JVM 的代码结构变得更加复杂和混乱。开发人员在阅读和理解 JVM 代码时,需要花费更多的时间和精力去梳理偏向锁相关的代码逻辑以及它与其他组件之间的交互关系。这对于新加入 JVM 开发的人员来说,学习成本大幅增加,也不利于对 JVM 整体架构和功能的理解
偏向锁本身用处不大但是却复杂,这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁