原子性

原子是最小单元、不可再分的意思。原子性是指某个操作在获取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并发编程(六)原子性与易变性的更多相关文章

  1. 【Java并发编程六】线程池

    一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...

  2. Java并发编程 (六) 线程安全策略

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.不可变对象-1 有一种安全的发布对象,即不可变对象. 1.不可变对象需要满足的条件 ① 对象创建以后 ...

  3. 02.java并发编程之原子性操作

    一.原子性操作 1.ThreadLocal 不同线程操作同一个 ThreadLocal 对象执行各种操作而不会影响其他线程里的值 注意:虽然ThreadLocal很有用,但是它作为一种线程级别的全局变 ...

  4. java 并发原子性与易变性 来自thinking in java4 21.3.3

    java 并发原子性与易变性  具体介绍请參阅thinking in java4 21.3.3 thinking in java 4免费下载:http://download.csdn.net/deta ...

  5. Java并发编程(六)volatile关键字解析

    由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...

  6. Java 并发编程(二):如何保证共享变量的原子性?

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...

  7. java并发编程笔记(六)——AQS

    java并发编程笔记(六)--AQS 使用了Node实现FIFO(first in first out)队列,可以用于构建锁或者其他同步装置的基础框架 利用了一个int类型表示状态 使用方法是继承 子 ...

  8. Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)

    摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...

  9. Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  10. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

随机推荐

  1. Angular面试题二

    十一.ng-repeat迭代数组的时候,如果数组中有相同值,会有什么问题,如何解决? 会提示 Duplicates in a repeater are not allowed. 加 track by ...

  2. Activiti 数据库表自动生成策略

    Activiti 引擎启动时默认会检测数据库版本与程序版本是否相符,不相符就会抛出异常停止引擎的初始化. 这一策略可以通过引擎的初始化配置参数databaseSchemaUpdate来控制, 如下图的 ...

  3. Fragment 重叠 遮盖问题

    1.导致Fragment 重叠 和遮盖的原因 主要还是因为Fragment的状态保存机制,当系统内存不足时,Fragment的主Activity被回收,Fragment的实例并没有随之被回收. Act ...

  4. maven项目在idea下右键不出现maven的解决办法

    重新删除项目,导出 再重新引入.

  5. JpaRepository 查询规范

    1.JpaRepository支持接口规范方法名查询.意思是如果在接口中定义的查询方法符合它的命名规则,就可以不用写实现,目前支持的关键字如下. Keyword Sample JPQL snippet ...

  6. Linux Swap扩容

    最近在做rac,在环境检查的时候发现swap空间检查不通过,所以我们第一想到的是对swap进行扩容,那么swap扩容有哪些方法呢?这里主要介绍两种方法,一通过添加额外磁盘,扩展swap分区,二是通过本 ...

  7. Python 解决写入csv中间隔一行空行问题

    转载解决写入csv中间隔一行空行问题 写入csv: with open(birth_weight_file,'w') as f: writer=csv.writer(f) writer.writero ...

  8. Linux 系统性能监控命令详解

    Linux 系统性能监控命令详解 CPU MEMORY IO NETWORK LINUX进程内存占用查看方法 系统负载过重时往往会引起其它子系统的问题,比如:->大量的读入内存的IO请求(pag ...

  9. video文件格式说明(笔记)

    video标签兼容IE8可使用html5media.js,具体demo可以下载文件中的压缩包 移动端兼容参考: http://www.xyhtml5.com/3252.html

  10. pythone 请求响应字典

    _RESPONSE_STATUSES = { # Informational 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing ...