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. axios详解以及完整封装方法

    """ 一.axios是什么 Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中. 它是 isomorphic 的(即同一套代码可以运行 ...

  2. 包管理工具npm和Yarn的区别,我们该如何选择?

    好家伙,学习新工具    1.为什么我们需要包管理器? 关于npm我们已经知道了,这是我们项目的包管理器, 我们现在用的无比顺手的工具,都是在无数的竞争中杀出来的,他们淘汰了无数的产品   首先,倘若 ...

  3. Linux 函数: my_func

    # A man and his 'fuctions' ;) # quick use ipmitool cmd to do something ipmi-ip-cmd () { local ip=$1 ...

  4. ASP.NET WebForm中asp:Repeater和UI:Grid数据为空时如何显示表头?

    一.asp:Repeater Repeater 控件用于显示被绑定在该控件上的项目的重复列表.Repeater 控件可被绑定到数据库表.XML 文件或者其他项目列表. 1.1-前台页面代码 <a ...

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

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

  6. VScode 中golang 基准测试 go test -bench .

    目的:基准测试的主要目的是比较不同实现方式之间的性能差异,找出性能瓶颈. 1 准备以_test.go结尾文件和导入testing包 在命名文件时需要让文件必须以_test结尾,在文件中导入testin ...

  7. 程序后台运行方法:使用守护进程 或 screen软件

    我们常需要SSH远程登录到Linux 服务器,经常运行一些需要很长时间才能完成的任务,在此期间不能关掉窗口或者断开连接,否则这个任务会被杀掉,一切就半途而废了. 可以使用以下两个方法: 方法一:noh ...

  8. Effective C++ 笔记(二)

    16.保证异常安全 1 void PrettyMenu::changBackground(std::istream &imgSrc) 2 { 3 lock(&mutex); 4 del ...

  9. 《Kali渗透基础》05. 主动信息收集(二)

    @ 目录 1:端口扫描 2:UDP 扫描 2.1:Scapy 2.2:nmap 3:半开放扫描 3.1:Scapy 3.2:nmap 3.3:hping3 4:全连接扫描 4.1:Scapy 4.2: ...

  10. 原来你是这样的JAVA[06]-反射

    1.JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息: 获取一个class对应的Class实例后,就可以获取该class的所有信 ...