JUC:
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了用于多线程上下文中的 Collection 实现等。
三个特性:
在并发编程中的三个特性:(1)互斥性(原子性)(2)内存可见性(3)指令重排序
- volatile 关键字:
volatile:易挥发的,不稳定
volatile 不具备"互斥性";不能保证变量的"原子性";当多个线程进行操作共享数据时,可以保证内存中的数据是可见的,相较于synchronized 是一种较为轻量级的同步策略
- synchronized 和 volatile 的区别:
(1)synchronized 可以保证互斥性,可以实现内存可见性,不能禁止指令重排序。
(2)volatile 不能保证原子性,可以实现内存可见性,禁止指令重排序。
-
内存可见性问题:
import java.io.IOException;
public class Demo {
public static void main(String[] args) throws IOException {
//1.创建线程对象
MyThread thread = new MyThread();
thread.start();
System.out.println("输入任意字符结束子线程:");
System.in.read();
thread.flag = true;
System.out.println(thread.flag);
System.out.println("主线程结束了");
}
}
class MyThread extends Thread{
public boolean flag;//(默认为false)
@Override
public void run() {
System.out.println("子线程开始执行");
while (true) {
if (flag) {
break;
}
}
System.out.println("子线程结束了");
}
}
运行结果:
可以看到程序还在运行,因为子线程并没有结束(子线程的缓存区保存着内存中 flag 的原始数据,并没有访问到主存中已更改的 flag)(为了运行效率,每个线程具备自己的缓存区)
即对于子线程来说,是没有访存的(内存的不可见性)
- 在线程类的共享资源 flag 上添加 volatile 关键字:
class MyThread extends Thread{
//volatile 去掉子线程的缓存(用内存屏障实现),效率降低,但是解决了内存可见性问题
public volatile boolean flag;
@Override
public void run() {
System.out.println("子线程开始执行");
while (true) {
if (flag) {
break;
}
}
System.out.println("子线程结束了");
}
}
运行结果:
- synchronized 实现内存可见性:
用 synchronized也可以实现内存可见性,但效率低。
public class Test {
public static void main(String[] args) throws IOException {
ThreadTest2 thread0 = new ThreadTest2("歪比歪比");
ThreadTest2 thread1 = new ThreadTest2("歪比巴布");
ThreadTest2 thread2 = new ThreadTest2("玛卡巴卡");
thread0.start();
thread1.start();
thread2.start();
System.out.println("输入任意字符结束子线程:");
System.in.read();
ThreadTest2.flag = true;
System.out.println(ThreadTest2.flag);
System.out.println("主线程结束了");
}
}
class ThreadTest2 extends Thread {
public static boolean flag;//(默认为false)
public ThreadTest2() {}
public ThreadTest2(String name) {
super(name);
}
@Override
public void run() {
System.out.println("子线程开始执行");
while (true) {
synchronized (this){
if (flag) {
break;
}
}
}
System.out.println("子线程"+Thread.currentThread().getName()+"结束了");
}
}
运行结果:
-
禁止指令重排序:
/*
* 单例设计模式:
* 懒汉式:
* 1.私有化构造方法
* 2.类内部创建对象
* 3.添加公开访问方法
*/
public class Demo1 {
private volatile static Demo1 instance;
private Demo1() {super();}
public static Demo1 getInstance() {
if(instance == null) {
synchronized (Demo1.class) {//此处消耗大,在外层加上判断
if (instance == null) { //双重检查:double check
instance = new Demo1();
/*
* 实例化过程:
* 1.堆中开辟空间
* 2.调用构造方法初始化对象
* 3.把对象的地址赋值给变量
*/
}
}
}
return instance;
}
}
判断同步性能消耗大 --> 双重检查机制 --> 实例化过程并不是原子性 --> JVM优化指令顺序后线程1执行了实例化过程的(1)和(3),但并未执行(2)初始化对象,此时线程2获得CPU,会拿到一个未初始化的对象并返回,出现异常!!!
----> 在对象上添加 volatile 关键字禁止指令重排序
- 单例的静态内部类写法:
/*
* 单例设计模式:
* 静态内部类写法:
* 1.私有化构造方法
* 2.创建静态内部类,在静态内部类中创建常量
* 3.添加公开方法,返回这个对象
*/
public class Demo2 {
private Demo2() {super();}
/*
* 静态内部类不用的时候不会初始化
* 1.节省内存空间
* 2.解决了线程安全问题
*/
private static class SingleTonHolder{
private static final Demo2 instance = new Demo2();
}
public static Demo2 getInstance() {
return SingleTonHolder.instance;
}
}
此写法在安全的基础上更节省了内存空间,比饿汉式更优秀!
-
原子性:
- i++ 的原子性问题:
public class Demo3 {
public static void main(String[] args) {
int num = 10;
num++;
System.out.println(num);
}
}
在相应类的字节码(.class)文件夹下运行cmd,用指令javap可查看字节码文件的内容:
(1) i++的操作实际上分为三个步骤: "读‐改‐写";
i++可拆分为:
int temp1=i;
int temp2=temp+1;
i=temp2;
(2) 原子性: 就是"i++"的"读‐改‐写"是不可分割的三个步骤;
(3) 原子变量: JDK1.5 以后, java.util.concurrent.atomic 包下,提供了常用的原子变量:
3.1 原子变量中的值,使用 volatile 修饰,保证了内存可见性
3.2 CAS(Compare‐And‐Swap) 算法保证数据的原子性
public class Demo4 {
public static void main(String[] args) {
TreadTest threadTest = new TreadTest();
for (int i = 0; i < 5; i++) {//一个对象5个线程引用
new Thread(threadTest).start();
}
}
}
class TreadTest implements Runnable{
private int num = 0;
@Override
public void run() {
try {
Thread.sleep(200);//线程休眠0.2秒,效果更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(++num);
}
}
运行结果:
- 3.1使用原子变量
import java.util.concurrent.atomic.AtomicInteger;
public class Demo4 {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
for (int i = 0; i < 5; i++) {
new Thread(threadTest).start();
}
}
}
class ThreadTest implements Runnable{
private AtomicInteger num = new AtomicInteger(0);
@Override
public void run() {
System.out.println(num.incrementAndGet());//++i;
// System.out.println(num.getAndIncrement());//i++;
}
}
运行结果:
- 3.2 CAS 算法
CAS(Compare-And-Swap) 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;CAS 是一种无锁的非阻塞算法(属于乐观锁)的实现;
CAS 包含了三个操作数:
进行比较的旧预估值:A
需要读写的内存值:V
将写入的更新值:B
当且仅当 A == V 时, V = B;否则,将不做任何操作,并且这个比较交换过程属于原子操作
- 模拟CAS算法
//模拟CAS算法
import java.util.Random;
public class Demo5 {
public static void main(String[] args) {
CompareAndSwap compareAndSwap = new CompareAndSwap();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
int expect = compareAndSwap.getValue();
boolean b =
compareAndSwap.compareAndSet(expect, new Random().nextInt(101));
System.out.println(b);
if (b) {
break;
}
}
}
}).start();
}
}
}
class CompareAndSwap{
private int value;//V
public synchronized int getValue() {
return value;
}
//A:expect B:newValue
public synchronized boolean compareAndSet(int expect,int newValue) {
if (expect == value) {
this.value = newValue;
System.out.println("赋值成功:"+this.value);
return true;
}else {
return false;
}
}
}
运行结果:
- ABA问题:
在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。
假设如下事件序列:
线程 1 从内存位置V中取出A。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,将B写入位置V。
线程 2 将A再次写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。
- 解决 ABA 问题
在多线程环境中,对内存值 V 加上版本戳,在比较时不只比较 A 和 V,也比较版本号,就可做到控制 ABA 问题了
//CAS算法中的 ABA 问题
import java.util.concurrent.atomic.AtomicStampedReference;
public class Demo6 {
//解决方法:加版本戳,在更改V(内存值)的时候,版本戳+1,通过版本号判断 V 是否变化
private static AtomicStampedReference<Integer> integer=new AtomicStampedReference<Integer>(0, 0);
//(0,0) (初始值,版本戳) --> 比较的时候除了比较期望值和原始值,还比较戳
public static void main(String[] args) throws Exception{
for(int i=0;i<100;i++) {
//Thread.sleep(10);
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
//1.获取版本号
int stamp = integer.getStamp();
//2.获取期望值
Integer reference = integer.getReference();
//compareAndSet(期望值,新值,期望戳,新戳)
boolean b = integer.compareAndSet(reference, reference+1, stamp, stamp+1);
if(b) {
System.out.println(reference+1);
break;
}
}
}
}).start();
}
}
}
运行结果:输出唯一的 1-100 ,但不保证输出顺序