JVM-Java虚拟机是怎么实现synchronized的?
1. JVM的锁优化
今天我介绍了 Java 虚拟机中 synchronized 关键字的实现,按照代价由高至低可分为重量级锁、轻量级锁和偏向锁三种。
重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区;
- 重量级锁:多个线程同时进入临界区。
还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:
synchronized (lockObject) {
// do something
}
上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:
- 情况一:只有Thread#1会进入临界区;
- 情况二:Thread#1和Thread#2交替进入临界区;
- 情况三:Thread#1和Thread#2同时进入临界区。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋-访问CPU空指令,为了避免更昂贵的线程阻塞、唤醒操作),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
1.1 重量级锁
1.2 轻量级锁
你可能见到过深夜的十字路口,四个方向都闪黄灯的情况。由于深夜十字路口的车辆来往可能比较少,如果还设置红绿灯交替,那么很有可能出现四个方向仅有一辆车在等红灯的情况。
因此,红绿灯可能被设置为闪黄灯的情况,代表车辆可以自由通过,但是司机需要注意观察(个人理解,实际意义请咨询交警部门)。
Java 虚拟机也存在着类似的情形:多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。
1.1 偏向锁
如果说轻量级锁针对的情况很乐观,那么接下来的偏向锁针对的情况则更加乐观:从始至终只有一个线程请求某一把锁。
这就好比你在私家庄园里装了个红绿灯,并且庄园里只有你在开车。偏向锁的做法便是在红绿灯处识别来车的车牌号。如果匹配到你的车牌号,那么直接亮绿灯。
具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中。
2. synchronized知识补充
A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
2.1 对象锁
例1:一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞
1 package syn; 2 3 /** 4 * 同步线程 5 */ 6 class SyncThread implements Runnable { 7 private static int count; 8 9 public SyncThread() { 10 count = 0; 11 } 12 13 public void run() { 14 synchronized(this) { 15 for (int i = 0; i < 5; i++) { 16 try { 17 System.out.println(Thread.currentThread().getName() + ":" + (count++)); 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 } 23 } 24 } 25 26 public int getCount() { 27 return count; 28 } 29 30 public static void main(String[] args) { 31 SyncThread syncThread = new SyncThread(); 32 Thread thread1 = new Thread(syncThread, "SyncThread1"); // 如果这里第一个参数是syncThread1,下面是syncThread2,那么synchronized锁没用(因为是对象锁),这是两个对象 33 Thread thread2 = new Thread(syncThread, "SyncThread2"); 34 thread1.start(); 35 thread2.start(); 36 } 37 }
结果:
SyncThread1:0 SyncThread1:1 SyncThread1:2 SyncThread1:3 SyncThread1:4 SyncThread2:5 SyncThread2:6 SyncThread2:7 SyncThread2:8 SyncThread2:9
例2:看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。
1 package syn; 2 3 class Counter implements Runnable{ 4 private int count; 5 6 public Counter() { 7 count = 0; 8 } 9 10 public void countAdd() { 11 synchronized(this) { 12 for (int i = 0; i < 5; i ++) { 13 try { 14 System.out.println(Thread.currentThread().getName() + ":" + (count++)); 15 Thread.sleep(100); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 } 20 } 21 } 22 23 //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized 24 public void printCount() { 25 for (int i = 0; i < 5; i ++) { 26 try { 27 System.out.println(Thread.currentThread().getName() + " count:" + count); 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 } 34 35 @Override 36 public void run() { 37 String threadName = Thread.currentThread().getName(); 38 if (threadName.equals("A")) { 39 countAdd(); 40 } else if (threadName.equals("B")) { 41 printCount(); 42 } 43 } 44 45 public static void main(String[] args) { 46 Counter counter = new Counter(); 47 Thread thread1 = new Thread(counter, "A"); 48 Thread thread2 = new Thread(counter, "B"); 49 thread1.start(); 50 thread2.start(); 51 } 52 }
例3:
1 package syn; 2 3 /** 4 * https://blog.csdn.net/luoweifu/article/details/46613015 5 * 银行账户类 6 */ 7 class Account { 8 String name; 9 float amount; 10 11 public Account(String name, float amount) { 12 this.name = name; 13 this.amount = amount; 14 } 15 //存钱 16 public void deposit(float amt) { 17 amount += amt; 18 try { 19 Thread.sleep(100); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 //取钱 25 public void withdraw(float amt) { 26 amount -= amt; 27 try { 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 public float getBalance() { 35 return amount; 36 } 37 } 38 39 /** 40 * 账户操作类 41 */ 42 class AccountOperator implements Runnable{ 43 private Account account; 44 public AccountOperator(Account account) { 45 this.account = account; 46 } 47 48 public void run() { 49 synchronized (account) { 50 account.deposit(500); 51 account.withdraw(500); 52 System.out.println(Thread.currentThread().getName() + ":" + account.getBalance()); 53 } 54 } 55 56 57 public static void main(String[] args) { 58 Account account = new Account("zhang san", 10000.0f); 59 AccountOperator accountOperator = new AccountOperator(account); 60 61 /** 62 * 运行结果表明,5条线程分别对account实例进行+500和-500的操作,并且他们是串行的。 63 * MyThread的run中,锁定得是account对象,执行的是对account进行+500和-500的操作。 64 * 程序执行新建了5条线程访问,分别执行MyThread中的run方法。因为传入的都是实例account, 65 * 所以5条线程之间是使用同一把锁,互斥,必须等当前线程完成后,下一条线程才能访问account。 66 */ 67 final int THREAD_NUM = 5; 68 Thread threads[] = new Thread[THREAD_NUM]; 69 for (int i = 0; i < THREAD_NUM; i ++) { 70 threads[i] = new Thread(accountOperator, "Thread" + i); 71 threads[i].start(); 72 } 73 74 } 75 }
结果:
1 Thread0:10000.0 2 Thread4:10000.0 3 Thread3:10000.0 4 Thread2:10000.0 5 Thread1:10000.0
2.2 类锁
例4:
1 package syn; 2 3 /** 4 * 同步线程 5 * 6 * 修饰方法-写法1: 7 * public synchronized void method() 8 * { 9 * // todo 10 * } 11 * 12 * 修饰方法-写法2: 13 * public void method() 14 * { 15 * synchronized(this) { 16 * // todo 17 * } 18 * } 19 */ 20 class SyncThreadStatic implements Runnable { 21 private static int count; 22 23 public SyncThreadStatic() { 24 count = 0; 25 } 26 27 /** 28 * syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。 29 * 这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。 30 */ 31 public synchronized static void method() { 32 for (int i = 0; i < 5; i ++) { 33 try { 34 System.out.println(Thread.currentThread().getName() + ":" + (count++)); 35 Thread.sleep(100); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 } 40 } 41 42 @Override 43 public void run() { 44 method(); 45 } 46 47 public static void main(String[] args) { 48 SyncThreadStatic syncThread1 = new SyncThreadStatic(); 49 SyncThreadStatic syncThread2 = new SyncThreadStatic(); 50 Thread thread1 = new Thread(syncThread1, "SyncThread1"); 51 Thread thread2 = new Thread(syncThread2, "SyncThread2"); 52 thread1.start(); 53 thread2.start(); 54 } 55 }
SyncThread1:0 SyncThread1:1 SyncThread1:2 SyncThread1:3 SyncThread1:4 SyncThread2:5 SyncThread2:6 SyncThread2:7 SyncThread2:8 SyncThread2:9
例5:
1 package syn; 2 3 /** 4 * 同步线程 5 */ 6 class SyncThreadClass implements Runnable { 7 private static int count; 8 9 public SyncThreadClass() { 10 count = 0; 11 } 12 13 /** 14 * synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。 15 */ 16 public void method() { 17 synchronized(SyncThread.class) { 18 for (int i = 0; i < 5; i ++) { 19 try { 20 System.out.println(Thread.currentThread().getName() + ":" + (count++)); 21 Thread.sleep(100); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26 } 27 } 28 29 @Override 30 public void run() { 31 method(); 32 } 33 34 public static void main(String[] args) { 35 SyncThreadClass syncThread1 = new SyncThreadClass(); 36 SyncThreadClass syncThread2 = new SyncThreadClass(); 37 Thread thread1 = new Thread(syncThread1, "SyncThread1"); 38 Thread thread2 = new Thread(syncThread2, "SyncThread2"); 39 thread1.start(); 40 thread2.start(); 41 } 42 }
SyncThread1:0 SyncThread1:1 SyncThread1:2 SyncThread1:3 SyncThread1:4 SyncThread2:5 SyncThread2:6 SyncThread2:7 SyncThread2:8 SyncThread2:9
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