详细讨论java中synchronized关键字底层加锁原理和应用。
文章目录
- java锁-synchronized
- 前言
- 1. 原理
- 1.1 monitorenter和monitorexit指令
- 2. 性能
- 3. 使用
- 3.1 静态方式加锁
- 3.2 实例方式加锁
- 3.3 代码块加锁(推荐)
- 总结
前言
对于学习了java的读者而言synchronized关键字肯定不会陌生, 初步学习java时synchronized就是锁的代名词,还有一部分人总是将synchronized当成万能锁来用。本文将详细讨论synchronized在jvm底层是如何加锁?滥用synchronized会面临怎么样的后果?应该如何合理使用?
1. 原理被synchronized修饰的java代码块或方法通过javac编译为字节码后,会在同步代码的前后生成monitorenter和monitorexit这两个字节码指令,这两个指令都需要一个reference类型的参数来明确锁定和解锁的对象(线程持有锁对象)。如果在java源码中显示的定义了synchronized参数对象则使用该参数的引用作为reference,如果synchronized没有指定参数,会根据synchronized修饰的方法(实例方法或静态方法)来决定是选择对应的实例对象还是Class对象作为reference。
1.1 monitorenter和monitorexit指令执行monitorenter指令时首先会尝试获取对象锁,如果这个对象没有被锁定或者是当前线程已经持有这个对象锁,则把锁的计数值加一。而执行monitorexit指令时会将锁的计数值减一,一旦锁的计数值为零,当前线程立即释放锁。如果monitorenter指令获取对象锁失败,则会将当前线程阻塞等待。
2. 性能synchronized在jdk1.5版本之前(包括1.5)采用重量级锁(将线程直接挂起或唤醒)在多线程环境下频繁切换用户态和内核态产生了相当大的额外性能开销,最终导致java程序的性能下降。sum公司在jdk1.6针对synchronized做了大量的优化,采用了锁升级的方式从偏向锁 -> 轻量级锁 -> 重量级锁,从而大大的提升了性能。每一个对象的加锁状态在jvm的内存的对象头中有相应的标志位标记,下图是一个java对象头在jvm中的存储信息。
到这里你会发现,在java对象头有多个标志位记录锁的多种状态,不同状态下锁的级别不同。一个对象的锁状态有未锁定,可偏向(偏向锁)、轻量级锁,重量级锁,从偏向锁到重量级锁这个过程一般叫做锁膨胀或升级。
3. 使用偏向锁: 对第一个获取该对象的线程进行消除锁操作直到有其他线程竞争资源才结束此模式,升级为轻量级锁。
轻量级锁: 对于两个线程之间竞争资源时采用CAS算法来实现加锁直到有第三个线程参与竞争资源才结束此模式,升级为重量级锁。
重量级锁:这是操作系统层面的锁,未获得锁的线程都会被挂起,直到被锁释放才通知其他竞争的线程,又因为HotSpot虚拟机是采用操作系统的核心线程来实现线程,导致线程的挂起和唤醒都要发生用户态和内核态之间的切换从而降低性能。
java中synchronized可以通过修饰静态方法、实例方法和代码块三种方式来使用。根据上文我们知道被加锁的代码块会指性能降低,所以推荐在方法内部需要保证数据同步的位置使用代码块的方式加锁即可,但如果在方法内部多次使用同步代码块这时应该将同步升级到方法级别。
3.1 静态方式加锁在静态方法中用synchronized关键字修饰即可
public static synchronized void sync(){
System.out.println("同步代码");
}
3.2 实例方式加锁
在实例方法中用synchronized关键字修饰即可
public synchronized void sync(){
System.out.println("同步代码");
}
3.3 代码块加锁(推荐)
在代码块前用synchronized关键字修饰即可
Object lock = new Object();
public void sync(){
synchronized(lock){
System.out.println("同步代码");
}
}
总结
- synchronized同步的方法或代码块可重入加锁
- synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数 据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
- synchronized在jdk1.6之后性能提升,语义明确,使用简单



