• 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
  • 相关阅读:
    Callable Future 和 FutureTask
    多线程常用工具类
    Servlet的forward与include方法
    Spring MVC 执行流程分析
    使用SpringEL表达式进行三目运算
    推荐10款Java程序员使用的单元测试工具
    使用SpringEL表达式进行方法调用
    使用SpringEL操作List和Map集合
    SpringEL表达式(一)-入门案例
    Servlet的生命周期
  • 原文地址:https://www.cnblogs.com/wxdlut/p/14187856.html
Copyright © 2020-2023  润新知