Java Synchronized关键字必知必会

tech2025-12-18  9

Synchronized

synchronized 可以用来修饰以下 3 个层面:

修饰实例方法;

修饰静态类方法;

修饰代码块。

synchronized 修饰实例方法

public class ZtengSyncSample { int num = 1; public synchronized void add() { for (int i=0;i< 5;i++) { num++; System.out.println(Thread.currentThread().getName()+"_"+num); } }

这种情况锁对象是当前实例对象,因此只有同一个实例对象调用此方法才会产生互斥效果,不同实例对象之间不会有互斥效果。

示例如下:

int num = 1; public synchronized void add() { for (int i=0;i< 5;i++) { num++; System.out.println(Thread.currentThread().getName()+"_"+num); } } public static void main(String[] args) { ZtengSyncSample ztengSyncSample11 = new ZtengSyncSample(); ZtengSyncSample ztengSyncSample22 = new ZtengSyncSample(); Thread mThread1 = new Thread("ztengSyncSample11_mThread1") { public void run() { ztengSyncSample11.add(); }; }; Thread mThread2 = new Thread("ztengSyncSample22_mThread2") { public void run() { ztengSyncSample22.add(); }; }; mThread1.start(); mThread2.start(); }

在不同的线程中调用的是不同对象的 add方法,因此彼此之间不会有排斥效果。示例执行结果为

可以看出,两个线程是交互执行的。

如果将代码进行修改,两个线程调用同一个对象的 add方法

public static void main(String[] args) { ZtengSyncSample ztengSyncSample11 = new ZtengSyncSample(); //ZtengSyncSample ztengSyncSample22 = new ZtengSyncSample(); Thread mThread1 = new Thread("mThread1") { public void run() { ztengSyncSample11.add(); }; }; Thread mThread2 = new Thread("mThread2") { public void run() { ztengSyncSample11.add(); }; }; mThread1.start(); mThread2.start(); }

执行结果如下:

可以看出:只有一个线程中的代码执行完之后,才会调用另一个线程中的代码。也就是说此时两个线程间是互斥的

修饰静态类方法

如果 synchronized 修饰的是静态方法,那么锁对象是当前类的 Class 对象。因此即使在不同线程中调用不同实例对象,也会有互斥效果。

可以看出,两个线程有互斥效果 并依次执行的

synchronized 修饰代码块

synchronized 还可以作用于代码块

修饰代码块时如果用的是同一对象,则有互斥效果,反之则没有,以上打印如下

synchronized实现细节

synchronized 既可以作用于方法,也可以作用于某一代码块。但在实现上是有区别的

使用 synchronized 作用于代码块

使用 javap -v xxx.class 查看字节码,可以看出,编译而成的字节码中会包含 monitorenter 和 monitorexit 这两个字节码指令

下面字节码中有 1 个 monitorenter 和 2 个 monitorexit,因为虚拟机需要保证当异常发生时也能释放锁。 2 个 monitorexit 一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。

synchronized 修饰方法 

被 synchronized 修饰的方法在被编译为字节码后,在方法的 flags 属性中会被标记为 ACC_SYNCHRONIZED 标志。当虚拟机访问一个被标记为 ACC_SYNCHRONIZED 的方法时,会自动在方法的开始和结束或异常位置添加 monitorenter 和 monitorexit 指令。

关于 monitorenter 和 monitorexit,可以理解为一把锁。在这个锁中保存着两个比较重要的属性:计数器和指针。

计数器代表当前线程一共访问了几次这把锁;

指针指向持有这把锁的线程。

计数器默认为0,当执行monitorenter指令时,如果计数器值为0 说明这把锁并没有被其它线程持有。那么这个线程会将计数器加1,并将锁中的指针指向自己。当执行monitorexit指令时,会将计数器减1。

Java 虚拟机 synchronized 的优化

从 Java 6 开始 对 synchronized 关键字做了多方面的优化,减少“重量级锁”的使用次数,并最终减少线程上下文切换的频率,主要做了以下几个优化: 锁自旋、轻量级锁、偏向锁。

锁自旋

所谓自旋,就是让该线程等待一段时间,不会被立即挂起,看当前持有锁的线程是否会很快释放锁。而所谓的等待就是执行一段无意义的循环即可(自旋)。

线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,所以 Java 引入了自旋锁的操作,自旋锁要占用 CPU,如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的 CPU 时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

轻量级锁

对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也就是不存在锁竞争的情况。在这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。

偏向锁

轻量级锁是在没有锁竞争情况下的锁状态,但是在有些时候锁不仅存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作

最新回复(0)