腾讯看点

沙海 2021年4月9日11:56:09杂谈 Java评论62字数 27744阅读92分28秒阅读模式
摘要

速读摘要

速读摘要文章源自JAVA秀-https://www.javaxiu.com/10415.html

时间片是CPU分配给各个线程的时间,因为时间片一般是几十毫秒,所以CPU需要通过不停地切换线程来执行。假设在同一个同步代码块上自旋10次就能获得锁,那么虚拟机就会认为这次也能够获得锁,还允许自旋的时间稍微长一点。JVM会探测一连串细小的操作都是用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。看上面代码,StringBuffer的append的方法里面是有加同步关键字的,然而我们在外面循环了100次,就要进入锁和退出锁各100次,所以这个时候JVM就会把锁粗化。文章源自JAVA秀-https://www.javaxiu.com/10415.html

原文约 1.4 万 | 图片 1 | 建议阅读 28 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/10415.html

下载文章源自JAVA秀-https://www.javaxiu.com/10415.html

腾讯看点文章源自JAVA秀-https://www.javaxiu.com/10415.html

作者 | IT贱男文章源自JAVA秀-https://www.javaxiu.com/10415.html

责编 | 夕颜文章源自JAVA秀-https://www.javaxiu.com/10415.html

出品 | CSDN(ID:CSDNnews)文章源自JAVA秀-https://www.javaxiu.com/10415.html

引言文章源自JAVA秀-https://www.javaxiu.com/10415.html

这篇文章码了小编***个小时,点个赞不过分吧~~文章源自JAVA秀-https://www.javaxiu.com/10415.html

文本内容有点多,如果有写错或者不好地方,还请多多指教~~~~~~~文章源自JAVA秀-https://www.javaxiu.com/10415.html

Table of Contents文章源自JAVA秀-https://www.javaxiu.com/10415.html

一、引言文章源自JAVA秀-https://www.javaxiu.com/10415.html

二、倔强青铜文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.1 多线程一定快吗?文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.2 上下文切换文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.3 测试上下文切换次数文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.4 Java内存模型文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.5 主内存与工作内存之间的数据交互过程文章源自JAVA秀-https://www.javaxiu.com/10415.html

三、秩序白银文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.1 多线程带来的可见性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.2 多线程带来的原子性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.3 多线程带来的有序性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

四、荣耀黄金文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.1 sync可重入特性文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.2 sync不可中断特性文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.3 反汇编学习sync原理文章源自JAVA秀-https://www.javaxiu.com/10415.html

五、尊贵铂金文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.1 montior 监视器锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.2 monitor 竞争文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.3. monitor 等待文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.4 monitor 释放文章源自JAVA秀-https://www.javaxiu.com/10415.html

六、永恒钻石文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.1 CAS 介绍文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.2 sync 锁升级过程文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.3 对象的布局文章源自JAVA秀-https://www.javaxiu.com/10415.html

七、至尊星耀文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.1 偏向锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.2 轻量级锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.3 自旋锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.4 消除锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.5 锁粗化文章源自JAVA秀-https://www.javaxiu.com/10415.html

八、最强王者文章源自JAVA秀-https://www.javaxiu.com/10415.html

终章:平时写代码如何对synchroized优化文章源自JAVA秀-https://www.javaxiu.com/10415.html

倔强青铜文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.1 多线程一定快吗?文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们先来看下面一段代码,有两个方法对各自a、b属性进行累加操作,其中concurrency方法是采用多线程进行操作,结果如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

/** * @Auther: IT贱男 * @Date: 2020/3/9 10:37 * @Description: */public class ConcurrencyTest {文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 累加次数 private static final long count = 10000L;文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) throws InterruptedException { concurrency(); serial(); }文章源自JAVA秀-https://www.javaxiu.com/10415.html

