JVM-Java虚拟机是怎么实现synchronized的?

1. JVM的锁优化

  今天我介绍了 Java 虚拟机中 synchronized 关键字的实现,按照代价由高至低可分为重量级锁、轻量级锁和偏向锁三种。

  重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。

  轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。

  偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。

  

java偏向锁,轻量级锁与重量级锁为什么会相互膨胀? 

首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:

  • 偏向锁:只有一个线程进入临界区;
  • 轻量级锁:多个线程交替进入临界区
  • 重量级锁:多个线程同时进入临界区。

还要明确的是,偏向锁、轻量级锁都是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 重量级锁

  重量级锁是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程
  Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合 posix 接口的操作系统(如 macOS 和绝大部分的 Linux),上述操作是通过 pthread 的互斥锁(mutex)来实现的。此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。为了尽量避免昂贵的线程阻塞、唤醒操作,Java 虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。与线程阻塞相比,自旋状态可能会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。
 
  我们可以用等红绿灯作为例子。Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如说我们在 synchronized 代码块里只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更加合适。
  然而,对于 Java 虚拟机来说,它并不能看到红灯的剩余时间,也就没办法根据等待时间的长短来选择自旋还是阻塞。Java 虚拟机给出的方案是自适应自旋,根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)。
  就我们的例子来说,如果之前不熄火等到了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等到绿灯,那么这次不熄火的时间就短一点。
  自旋状态还带来另外一个副作用,那便是不公平的锁机制。处于阻塞状态的线程,并没有办法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。

1.2 轻量级锁

   你可能见到过深夜的十字路口,四个方向都闪黄灯的情况。由于深夜十字路口的车辆来往可能比较少,如果还设置红绿灯交替,那么很有可能出现四个方向仅有一辆车在等红灯的情况。

  因此,红绿灯可能被设置为闪黄灯的情况,代表车辆可以自由通过,但是司机需要注意观察(个人理解,实际意义请咨询交警部门)。

  Java 虚拟机也存在着类似的情形:多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。

1.1 偏向锁

  如果说轻量级锁针对的情况很乐观,那么接下来的偏向锁针对的情况则更加乐观:从始至终只有一个线程请求某一把锁。

  这就好比你在私家庄园里装了个红绿灯,并且庄园里只有你在开车。偏向锁的做法便是在红绿灯处识别来车的车牌号。如果匹配到你的车牌号,那么直接亮绿灯。

  具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中。

2. synchronized知识补充

A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

 Java中Synchronized的用法

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
原文地址:https://www.cnblogs.com/wxdlut/p/14187856.html