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在执行临界区代码的时候也会发生上下文切换。然而在逻辑上我们可以理解为原子性+易变性=锁,因为即使临界区内发生了上下文切换,其它线程也不会进入临界区,因此不会对临界区的结果造成影响。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

原文地址:https://www.cnblogs.com/victorwux/p/9095139.html

时间: 2024-10-09 17:23:51

Java并发编程(六)原子性与易变性的相关文章

Java并发编程(六) 一个日志服务的例子

日志服务需要提供的功能有: 可以从外部安全地开启和关闭日志服务: 可以供多个线程安全地记录日志消息: 在日志服务关闭后,可以把剩余未记录的消息写入日志文件: public class LogService { private final BlockingQueue<String> msgQueue; //阻塞的消息队列保存日志消息 private final PrintWrite writer; //写消息到日志文件 private final LoggerThread logThread;

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

java 并发原子性与易变性  具体介绍请參阅thinking in java4 21.3.3 thinking in java 4免费下载:http://download.csdn.net/detail/liangrui1988/7580155 package org.rui.thread.volatiles; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 假设

《Java并发编程实战》第十六章 Java内存模型 读书笔记

Java内存模型是保障多线程安全的根基,这里仅仅是认识型的理解总结并未深入研究. 一.什么是内存模型,为什么需要它 Java内存模型(Java Memory Model)并发相关的安全发布,同步策略的规范.一致性等都来自于JMM. 1 平台的内存模型 在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证. JVM通过在适当的位置上插入内存栅栏来屏蔽在JVM与底层平台内存模型之间的

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

线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心扑在工作上面,所以处理的蛮得心应手,心态也一直保持的不错:但有了副业之后,心态就变得像坐过山车一样.副业收入超过主业的时候,人特别亢奋,像打了鸡血一样:副业迟迟打不开局面的时候,人就变得惶惶不可终日. 仿佛我就只能是个单线程,副业和主业并行开启多线程模式的时候,我就变得特别没有安全感,尽管整体的收入

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

通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯哥Java:kaigejava)将通过代码演示来证明为什么说volatile不能够保证共享变量的原子性操作. 我们来举个现实生活中的例子: 中午去食堂打饭,假设你非常非常的饥饿,需要一荤两素再加一份米饭.如果食堂打饭的阿姨再给你打一个菜的时候,被其他人打断了,给其他人打饭,然后再回过头给你打饭.你选一荤两素再加一份米饭打完的过程被打断了四次耗时30分钟

Java并发编程的艺术(六)——线程间的通信

多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java并发编程的艺术(三)--volatile 1.1 如何实现通信? 这两种方式都采用了同步机制实现多条线程间的数据通信.与其说是"通信",倒不如说是"共享变量"来的恰当.当一个共享变量被volatile修饰 或 被同步块包裹后,他们的读写操作都会直接操作共享内存,从而各个

[Java 并发] Java并发编程实践 思维导图 - 第六章 任务执行

根据<Java并发编程实践>一书整理的思维导图.希望能够有所帮助. 第一部分: 第二部分: 第三部分:

Java并发编程(六)阻塞队列

相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)ConcurrentHashMap的实现原理和源码分析 前言 在Android多线程(一)线程池这篇文章时,当我们要创建ThreadPoolExecutor的时候需要传进来一个类型为BlockingQueue的参数,它就是阻塞队列,在这一篇文章里我们会介绍阻塞队列的定义.种类.实现原理以及应用. 1.什么是阻塞队

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显

Java 并发编程:核心理论

并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系列会从线程间协调的方式(wait.notify.notifyAll).Synchronized及Volatile的本质入手,详细解释JDK为我们提供的每种并发工具和底层实现机制.在此基础上,我们会进一步分析java.util.concurrent包的工具类,包括其使用方式.实现源码及其背后的原理.本