/** * 多线程累加 * * @throws InterruptedException */ private static void concurrency() throws InterruptedException { long start = System.currentTimeMillis();文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 启动新线程执行运行操作 Thread thread = new Thread(new Runnable() { @Override public void run() { int a = 0; for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

int b = 0; for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

/** * 单线程累加 */ private static void serial() { long start = System.currentTimeMillis(); int a = 0; for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

那这边的答案是"不一定"的,小编测试了几组数据如下(抽取部分结果):文章源自JAVA秀-https://www.javaxiu.com/10415.html

多线程与单线程效率测试文章源自JAVA秀-https://www.javaxiu.com/10415.html

由以上的结果可以明确我们的答案是正确的,那为什么多线程在某些情况下会比单线程还要慢呢?这是因为多线程有创建和上下文切换的开销。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.2 上下文切换文章源自JAVA秀-https://www.javaxiu.com/10415.html

那什么是上下文切换呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

目前来说即使是单核处理器也支持多线程执行代码,CPU通过给个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片一般是几十毫秒,所以CPU需要通过不停地切换线程来执行。假设当我们线程A获得CPU分配的时间片等于10毫秒,执行10毫秒之后,CPU需要切换到线程B去执行程序。等线程B的时间片执行完事了,又切回线程A继续执行。文章源自JAVA秀-https://www.javaxiu.com/10415.html

显然易见,我们CPU相当于是循环的切换上下文,来达到同时执行的效果。当前执行完一个时间片后会切换下一个任务。但是在切换前会保存当前任务的状态,方便下次切换会这个任务的时候,可以恢复这个任务之前的状态。所以任务从保存到再次被加载的过程就是一次上下文切换。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.3 测试上下文切换次数文章源自JAVA秀-https://www.javaxiu.com/10415.html

这里我们需要使用一个命令叫做:"vmstat 1",这个命令是linux系统上的,可对操作系统的进程、虚拟内存、CPU活动进行监控。看下图CS(Content Switch) 表示上下文切换的次数,从图可见系统一般CS的值维持在600~800之间,当我们一直在运行ConcurrencyTest程序时,很明细发现CS飙升到1000以上。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.4 Java内存模型文章源自JAVA秀-https://www.javaxiu.com/10415.html

在我们学习sync原理之前,我们需要搞清楚Java内存模型的一个概念知识。很重要、很重要、很重要文章源自JAVA秀-https://www.javaxiu.com/10415.html

Java内存模型全称:Java Memory Model ,简称Java内存模型或者JMM,Java线程之间的通信由JMM来控制,JMM决定一个线程对共享变量的写入,何时对另外一个线程可见。我们由图可见,线程之间的共享变量是存储在主内存当中,每一个线程都有一个属于自己的本地内存(也可以叫做工作内存),这个本地内存中存储了主内存当中的共享变量。就相当于把主内存的共享变量copy了一份给自己。为了提供效率,线程是不会直接与主内存进行打交道,而是通过本地内存来进行数据的读取。文章源自JAVA秀-https://www.javaxiu.com/10415.html

如果线程A与线程B之间要通信,需要经历下面两个步骤:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1 )线程A把本地内存A中更新过的共享变量,刷新到主内存当中去。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2 )线程B到主内存中重新读取更新后的共享变量。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2.5 主内存与工作内存之间的数据交互过程文章源自JAVA秀-https://www.javaxiu.com/10415.html

那么主内存与工作内存之间的交互经过了哪些步骤呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。文章源自JAVA秀-https://www.javaxiu.com/10415.html

unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放之后的变量才可以被其他线程锁定。文章源自JAVA秀-https://www.javaxiu.com/10415.html

read(读取):作用于主内存的变量,读取主内存变量的值。文章源自JAVA秀-https://www.javaxiu.com/10415.html

load(载入):作用于主内存的变量,把read操作从主内存中得到的变量值放入到线程本地内存的变量副本中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

use(使用):作用于工作内存的变量,把工作内存中的一个变量传递给执行引擎。文章源自JAVA秀-https://www.javaxiu.com/10415.html

assign(赋值):作用域工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量。文章源自JAVA秀-https://www.javaxiu.com/10415.html

store(存储):作用域工作内存的变量,把工作内存中的一个变量值传输到主内存中,以便随后的write操作。文章源自JAVA秀-https://www.javaxiu.com/10415.html

write(写入):作用域工作内存的变量,把stroe操作从工作内存中一个变量的值传送到主内存的变量中去。文章源自JAVA秀-https://www.javaxiu.com/10415.html

上个笔记图:更加详细的解释如上几个步骤文章源自JAVA秀-https://www.javaxiu.com/10415.html

JMM是一种规范,其中定义几条规则,小编挑选出相对本文比较重要的:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1、如果想要把一个变量从主内存复制到工作内存,就需要按照顺序执行read和load操作,如果把变量从工作内存同步到主内存中,就要按照顺序执行store和write操作。但Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2、程序中如果有同步操作才会有lock和unlock操作,一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,执行多次后,必须执行相对应次数但unlock操作,变量才会被解锁。lock和unlock必须成对出现。文章源自JAVA秀-https://www.javaxiu.com/10415.html

3、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或者assign操作初始化变量但值。文章源自JAVA秀-https://www.javaxiu.com/10415.html

4、java内存模型同步规则小编暂时提到这么多,感兴趣的小伙伴可以自行去了解一下文章源自JAVA秀-https://www.javaxiu.com/10415.html

秩序白银文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.1 多线程带来的可见性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是可见性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

所谓可见性:一个线程对主内存的修改可以及时被其他线程观察到。文章源自JAVA秀-https://www.javaxiu.com/10415.html

当一个共享属性,被线程二修改了,但是线程一无法获得最新的值,导致死循环。原因Java内存模型也说清楚了,线程是和本地内存做交互的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

1、线程一把falg属性读取到线程私有的本地内存中,值为true。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2、线程二把falg属性修改为false,并且刷新到主内存当中,但是线程一它是不知道falg被修改了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample5 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

static boolean falg = true;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 锁对象 static Object lock = new Object();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) throws InterruptedException {文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 线程一 new Thread(new Runnable() { @Override public void run() { while (falg) { // 默认不可见,死循环,放开以下注释即可解决不可见操作文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 方式一,加上sycn操作即可解决可见性问题 // synchronized (lock){}文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 方式二, println 方法实现加上了同步机制,保证每次输出都是最新值 // System.out.println(falg); } } }).start();文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 睡眠两秒 Thread.sleep(2000L);文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 线程二 new Thread(new Runnable() { @Override public void run() { falg = false; System.out.println("falg 值已修改"); } }).start(); }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

sync怎么解决可见性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

这个就涉及到本地内存与工作内存交互的步骤了,还记得文本上面有讲的8个步骤吗?文章源自JAVA秀-https://www.javaxiu.com/10415.html

如果程序中有加同步的机制,则会有Lock、Unlock操作,Lock操作会使本地内存中的属性失效,从而去主内存中重新读取数据。文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.2 多线程带来的原子性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是原子性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

所谓原子性:提供了互斥访问,同一个时刻只能有一个线程来对它进行操作。文章源自JAVA秀-https://www.javaxiu.com/10415.html

这里一次任务累加1千次,同时启动5个线程进行累加,最后的结果正常应该是5000才对,但由于多线程会造成不一样的结果。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample6 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

static int index = 0;文章源自JAVA秀-https://www.javaxiu.com/10415.html

static Object lock = new Object();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) throws InterruptedException {文章源自JAVA秀-https://www.javaxiu.com/10415.html

// index 累加 1000次,使用lambda表达式 Runnable task = () -> { // 不加sync则不能保证原子操作 // synchronized (lock) { for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 启动五个线程来执行任务 for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 为了代码直观直接睡眠等待结果,实际需要调用线程的join方法等待线程结束 Thread.sleep(2000L); System.out.println("index = " + index); }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们使用java命令来编译以上代码:文章源自JAVA秀-https://www.javaxiu.com/10415.html

javac SyncExample6.java文章源自JAVA秀-https://www.javaxiu.com/10415.html

javap -p -v SyncExample6.class ,这样我们就能看到sync到底在底层做了什么事。文章源自JAVA秀-https://www.javaxiu.com/10415.html

编译代码之后找到“lambda$main$0”,因为我们同步机制是写在main方法中,用lambda表达式所写。文章源自JAVA秀-https://www.javaxiu.com/10415.html

private static void lambda$main$0(); descriptor: ()V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=3, args_size=0 0: iconst_0 1: istore_0 2: iload_0 3: sipush 1000 6: if_icmpge 39 9: getstatic #18 // Field lock:Ljava/lang/Object; 12: dup 13: astore_1 14: monitorenter 15: getstatic #14 // Field index:I 18: iconst_1 19: iadd 20: putstatic #14 // Field index:I 23: aload_1 24: monitorexit 25: goto 33 28: astore_2文章源自JAVA秀-https://www.javaxiu.com/10415.html

造成原子性的问题的原因是什么?文章源自JAVA秀-https://www.javaxiu.com/10415.html

这个就涉及到文章一开始所讲的上下文切换的知识点,index ++ 一共涉及到4条指令,如下文章源自JAVA秀-https://www.javaxiu.com/10415.html

15: getstatic #14 // 步骤一:获取index值18: iconst_1 // 步骤二:准备常量119: iadd // 步骤三:相加操作20: putstatic #14 // 步骤四:重新赋值文章源自JAVA秀-https://www.javaxiu.com/10415.html

以上这4条指令就是index ++ 的四个步骤,假设我们线程一进来,执行到步骤三,这个时候CPU切换线程。切换到线程二,线程二执行步骤一,这个时候index的值还是等于0,因为线程一并没有执行步骤四就被切换上下文了。等线程二执行完成,又切回到线程一,线程一会接着执行步骤三,并不会重新获取index的值,这就导致计算结果不正确了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

sync怎么解决原子性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

14: monitorenter 15: getstatic #14 // Field index:I 18: iconst_1 19: iadd 20: putstatic #14 // Field index:I 23: aload_1 24: monitorexit文章源自JAVA秀-https://www.javaxiu.com/10415.html

当我们加上了sync同步机制之后, 会插入monitorenter、monitorexit两条指令。文章源自JAVA秀-https://www.javaxiu.com/10415.html

又到了假设环节:假设线程一执行到步骤三,被切换到线程二,当我们线程二执行monitorenter这个指令会发现,这个对象已经被其他线程占用了,所以就只能等待着不会进行操作。现在又切回到线程一,线程一操作完整个步骤执行monitorexit来释放锁。这个时候线程二才可以获得锁。这样一操作就能保证同一个时刻只能有一个线程来对它进行操作,从而保证原子性。文章源自JAVA秀-https://www.javaxiu.com/10415.html

monitorenter指令是在编译后插入到同步代码块到开始位置,而monitorexit是插入到同步代码块结束位置和异常位置。JVM需要保障每个monitorenter必须有对应的monitorexit。任何一个对象都会有一个monitor来关联,当且一个monitor被持有后,它就处理锁定状态。当线程执行到monitorenter指令的时候,将会尝试获取对象所对应的monitor的所有权,即尝试获取锁对象。文章源自JAVA秀-https://www.javaxiu.com/10415.html

3.3 多线程带来的有序性问题文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是有序性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

有序性,指的是程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 指定使用并发测试@JCStressTest // 预测的结果与类型,附加描述信息,如果1,4 则是ok,如果结果有为0也能勉强接受@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") @Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")// 标注需要测试的类@Statepublic class TestJMM {文章源自JAVA秀-https://www.javaxiu.com/10415.html

int num = 0; boolean ready = false;文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Actor public void actor1(I_Result r) { if (ready) { r.r1 = num + num; } else { r.r1 = 1; } }文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Actor public void actor2(I_Result r) { num = 2; ready = true; }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

是时候贴一段代码凑文章字数了,这里代码用了Jcstress高并发测试框架,目的是为了能够演示有序性所导致到问题。文章源自JAVA秀-https://www.javaxiu.com/10415.html

小伙伴可以先仔细看以上代码,假设actor1、actor2 各有一个线程进来,想想 r.r1 的值会产生几种情况。文章源自JAVA秀-https://www.javaxiu.com/10415.html

小编告诉你吧,其实答案有三种,分别是:1、4、0文章源自JAVA秀-https://www.javaxiu.com/10415.html

出现1的情况:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)假设 actor1先获得执行权,ready = false ,则 r.r1 = 1;文章源自JAVA秀-https://www.javaxiu.com/10415.html

2)假设 actor2先获得执行权,执行到num = 2, 线程切换到actor1,ready还是为false,r.r1 = 1;文章源自JAVA秀-https://www.javaxiu.com/10415.html

出现4的情况:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)假设actor2先获得执行权,执行完,此时ready = true,num = 2 ,等到在执行actor1时,结果为4;文章源自JAVA秀-https://www.javaxiu.com/10415.html

出现0的情况:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)这里就是重点了,假设actor2获得执行权,由于指令重排序导致actor2代码顺序更换。文章源自JAVA秀-https://www.javaxiu.com/10415.html

这个时候执行到ready = true,线程切换到actor1,这个时候ready已经等于true了,但是num还是0,所以就出现了0的情况。文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Actor public void actor2(I_Result r) { // 由于指令重排序,导致下面代码更换了顺序,如下: ready = true; num = 2; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们用压测来执行以下代码吧,使用maven 执行 clean install,会生成一个jar包,直接用命令启动jar包就行了,Jcstress使用方式小编就不多说了,感兴趣的小伙伴可以自行学习下, 执行的结果也符合我们预期的值。文章源自JAVA秀-https://www.javaxiu.com/10415.html

sync怎么解决有序性问题呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

这个时候只需要在actor1和actor2分别加上锁操作,由于它们的锁对象都是同一个,哪怕由于指令重排序执行到actor2的ready = true,这个时候线程切换到actor1,但是有加锁所以actor1也只能等着。等到actor2 把 num = 2 执行完,actor1 才可以拿到锁对象。文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 指定使用并发测试@JCStressTest// 预测的结果与类型,附加描述信息@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "ok")// 因为sync解决有序性问题,不会有0的出现,为了方便观察结果,我们把4设置成能勉强接受的值@Outcome(id = {"4"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")// 标注需要测试的类@Statepublic class TestJMM {文章源自JAVA秀-https://www.javaxiu.com/10415.html

int num = 0; boolean ready = false;文章源自JAVA秀-https://www.javaxiu.com/10415.html

Object lock = new Object();文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Actor public void actor1(I_Result r) { synchronized (lock) { if (ready) { r.r1 = num + num; } else { r.r1 = 1; } } }文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Actor public void actor2(I_Result r) { synchronized (lock) { num = 2; ready = true; } }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

测试结果如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

荣耀黄金文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.1 sync可重入特性文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是可重入呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

即一个线程可以多次执行synchronzied重复获取同一把锁。sync底层锁对象中包含了一个计数器(recursions 变量),会记录线程获得了几次锁。当我们同一个线程获得了锁,计数器则会+1,执行完同步代码块,计数器-1。直到计数器的数量为0,就释放这个锁对象。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample8 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { new MyThread().start(); }文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

class MyThread extends Thread { @Override public void run() { synchronized (MyThread.class) { System.out.println(getName() + "进入了同步代码块1"); synchronized (MyThread.class) { System.out.println(getName() + "进入了同步代码块2"); } } }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

运行结果如下,我们可以很明细的看出在输出“同步代码块1”之后,不需要等待锁释放,即可进入第二个同步代码块。这样的一个特性可以避免死锁的发生,也可以更好的封装代码(即:同步代码块中的代码,可以分成多个方法来写)。文章源自JAVA秀-https://www.javaxiu.com/10415.html

输入结果如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

Thread-0进入了同步代码块1文章源自JAVA秀-https://www.javaxiu.com/10415.html

Thread-0进入了同步代码块2文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.2 sync不可中断特性文章源自JAVA秀-https://www.javaxiu.com/10415.html

不可中断只指,线程二在等待线程一释放锁的时候,是不可被中断的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

当一个线程获得锁之后,另外一个线程一直处于堵塞或者等待状态,前一个线程不释放锁,后一个线程会一直被阻塞或等待,所以sync是不可中断锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample9 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

private static Object lock = new Object();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) throws InterruptedException { Runnable run = () -> { synchronized (lock) { String name = Thread.currentThread().getName(); System.out.println(name + "进入同步代码块"); try { // 让线程一持有锁 Thread.sleep(888888L); } catch (InterruptedException e) { e.printStackTrace(); } } };文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 创建线程一先执行同步代码快 Thread t1 = new Thread(run); t1.start();文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 主线程睡眠一下,保证上面线程先执行 Thread.sleep(1000L);文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 后开启线程取执 Thread t2 = new Thread(run); t2.start();文章源自JAVA秀-https://www.javaxiu.com/10415.html

System.out.println("开始中断线程二"); // 强行线程二中断 t2.interrupt(); System.out.println("线程一状态" + t1.getState()); System.out.println("线程二状态" + t2.getState());文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

当我们线程一进入同步代码之后,一直持有锁,并且睡眠了(也证实了sleep方法睡眠不会释放锁对象)。文章源自JAVA秀-https://www.javaxiu.com/10415.html

此时线程二启动去尝试获取锁,获取失败之后就变成堵塞状态,哪怕我们强行中断线程二,最后看到线程二的状态仍是堵塞的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

Thread-0进入同步代码块文章源自JAVA秀-https://www.javaxiu.com/10415.html

开始中断线程二文章源自JAVA秀-https://www.javaxiu.com/10415.html

线程一状态TIMED_WAITING文章源自JAVA秀-https://www.javaxiu.com/10415.html

线程二状态BLOCKED文章源自JAVA秀-https://www.javaxiu.com/10415.html

4.3 反汇编学习sync原理文章源自JAVA秀-https://www.javaxiu.com/10415.html

使用javap反汇编java代码,引入monitor概念。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample10 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

private static Object lock = new Object();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) throws InterruptedException { synchronized (lock) { System.out.println("1"); } }文章源自JAVA秀-https://www.javaxiu.com/10415.html

public synchronized void test() { System.out.println("1"); }文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们使用javac、javap 两个命令对SyncExample10来进行编译文章源自JAVA秀-https://www.javaxiu.com/10415.html

javac SyncExample10.java javap -v -p SyncExample10.class文章源自JAVA秀-https://www.javaxiu.com/10415.html

编译后的指令就如下啦,我们主要看main方法里面的内容,着重看 monitorenter、monitorexit 两个指令文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(java.lang.String[]) throws java.lang.InterruptedException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 3: dup 4: astore_1 5: monitorenter // 这里 6: getstatic #3 9: ldc #4 11: invokevirtual #5 14: aload_1 15: monitorexit // 这里 16: goto 24 19: astore_2 20: aload_1 21: monitorexit // 这里 22: aload_2 23: athrow 24: return文章源自JAVA秀-https://www.javaxiu.com/10415.html

monitorenter 指令文章源自JAVA秀-https://www.javaxiu.com/10415.html

当我们进入同步代码块的时候会先执行monitorenter指令,每一个对象都会和一个monitor监视器关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。当其他线程执行monitorente指令时,它会尝试去获取当前对象对应的monitor的所有权。文章源自JAVA秀-https://www.javaxiu.com/10415.html

monitor里面有两个很重要成员变量:文章源自JAVA秀-https://www.javaxiu.com/10415.html

owner:当一个线程获取到该对象的锁,就把线程当前赋值给owner。文章源自JAVA秀-https://www.javaxiu.com/10415.html

recursions:会记录线程拥有锁的次数,重复获取锁当前变量也会+1,当一个线程拥有monitor后,其他线程只能等待。文章源自JAVA秀-https://www.javaxiu.com/10415.html

monitorenter执行流程如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)若monitor的进入次数为0时,线程可以进入monitor,并将monitor进入的次数(recursions)+1,当前线程成为montiro的owner(所有者);文章源自JAVA秀-https://www.javaxiu.com/10415.html

2)若线程已拥有monitor的所有权,允许它重入monitor,进入一次次数+1 (可重复特性);文章源自JAVA秀-https://www.javaxiu.com/10415.html

3)若其他线程已经占有monitor,那么当前尝试获取monitor的线程会被阻塞,一直到monitor进入次数为变0,才能重新被再次获取。文章源自JAVA秀-https://www.javaxiu.com/10415.html

要注意,能够执行monitorexit指令的线程,一定是拥有当前对象的monitor所有权的线程。当我们执行monitorexit指令计数器减到为0时,当前线程就不再拥有monitor所有权。其他被阻塞的线程即可再一次去尝试获取这个monitor的所有权。文章源自JAVA秀-https://www.javaxiu.com/10415.html

大家仔细看看上面编译出来的指令,其实monitoreexit是有两个的,为什么呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

因为需要保证如果同步代码块执行抛出了异常,则也需要释放锁对象。等到下次面试官问你,synchronized如果抛异常了,会不会释放锁对象,答案是:会的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

ACC_SYNCHRONIZED 修饰文章源自JAVA秀-https://www.javaxiu.com/10415.html

刚刚我们所看到的是mian方法中同步代码块所编译后的指令,以下是同步方法编译后指令文章源自JAVA秀-https://www.javaxiu.com/10415.html

可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter、mointorexit,在执行同步方法前会调用monitorenter,在方法结束之后会调用monitorexit。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public synchronized void test(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String 1 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 19: 0 line 20: 8文章源自JAVA秀-https://www.javaxiu.com/10415.html

尊贵铂金文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.1 montior 监视器锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

刚刚上文有提到每一个对象都会和一个monitor监视器关联,真正的锁都是靠monitor监视器来完成,文章源自JAVA秀-https://www.javaxiu.com/10415.html

那monitor到底是个啥玩意呢?小编偷偷告诉你,其实monitor是用C++所写。文章源自JAVA秀-https://www.javaxiu.com/10415.html

http://hg.openjdk.java.net/jdk8/jdk8/hotspot/ 网址都给你们找好了,点击左边zip、gz下载都行。网速不好的同学可以在网上“hotspot 源码下载” ,下载之后文件如下图:文章源自JAVA秀-https://www.javaxiu.com/10415.html

下载之后为了方便浏览,小编建议你们可以去下载一个CLion工具来看代码,或者直接用文本编辑器打开也行。文章源自JAVA秀-https://www.javaxiu.com/10415.html

java对象怎么和monitor关联的呢?文章源自JAVA秀-https://www.javaxiu.com/10415.html

这里就牵扯到另外一个知识点,我们每一个对象在内存中分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。而这个对象头就包含了一个monitor的引用地址,指向了一个具体的monitor对象。文章源自JAVA秀-https://www.javaxiu.com/10415.html

monitor里面包含了什么?文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们先找到monitor对象对应的源文件:/src/share/vm/runtime/objectMonitor.hpp,往下翻可以看到ObjectMonitor的构造方法,里面有一系列成员属性。文章源自JAVA秀-https://www.javaxiu.com/10415.html

ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 记录线程的重入次数 _object = NULL; _owner = NULL; // 标识拥有该monitor的线程 _WaitSet = NULL; // 存储正处于wait状态的线程 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 存放竞争失败线程的单向链表 FreeNext = NULL ; _EntryList = NULL ; // 存储等待锁block状态的线程 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0;}文章源自JAVA秀-https://www.javaxiu.com/10415.html

挑几个比较重要的来说一下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

_recursions:这个在上文讲monitorenter指令的时候有提到,就是记录线程线程获取锁的次数,获取到锁该属性则会+1,退出同步代码块则-1;文章源自JAVA秀-https://www.javaxiu.com/10415.html

_owner:当一个线程获得了monitor的所有权,则该对象会保存到_owner中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

_WaitSet:当线程入wait状态,则会存储到_WaitSet当中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

_cxq :当线程之间开始竞争锁,如果锁竞争失败后,则会加入_cxq链表中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

_EntryList:当新线程进来尝试去获取锁对象,又没有获取到对象的时候,则会存储到_EntryList当中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.2 monitor 竞争文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么情况下会竞争?文章源自JAVA秀-https://www.javaxiu.com/10415.html

当多个线程执行同步代码块的时候,这个时候就会出现锁竞争。文章源自JAVA秀-https://www.javaxiu.com/10415.html

当线程执行同步代码块时,先执行monitorenter指令, 这个时候会调用interpreterRuntime.cpp中的函数文章源自JAVA秀-https://www.javaxiu.com/10415.html

源文件如下:src/share/vm/interpreter/interpreterRuntime.cpp,搜索:monitorenter文章源自JAVA秀-https://www.javaxiu.com/10415.html

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) // 代码省略文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 是否用偏向锁 if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { // 重量级锁 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 代码省略IRT_END文章源自JAVA秀-https://www.javaxiu.com/10415.html

线程之间如何竞争锁的?文章源自JAVA秀-https://www.javaxiu.com/10415.html

对于重量级锁,monitorenter函数中会调用 :ObjectSynchronizer::slow_enter,文章源自JAVA秀-https://www.javaxiu.com/10415.html

最终调用到这个函数上:ObjectMonitor::enter,源码位于:/src/share/vm/runtime/objectMonitor.cpp文章源自JAVA秀-https://www.javaxiu.com/10415.html

void ATTR ObjectMonitor::enter(TRAPS) { // The following code is ordered to check the most common cases first // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors. Thread * const Self = THREAD ; void * cur ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 1、通过CAS操作尝试把monitor的_owner设置成当前线程 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; return ;} // 2、重入锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

if (cur == Self) {文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 重入锁计数器也需要+1文章源自JAVA秀-https://www.javaxiu.com/10415.html

_recursions ++ ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

return ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 3、如果是当前线程第一次进入该monitor文章源自JAVA秀-https://www.javaxiu.com/10415.html

if (Self->is_lock_owned ((address)cur)) {文章源自JAVA秀-https://www.javaxiu.com/10415.html

assert (_recursions == 0, "internal state error");文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 计数器+1文章源自JAVA秀-https://www.javaxiu.com/10415.html

_recursions = 1 ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 把当前线程设置赋值给_owner文章源自JAVA秀-https://www.javaxiu.com/10415.html

_owner = Self ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

OwnerIsThread = 1 ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

return ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

// TODO-FIXME: change the following for(;;) loop to straight-line code.文章源自JAVA秀-https://www.javaxiu.com/10415.html

for (;;) {文章源自JAVA秀-https://www.javaxiu.com/10415.html

jt->set_suspend_equivalent();文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 4、获取锁失败,则等待锁释放文章源自JAVA秀-https://www.javaxiu.com/10415.html

EnterI (THREAD) ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

if (!ExitSuspendEquivalent(jt)) break ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

_recursions = 0 ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

_succ = NULL ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

exit (false, Self) ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

jt->java_suspend_self();文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

此处省略了锁的自旋优化等操作,文章后面会讲到文章源自JAVA秀-https://www.javaxiu.com/10415.html

以上代码具体的操作流程如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)通过CAS尝试把monitor的_owner属性设置为当前线程文章源自JAVA秀-https://www.javaxiu.com/10415.html

2)如果之前设置的owner等于当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ; 记录重入次数。文章源自JAVA秀-https://www.javaxiu.com/10415.html

3)如果当前线程是第一次进入monitor,设置_recursions = 1,_owner = 当前线程,该线程成功获得锁并返回。文章源自JAVA秀-https://www.javaxiu.com/10415.html

4)、如果获取锁失败,等待锁释放文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.3. monitor 等待文章源自JAVA秀-https://www.javaxiu.com/10415.html

上文有提到,如果锁竞争失败后,会调用EnterI (THREAD) 函数,还是在objectMonitor.cpp源码中搜索:::EnterI文章源自JAVA秀-https://www.javaxiu.com/10415.html

以下代码小编省略了部分:文章源自JAVA秀-https://www.javaxiu.com/10415.html

void ATTR ObjectMonitor::EnterI (TRAPS) { Thread * Self = THREAD ; assert (Self->is_Java_thread(), "invariant") ; assert (((JavaThread *) Self)->thread_state() == _thread_blocked , "invariant") ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 尝试获取锁 if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 自旋操作尝试获取锁 if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 当前线程封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 通过CAS把node节点push到_cxq队列中 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. // 再次尝试获取锁 if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 挂起线程 for (;;) { // 挂起之前再次尝试获取锁 if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; // 通过park将当前线程挂起,等待锁释放 Self->_ParkEvent->park() ; } // 尝试获取锁 if (TryLock(Self) > 0) break ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

return ;}文章源自JAVA秀-https://www.javaxiu.com/10415.html

以上代码具体流程概括如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)进入EnterI后,先会再次尝试获取锁对象文章源自JAVA秀-https://www.javaxiu.com/10415.html

2)把当前线程封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

3)在for循环中,通过CAS把node节点push到_cxq(上文有提到这个属性)列表中,同一时刻可能有多个线程把自己到node节点push到_cxq列表中。文章源自JAVA秀-https://www.javaxiu.com/10415.html

4)node节点push到_cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待唤醒。文章源自JAVA秀-https://www.javaxiu.com/10415.html

