【漫画】JAVA并发编程 如何解决可见性和有序性问题
原创声明:本文来自公众号【胖滚猪学编程】,以漫画形式让编程so easy and interesting,转载请注明出处!
在上一篇文章并发编程三大源头中,我们初识了并发编程的三个bug源头:可见性、原子性、有序性。明白了它们究竟为什么会发生,那么今天我们就来聊聊如何解决这三个问题吧。
序幕
Happens-Before是什么?
A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
Happens-Before的作用
原创声明:本文来自公众号【胖滚猪学编程】,以漫画形式让编程so easy and interesting,转载请注明出处!
happens-before原则非常重要,它是判断线程是否安全的主要依据,依靠这个原则,我们就能解决在并发环境下可见性和有序性问题。
比如某天老板问你“胖滚猪,我这段并发代码会有线程安全问题吗”,那么你可以对照着happens-before原则一个个看,要是符合其中之一并且是原子性的,你就可以大声告诉老板“没得问题!”
比如这段代码:
i = 1; //线程A执行 j = i ; //线程B执行
j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。
这就是happens-before原则的威力!让我们走进它的世界吧!
Happens-Before八大原则 解决原子性和有序性问题
规则一:程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这规则挺好理解的,毕竟是在一个线程中呐。
你会觉得这是个废物规则。其实这个规则是一个基础规则,happens-before 是多线程的规则,所以要和其他规则约束在一起才能体现出它的顺序性,别着急,继续向下看。
规则二: Volatile变量规则
这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。我们在上篇文章说过,因为缓存的原因,每个线程有自己的工作内存,如果共享变量没有及时刷到主内存中,那就会导致可见性问题,线程B没有及时读到线程A的写。但是只要加上Volatile,就可以避免这个问题,相当于volatile的作用是对变量的修改会绕过高速缓存立刻刷新到主存。不过要注意一下,volatile除了保证可用性,它还可以禁止指定重排序哦!
public class TestVolatile1 { private volatile static int count = 0; public static void main(String[] args) throws Exception { final TestVolatile1 test = new TestVolatile1(); Thread th1 = new Thread(() -> { count = 10; }); Thread th2 = new Thread(() -> { //没有volatile修饰count的话极小概率会出现等于0的情况 System.out.println("count=" + count); }); // 启动两个线程 th1.start(); th2.start(); } }
规则三: 传递性规则
这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。这也很好理解。我们举个例子,writer和reader是两个不同的线程,它们有如下操作:
int x = 0; volatile boolean v = false; public void writer() { x = 42; //(1) v = true; //(2) } public void reader() { if (v == true) { //(3) // 这里 x 会是多少呢?(4) } }
这个例子和上面那个Volatile的例子有个区别就是,有两个变量。那么我们来分析一下:
(1)和(2)在同一个线程中,根据规则1,(1)Happens-Before于(2)
(3)和(4)在同一个线程中,同理,(3)Happens-Before于(4)
根据规则2,由于v用了volatile修饰,那么(2)必然 Happens-Before于(3)。
那么根据传递性规则可得:(1)Happens-Before于(4),因此x必然为42。
所以即使x没有用volatile,它也是可以保证可见性的!所以为啥刚刚说规则1要和其他规则联合起来看才有意思,现在你知道了吧!
规则四: 管程中的锁规则
指管程中的解锁必然发生在随后的加锁之前。管程是一种通用的同步原语,synchronized 是 Java 里对管程的实现。管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。
synchronized (this) { // 此处自动加锁 if (this.x < 10) {//临界区 } } // 此处自动解锁
这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
规则五: 线程启动规则
主线程 A 启动子线程 B 后(线程 A 调用线程 B 的 start() 方法),子线程 B 能够看到主线程在启动子线程 B 前的操作。
private static long count = 0; public static void main(String[] args) throws InterruptedException { Thread B = new Thread(() -> { // 主线程调用 B.start() 之前 所有对共享变量的修改,此处皆可见 // 因此count肯定为10 System.out.println(count); }); // 此处对共享变量count修改 count = 10; // 主线程启动子线程 B.start(); }
规则六: 线程终止规则
主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么主线程能够看到子线程的操作(指共享变量的操作),换句话说就是线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。
private static long count = 0; public static void main(String[] args) throws InterruptedException { Thread B = new Thread(() -> { // 主线程调用 B.start() 之前 所有对共享变量的修改,此处皆可见 // 因此count肯定为10 count = 10; }); // 主线程启动子线程 B.start(); // 主线程等待子线程完成 B.join(); // 子线程所有对共享变量的修改 在主线程调用 B.join() 之后皆可见 System.out.println(count);//count必然为10 }
规则七:线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。即线程A调用线程B的interrupt()方法,happens-before于线程A发现B被A中断(通过Thread.interrupted()方法检测到是否有中断发生)。
private static long acount = 0; private static long bcount = 0; public static void main(String[] args) throws InterruptedException { Thread B = new Thread(() -> { bcount = 7; System.out.println("Thread A被中断前bcount="+bcount+" acount="+acount); while (true){ if (Thread.currentThread().isInterrupted()){ bcount = 77; System.out.println("Thread A被中断后bcount="+bcount+" acount="+acount); return; } } }); B.start(); Thread A = new Thread(() -> { acount = 10; System.out.println("Thread B 中断A前bcount="+bcount+" acount="+acount); B.interrupt(); acount = 100; System.out.println("Thread B 中断A后bcount="+bcount+" acount="+acount); }); A.start(); }
规则八:对象规则
一个对象的初始化完成(构造函数执行结束,一般都是用new初始化)happen—before它的finalize()方法的开始。finalize()是在java.lang.Object里定义的,即每一个对象都有这么个方法。这个方法在该对象被回收的时候被调用。该条原则强调的是多线程情况下对象初始化的结果必须对发生于其后的对象销毁方法可见。
public HappensBefore8(){ System.out.println("构造方法"); } @Override protected void finalize() throws Throwable { System.out.println("对象销毁"); } public static void main(String[] args){ new HappensBefore8(); System.gc(); }
关于有序性的那些疑问
原创声明:本文来自公众号【胖滚猪学编程】,以漫画形式让编程so easy and interesting,转载请注明出处!
扩展有序性的概念:Java内存模型中的程序天然有序性可以总结为一句话,如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。 这其实还涉及到一个高频面试考点:as-if-serial语义
as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
划重点:单线程中保证按照顺序执行。
synchronized同一时刻只有一个线程在运行,也就相当于保证了有序性。至于这个双重检查案例,出问题,并不是因为synchronized没有保证有序性。而是指令重排导致了在多个线程中无序。
总结
原创声明:本文来自公众号【胖滚猪学编程】,以漫画形式让编程so easy and interesting,转载请注明出处!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
手把手教你爬取天堂网1920*1080大图片(批量下载)——理论篇
/1 前言/ 平时我们要下载图片,要要一个一个点击下载是不是觉得很麻烦?那有没有更加简便的方法呢?答案是肯定的,这里我们以天堂网为例,批量下载天堂网的图片。 /2 项目准备工作/ 首先 我们第一步我们要安装一个pycham的软件。可以参考这篇文章:Python环境搭建—安利Python小白的Python和Pycharm安装详细教程。 天堂网的网址: https://www.ivsky.com/bizhi/1920x1080/ 我们需要下载几个库,怎么下载呢?打开pycharm,依次点击File,再点开Settings,如下图所示。 打开后会出现这个界面点击你的项目名字(project:(你的项目名字)),之后在project interpreter下,点击加号,而后下载我们需要的库,如下图所示。 本项目需要用到的是库是requests、lxml、fake_useragent,如下图所示。fake_useragent一般是没有的,需要通过下面的命令进行安装: pip install fake_useragent /3 项目实现/ 1. 导入需要的库(requests,lxml, fake...
- 下一篇
PrestaShop 1.7.6 在访问分类的时候提示错误
PrestaShop 1.7.6 在访问分类的时候提示内部错误。 当打开 Debug 模式的时候,提示的错误是 mb_split 方法不能全局调用的错误。 从错误提示上来看,你应该是在 PHP 中没有安装 mbstring 这个扩展。 如果你使用的是 CentOS 的话,你可以使用命令: yum install php-mbstring 进行安装。 各种语言都有自己的编码,他们的字节数是不一样的,mbstring扩展就是为确保不同编码的语言在PHP程序中能够正常显示提供解决方案。 mbstring扩展库用于处理多字节字符串,对PHP来说很重要的。 不同字符编码,其在内存占用的字节数不一样。如ASCII编码字符占用1个字节,UTF-8编码的中文字符是3字节,GBK为2个字节 ** 原因:*PHP不支持UNICODE,只有PHP变量支持中文,PHP变量的命名规则是a-zA-Z_x7f-xff,这个只是字符集的一部分,如果是unicode,就没必要限定这个范围了。 https://www.cwiki.us/display/PrestaShop/questions/62619727
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7