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

JVM-Java虚拟机是怎么实现synchronized的?的更多相关文章

  1. 从头捋捋jvm(-java虚拟机)

    jvm 是Java Virtual Machine(Java虚拟机)的缩写,java 虚拟机作为一种跨平台的软件是作用于操作系统之上的,那么认识并了解它的底层运行逻辑对于java开发人员来说很有必要! ...

  2. jvm java虚拟机 新生代的配置

    1.1.1.1. -Xmn参数 参数-Xmn1m可以用于设置新生代的大小.设置一个较大的新生代会影响老生代的大小,因为这两者的总和是一定的,这个系统参数对于系统性能以及GC行为有很大的影响,新生代一般 ...

  3. (转)JVM——Java虚拟机架构

    背景:最近开始忙着找工作了,把需要储备的知识再整理总结一遍!关于JVM的总结,是转自下面的连接.结合<深入理解java虚拟机>,看起来有更清晰的认识. 转载自:http://blog.cs ...

  4. JVM——Java虚拟机架构

    0. 前言 Java虚拟机(Java virtualmachine)实现了Java语言最重要的特征:即平台无关性. 平台无关性原理:编译后的 Java程序(.class文件)由 JVM执行.JVM屏蔽 ...

  5. JVM,Java虚拟机基础知识新手入门教程(超级通熟易懂)

    作者:请叫我红领巾,转载请注明出处http://www.cnblogs.com/xxzhuang/p/7453746.html,简书地址:http://www.jianshu.com/p/b963b3 ...

  6. 深入了解JVM(Java虚拟机)

    虚拟机 JRE由Java API和JVM组成,JVM通过类加载器(Class Loader)加类Java应用,并通过Java API进行执行. 虚拟机(VM: Virtual Machine)是通过软 ...

  7. 深入学习重点分析java基础---第一章:深入理解jvm(java虚拟机) 第一节 java内存模型及gc策略

    身为一个java程序员如果只会使用而不知原理称其为初级java程序员,知晓原理而升中级.融会贯通则为高级 作为有一个有技术追求的人,应当利用业余时间及零碎时间了解原理 近期在看深入理解java虚拟机 ...

  8. JVM - Java虚拟机规范官方文档

    Java虚拟机规范官方文档    

  9. 5.1.3.jvm java虚拟机系统参数查看

    不同的参数配置对系统的执行效果有较大的影响,因此,我们有必要了解系统实际的运行参数. 1.1.1.1. -XX:+PrintVMOptions 参数-XX:+PrintVMOptions可以在程序运行 ...

  10. 深入解析java虚拟机-jvm运行机制

    转自oschina 一:JVM基础概念 JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现.编译虚拟机的指令集与编译微处理器的指令集非常类似.Java虚拟机包括一套字 ...

随机推荐

  1. 使用官方推荐的库来测react hook组件

    最近写单元测试的时候遇见了一些问题,当我使用使用jest测React. useRef,  React. useEffect时,总是测不到, 然后我去查阅了一下官方文档,它推荐了使用下面这个库 @tes ...

  2. 刷了一个月AI歌唱的视频 做一个大胆预测

    现在的AI热点转到ChatAI和AI唱歌去了 很好理解(现在每天在看Neuro的切片 感慨这才是看V的初心 可惜Neuro这个形象在创立的时候只是一个ChatAI 和游戏用的GameBOT并不是同一个 ...

  3. 最全面的JAVA多线程知识总结

    ​ 背景: 2023年经营惨淡,经历了裁员就业跳槽再就业,在找工作过程中对于知识的梳理和总结,本文总结JAVA多线程. 应用场景: 需要同时执行多个任务或处理大量并发请求时, 目前常用的场景有: We ...

  4. 使用JMeter连接达梦数据库的步骤和示例

    引言: 本文将介绍如何使用JMeter连接达梦数据库,并提供连接达梦数据库的步骤和示例,帮助您快速开始进行数据库性能测试. 步骤: 1. 下载并安装JMeter:首先,从JMeter官方网站下载并安装 ...

  5. 渗透-02:HTTPS主干-分支和HTTPS传输过程

    一.HTTPS主干-分支 第一层 第一层,是主干的主干,加密通信就是双方都持有一个对称加密的秘钥,然后就可以安全通信了. 问题就是,无论这个最初的秘钥是由客户端传给服务端,还是服务端传给客户端,都是明 ...

  6. Python类型提示

    摘自:Python 类型提示简介 - FastAPI (tiangolo.com) 快速入门 类型提示用于声明一个变量的类型,在Python 3.6+版本的时候引入. 示例: def get_full ...

  7. composer 的使用和常用命令大全

    composer 常用命令 1.composer初始化 init 如何手动创建 composer.json 文件.实际上还有一个 init 命令可以更容易的做到这一点. 查看当前版本composer ...

  8. JNDI注入的本地搭建和分析

    JNDI注入的本地搭建和分析   JNDI概述 JNDI(The java Naming and Directory Interface,java命名和目录接口)是一组在Java应用中访问命名和目录服 ...

  9. mac安装mysql8.0

    1.进入下载页 历史版本:https://downloads.mysql.com/archives/community/ 最新版本:https://dev.mysql.com/downloads/my ...

  10. from my mac

    hello