5)当前线程被唤醒时,会从挂起到点继续执行,通过TryLock再次尝试锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

5.4 monitor 释放文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么时候会释放monitor?文章源自JAVA秀-https://www.javaxiu.com/10415.html

当线程执行完同步代码块时,调用monitorexit指令释放锁,这个时候锁就会被释放。文章源自JAVA秀-https://www.javaxiu.com/10415.html

还是在objectMonitor.cpp源码中搜索:::exit文章源自JAVA秀-https://www.javaxiu.com/10415.html

释放monitor过程是什么?文章源自JAVA秀-https://www.javaxiu.com/10415.html

exit函数代码如下,当然小编也有大部分的删减,留下比较主要的代码部分。文章源自JAVA秀-https://www.javaxiu.com/10415.html

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 判断计数器,不等于0则执行-1 if (_recursions != 0) { _recursions--; // this is simple recursive enter TEVENT (Inflated exit - recursive) ; return ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// w = 最后被唤醒的线程 ObjectWaiter * w = NULL ; int QMode = Knob_QMode ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// QMode == 2,会绕过EntryList队列,从cxq队列中获取线程用于竞争锁 if (QMode == 2 && _cxq != NULL) { w = _cxq ; assert (w != NULL, "invariant") ; assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; // 唤醒线程 ExitEpilog (Self, w) ; return ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

// QMode还有还好几种策略,小编就不一一列举了文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 最后拿到了要被唤醒的线程 w = _EntryList ; if (w != NULL) { guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; // 唤醒线程 ExitEpilog (Self, w) ; return ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

观察以上代码,都需要调用ExitEpilog函数来唤醒线程, 还是在objectMonitor.cpp源码中搜索:::ExitEpilog文章源自JAVA秀-https://www.javaxiu.com/10415.html

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { assert (_owner == Self, "invariant") ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; ParkEvent * Trigger = Wakee->_event ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

Wakee = NULL ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// Drop the lock OrderAccess::release_store_ptr (&_owner, NULL) ; OrderAccess::fence() ; // ST _owner vs LD in unpark()文章源自JAVA秀-https://www.javaxiu.com/10415.html

if (SafepointSynchronize::do_call_back()) { TEVENT (unpark before SAFEPOINT) ; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 最重要的时候这里,调用unpark来进行唤醒 Trigger->unpark() ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

// Maintain stats and report events to JVMTI if (ObjectMonitor::_sync_Parks != NULL) { ObjectMonitor::_sync_Parks->inc() ; }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

以上代码具体流程概括如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1)退出同步代码块时会让_recursions - 1,当_recursions的值等于0的时候,说明线程释放了锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2)根据不同的策略(由QMode来指定),最终获取到需要被唤醒的线程(代码中是:w)文章源自JAVA秀-https://www.javaxiu.com/10415.html

3)最后调用ExitEpilog函数中,最终由unpark来执行唤醒操作。文章源自JAVA秀-https://www.javaxiu.com/10415.html

