Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例
2018-03-18 00:32
656 查看
[超级链接:Java并发学习系列-绪论]
本章主要学习Java并发中的三个特性:原子性、可见性和有序性。
在Java并发编程中,如果要保证代码的安全性,则必须保证代码的原子性、可见性和有序性。
本章的很多概念可以参考:Java并发11:Java内存模型、指令重排、happens-before原则
正确理解Java自带的原子性。下面的变量a、b都是基本数据类型的变量。
上面的5个基本数据类型的操作,只有1和2是原子性的。
a = true:包含一个操作,1.将true的赋值给a。
a = 5:包含一个操作,1.将5的赋值给a。
a = b:包含两个操作,1.读取b的值;2.将b的值赋值给a。
a = b + 2:包含三个操作,1.读取b的值;2.计算b+2;3.将b+2的计算结果赋值给a。
a ++:即a = a + 1,包含三个操作,读取a的值;2计算a+1;3.将a+1的计算结果赋值给a。
下面的代码实现了一个自增器(不是原子性的)。
下面的代码展示了在多线程环境中,调用此自增器进行自增操作。
某次运行结果:
通过观察结果,发现程序确实存在原子性问题。
通过synchronized关键字定义同步代码块或者同步方法保障原子性。
通过Lock接口保障原子性。
通过Atomic类型保障原子性。
以上三种原子性保障技术会在后续章节中继续学习。
有前面的文章可知,JVM对象变量的修改存在从Heap加载和到Heap更新的过程,所以存在可见性问题。
存在两个线程A、线程B和一个共享变量stop。
如果stop变量的值是false,则线程A会一直运行。如果stop变量的值是true,则线程A会停止运行。
线程B能够将共享变量stop的值修改为ture。
代码:
首先,定义一个共享变量stop(存在可见性问题):
然后,启动线程A和线程B:
运行结果:
从结果观察,发现线程B运行结束了,也就是说已经修改了共享变量stop的值。但是线程A还在运行,也就是说线程A并没有用接收到stop=true这个修改。
通过volatile关键字标记内存屏障保证可见性。
通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
通过Atomic类型保障可见性。
以上四种可见性保障技术会在后续章节中继续学习。
有前面的文章可知,JVM存在指令重排,所以存在有序性问题。
在Java中,由于happens-before原则,单线程内的代码是有序的,可以看做是串行(as-if-serial)执行的。但是在多线程环境下,多个线程的代码是交替的串行执行的,这就产生了有序性问题。
线程内部规则:在同一个线程内,前面操作的执行结果对后面的操作是可见的。
同步规则:如果一个操作x与另一个操作y在同步代码块/方法中,那么操作x的执行结果对操作y可见。
传递规则:如果操作x的执行结果对操作y可见,操作y的执行结果对操作z可见,则操作x的执行结果对操作z可见。
对象锁规则:如果线程1解锁了对象锁a,接着线程2锁定了a,那么,线程1解锁a之前的写操作的执行结果都对线程2可见。
volatile变量规则:如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1写入v及之前的写操作的执行结果都对线程2可见。
线程start原则:如果线程t在start()之前进行了一系列操作,接着进行了start()操作,那么线程t在start()之前的所有操作的执行结果对start()之后的所有操作都是可见的。
线程join规则:线程t1写入的所有变量,在任意其它线程t2调用t1.join()成功返回后,都对t2可见。
而有序性问题,都是发生在happens-before原则之外的状况。
不过本人通过实际编程,并没有重现这段程序的无序性。
所以为了更方便的理解有序性问题,本人使用了后面的示例,虽然这个示例有些不太匹配。
场景说明:
有两个线程A和线程B。
线程A对变量x进行加法和减法操作。
线程B对变量x进行乘法和出发操作。
代码:
这里的示例只是为了方便得到无序的结果而专门写到,所以有些奇特。
运行结果:
通过运行结果发现,多线程环境中,代码是交替的串行执行的,这样会导致产生意料之外的结果。
通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
以上两种有序性保障技术会在后续章节中继续学习。
[2] 并发编程之原子性、可见性、有序性的简单理解
[3] java并发之原子性、可见性、有序性
[4] 同步和Java内存模型(四)有序性
本章主要学习Java并发中的三个特性:原子性、可见性和有序性。
在Java并发编程中,如果要保证代码的安全性,则必须保证代码的原子性、可见性和有序性。
本章的很多概念可以参考:Java并发11:Java内存模型、指令重排、happens-before原则
1.原子性(Atomicity)
1.1.原子性定义
原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。1.2.Java自带的原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作。正确理解Java自带的原子性。下面的变量a、b都是基本数据类型的变量。
a = true;//1 a = 5;//2 a = b;//3 a = b + 2;//4 a ++;//5
上面的5个基本数据类型的操作,只有1和2是原子性的。
a = true:包含一个操作,1.将true的赋值给a。
a = 5:包含一个操作,1.将5的赋值给a。
a = b:包含两个操作,1.读取b的值;2.将b的值赋值给a。
a = b + 2:包含三个操作,1.读取b的值;2.计算b+2;3.将b+2的计算结果赋值给a。
a ++:即a = a + 1,包含三个操作,读取a的值;2计算a+1;3.将a+1的计算结果赋值给a。
1.3.原子性问题示例
由上面的章节已知,不采取任何的原子性保障措施的自增操作并不是原子性的。下面的代码实现了一个自增器(不是原子性的)。
/** * <p>原子性示例:不是原子性</p> * * @author hanchao 2018/3/10 14:58 **/ static class Increment { private int count = 1; public void increment() { count++; } public int getCount() { return count; } }
下面的代码展示了在多线程环境中,调用此自增器进行自增操作。
int type = 0;//类型 int num = 50000;//自增次数 int sleepTime = 5000;//等待计算时间 int begin;//开始的值 Increment increment; //不进行原子性保护的大范围操作 increment = new Increment(); begin = increment.getCount(); LOGGER.info("Java中普通的自增操作不是原子性操作。"); LOGGER.info("当前运行类:" +increment.getClass().getSimpleName() + ",count的初始值是:" + increment.getCount()); for (int i = 0; i < num; i++) { new Thread(() -> { increment.increment(); }).start(); } //等待足够长的时间,以便所有的线程都能够运行完 Thread.sleep(sleepTime); LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment.getCount());
某次运行结果:
2018-03-17 22:52:23 INFO ConcurrentAtomicityDemo:132 - Java中普通的自增操作不是原子性操作。 2018-03-17 22:52:23 INFO ConcurrentAtomicityDemo:133 - 当前运行类:Increment,count的初始值是:1 2018-03-17 22:52:33 INFO ConcurrentAtomicityDemo:141 - 进过50000次自增,count应该 = 50001,实际count = 49999
通过观察结果,发现程序确实存在原子性问题。
1.4.原子性保障技术
在Java中提供了多种原子性保障措施,这里主要涉及三种:通过synchronized关键字定义同步代码块或者同步方法保障原子性。
通过Lock接口保障原子性。
通过Atomic类型保障原子性。
以上三种原子性保障技术会在后续章节中继续学习。
2.可见性(Visibility)
2.1.可见性定义
可见性:当一个线程修改了共享变量的值,其他线程能够看到修改的值。有前面的文章可知,JVM对象变量的修改存在从Heap加载和到Heap更新的过程,所以存在可见性问题。
2.2.可见性问题示例
场景说明:存在两个线程A、线程B和一个共享变量stop。
如果stop变量的值是false,则线程A会一直运行。如果stop变量的值是true,则线程A会停止运行。
线程B能够将共享变量stop的值修改为ture。
代码:
首先,定义一个共享变量stop(存在可见性问题):
//普通情况下,多线程不能保证可见性 private static boolean stop;
然后,启动线程A和线程B:
//普通情况下,多线程不能保证可见性 new Thread(() -> { System.out.println("Ordinary A is running..."); while (!stop) ; System.out.println("Ordinary A is terminated."); }).start(); Thread.sleep(10); new Thread(() -> { System.out.println("Ordinary B is running..."); stop = true; System.out.println("Ordinary B is terminated."); }).start();
运行结果:
Ordinary A is running... Ordinary B is running... Ordinary B is terminated.
从结果观察,发现线程B运行结束了,也就是说已经修改了共享变量stop的值。但是线程A还在运行,也就是说线程A并没有用接收到stop=true这个修改。
1.4.可见性保障技术
在Java中提供了多种可见性保障措施,这里主要涉及四种:通过volatile关键字标记内存屏障保证可见性。
通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
通过Atomic类型保障可见性。
以上四种可见性保障技术会在后续章节中继续学习。
3.有序性(orderly)
3.1.有序性定义
有序性:即程序执行的顺序按照代码的先后顺序执行。有前面的文章可知,JVM存在指令重排,所以存在有序性问题。
在Java中,由于happens-before原则,单线程内的代码是有序的,可以看做是串行(as-if-serial)执行的。但是在多线程环境下,多个线程的代码是交替的串行执行的,这就产生了有序性问题。
3.2.Java自带的有序性
在前面的文章可知,Java提供了happens-before原则保证程序基本的有序性,主要规则如下:线程内部规则:在同一个线程内,前面操作的执行结果对后面的操作是可见的。
同步规则:如果一个操作x与另一个操作y在同步代码块/方法中,那么操作x的执行结果对操作y可见。
传递规则:如果操作x的执行结果对操作y可见,操作y的执行结果对操作z可见,则操作x的执行结果对操作z可见。
对象锁规则:如果线程1解锁了对象锁a,接着线程2锁定了a,那么,线程1解锁a之前的写操作的执行结果都对线程2可见。
volatile变量规则:如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1写入v及之前的写操作的执行结果都对线程2可见。
线程start原则:如果线程t在start()之前进行了一系列操作,接着进行了start()操作,那么线程t在start()之前的所有操作的执行结果对start()之后的所有操作都是可见的。
线程join规则:线程t1写入的所有变量,在任意其它线程t2调用t1.join()成功返回后,都对t2可见。
而有序性问题,都是发生在happens-before原则之外的状况。
3.3.有序性问题示例
前置说明,其实网上有很多关于有序性的实例,类似如下://线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
不过本人通过实际编程,并没有重现这段程序的无序性。
所以为了更方便的理解有序性问题,本人使用了后面的示例,虽然这个示例有些不太匹配。
场景说明:
有两个线程A和线程B。
线程A对变量x进行加法和减法操作。
线程B对变量x进行乘法和出发操作。
代码:
这里的示例只是为了方便得到无序的结果而专门写到,所以有些奇特。
static String a1 = new String("A : x = x + 1"); static String a2 = new String("A : x = x - 1"); static String b1 = new String("B : x = x * 2"); static String b2 = new String("B : x = x / 2"); //不采取有序性措施,也没有发生有序性问题..... LOGGER.info("不采取措施:单线程串行,视为有序;多线程交叉串行,视为无序。"); new Thread(() -> { System.out.println(a1); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a2); }).start(); new Thread(() -> { System.out.println(b1); System.out.println(b2); }).start();
运行结果:
2018-03-18 00:16:20 INFO ConcurrentOrderlyDemo:63 - 不采取措施:单线程串行,视为有序;多线程交叉串行,视为无序。 A : x = x + 1 B : x = x * 2 B : x = x / 2 A : x = x - 1
通过运行结果发现,多线程环境中,代码是交替的串行执行的,这样会导致产生意料之外的结果。
3.4.有序性保障技术
在Java中提供了多种有序性保障措施,这里主要涉及两种:通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
以上两种有序性保障技术会在后续章节中继续学习。
参考文献
[1] Java并发编程:volatile关键字解析[2] 并发编程之原子性、可见性、有序性的简单理解
[3] java并发之原子性、可见性、有序性
[4] 同步和Java内存模型(四)有序性
相关文章推荐
- Java并发15:并发三特性-有序性定义、有序性问题与有序性保证技术
- Java并发13:并发三特性-原子性定义、原子性问题与原子性保证技术
- Java多线程总结(5)— 原子性、可见性、有序性和并发库的原子性操作
- java 并发概念与内存分析,原子性、可见性、有序性
- 精确解释java的volatile之可见性、原子性、有序性(通过汇编语言)
- Java 线程概述: 线程种类、状态,原子性、内存可见性、synchronized、volatile
- java并发编程--原子性、可见性、有序性
- java并发-原子性和可见性(7)
- java并发之原子性、可见性、有序性
- Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术
- 聊聊高并发(十九)理解并发编程的几种"性" -- 可见性,有序性,原子性
- Java| Java并发特性: 原子性、有序性、可见性
- Java并发_volatile实现可见性但不保证原子性
- java得用锁解决并发问题的简单示例
- 【并发】java并发之可见性与原子性
- java并发问题概述
- Java并发_volatile实现可见性但不保证原子性
- java并发特性,原子性、有序性、可见性
- 聊聊高并发(十九)理解并发编程的几种"性" -- 可见性,有序性,原子性
- java面试题-用JAVA中的多线程示例火车站售票问题