Java并发编程(六)原子性与易变性
原子性
原子是最小单元、不可再分的意思。原子性是指某个操作在获取CPU时间时,要么就给它足够时间,让这个操作执行完,要么就不执行这个操作,执行时不能出现上下文切换(把CPU时间从一个线程分配到另一个线程)。
Java中对变量的读取和赋值都是原子操作,但long、double类型除外,只有使用volatile修饰之后long、double类型的读取和赋值操作才具有原子性。除此之外Java还提供了几个常用的原子类,原子类的方法是具有原子性的方法,也就是说原子类在执行某个方法的过程中不会出现上下文切换。
前面两篇我们讲的锁,锁可以保证当两个线程同时对一个整型变量进行自增操作时的正确性。自增操作分为三步:1. 读取变量的值;2. 将这个值加一;3. 将加一后的值写入到变量中。不使用锁导致计算结果错误的根源就是一个线程在执行这三个操作的过程中发生了上下文切换。通过使用锁可以保证在进行这三个操作的过程中只有一个线程执行临界区的代码,其余想获取锁的线程都被阻塞了(注:这时也是会发生上下文切换的,只是不会把CPU时间分配给阻塞线程而已);而使用原子类可以使CPU在自增操作时不切换时间片,从而在根本上解决了问题。
我们使用原子类来进行变量自增:
class IncreaseThread implements Runnable {
@Override
public void run() {
for(int i=0;i < 100000; i++) {
AtomicIntegerTest.value.incrementAndGet();
}
}
}
public class AtomicIntegerTest {
public static AtomicInteger value = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new IncreaseThread());
exec.execute(new IncreaseThread());
exec.shutdown();
Thread.sleep(5000);//等待两个线程执行结束
System.out.println("Value = " + value);
}
}
五秒后输出如下结果:
Value = 200000
我们使用线程池创建了两个线程,这两个线程同时对AtomicIntegerTest的value属性进行自增操作。AtomicInteger是int类型对应的原子类,调用这个类的incrementAndGet()方法可以实现自增,并且不需要使用锁的保护就可以得到正确的结果。
除了AtomicInteger之外,Java中还实现了AtomicLong、AtomicBoolean、AtomicReference等原子类,其使用方法与AtomicInteger类似,读者可自行测试。
易变性
Java volatile关键字用于通知虚拟机这个变量具有易变性,那么什么是易变性呢?易变性比原子性更为复杂,在工业上导致的问题也更多,其中易变性有两层含义:
1. 可见性
Java虚拟机会为每个线程分配一块专属的内存,称之为工作内存;不同的线程之间共享的数据会被放到主内存中。工作内存主要包含方法的参数、局部变量(在函数中定义的变量),这些变量都是线程私有的,不会被其它线程共用。实例的属性、类的静态属性都是可以被共享的,每个线程在操作这些数据时都是先从主内存中读取到工作内存再进行操作,操作结束后再写入到主内存中。可见性要求线程对共享变量修改后立即写入到主内存中,线程读取共享变量时也必须去主内存中重新加载,不能直接使用工作内存中的值。Java中的变量在默认情况下是不具有可见性的,需要用volatile关键字修饰才具有可见性,让我们做一个测试:
class NewThread implements Runnable {
public volatile static long value;
public void run() {
while(VolatileTest.run) {
value++;
}
System.out.println("Done");
}
}
public class VolatileTest {
public static boolean run = true;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new NewThread());
exec.shutdown();
Thread.sleep(500);
run = false;
System.out.println("run: " + run);
System.out.println("value: " + NewThread.value);
Thread.sleep(500);
System.out.println("value: " + NewThread.value);
}
}
一秒后输出如下结果,并且程序始终没有停止:
run: false
value: 1655066633
value: 3319764420
在VolatileTest类中定义了一个静态的布尔属性,这个布尔属性用于控制新建线程中是否继续循环,每次循环都对value值加一,为了保证value的值对其它线程可见,我们使用了volatile来修饰它。启动新线程0.5秒后我们将run的值改成false并打印出当前的value值,再过0.5秒又打印了一遍,这次的值比上一个值更大,说明新线程并没有因为run值变成了false而停止,因为新线程没有看到run值的变化。示意图如下所示:
如果我们将run变量用volatile修饰,打印两次value的值就会得到相同的结果,感兴趣的读者可以自行测试。
2. 有序性
易变性另一层含义就是有序性,是指禁止CPU对指令重排优化,默认情况下CPU会对指令进行合理的重排优化,重排优化仅保证单线程运行时结果的正确性,不保证执行顺序。但是虚拟机不会对指令任意重排,而是有一定的规则。
不可重排的情况:
int a = 1;
int b = a;
上面代码的两个语句之间存在依赖关系,如果两个语句的执行顺序被改变将导致逻辑的变化,准确的说会导致执行错误。
可重排的情况:
int a = 1;
int b = 1;
a++;
上面代码中是可以发生指令重排的,其中只要保证第一行始终在第三行之前执行,就不会导致逻辑错误。虚拟机会根据执行的具体情况进行指令重排优化,在单线程执行时,这种重排不会导致程序的逻辑问题,而多线程并发执行时就会存在逻辑问题,伪代码如下:
int a;
int b; //线程1执行initialize()方法
initialize() {
a = 1;
b = 1;
} //线程2执行
monitor() {
if(b == 1) {
print("初始化完毕");
}
else {
print("初始化还没有结束");
}
}
两个线程分别执行initialize()方法和monitor()方法,如果没有发生指令重排,线程2根据b是否等于1来判断初始化是否结束是没有逻辑问题的。但是初始化a,b两个变量之间没有依赖关系,虚拟机是可以根据需要来指令重排的,这时再根据b是否等于1来判断就是错误的,虚拟机有可能先初始化变量b后初始化变量a。除了保证可见性之外,volatile第二个功能就是保证有序性,即禁止虚拟机对该变量进行指令重排。
3. 锁与易变性
volatile保证了易变性,锁不仅保证了易变性,还保证了线程间的互斥性,即所有线程在进入临界区之前都必须排队,当使用锁时不需要临界区内所有的变量都不需要声明为volatile。volatile相当于是轻量级的锁,volatile关键字的功能没有锁更强大,但是其性能也会比锁更好。
总结
本章讲了原子性和易变性,原子性是指CPU在执行指令集的过程中不能发生上下文切换,易变性指变量的变化对所有线程可见,并且JVM对该变量的操作不能发生指令重排。理论上讲原子性和易变性是两个平行的概念,然而Java中的原子类(AtomicInteger等)在实现的时候使用了volatile关键字,所以Java中的原子类的操作也具有易变性。实际上原子性+易变性>锁,CPU在执行临界区内的代码时也会发生上下文切换,比如临界区的代码是打印一万个Hello World,一个线程执行临界区,另一个线程负责打印World Hello,执行代码会发现万军丛中有一个World Hello,从而证明CPU在执行临界区代码的时候也会发生上下文切换。然而在逻辑上我们可以理解为原子性+易变性=锁,因为即使临界区内发生了上下文切换,其它线程也不会进入临界区,因此不会对临界区的结果造成影响。
公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。
Java并发编程(六)原子性与易变性的更多相关文章
- 【Java并发编程六】线程池
一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...
- Java并发编程 (六) 线程安全策略
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.不可变对象-1 有一种安全的发布对象,即不可变对象. 1.不可变对象需要满足的条件 ① 对象创建以后 ...
- 02.java并发编程之原子性操作
一.原子性操作 1.ThreadLocal 不同线程操作同一个 ThreadLocal 对象执行各种操作而不会影响其他线程里的值 注意:虽然ThreadLocal很有用,但是它作为一种线程级别的全局变 ...
- java 并发原子性与易变性 来自thinking in java4 21.3.3
java 并发原子性与易变性 具体介绍请參阅thinking in java4 21.3.3 thinking in java 4免费下载:http://download.csdn.net/deta ...
- Java并发编程(六)volatile关键字解析
由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...
- Java 并发编程(二):如何保证共享变量的原子性?
线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...
- java并发编程笔记(六)——AQS
java并发编程笔记(六)--AQS 使用了Node实现FIFO(first in first out)队列,可以用于构建锁或者其他同步装置的基础框架 利用了一个int类型表示状态 使用方法是继承 子 ...
- Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)
摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...
- Java并发编程之验证volatile不能保证原子性
Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
随机推荐
- Windows下本机简易监控系统搭建(Telegraf+Influxdb+Grafana)
一.文件准备 1.1 文件名称 telegraf-1.2.1_windows_amd64.zip influxdb-1.2.2_windows_amd64.zip grafana-4.2.0.wind ...
- Populating Next Right Pointers in Each Node 设置二叉树的next节点
Given a binary tree struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *nex ...
- Android获取蓝牙地址
最近做一个项目,发现Android6.0以上的版本获取的蓝牙地址始终为02:00:00:00, Google一下发现Android早就封掉了相关接口,于是想到反射的方式去获取Mac地址,在此记录一下 ...
- 更改 centos yum 源
1.进入存放源配置的文件夹 cd /etc/yum.repos.d 2.检查wget是否安装,没有安装先安装wget 3.备份默认源 mv ./CentOS-Base.repo ./CentOS- ...
- 简单的3proxy配置
timeouts 1 5 30 60 180 1800 15 60log "D:\Program Files\3proxy-0.6.1-x64\cfg\3proxy.log" Dl ...
- 《SQL必知必会》知识点汇总
select CustomerNo from dbo.Customers; 通配符的使用 select *from dbo.Customers; select CustomerNo from dbo. ...
- Python学习---网络编程 1217【all】
OSI七层模型: 物理层, 数据链路层, 网络层,传输层,会话层,表达层,应用层 应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等 传输层:TCP,UDP 网络层:I ...
- 25个最佳的SSH命令
参考文献地址(SSH原理与运用(一):远程登录): http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html 参考文献地址(SSH原理与 ...
- 整合VIM和Graphviz,并且使用本办法实现实时预览
在编程或是整理知识的时候一直苦于没有一款可以帮助理清思路的工具. 在网上苦寻良久,终于找到了一款可心可意的小软件 —— Graphviz. 折腾了一番,终于可以凑合着用了. 现将折腾的成果记录于此以作 ...
- linux性能系列--网络
一.为啥网络监控不好做? 回答:网络是所有子系统中最难监控的了.首先是由于网络是抽象的,更重要的是许多影响网络的因素并不在我们的控制范围之内.这些因素包括,延迟.冲突.阻塞等 等.由于网络监控中, ...