永恒钻石文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.1 CAS 介绍文章源自JAVA秀-https://www.javaxiu.com/10415.html

CAS的英文单词CompareAndSwap的缩写,比较并替换。CAS需要有3个操作数:内存地址V、旧的预期值A、即将要更新的目标值B。文章源自JAVA秀-https://www.javaxiu.com/10415.html

CAS指令执行时,当内存地址V的值与预期值A相等时,将目标值B保存到内存当中,否则就什么都不做。整个比较并替换的操作是一个原子操作。文章源自JAVA秀-https://www.javaxiu.com/10415.html

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会挂起,而是被告知这次竞争失败,并可以再次尝试。文章源自JAVA秀-https://www.javaxiu.com/10415.html

优点:可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的力度级别,允许更高程度的并行机制等等。文章源自JAVA秀-https://www.javaxiu.com/10415.html

缺点:文章源自JAVA秀-https://www.javaxiu.com/10415.html

1、循环时间长开销很大,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。文章源自JAVA秀-https://www.javaxiu.com/10415.html

2、只能保证一个共享的原子操作,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。文章源自JAVA秀-https://www.javaxiu.com/10415.html

3、ABA问题,如果内存地址V初次读取的值是A,并且在准备赋值的时候检查仍然为A,那我们就能说它的值没有被其他线程改变过吗?文章源自JAVA秀-https://www.javaxiu.com/10415.html

