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
JVM-Java虚拟机是怎么实现synchronized的?的更多相关文章
- 从头捋捋jvm(-java虚拟机)
jvm 是Java Virtual Machine(Java虚拟机)的缩写,java 虚拟机作为一种跨平台的软件是作用于操作系统之上的,那么认识并了解它的底层运行逻辑对于java开发人员来说很有必要! ...
- jvm java虚拟机 新生代的配置
1.1.1.1. -Xmn参数 参数-Xmn1m可以用于设置新生代的大小.设置一个较大的新生代会影响老生代的大小,因为这两者的总和是一定的,这个系统参数对于系统性能以及GC行为有很大的影响,新生代一般 ...
- (转)JVM——Java虚拟机架构
背景:最近开始忙着找工作了,把需要储备的知识再整理总结一遍!关于JVM的总结,是转自下面的连接.结合<深入理解java虚拟机>,看起来有更清晰的认识. 转载自:http://blog.cs ...
- JVM——Java虚拟机架构
0. 前言 Java虚拟机(Java virtualmachine)实现了Java语言最重要的特征:即平台无关性. 平台无关性原理:编译后的 Java程序(.class文件)由 JVM执行.JVM屏蔽 ...
- JVM,Java虚拟机基础知识新手入门教程(超级通熟易懂)
作者:请叫我红领巾,转载请注明出处http://www.cnblogs.com/xxzhuang/p/7453746.html,简书地址:http://www.jianshu.com/p/b963b3 ...
- 深入了解JVM(Java虚拟机)
虚拟机 JRE由Java API和JVM组成,JVM通过类加载器(Class Loader)加类Java应用,并通过Java API进行执行. 虚拟机(VM: Virtual Machine)是通过软 ...
- 深入学习重点分析java基础---第一章:深入理解jvm(java虚拟机) 第一节 java内存模型及gc策略
身为一个java程序员如果只会使用而不知原理称其为初级java程序员,知晓原理而升中级.融会贯通则为高级 作为有一个有技术追求的人,应当利用业余时间及零碎时间了解原理 近期在看深入理解java虚拟机 ...
- JVM - Java虚拟机规范官方文档
Java虚拟机规范官方文档
- 5.1.3.jvm java虚拟机系统参数查看
不同的参数配置对系统的执行效果有较大的影响,因此,我们有必要了解系统实际的运行参数. 1.1.1.1. -XX:+PrintVMOptions 参数-XX:+PrintVMOptions可以在程序运行 ...
- 深入解析java虚拟机-jvm运行机制
转自oschina 一:JVM基础概念 JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现.编译虚拟机的指令集与编译微处理器的指令集非常类似.Java虚拟机包括一套字 ...
随机推荐
- axios详解以及完整封装方法
""" 一.axios是什么 Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中. 它是 isomorphic 的(即同一套代码可以运行 ...
- 包管理工具npm和Yarn的区别,我们该如何选择?
好家伙,学习新工具 1.为什么我们需要包管理器? 关于npm我们已经知道了,这是我们项目的包管理器, 我们现在用的无比顺手的工具,都是在无数的竞争中杀出来的,他们淘汰了无数的产品 首先,倘若 ...
- Linux 函数: my_func
# A man and his 'fuctions' ;) # quick use ipmitool cmd to do something ipmi-ip-cmd () { local ip=$1 ...
- ASP.NET WebForm中asp:Repeater和UI:Grid数据为空时如何显示表头?
一.asp:Repeater Repeater 控件用于显示被绑定在该控件上的项目的重复列表.Repeater 控件可被绑定到数据库表.XML 文件或者其他项目列表. 1.1-前台页面代码 <a ...
- 渗透-02:HTTPS主干-分支和HTTPS传输过程
一.HTTPS主干-分支 第一层 第一层,是主干的主干,加密通信就是双方都持有一个对称加密的秘钥,然后就可以安全通信了. 问题就是,无论这个最初的秘钥是由客户端传给服务端,还是服务端传给客户端,都是明 ...
- VScode 中golang 基准测试 go test -bench .
目的:基准测试的主要目的是比较不同实现方式之间的性能差异,找出性能瓶颈. 1 准备以_test.go结尾文件和导入testing包 在命名文件时需要让文件必须以_test结尾,在文件中导入testin ...
- 程序后台运行方法:使用守护进程 或 screen软件
我们常需要SSH远程登录到Linux 服务器,经常运行一些需要很长时间才能完成的任务,在此期间不能关掉窗口或者断开连接,否则这个任务会被杀掉,一切就半途而废了. 可以使用以下两个方法: 方法一:noh ...
- Effective C++ 笔记(二)
16.保证异常安全 1 void PrettyMenu::changBackground(std::istream &imgSrc) 2 { 3 lock(&mutex); 4 del ...
- 《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: ...
- 原来你是这样的JAVA[06]-反射
1.JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息: 获取一个class对应的Class实例后,就可以获取该class的所有信 ...