1 synchronized实操
关键字synchronized可以用来保证多线程并发安全的**原子性、可见、有序性。**关键字synchronized不仅可以作用于方法还可以作用于同步代码块,能够保证作用范围中线程访问安全。
注意:局部变量是线程安全的。线程不安全问题只存在于实例变量。
synchronized关键字作用如下图:
- 同步方法:分为静态方法和非静态方法,对静态方法,锁作用对象是类对象。对非静态方法,锁作用对象是实例对象。
- 同步方法块:通过
synchronized(obj)
来对某个对象进行加锁。如果是this表示实例对象。如果是xx.class表示作用于类对象。
1.1 锁作用于类对象和实例对象的区别?
- 锁作用于实例对象:当多个线程访问同一个实例对象的同步块的时候,存在竞争关系。只能有一个线程能访问当前实例对象的锁。其他线程只能等待,直到占所有的线程释放实例对象的锁。
- 锁作用于类对象:当多个线程访问同一个类的不同实例对象的同步块的时候,存在竞争关系。因为锁所用于类上,虽然是不同的实例对象但是所属同一个类,只有一把类锁。因此多个线程仍然存在竞争关系。
举例
public class SynchronizedTest {
public synchronized static void test1() {
// 类锁
}
public synchronized void test2() {
// 实例对象锁
}
public void test3() {
synchronized (SynchronizedTest.class) {
// 类锁
}
}
public void test4() {
synchronized (this) {
// 实例对象锁
}
}
public void test5() {
synchronized ("a") {
// 实例对象锁
}
}
}
非静态同步方法用的是同一把锁:实例对象本身
静态同步方法用的是同一把锁:类本身
注意:无加锁的方法块和加锁的方法块不存在竞争关系。静态同步块和非静态同步块也不存在竞争关系,因为持有的是不同的锁。
2 synchronized原理
2.1 synchronized对应字节码指令
2.1.1 同步方法
使用命令:
javap -c -v synchronizedTest.class
注:-c是查看字节码指令 -v是打印附加信息
在同步方法中,加了synchronized关键字,方法的flag标志有ACC_SYNCHRONIZED标志
2.1.2 同步代码块
同样的使用上述命令查看字节码
可以看到当用synchronized()
表示同步代码块后,字节码对应会有monitorenter
和monitorexit
指令,表示获取锁,释放锁。
3 synchronized锁升级
jdk1.6之前synchronized的实现都为重量锁,重量锁需要用户态切入到内核态获取锁对象,影响性能。
jdk1.6及其之后进行了优化,引入了偏向锁和轻量级锁(性能比重量锁更好),以及锁存储结构和锁升级,也就是说synchronized同步块会根据具体情况从无锁->偏向锁->轻量级锁->重量级锁 升级锁。
3.1 无锁
线程之间不会有锁的竞争。多线程下会造成数据的不安全。
3.2 偏向锁
顾名思义,偏向于某个线程的锁。会偏向于第一个访问锁的线程
原理:
当线程进入同步块的时候,获取到锁之后,退出同步块的时候不会主动释放锁。当线程第二次进入同步代码块的时候,会判断此时持有锁的线程是不是自己(持有锁对象的线程存放在对象头中)。如果是自己正常执行。从始至终能使用锁的只有一个线程,性能很高。(java 15被废弃)
简言之,线程进入的时候获取锁,不会释放。下次线程来的时候直接用。
偏向锁只有在其他线程尝试获取偏向锁的时候才会释放。竞争的线程用CAS来替换对象头中的持有锁线程ID。
- 如果竞争成功,仍然为偏向锁。只不过偏向新的线程。
- 如果竞争失败,升级为轻量级锁。
3.3 轻量级锁
多个线程下,基本不会出现或者轻微的锁的竞争,那么synchronized就处于轻量级锁的状态下。这种状态允许线程出现短时间的CAS自旋空转。
没有抢到的锁会自旋,即不停地循环判断是否能够获取锁。获取锁的操作其实是通过CAS修改对象头中的锁标志位。
注:轻量级锁和偏向锁有一个重要的区别是偏向锁获得锁之后不会主动释放。轻量级锁获得锁之后会主动释放。
3.4 重量级锁
线程竞争锁过程中长时间的自旋是非常影响性能的。因此会用计数器记录自旋次数(默认是10次)如果超出限定次数。会将轻量级锁升级为重量级锁。
进入重量级锁状态,当一个线程获取锁之后。其余线程会进入阻塞状态。简言之,就是所有的控制权交给了操作系统,操作系统来负责线程的切换,会频繁的从用户态到内核态的切换,严重影响性能。
4 synchronized锁升级总结
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁都不需要额外的消耗 | 如果多个线程竞争会带来额外的锁撤销消耗 | 只有一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高程序的相应速度 | 线程竞争一直得不到锁,会自旋消耗cpu性能 | 追求响应时间,同步块执行速度特别快 |
重量级锁 | 竞争的线程会自旋,不会消耗CPU | 线程阻塞,用户态和内核态切换时间长,响应时间慢 | 追求吞吐量,同步执行时间长 |
简单描述锁升级过程:
单个线程竞争适合偏向锁。两个线程竞争锁的时候,竞争成功,会偏向另一个线程。竞争失败,升级为轻量级锁。轻量级锁适合多个线程少量竞争,CAS自旋次数有一定限制的锁争抢。当CAS自旋次数超过限度,会升级为重量级锁。
5 思考问题
- 为什么会存在锁升级现象?
JDK1.6之前sychronized只有重量级锁。重量级锁需要依靠操作系统来完成线程的切换。线程切换需要从用户态到内核态,他们分别有自己专用的内存空间,导致用户态到内核态切换很耗时,影响程序性能。
为了减少线程获取锁带来的性能消耗,引入了偏向锁和轻量级锁。 - 为什么每个对象都可以成为一个锁?
因为每个对象的对象头里面都有一个锁标志,表示这个对象的锁的状态。深入底层理解的话,因为每个对象实例都有一个Monitor,Monitor和实例对象一起创建一起销毁。实例对象中头中有标志关联Monitor,而Monitor中owner属性存放持有锁的线程id。 - 锁消除是什么?
锁消除是JIT即时编译器的优化手段。属于代码bug,代码虽然加锁了,但是每个线程都不会出现竞争的情况(每把锁都不相同)。比如下面代码:
public class SynchronizedTest2 {
public static void main(String[] args) {
Cat cat = new Cat();
for (int i=0;i<5;i++){
new Thread(()->{
cat.test();
}).start();
}
}
private static class Cat{
static Object object = new Object();
public void test(){
// 每个线程进来都新new一个对象,所以每个线程的锁都不一样,相当于无锁
Object o = new Object();
synchronized (o){
System.out.println(object.hashCode()+"===="+o.hashCode());
}
}
}
}
- 锁粗化是什么?
也是JIT即时编译器优化手段。将相邻的锁住同一个锁对象的代码合并。比如
public class LockBigDemo
{
static Object objectLock = new Object();
public static void main(String[] args)
{
new Thread(() -> {
synchronized (objectLock){
System.out.println("111111");
}
synchronized (objectLock){
System.out.println("222222");
}
synchronized (objectLock){
System.out.println("333333");
}
synchronized (objectLock){
System.out.println("444444");
}
// JTI进行锁粗化后的代码相当于:
synchronized (objectLock){
System.out.println("111111");
System.out.println("222222");
System.out.println("333333");
System.out.println("444444");
}
},"t1").start();
}
}
6 参考链接
- 锁升级:无锁、偏向锁、轻量级锁、重量级锁
- synchronized无锁、偏向锁、轻量级锁、重量级锁