如果在这段期间它的值曾被改成了B,后来又被改回A,那CAS就会误认为它从来没有被改变过,这个漏洞称之为CAS操作的ABA问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类 “AtomicStampendReference”,它可以通过控制变量值的版本来保证CAS的正确性。文章源自JAVA秀-https://www.javaxiu.com/10415.html

因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发性的正确性,如果需要解决ABA问题,改用传统的互斥同步可能比原子类更高效文章源自JAVA秀-https://www.javaxiu.com/10415.html

介绍完CAS,那么肯定就多多少少介绍以下实现原理,我们以AtomicInteger为例,它是JDK中提供能够保障原子性操作的类。文章源自JAVA秀-https://www.javaxiu.com/10415.html

/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们点进去看它里面的方法,拿incrementAndGet方法为例子,这个方法是在原有值的基础上进行+1操作,它的实现调用Unfafe类的方法,我们再点进去看。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));文章源自JAVA秀-https://www.javaxiu.com/10415.html

return var5; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

Unfafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针问题,过度的使用Unsafe类会使得出错的几率变大。因此Java官方不建议使用的,Unsafe对象也不能直接调用,只能通过放射来获取。文章源自JAVA秀-https://www.javaxiu.com/10415.html

小编这里说一下getandAddInt方法的执行流程,文章源自JAVA秀-https://www.javaxiu.com/10415.html

