7个连环问揭开java多线程背后的弯弯绕
摘要:很多java入门新人一想到java多线程, 就会觉得很晕很绕,什么可见不可见的,也不了解为什么sync怎么就锁住了代码。
本文分享自华为云社区《java多线程背后的弯弯绕绕到底是什么? 7个连环问题为你逐步揭开背后的核心原理!》,作者:breakDraw 。
很多java入门新人一想到java多线程, 就会觉得很晕很绕,什么可见不可见的,也不了解为什么sync怎么就锁住了代码。
因此我在这里会提多个问题,如果能很好地回答这些问题,那么算是你对java多线程的原理有了一些了解,也可以借此学习一下这背后的核心原理。
Q: java中的主内存和工作内存是指什么?
A:java中, 主内存中的对象引用会被拷贝到各线程的工作内存中, 同时线程对变量的修改也会反馈到主内存中。

- 主内存对应于java堆中的对象实例部分(物理硬件的内存)
- 工作内存对应于虚拟机栈中的部分区域( 寄存器,高速缓存)
- 工作内存中是拷贝的工作副本
- 拷贝副本时,不会吧整个超级大的对象拷贝过来, 可能只是其中的某个基本数据类型或者引用。
因此我们知道各线程使用内存数据时,其实是有主内存和工作内存之分的。并不是一定每次都从同一个内存里取数据。
或者理解为大家使用数据时之间有一个缓存。
Q: 多线程不可见问题的原因是什么?
A:这里先讲一下虚拟机定义的内存原子操作:
- lock: 用于主内存, 把变量标识为一条线程独占的状态
- unlock : 主内存, 把锁定状态的变量释放
- read: 读取, 从主内存读到工作线程中
- load: 把read后的值放入到 工作副本中
- use: 使用工作内存变量, 传给工作引擎
- assign赋值: 把工作引擎的值传给工作内存变量
- store: 工作内存中的变量传到主内存
- write: 把值写入到主内存的变量中
根据这些指令,看一下面这个图, 然后再看图片之后的流程解释,就好理解了。

- read和load、store、write是按顺序执行的, 但是中间可插入其他的操作。不可单独出现。
- assgin之后, 会同步后主内存。即只有发生过assgin,才会做工作内存同步到主内存的操作。
- 新变量只能在主内存中产生
- 工作内存中使用某个变量副本时,必须先经历过assign或者load操作。 不可read后马上就use
- lock操作可以被同一个线程执行多次,但相应地解锁也需要多次。
- 执行lock时,会清空工作内存中该变量的值。 清空后如果要使用,必须重新做load或者assign操作
- unlock时,需要先把数据同步回主内存,再释放。
因此多线程普通变量的读取和写入操作存在并发问题, 主要在于2点:
- 只有assgin时, 才会更新主内存, 但由于指令重排序的情况,导致有时候某个assine指令先执行,然后这个提前被改变的变量就被其他线程拿走了,以至于其他线程无法及时看到更新后的内存值。
- assgin时从工作内存到主内存之间,可能存在延迟,同样会导致数据被提前取走存到工作线程中。
Q: 那么volatile关键字为什么就可以实现可见性?
可见性就是并发修改某个值后,这个值的修改对其他线程是马上可见的。
A: java内存模型堆volatile定义了以下特殊规则:
- 当一个线程修改了该变量的值时,会先lock住主存, 再立刻把新数据同步回内存。
- 使用该值时,其他工作内存都要从主内存中刷新!
- 这个期间会禁止对于该变量的指令重排序
禁止指令重排序的原理是在给volatile变量赋值时,会加1个lock动作, 而前面规定的内存模型原理中, lock之后才能做load或者assine,因此形成了1个内存屏障。
Q: 上面提到lock后会限制各工作内存要刷新主存的值load进来后才能用, 这个在底层是怎么实现的?
A:利用了cpu的总线锁+ 缓存一致性+ 嗅探机制实现, 属于计算机组成原理部分的知识。

这也就是为什么violate变量不能设置太多,如果设置太多,可能会引发总线风暴,造成cpu嗅探的成本大大增加。
Q: 那给方法加上synchronized关键字的原理是什么?和volatie的区别是啥?
A:
- synchronized的重量级锁是通过对象内部的监视器(monitor)实现
- monitor的线程互斥就是通过操作系统的mutex互斥锁实现的,而操作系统实现线程之间的切换需要从用户态到内核态的切换,所以切换成本非常高。
- 每个对象都持有一个moniter对象
具体流程如下:
- 首先,class文件的方法表结构中有个访问标志access_flags, 设置ACC_SYNCHRONIZED标志来表示被设置过synchronized。
- 线程在执行方法前先判断access_flags是否标记ACC_SYNCHRONIZED,如果标记则在执行方法前先去获取monitor对象。
- 获取成功则执行方法代码且执行完毕后释放monitor对象
- 如果获取失败则表示monitor对象被其他线程获取从而阻塞当前线程

