1、Java JUC简介
在Java5.0提供了 java.util.concurrent(简称JUC)包,此包中增加在并发编程中常用的工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection实现等
2、volatile关键字-内存可见性
2.1内存可见性
Java内存模型规定,对于多个线程共享的变量,存储在主内存中;每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作共享变量,只能通过操作工作内存中的副本来实现,操作完成之后在同步到主内存中
JVM模型规定:1)线程对共享变量的所有操作必须在自己的工作内存中进行,不能从主存中读写;2)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需通过主内存来完成
线程对共享变量的修改没能及时更新到主内存,这就引出了内存可见性
内存可见性(Memory Visibility),指当某个线程正在使用对象状态,而另一个线程在同时修改状态,需确保当一个线程修改了对象状态后,其他线程能及时得到发生的状态变化
//案例 main线程和自定义线程同时操作共享变量flag
//td线程对flag操作,修改结果为true
//main线程得到状态仍为false,while循环一直执行
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){
if(td.isFlag()){
System.out.println("------------");
break;
}
}
}
}
class ThreadDemo implements Runnable{
//共享数据,定义标记初始值为false
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag:"+isFlag());
}
}
//解决方案1,加入同步代码块中
while(true){
synchronized (td){
if(td.isFlag()){
System.out.println("------------");
break;
}
}
}
//解决方案2,给共享数据添加volatile关键字
private volatile boolean flag = false;
2.2 volatile关键字
Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作及时通知到其它线程。
使用volatile声明共享变量后,线程对该变量的修改会立即刷新主内存,同时使其他线程中缓存的该变量无效,从而其他线程在读取该值时会从主内存中重新读取该值。保证了总是会返回最新写入的值
volatile屏蔽掉了JVM中的代码优化(指令重排序),并且每次都从主存中读取数据进行操作,所以效率会降低
volatile关键字主要作用:
- 保证共享变量的内存可见性
- 阻止指令重排序
相较于synchronized,volatile是一种较为轻量级的同步策略
- volatile不具备“互斥性”
- 不能保证变量的“原子性”
3、原子变量-CAS算法
3.1 原子变量
保证线程安全是 Java 并发编程必须要解决的重要问题。Java 从原子性、可见性、有序性这三大特性入手,确保多线程的数据一致性。
- 确保线程安全最常见的做法是利用锁机制(
Lock
、sychronized
)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,那么操作必然是原子性的,线程安全的。互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题。 volatile
可以看成是轻量级的锁(比普通锁性能要好),它保证了共享变量在多线程中的可见性,但无法保证原子性。所以它只能在一些特定场景下使用。- 为了兼顾原子性以及锁带来的性能问题,Java 引入了 CAS (主要体现在
Unsafe
类)来实现非阻塞同步(也叫乐观锁)。并基于 CAS ,提供了一套原子工具类。
原子变量类 比锁的粒度更细,更轻量级,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上
原子变量类相当于一种泛化的
volatile
变量,能够支持原子的、有条件的读/改/写操作。原子类在内部使用 CAS 指令(基于硬件的支持)来实现同步。这些指令通常比锁更快。
java.util.concurrent.atomic包定义了一些常见类型的原子变量类
- 基本类型
- AtomicBoolean - 布尔类型原子类
- AtomicInteger - 整型原子类
- AtomicLong - 长整型原子类
- 引用类型
- AtomicReference - 引用类型原子类
- AtomicMarkableReference - 带有标记位的引用类型原子类
- AtomicStampedReference - 带有版本号的引用类型原子类
- 数组类型
- AtomicIntegerArray - 整形数组原子类
- AtomicLongArray - 长整型数组原子类
- AtomicReferenceArray - 引用类型数组原子类
- 属性更新器类型
- AtomicIntegerFieldUpdater - 整型字段的原子更新器
- AtomicLongFiledUpdater - 长整型字段的原子更新器
- AtomicReferenceFieldUpdater - 原子更新引用类型里的字段
这些原子变量提供了一种操作单一变量无锁(lock-free)的线程安全(thread-safe)方式
原子变量由volatile保证内存可见性,由CAS(Compare-And-Swap)算法保证数据的原子性
3.2 CAS算法
Compare And Swap(比较并替换)
CAS是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。CAS 是一种无锁的非阻塞算法的实现,能够在不使用锁的情况下实现多线程之间的变量同步(非阻塞同步)。
当进行CAS操作时,先比较内存地址和原先预期地址是否相同,如果相同表示内存地址未被修改,可以用新地址替换;如果不相同则不执行操作,整个过程是一个原子操作
CAS包含了三个操作数:
- 需要读写的内存值V
- 进行比较的值A
- 拟写入的新值B
仅当 V == A 时,V = B,否则将不做任何操作
ABA问题:
CAS会导致ABA问题,线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的线程已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号
模拟CAS算法
/**
* 模拟CAS算法
* */
public class TestCompareAndSwap {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expectedValue = cas.getValue();
boolean result = cas.compareAndSet(expectedValue, new Random().nextInt(101));
System.out.println(result);
}
}).start();
}
}
}
class CompareAndSwap{
private int value;
//获取内存值
public synchronized int getValue(){
return value;
}
//比较
public synchronized int compareAndSwap(int expectedValue, int newValue){
int oldValue = value;
if(oldValue == expectedValue){
this.value = newValue;
}
return oldValue;
}
//设置
public synchronized boolean compareAndSet(int expectedValue, int newValue){
return expectedValue == compareAndSwap(expectedValue, newValue);
}
}
4、ConcurrentHashMap 锁分段机制
ConcurrentHashMap,内部细分了若干个小的hashmap,称之为段(Segment),默认情况下一个ConcurrentHashMap被进一步细分为16个段,即是锁的并发度
如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首 先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程 环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
java.util.concurrent包中还提供了设计用于多线程上下文中的 Collection 实现:
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- CopyOnWriteArrayList
- CopyOnWriteArraySet
CopyOnWriteArrayList 写入并复制
注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。
/**
* 写入并复制
* */
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
HelloThread ht = new HelloThread();
for (int i = 0; i < 10; i++) {
new Thread(ht).start();
}
}
}
class HelloThread implements Runnable{
//并发修改异常
//private static List<String> list = Collections.synchronizedList(new ArrayList<>());
//在每次写入时,都会在底层完成一次复制,复制一份新的列表,在进行添加
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
static {
list.add("alice");
list.add("ram");
list.add("rem");
}
@Override
public void run() {
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
list.add("AA");
}
}
}
5、CountDownLatch 闭锁
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
- 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
- 等待直到某个操作所有参与者都准备就绪再继续执行。
给定的计数 初始化 CountDownLatch,new CountDownLatch(10); 在计数到达0之前,await方法会一直阻塞,直到将线程中操作完成;计数到达0之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。
核心方法:
- countDown() - 每次调用都会计数一次
- await() - 使当前线程等待,直到锁向下计数为0
/**
* 闭锁,在完成某些运算时,之后其他所有线程的运算全部完成,当前运算才能继续执行
* */
public class TestCountDownLatch {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);//设置初始计数为10
LatchDemo latchDemo = new LatchDemo(latch);
LocalDateTime start = LocalDateTime.now();
for (int i = 0; i < 10; i++) {
new Thread(latchDemo).start();
}
try {
latch.await(); //所有10个线程执行完之后,才能执行main线程
} catch (