var1:传进来的是this,也就是AtomicInteger实例对象;文章源自JAVA秀-https://www.javaxiu.com/10415.html

var2:偏移量,通过结合var1就能够获得在内存中的最新值;文章源自JAVA秀-https://www.javaxiu.com/10415.html

var4:要进行累加的值,也就是 1 ;文章源自JAVA秀-https://www.javaxiu.com/10415.html

先通过var1+var2 获取到内存中最新的值,然后再调用compareAndSwapInt方法,这个方法又会通过var1+var2参数获取内存中最新的值,与var5的值进行比较,如果比较成功,这把var5+var4的结果更新到内存中去。如果不成功,则继续循环操作。也就是我们刚刚介绍CAS所说,比较并替换。文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.2 sync 锁升级过程文章源自JAVA秀-https://www.javaxiu.com/10415.html

在JDK1.5以前,sync是一个重量级的锁,在1.6以后,对sync做了大量的各种优化,包含偏向锁、轻量级锁、适应性自旋、锁消除、锁粗化等等,这些技术都是为了线程之间更加高效的共享数据,以及解决竞争问题,从而达到程序的执行效率。文章源自JAVA秀-https://www.javaxiu.com/10415.html

当然锁肯定升级的过程:无锁 —— 偏向锁 —— 轻量级锁 —— 重量级锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

每个不同的锁都有不同的使用藏场景,在了解各种锁的特性之前,我们还需要搞清楚对象在内存中的布局!文章源自JAVA秀-https://www.javaxiu.com/10415.html

6.3 对象的布局文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们每一个对象在内存中分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。文章源自JAVA秀-https://www.javaxiu.com/10415.html

对象头:文章源自JAVA秀-https://www.javaxiu.com/10415.html

当一个线程尝试访问sync修饰的代码块时,它先要获得锁,这个锁对象是存在对象头中的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

以Hotspot虚拟机为例,对象头里面主要包含了Mark Word(字段标记)、Klass Pointer (指针类型),如果对象是数组类型,还包含了数组的长度。文章源自JAVA秀-https://www.javaxiu.com/10415.html

怎么又扯到Hotspot虚拟机呢?小伙伴可以这样理解,JVM可以理解为一套规范,而Hotspot是具体的虚拟机产品。就好比如你们要找女朋友、或者男朋友,既然找朋友是不是就要有一定的要求或者规范,JVM就可以看作这个规范,而Hotspot就是具体的男朋友或者女朋友了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

你不信?System.out.println(System.getProperties()); 运行这个代码吧,找找你们java.vm.name等于什么。文章源自JAVA秀-https://www.javaxiu.com/10415.html

java.vm.name=Java HotSpot(TM) 64-Bit Server VM文章源自JAVA秀-https://www.javaxiu.com/10415.html

Mark Word :里默认存储对象的HashCode、分代年龄和锁位标记。这个也是sync锁实现的重要部分了,在运行期间,Mark Word 里存储的数据会随着锁标位置的变化而变化。在64位虚拟机下,Mark Word是64bit大小的,其存储结构如图:文章源自JAVA秀-https://www.javaxiu.com/10415.html

Mark Word 64位虚拟机存储结构文章源自JAVA秀-https://www.javaxiu.com/10415.html

以上这个表格数据不能乱来对不对,我们可以查看源码:src/share/vm/oops/markOop.hpp文章源自JAVA秀-https://www.javaxiu.com/10415.html

里面注释写的很清楚了,对照以下注释反映出上面的表格,更加直观。文章源自JAVA秀-https://www.javaxiu.com/10415.html

// 32 bits:// --------// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)// size:32 ------------------------------------------>| (CMS free block)// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)//// 64 bits:// --------// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)// size:64 ----------------------------------------------------->| (CMS free block)文章源自JAVA秀-https://www.javaxiu.com/10415.html

Klass Pointer:用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定是哪个对象的实例。文章源自JAVA秀-https://www.javaxiu.com/10415.html

对象头 = Mark Word + Klass Point 在未开启指针压缩对情况下所占大小:文章源自JAVA秀-https://www.javaxiu.com/10415.html

以64位系统为例:Mark Word = 8 bytes,指针类型 = 8 bytes ,对象头 = 16 bytes = 128bits;文章源自JAVA秀-https://www.javaxiu.com/10415.html

实例数据:文章源自JAVA秀-https://www.javaxiu.com/10415.html

类中定义的成员变量文章源自JAVA秀-https://www.javaxiu.com/10415.html

对齐填充:文章源自JAVA秀-https://www.javaxiu.com/10415.html

对齐填充并不是必然存在的,也没有什么特殊的意义,它仅仅只是占位符的作用。由于HotPort VM的自动内存管理系统要求对象起始地址必须是8字节的整倍数,当对象的实例数据部分没有对齐时,就需要通过对齐填充来不补齐。文章源自JAVA秀-https://www.javaxiu.com/10415.html

说了这么多,都是概念性的东西,说谁不会说对不对,接下来我们尝试在把一个对象在内存中都布局输出看下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

先引入这个jar包,它能够提供我们想要看到的东西,使用方式如下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

org.openjdk.jol jol-core 0.10public class SyncExample4 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