注意,如果是sync{}代码块,则是通过在代码中添加monitorEnter和monitorExit指令来实现获取和退出操作的。
如果对C语言有了解的,可以看看这个大哥些的文章Java精通并发-通过openjdk源码分析ObjectMonitor底层实现
Q: synchronized每次加锁解锁需要切换内核态和用户态, jvm是否有对这个过程做过一些优化?
A:jdk1.6之后, 引入了锁升级的概念,而这个锁升级就是针对sync关键字的
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,
只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别)
因此sync关键字不是一开始就直接使用很耗时的同步。而是一步步按照情况做升级
- 当对象刚建立,不存在锁竞争的时候, 每次进入同步方法/代码块会直接使用偏向锁
- 偏向锁原理: 每次尝试在对象头里设置当前使用这个对象的线程id, 只做一次,如果成功了就设置好threadId, 只要没有出现新的thread访问且markWord被修改,那么久)
2. 当发现对象头的线程id要被修改时,说明存在竞争时。升级为轻量级锁
- 轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效的。 CAS的对象是对象头的Mark Word, 此时仍然不会去调系统底层的方法做阻塞。
3. 但是如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就会升级为重量级锁,也就是上面那个问题中提到的操作。
Q: 锁只可以升级不可以降级, 确定是都不能降级吗?
A:有可能被降级, 不可能存在共享资源竞争的锁。
java存在一个运行期优化的功能
需要开启server模式外加+DoEscapeAnalysis表示开启逃逸分析。
如果运行过程中检测到共享变量确定不会逃逸,则直接在编译层面去掉锁
举例:
StringBuffer.append().append()
例如如果发现stringBuffer不会逃逸,则就会去掉这里append所携带的同步
而这种情况肯定只能发生在偏向锁上, 所以偏向锁可以被重置为无锁状态。
7个连环问揭开java多线程背后的弯弯绕的更多相关文章
- 那些面试官必问的JAVA多线程和并发面试题及回答
Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环 ...
- 去年去阿里面试,被问到java 多线程,我是这样手撕面试官的
1.多线程的基本概念 1.1进程与线程 程序:是为完成特定任务,用某种语言编写的一组指令的集合,即一段静态代码,静态对象. 进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,每个程 ...
- 面试必问!Java 多线程中两个线程交替执行,一个输出偶数,一个输出奇数
前言 楼主今天在面经上看到这个题,挺有意思,小小的题目对多线程的考量还挺多.大部分同学都会使用 synchronized 来实现.楼主今天带来另外两种优化实现,让你面试的时候,傲视群雄! 第一种 sy ...
- Java多线程的实现
记得面试的时候,面试官问了Java多线程实现的方式有几种,它们之间的区别是什么?作为一个Java新手,将最近的学习总结如下: 1.Java多线程实现方式 Java多线程实现方式主要有三种:继承Thre ...
- 一线大厂面试官最喜欢问的15道Java多线程面试题
前言 在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分.如果你想获得更多职位,那么你应该准备很多关于多线程的问题. 他们会问面试者很多令人混淆的Java线程问题.面试官只是想确信面试者 ...
- 新鲜出炉!面试90%会被问到的Java多线程面试题,史上最全系列!
前言 最近很多粉丝朋友私聊我说能不能给整理出一份多线程面试题出来,说自己在最近的面试中老是被问到这一块的问题被问的很烦躁,前一段时间比较忙没时间回私信,前两天看到私信我也是赶紧花了两天给大家整理出这一 ...
- java多线程模拟生产者消费者问题,公司面试常常问的题。。。
package com.cn.test3; //java多线程模拟生产者消费者问题 //ProducerConsumer是主类,Producer生产者,Consumer消费者,Product产品 // ...
- Java多线程问题总结
前言 Java多线程分类中写了21篇多线程的文章,21篇文章的内容很多,个人认为,学习,内容越多.越杂的知识,越需要进行深刻的总结,这样才能记忆深刻,将知识变成自己的.这篇文章主要是对多线程的问题进行 ...
- Java多线程面试题整理
部分一:多线程部分: 1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速. ...
随机推荐
- EFCore_环境搭建与简单使用_01
开发环境搭建 经典步骤:建实体类.建DbContext.生成数据库 本次使用codefirst模式,走下流程,(你也可以先建好数据库,用命令行的形式,直接生成DbContext,而且生成的DbCont ...
- 第34篇-解析invokeinterface字节码指令
与invokevirtual指令类似,当没有对目标方法进行解析时,需要调用LinkResolver::resolve_invoke()函数进行解析,这个函数会调用其它一些函数完成方法的解析,如下图所示 ...
- 【JavaScript基础】Js的定时器(你想看的原理也在哟)
[JavaScript基础]Js的定时器(你想看的原理也在哟) 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 说明 本章是经历 ...
- 顺时针打印矩阵 牛客网 剑指Offer
顺时针打印矩阵 牛客网 剑指Offer 题目描述 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 ...
- Tarjan算法 求 有向图的强连通分量
百度百科 https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin 参考博文 http://blog.csdn ...
- 20191310Lee_yellow缓冲区溢出实验
缓冲区溢出实验 1.什么是缓冲区溢出 缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况.这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段.这一漏洞的出现是由于数据 ...
- Django 实现分页功能(django 2.2.7 python 3.7.5 )
Django 自带名为 Paginator 的分页工具, 方便我们实现分页功能.本文就讲解如何使用 Paginator 实现分页功能. 一. Paginator Paginator 类的作用是将我们需 ...
- Java多线程| 01 | 线程概述
Java多线程| 01 | 线程概述 线程相关概念 进程与线程 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位.可以把进程简单的理解 ...
- LeetCode 199. 二叉树的右视图 C++ 用时超100%
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode ...
- 大一C语言学习笔记(2)---快捷键篇
大家好,博主呢,是一位刚刚步入大一的软件工程专业的大学生,之所以写博客,是想要与同样刚刚接触程序员一行的朋友们一起讨论,进步,在这里记录我的一些学习笔记及心得,希望通过这些点点滴滴的努力,可以让我们离 ...