static Apple apple = new Apple();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { // 这里使用ClassLayout来查看 System.out.println(ClassLayout.parseInstance(apple).toPrintable()); }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

class Apple { private int count; private boolean isMax;文章源自JAVA秀-https://www.javaxiu.com/10415.html

}文章源自JAVA秀-https://www.javaxiu.com/10415.html

以下内容就是我们Java对象内存分布所查看到的内容,我们能直接看到内容有object header 翻译过来就是对象头呀, 再往下看就是loss due to the next object alignment,这个就是对齐填充,由于Apple 有一个boolean的属性,占了一个字节,所以计算机为了提高执行效率和GC垃圾回收的效率,进行了7个字节的填充(这里涉及到CPU运行小编就不多扯了)。文章源自JAVA秀-https://www.javaxiu.com/10415.html

com.example.concurrency.sync.Apple object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509) 12 4 int Apple.count 0 16 1 boolean Apple.isMax false 17 7 (loss due to the next object alignment)Instance size: 24 bytesSpace losses: 0 bytes internal + 7 bytes external = 7 bytes total文章源自JAVA秀-https://www.javaxiu.com/10415.html

看到这里我们确实能够确定对象头的存在,那么对象头里面不是说用31 bit存储了HashCode吗?怎么没看见文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们再来执行一段代码, 计算一下apple的HashCode是多少,看运行结果可知,本次运行apple的HashCode是7ea987ac,我们再看看对应VALUE值也发生了改变。这里有一个概念,由于存在大小端存储方式,我们需要从后往前看。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample4 {文章源自JAVA秀-https://www.javaxiu.com/10415.html

static Apple apple = new Apple();文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { // 查看HashCode System.out.println(Integer.toHexString(apple.hashCode())); System.out.println(ClassLayout.parseInstance(apple).toPrintable()); }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

class Apple { private int count; private boolean isMax;}7ea987ac# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scopecom.example.concurrency.sync.Apple object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 ac 87 a9 (00000001 10101100 10000111 10101001) (-1450726399) 4 4 (object header) 7e 00 00 00 (01111110 00000000 00000000 00000000) (126) 8 4 (object header) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509) 12 4 int Apple.count 0 16 1 boolean Apple.isMax false 17 7 (loss due to the next object alignment)Instance size: 24 bytesSpace losses: 0 bytes internal + 7 bytes external = 7 bytes total文章源自JAVA秀-https://www.javaxiu.com/10415.html

细心一点的小伙伴就会发现,上文不是说了对象头一共占了16个字节吗?这里三个object header 才12个字节也不对呀?文章源自JAVA秀-https://www.javaxiu.com/10415.html

这里JVM默认会开启指针压缩,我们可以通过参数把它关掉:文章源自JAVA秀-https://www.javaxiu.com/10415.html

在打印看结果,就是16个字节。文章源自JAVA秀-https://www.javaxiu.com/10415.html

OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 80 68 f5 1f (10000000 01101000 11110101 00011111) (536176768) 12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)文章源自JAVA秀-https://www.javaxiu.com/10415.html

最后总结以下:Java对象有三个部分组成:对象头、实例数据、对齐填充,其中对象头又包含Mark Word、Klass Pointer(如果对象是数组类型,还包含了数组的长度)。文章源自JAVA秀-https://www.javaxiu.com/10415.html

至尊星耀文章源自JAVA秀-https://www.javaxiu.com/10415.html

Mark Word 64位虚拟机存储结构文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.1 偏向锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁的原理文章源自JAVA秀-https://www.javaxiu.com/10415.html

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获得的锁的代价更低,从而引入偏向锁的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们对照Mark Word存储结构来看,当一个线程访问同步代码快之后,会把Mark Word中的偏向锁标识由0改为1,并且存储当前线程的ID,以后该线程进入和退出同步代码的的时候,则不需要进行CAS操作来加锁和解锁。只需要简单的测试一下对象头里是否存储着指向当先线程的偏向锁,如果结果成功,表示线程已经获得了锁。如果失败,需要再查看Mark Word中的偏向锁标识是否设置成1,如果没有,则使用CAS竞争锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们可以使用代码来观察下:文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁在Java 6 和Java 7中默认是开启的,但是他在应用程序启动几秒钟之后才激活,我们需要先来关闭延迟启动。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample4 { public static void main(String[] args) { Apple apple = new Apple(); apple.start(); }} class Apple extends Thread { private Object lock = new Object(); @Override public void run() { synchronized (lock) { System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 d8 86 22 (00000101 11011000 10000110 00100010) (579262469) 4 4 (object header) 9c 7f 00 00 (10011100 01111111 00000000 00000000) (32668) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total文章源自JAVA秀-https://www.javaxiu.com/10415.html

由于大小端存储,原本偏向锁和锁位标识是在最后的,现在我们需要看最前8位数:00000101文章源自JAVA秀-https://www.javaxiu.com/10415.html

第一个1 代表是偏向锁,并且锁标识01,和我们的表格也能够对应上。文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁的撤销文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁使用了一种等到竞争出现了才释放锁的机制,所以当其他线程来进行争夺锁的时候,持有偏向锁的线程才会释放锁。但是偏向锁的撤销的时候,需要等到一个全局安全点,也就是在这个时间点上没有正在执行的字节码。它首先会暂停所有线程(包括拥有偏向锁的线程),然后在判断当前是不是偏向锁,如果偏向锁标识等于1,就撤销回0;文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁的好处文章源自JAVA秀-https://www.javaxiu.com/10415.html

偏向锁的好处也很显而易见,只有同一个线程来访问同步代码块的时候,效率是很高的,只需要判断当先线程和Mark Word里面存储的线程是否是一致就行了。如果程序中大多数的锁都是不同的线程来进行访问,那么这个时候偏向锁就是多余的了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们可以通过JVM参数来关闭偏向锁:-XX:-UseBiasedLocking文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.2 轻量级锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是轻量级锁?文章源自JAVA秀-https://www.javaxiu.com/10415.html

轻量级锁是在JDK6中加入的新型锁机制,引入轻量级锁的目的是为了,在多线程交替执行同步代码块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁,所以轻量级锁的出现并非代替重量级锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

栈桢文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们在JVM虚拟中,有堆和栈,而在栈中还包含了我们对象的各种方法,一个方法就相当于一个“栈桢”。其中方法中也是可以存储内容的,其中就包含了Displaced Mark Word,这个有什么作用呢?接着往下看文章源自JAVA秀-https://www.javaxiu.com/10415.html

轻量级锁原理文章源自JAVA秀-https://www.javaxiu.com/10415.html

线程在执行同步代码快之前,JVM会现在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word 复制到锁记录当中。这个就是我们刚刚所说Displaced Mark Word了。JVM利用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针。如果成功,当先线程获得锁并且将锁位标识改为00,如果失败了则需要判断当前对象的Mark Word是否指向当前线程的指针,如果是则表示当线程已经持有对象的锁,执行同步代码快。如果不是只能说明该锁对象被其他线程占用,这时的轻量级需要膨胀到重量级锁,锁位标识改为10,后面的线程进入阻塞状态。文章源自JAVA秀-https://www.javaxiu.com/10415.html

轻量级锁的释放文章源自JAVA秀-https://www.javaxiu.com/10415.html

解锁的时候,会使用CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.3 自旋锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

自旋锁是在JDK1.4中就已经引入了,默认是关闭的,在JDK1.6中默认几句开启了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

为什么要用自旋锁呢?自旋锁通俗易懂的来说,就是循环去获取锁。因为在我们锁升级的过程中,如果线程竞争锁失败,就立即被挂起,然后等待被唤醒,其实这个时候性能开销是比较大的。可能线程还正在被挂起的时候,锁已经被释放掉了,所以就有了自旋锁的操作。文章源自JAVA秀-https://www.javaxiu.com/10415.html

当线程竞争锁失败之后,先自旋来尝试获取锁,如果锁被占用的时间很短,自旋等待的效果就非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会拜拜消耗处理器资源,而不会有任何的作用。自旋默认的默认值是10次,可使用参数-XX:PreBlockSpin来更改。文章源自JAVA秀-https://www.javaxiu.com/10415.html

适应性自选锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

由于我们自旋锁可能回带来一定的性能消耗,但是我们又不清楚设置自旋次数多少合适,所以这个时候适应性自选锁就来了。适应性自选就意味着自旋的时间不再固定了,而是由前一次在同一个锁的自旋时间及所得拥有者的状态来决定。假设在同一个同步代码块上自旋10次就能获得锁,那么虚拟机就会认为这次也能够获得锁,还允许自旋的时间稍微长一点。那么再假设一个同步代码块从来都没有自旋成功过,那么虚拟机就可能省略自旋的过程,以免浪费性能。文章源自JAVA秀-https://www.javaxiu.com/10415.html

光说还不如来点实际的代码,源码路径:src/share/vm/runtime/objectMonitor.cpp ,搜索::TrySpin_VaryDuration文章源自JAVA秀-https://www.javaxiu.com/10415.html

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) { // 固定自旋次数 int ctr = Knob_FixedSpin ; if (ctr != 0) { while (--ctr >= 0) { if (TryLock (Self) > 0) return 1 ; SpinPause () ; } return 0 ; } // 适应式自旋 for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) { if (TryLock(Self) > 0) { // 成功后,修改自旋的时间 int x = _SpinDuration ; if (x文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.4 消除锁文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们先来看以下代码:文章源自JAVA秀-https://www.javaxiu.com/10415.html

public String getContent() { return new StringBuffer().append("a").append("b").append("c").toString(); } @Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

StringBuffer中的append是同步的,但是我们这个getContent这个方法,每次都是新new一个对象来进行操作。所以不同的线程进来,锁住的对象也是不同的,所以就根本不会造成线程上的问题。这个时候虚拟机即使编译器(JIT)在运行时,对一些代码上的要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,这个就是锁消除。文章源自JAVA秀-https://www.javaxiu.com/10415.html

7.5 锁粗化文章源自JAVA秀-https://www.javaxiu.com/10415.html

什么是锁粗化呢?JVM会探测一连串细小的操作都是用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { StringBuffer sb = new StringBuffer(); for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

@Override文章源自JAVA秀-https://www.javaxiu.com/10415.html

public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }文章源自JAVA秀-https://www.javaxiu.com/10415.html

看上面代码,StringBuffer的append的方法里面是有加同步关键字的,然而我们在外面循环了100次,就要进入锁和退出锁各100次,所以这个时候JVM就会把锁粗化。把append方法同步关键字去掉,扩大在外面来,就只需要进入和退出1次即可。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { StringBuffer sb = new StringBuffer(); synchronized (sb) { for (int i = 0; i文章源自JAVA秀-https://www.javaxiu.com/10415.html

最强王者文章源自JAVA秀-https://www.javaxiu.com/10415.html

终章:平时写代码如何对synchroized优化文章源自JAVA秀-https://www.javaxiu.com/10415.html

终于打上王者了,不要以为打上王者就行啦,还有一些日常操作我们还需要注意到的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

减少sync的同步代码块的范围:文章源自JAVA秀-https://www.javaxiu.com/10415.html

同步代码块精简,执行就会更快,可能轻量级锁、自旋锁就搞定了,不会升级为重量级锁。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public static void main(String[] args) { StringBuffer sb = new StringBuffer(); synchronized (sb) { System.out.println("a"); } }文章源自JAVA秀-https://www.javaxiu.com/10415.html

降低sync锁的粒度:文章源自JAVA秀-https://www.javaxiu.com/10415.html

锁的对象也是有讲究的,假设test01和02本身没有任何业务相关的代码,但是锁的对象越是同一个,这样岂不是并发效率就很低了。文章源自JAVA秀-https://www.javaxiu.com/10415.html

public class SyncExample4 { public void test01(){ synchronized (SyncExample4.class){} } public void test02(){ synchronized (SyncExample4.class){} }}文章源自JAVA秀-https://www.javaxiu.com/10415.html

读写分离:文章源自JAVA秀-https://www.javaxiu.com/10415.html

我们尽量可以做到,读的时候不加锁,写入和删除的时候加锁,这样就可以保证多个线程同时来读取数据。文章源自JAVA秀-https://www.javaxiu.com/10415.html

举个例子:文章源自JAVA秀-https://www.javaxiu.com/10415.html

HashTable容器竞争激烈的并发环境下,效率低是因为多个线程竞争同一把锁,假如容器有多把锁,每一把锁用于锁住容器中一部分数据,那么多线程访问容器里面不同的数据段的数据时,线程间不会存在锁竞争,从而有效提高并发访问率。这就是ConcurrentHashMap的锁分段技术,将数据分成一段一段的存储,然后把每一段数据分配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段段数据也能被其他线程访问。文章源自JAVA秀-https://www.javaxiu.com/10415.html

小编我终于写完了,温馨提示光看一遍印象不会特别深刻,最好能够实际动手操作一下,看下源码如何实现之类的。文章源自JAVA秀-https://www.javaxiu.com/10415.html

原文链接:https://blog.csdn.net/weixin_38111957/article/details/104902100文章源自JAVA秀-https://www.javaxiu.com/10415.html

【END】文章源自JAVA秀-https://www.javaxiu.com/10415.html

更多精彩推荐文章源自JAVA秀-https://www.javaxiu.com/10415.html

比特币当赎金,WannaRen 勒索病毒二度来袭!文章源自JAVA秀-https://www.javaxiu.com/10415.html

深度|一文读懂“情感计算”在零售中的应用发展文章源自JAVA秀-https://www.javaxiu.com/10415.html

三大运营商将上线5G消息;苹果谷歌联手,追踪30亿用户;jQuery 3.5.0发布|极客头条文章源自JAVA秀-https://www.javaxiu.com/10415.html

曾遭周鸿祎全网封杀的360猛将:草根打工到36岁身家上亿的逆袭!文章源自JAVA秀-https://www.javaxiu.com/10415.html

你公司的虚拟机还闲着?基于Jenkins和Kubernetes的持续集成测试实践了解一下!文章源自JAVA秀-https://www.javaxiu.com/10415.html

从Web1.0到Web3.0:详析这些年互联网的发展及未来方向文章源自JAVA秀-https://www.javaxiu.com/10415.html

一文读懂“情感计算”在零售中的应用发展文章源自JAVA秀-https://www.javaxiu.com/10415.html

你点的每个“在看”,我都认真当成了喜欢文章源自JAVA秀-https://www.javaxiu.com/10415.html

文章源自JAVA秀-https://www.javaxiu.com/10415.html

展开阅读文章源自JAVA秀-https://www.javaxiu.com/10415.html

去APP阅读参与评论互动文章源自JAVA秀-https://www.javaxiu.com/10415.html

继续阅读
速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
weinxin
资源分享QQ群
本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
  • 版权声明:本站是JAVA秀团队的技术分享社区,我们会经常分享资源和教程。
  • 转载请注明:腾讯看点 - JAVA秀 ☜(ˆ▽ˆ)
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定