- 1.初识 JUC
- 2.了解 AQS
- 1.AQS 结构图
- 2.AQS 框架-重要属性/字段介绍
- 3.CLH 队列
- 3.JUC 工具类
- 1.Lock 锁
- 1.ReentrantLock
- 1.ReentrantLock 例子
- 2.ReentrantLock 源码分析
- 2.ReentrantReadWriteLock
- 2.Tools 工具类
- 1.CountDownLatch(闭锁)
- 2.CyclicBarrier(循环栅栏)
- 3.Semaphore(信号量)
- 4.Exchanger(交换器)
- 3.Atomic 原子类 & UnSafe 魔法类
- 1.Atomic 原子类
- 2.UnSafe 魔法类
- 1.获取 Unsafe 类实例
- 2.Unsafe 类功能
- 1.内存操作
- 2.CAS相关
- 3.线程调度
- 4.内存屏障
- 3.CAS
- 1.CAS 源码简单分析
- 2.ABA 问题
- 4.阻塞队列
- 5.Executor 线程池
- 6.Collections 集合类
- 7.并发编程 Future
- 8.Fork/Join 框架
既然谈到 Java 并发编程,就得聊聊 JUC 。在了解了 JMM 内存模型、MESI 缓存一致性协议、三大特性、synchronized 原理等后,我们就来谈谈 JUC。
在 JDK 5 提供了 java.util.concurrent 工具包(简称:JUC),这是一个处理线程的工具包,JUC工具包的出现,目的就是为了更好的支持高并发任务,让开发者利用这个包在进行多线程编程时,可以有效的减少竞争条件和死锁线程。
下面一起来看看它怎么使用。JUC 包增加了在并发编程中很常用的工具类 Tools,锁相关类 Lock,原子操作类 Atomic、集合相关类 Collections、线程池相关 Executor 五大类。本系列文章,就从以上五类入手,逐个介绍。
JUC 结构如图所示:
2.了解 AQSJUC结构图下载地址:https://pan.baidu.com/s/1apSydPeXgRgfbxRAlX0pwA (提取码:pwyl )
JUC 当中的大多数同步器实现都是围绕着共同的基础行为,比如同步等待队列(CLH队列)、条件队列(Condition队列)、独占获取、共享获取等,而这个行为的抽象就是基于 AbstractQueuedSynchronizer 来实现,简称AQS。AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
AQS 具备:1.阻塞等待队列 2.共享 / 独占 3.公平 / 非公平 4.可重入 5.允许中断 五大特性。
JUC 中的同步器实现,比如常用的 ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore 等,底层都是基于 AQS 框架实现的。一般都是通过定义内部类 Sync 继承 AQS, 将同步器的所有调用都映射到 Sync 对应的方法中去。
1.AQS 结构图 2.AQS 框架-重要属性/字段介绍- AQS内部维护属性 volatile int state (AQS接口中定义)
- state 表示资源的可用状态
- State三种访问方式
- getState()、setState()、compareAndSetState()
- AQS定义两种资源共享方式(Node内部类中定义)
- Exclusive - 独占,只有一个线程能执行,如 ReentrantLock
- Share - 共享,多个线程可以同时执行,如 Semaphore / CountDownLatch
- AQS定义两种队列
- 同步等待队列(CLH 队列(类似),Node内部类用来定义 CLH 队列,在ReentrantLock 源码中介绍)
- 条件等待队列(Condition 队列,ConditionObject 内部类用来定义条件队列,条件队列只有在独占模式下才会被访问,在Semaphore 源码中介绍)
- 当前锁持有者线程(AbstractOwnableSynchronizer 接口中定义)
- exclusiveOwnerThread 记录当前持有锁的线程
- AQS定义了两种锁
- 公平锁(FairSync 内部类)
- 非公平锁(NonfairSync 内部类)
- 队列在Node内部类中,定义了四种节点状态(也可以叫做信号量)
- CANCELLED(节点结束。在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,值为 1 )
- SIGNAL(节点可被唤醒。后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。值为 -1)
- CONDITION(节点可转移至条件队列。节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中。值为 -2)
- PROPAGATE(节点可传播。表示下一次共享式同步状态获取将会被无条件地传播下去,在。值为 -3)
- 初始值(该值未在 Node 内部类中定义,了解即可。值为 0,代表锁的初始状态)
- 其他属性介绍
- waitStatus (标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态)
- prev(前驱节点)
- next(后继节点)
- nextWaiter(Node 既可以作为同步队列节点使用,也可以作为 Condition 的等待队列节点使用(将会在 Semaphore 中介绍 Condition 条件队列时讲到)。在作为同步队列节点时,nextWaiter 可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式。独占模式 nextWaiter 是 null,共享模式 nextWaiter 是一个 new Node();在作为条件队列节点使用时,nextWaiter 保存后继节点。)
- firstWaiter、lasterWaiter(定义在 ConditionObject 内部类中。用在 Condition 条件队列中,条件队列只有在独占模式下才会被访问,不需要使用 CLH 队列中的 prev 和 next 属性,配合 nextWaiter 属性,使其变成了一个单向链表即可)
- head(CLH 等待队列的头部,定义在 AbstractQueuedSynchronzier 抽象类中)
- tail(CLH 等待队列的尾部,定义在 AbstractQueuedSynchronzier 抽象类中)
AQS 重要属性上面就介绍完了。
AQS 源码,分为 CLH 队列 和 Condition 队列,这两个内容主要在 ReentrantLock 源码、Semaphore 源码分析中介绍。要了解 AQS 相关内容,就从以下两篇内容中了解吧
- ReentrantLock 源码分析
- 阻塞队列 源码分析(未完成)
CLH 同步队列,是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是 FIFO 先入先出线程等待队列,Java 中的 CLH 队列是原 CLH 队列的一个变种,线程由原自旋机制改为阻塞机制。
CLH 队列,在 AbstractQueuedSynchronizer 中的 Node 内部类中有介绍。可以翻译后进行了解。来 https://translate.google.cn/ 翻译。
3.JUC 工具类 1.Lock 锁Wait queue node class.
The wait queue is a variant of a “CLH” (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks. We instead use them for blocking synchronizers, but use the same basic tactic of holding some of the control information about a thread in the predecessor of its node. A “status” field in each node keeps track of whether a thread should block. A node is signalled when its predecessor releases. Each node of the queue otherwise serves as a specific-notification-style monitor holding a single waiting thread. The status field does NOT control whether threads are granted locks etc though. A thread may try to acquire if it is first in the queue. But being first does not guarantee success; it only gives the right to contend. So the currently released contender thread may need to rewait.
To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you just set the head field.
Insertion into a CLH queue requires only a single atomic operation on “tail”, so there is a simple atomic point of demarcation from unqueued to queued. Similarly, dequeuing involves only updating the “head”. However, it takes a bit more work for nodes to determine who their successors are, in part to deal with possible cancellation due to timeouts and interrupts.
The “prev” links (not used in original CLH locks), are mainly needed to handle cancellation. If a node is cancelled, its successor is (normally) relinked to a non-cancelled predecessor. For explanation of similar mechanics in the case of spin locks, see the papers by Scott and Scherer at http://www.cs.rochester.edu/u/scott/synchronization/
We also use “next” links to implement blocking mechanics. The thread id for each node is kept in its own node, so a predecessor signals the next node to wake up by traversing next link to determine which thread it is. Determination of successor must avoid races with newly queued nodes to set the “next” fields of their predecessors. This is solved when necessary by checking backwards from the atomically updated “tail” when a node’s successor appears to be null. (Or, said differently, the next-links are an optimization so that we don’t usually need a backward scan.)
Cancellation introduces some conservatism to the basic algorithms. Since we must poll for cancellation of other nodes, we can miss noticing whether a cancelled node is ahead or behind us. This is dealt with by always unparking successors upon cancellation, allowing them to stabilize on a new predecessor, unless we can identify an uncancelled predecessor who will carry this responsibility.
CLH queues need a dummy header node to get started. But we don’t create them on construction, because it would be wasted effort if there is never contention. Instead, the node is constructed and head and tail pointers are set upon first contention.
Threads waiting on Conditions use the same nodes, but use an additional link. Conditions only need to link nodes in simple (non-concurrent) linked queues because they are only accessed when exclusively held. Upon await, a node is inserted into a condition queue. Upon signal, the node is transferred to the main queue. A special value of status field is used to mark which queue a node is on.
Thanks go to Dave Dice, Mark Moir, Victor Luchangco, Bill Scherer and Michael Scott, along with members of JSR-166 expert group, for helpful ideas, discussions, and critiques on the design of this class.
当多个线程需要同时访问某个公共资源时,我们需要通过加锁的操作来保证线程的安全。我们已经知道 JVM 内置锁 synchronized 锁的原理(参考:synchronized 原理、使用、锁升级过程)。
除了 synchronized 关键字这种方式外,JUC 工具包还为我们提供了 Lock 接口来实现锁的功能,并且还提供了更灵活的 API 来方便我们调用。Lock 作为一个接口类,在学习 Lock 锁之前,先来看看 synchronzied 和 Lock 锁的区别
| Lock | synchronized | |
|---|---|---|
| 层次方面 | ① Lock 是一个接口,是类级别的实现 ② JDK 层次的实现 | ① 是 Java 关键字 ② 在 JVM 层次定义的 |
| 灵活性方面 | Lock 接口提供的 lock() 和 unlock() 方法,可以随时获得锁、释放锁,非常灵活 Lock 在发生异常时,如果没有主动通过 unlock() 去释放锁,则很可能会造成死锁,因此使用 Lock 时需要在 finally 块中释放锁 | 释放锁、获得锁是被动的 释放锁只有两种情况: ① 同步代码块执行完毕 ② 抛出异常,同步器执行 monitorexit 释放锁 |
| 锁的状态方面 | ① Lock 可以判断锁的状态,它会提供 tryLock() 方法来告诉我们是否获得锁成功 ② tryLock() 方法有返回值,用来尝试获取锁,如果获取成功,则返回 true;获取失败,返回 false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。 | ① 在锁的状态方面,synchronized 完全是被动的,没法判断锁的状态 ② synchronized 在拿不到锁时,则会阻塞在那里,一直等待 |
| 锁的类型方面 | 基于 Lock 接口,有多种锁的实现。如: ① 可重入锁:ReentrantLock ② 可重入读写锁:ReentrantReadWriteLock 等 针对可重入锁,还有 ①公平锁 和 ②非公平锁 之分 | 对于 synchronized 来说,它只是一个 JVM 层次的关键字,并不是一个接口,没有具体实现。 synchronized 是可重入锁、互斥锁、非公平锁。 |
Lock 只是一个接口,我们来看一下 Lock 接口具体实现类吧
1.ReentrantLockReentrantLock,翻译过来就是:可重入锁。
即:线程获得锁后,可以重复获取这把锁。
1.ReentrantLock 例子举例: 线程 t1 和 t2 处于并发状态,线程 t1 方法获得锁后,执行同步方法,方法中还有一个锁(这两个锁相同),此时线程 t1 可以再次获取这把锁,而不需要阻塞。
public class ReentrantLockTest {
// ReentrantLock 可重入锁
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 线程t1
Thread t1 = new Thread(() -> {
try {
lock.lock();
System.out.println("第一次获得锁");
try {
lock.lock();
System.out.println("第二次获得锁");
// 执行业务逻辑(以休眠1s代替)
TimeUnit.SECONDS.sleep(1);
System.out.println("线程t1执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("释放第二个锁");
}
} finally {
lock.unlock();
System.out.println("释放第一个锁");
}
}, "t1");
// 线程t2
Thread t2 = new Thread(() -> {
try {
lock.lock();
TimeUnit.SECONDS.sleep(1);
System.out.println("线程t2执行中...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2");
// 启动
t1.start();
t2.start();
}
}
运行结果:
第一次获得锁
第二次获得锁
线程t1执行完成
释放第二个锁
释放第一个锁
线程t2执行中…
可以看到:
线程 t1 可以重复获取相同锁,并不会被阻塞。线程 t1 执行完成并释放锁后,线程 t2 获得锁,开始执行。
2.ReentrantLock 源码分析我们知道了 ReentrantLock 是可重入锁,也知道了它的底层实现就是 AQS 。通过 AQS 结构图,ReentrantLock 中定义 Sync 内部类,Sync 继承自 AbstractQueuedSynchronizer。ReentrantLock 就是通过这个 Sync 类,同时配合使用父类AbstractQueuedSynchronizer中的公共方法,进行加锁/解锁,CLH 队列入队/唤醒 等操作。
ReentrantLock 基于独占模式实现,是一个悲观锁。下图源码 addWaiter(Node.EXCLUSIVE) 方法,就指定了 ReentrantLock 是一个 独占锁。(ReentrantLock 中 CLH 同步队列采用的是 EXCLUSIVE 独占模式,Semaphore 中 CLH 同步队列采用的是 SHARED 共享模式)
对照上面的例子 1.ReentrantLock 示例 ,直接开始源码解析,如下图所示:
原图地址:ReentrantLock 源码分析
可重入读写锁如何使用,参考这篇文章:ReentrantReadWriteLock
2.Tools 工具类Java 多线程环境下,JUC 包下也为我们提供了很多工具类,使得我们不需要过多的关心具体业务场景下,应该如何写出同时兼顾线程安全性与高效率的代码。JUC包下的这些工具类都是基于 CAS 机制来保证线程的安全。在项目中使用这些工具类,我们不再需要过多的了解底层原理,工作中只需要了解如何使用即可。
本文主要介绍 JUC 并发包下这些常用的工具类:CountDownLatch、CyclicBarrier、Semaphore、Exchanger这些类的使用。尤以 CountDownLatch 类使用最多。
1.CountDownLatch(闭锁)参考这篇文章:Java并发工具类 CountDownLatch
2.CyclicBarrier(循环栅栏)参考这篇文章:Java并发工具类 CyclicBarrier
3.Semaphore(信号量)参考这篇文章:Java并发工具类 Semaphore
4.Exchanger(交换器)参考这篇文章:Java并发工具类 Exchanger
3.Atomic 原子类 & UnSafe 魔法类 1.Atomic 原子类参考这篇文章:Atomic 原子类
2.UnSafe 魔法类Unsafe 类是在 sun.misc 包下,并不属于 Java 标准,但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类来开发的,比如:Netty、Hadoop、Kafka 等。
我们可以认为Unsafe 类是 Java 中留下的后门。在 JDK 层面上定义的这么一个 Unsafe 类,它提供了一下偏底层操作,可以直接绕过 JVM 去直接操作内存。如:使用Unsafe类可以 ①直接访问内存 ②线程调度 等。
1.获取 Unsafe 类实例我们可以通过如下方式,来获取 Unsafe 类实例
public class UnsafeInstance {
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
2.Unsafe 类功能
Unsafe 类仅做了解,CAS、内存屏障、线程调度 可以自己使用,其他5项不建议使用,用不好会造成不可预知的影响。
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法
// 分配内存, 相当于C++的malloc函数 public native long allocateMemory(long bytes); // 扩充内存 public native long reallocateMemory(long address, long bytes); // 释放内存 public native void freeMemory(long address); // 在给定的内存块中设置值 public native void setMemory(Object o, long offset, long bytes, byte value); // 内存拷贝 public native void copyMemory(Object srcbase, long srcOffset, Object destbase, long destOffset, long bytes); // 获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); // 为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有:putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); // 根据给定的地址,获取byte值 public native byte getByte(long address); // 为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的) public native void putByte(long address, byte x);2.CAS相关
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);3.线程调度
包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程 public native void unpark(Object thread); //阻塞线程 public native void park(boolean isAbsolute, long time); //获得对象锁(可重入锁) @Deprecated public native void monitorEnter(Object o); //释放对象锁 @Deprecated public native void monitorExit(Object o); //尝试获取对象锁 @Deprecated public native boolean tryMonitorEnter(Object o);
方法 park、unpark 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark 可以终止一个挂起的线程,使其恢复正常。
典型应用:
Java锁和同步器框架的核心类 AbstractQueuedSynchronizer(AQS),就是通过调用 LockSupport.park() 和 LockSupport.unpark() 实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式来实现。
在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence();
内存屏障的使用,参考:synchronized 原理,里面有介绍使用到
3.CAS我们已经知道,JUC包下的这些工具类都是基于 CAS 机制来保证线程的安全。Atomic 包里的类基本也都是使用 Unsafe 实现的,底层使用的还是 CAS。
CAS 即 Compare And Swap 的缩写,翻译成中文就是比较并交换,其作用是让 CPU 比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,也就是CAS是原子性的操作(读和写两者同时具有原子性),其实现方式是通过借助C/C++调用CPU指令完成的,所以效率很高。
1.CAS 源码简单分析CAS 能够解决线程在高并发情况下的线程安全问题。接下来我们就从 compareAndSetHead() 和 compareAndSetTail() 方法入手分析 。
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
1.unsafe.compareAndSwapObject() 分析
在 AQS 设置首节点和尾节点的方法中,我们发现它们使用的都是 Unsafe 类,调用的都是 unsafe.compareAndSwapObject() 方法。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
我们发现 compareAndSwapObject() 方法,是一个由 native 修饰的方法(Java Native Interface,简称:JNI)。Native Method 就是一个 java 调用非 java 代码的接口。这个也比较好理解,因为 java 底层本来就是用 C/C++ 来开发的,所以当然有对应接口去直接调用C/C++写的方法。众所周知 java 对底层的操作远不如 C/C++ 灵活,所以可以通过直接调用非 java 代码来实现对底层的操作。
参考 compareAndSetTail() 方法,来分析一下 compareAndSwapObject() 方法具体传递的参数情况吧。
private final boolean compareAndSetTail(Node expect, Node update) {
//this:需要改变的对象
//tailOffset:偏移量
//expect:期望值
//update:更新后的值
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
此处 tailOffset 偏移量,具体又是个什么东西呢?我们接下来继续深入,分析 tailOffset 参数。
2.tailOffset/headOffset 偏移量参数分析
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
} catch (Exception ex) {
throw new Error(ex);
}
}
我们发现偏移量这块,调用的也是 Unsafe 类。通过调用 Unsafe 类中的 objectFieldOffset() 方法,通过反射的方式去获得当前成员变量的内存地址。
内存地址又是用来干嘛的呢?
任何一个类中的字段,都是会按照一定的顺序在内存中有一个地址进行存放。然后通过 unsafe.objectFieldOffset() 方法,我们可以准确的知道 head、tail 等这些对象字段在内存地址中的偏移量。
偏移量又是干嘛的呢?
JVM 会根据偏移量来找到该对象(成员变量)在内存中的具体位置。
分析完偏移量之后,我们回到 unsafe.compareAndSwapObject() 方法。如果想要继续了解 native 定义的方法,那么你则需要进入到 JVM 层面进行分析,此处不再继续介绍。我们只要知道 compareAndSetHead()、compareAndSetTail() 方法的大致操作就可以了。
2.ABA 问题CAS 可以保证并发过程中的原子性操作。但是 CAS 也存在着一个很大的缺点:ABA 问题。。
示例:
CAS 在修改时,一个期望值,一个更新值。2 个线程 t1、t2,t1 和 t2 对数值进行更新,期望值 1,更新值 3。
- t1 修改成功。t2 在更新时被阻塞;
- 此时来了个线程 t3 同样对该值进行处理,将 3 又更新成了 1,t3 更新完成。
- 此时 t2 被唤醒,t2 发现期望值还是1,于是更新为 3 成功。(殊不知,在此期间,该值已经被线程 t3 修改过了)
public class CASABATest {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread main = new Thread(()->{
int a = atomicInteger.get();
System.out.println("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+"--CAS修改后操作数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
},"主线程");
Thread other = new Thread(()->{
atomicInteger.incrementAndGet(); // 1+1 = 2;
System.out.println("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
atomicInteger.decrementAndGet(); // atomic-1 = 2-1;
System.out.println("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
},"干扰线程");
main.start();
other.start();
}
}
Atomic 原子类中,提供了 AtomicStampedReference 类来解决 CAS 中的 ABA 问题。就类似提供了一个版本号,通过版本号判断是否有被修改过。
public class AtomicStampedReferenceTest {
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference<>(1, 0); // initialStamp 为初始版本号
public static void main(String[] args) {
Thread main = new Thread(() -> {
int stamp = atomicStampedRef.getStamp(); //获取当前标识别
System.out.println("操作线程" + Thread.currentThread() + "stamp=" + stamp + ",初始值 a = " + atomicStampedRef.getReference());
try {
Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作线程" + Thread.currentThread() + "stamp=" + stamp + ",CAS操作结果: " + isCASSuccess);
}, "主操作线程");
Thread other = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);
System.out.println("操作线程" + Thread.currentThread() + "stamp=" + atomicStampedRef.getStamp() + ",【increment】 ,值 = " + atomicStampedRef.getReference());
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2, 1, stamp, stamp + 1);
System.out.println("操作线程" + Thread.currentThread() + "stamp=" + atomicStampedRef.getStamp() + ",【decrement】 ,值 = " + atomicStampedRef.getReference());
}, "干扰线程");
main.start();
other.start();
}
}
4.阻塞队列
参考这篇文章:JUC阻塞队列
5.Executor 线程池之前文章介绍的:Java 线程池内容,全在这里了,也是 JUC 包下的工具类。
6.Collections 集合类关于 JUC 集合类内容,主要涉及到 ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet 这三个。关于面试中常问到的 HashMap 源码,以及 ConcurrentHashp 实现,我在 blog 中并没有写到,我在我的小本本里做的笔记呢。哈哈。有需要可以发上来。只看看总结的话,只有个映像罢了。不过还是建议对着源码看看,了解更深入。
在并发环境下:
List 接口的实现类:ArrayList、linkedList、 Vector。 只有 Vector 是线程安全的,Java 还是不推荐使用 Vector,为什么呢,我给你们找来了篇文章,了解一下就行了。
Set 接口的实现类:HashSet、linkedHashSet、TreeSet ,三个都是线程不安全的。
Map 接口的实现类:HashMap、linkedHashMap、TreeMap、Hashtable 。 只有 Hashtable 是线程安全的。JDK 7 HashMap 并发情况下,在扩容转移数据时,会出现死锁情况,JDK 8 HashMap 加入红黑树,解决了死锁问题,但是在并发情况下,还是会存在数据丢失问题。为什么呢,我给你们找来了篇文章,了解一下就行了。
如何保证并发环境下的线程安全:
1.Collections 工具类为我们提供的方法:Collections.synchronziedList()、Collections.synchronziedSet()、Collections.synchronziedMap() 方法,来保证并发安全。
2.除此之外,我们还可以使用 JUC 并发包中为我们提供的:CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap 这三个类,来保证并发安全。
写时复制技术:
CopyOnWriteArrayList、CopyOnWriteArraySet 这两个类的实现,采用的就是写时复制技术。在写入数据时,会将旧数组 copy 一份,然后对 copy 的数组进行操作。此处不做过多介绍,附上部分源码了解以下即可。
参考:
1.为什么不推荐使用Vector以及集合的线程安全问题
2.为什么java不推荐使用vector
3.HashMap多线程并发情况(JDK1.8)
4.jdk1.8中为什么说hashMap并发问题?
5.写时复制技术
参考:
1.Java实现多线程的三种方式(看:Future 那种方式即可)
2.多线程异步调用实现方式:CompletableFuture(JDK8使用,lambda表达式实现)
参考:Fork/Join 框架
并发目录:
1.计算机原理结构
2.MESI 缓存一致性协议
3.提起线程,你不了解的那些事
4.JMM内存模型 & 多线程三大特性
5.synchronized 原理、使用、锁升级过程,写到我要吐血了
6.JUC全家桶系列,一键三连就完事了
7.可重入读写锁:ReentrantReadWriteLock
8.Java并发工具类:CountDownLatch、Semaphore、CyclicBarrier、Exchanger
9.Atomic原子类
10.JUC阻塞队列
11.Java线程池创建,全部考点都在这里了
12.为什么阿里巴巴要禁用Executors创建线程池?
13.ThreadLocal,你想了解的都在这里(进来瞧瞧不后悔)
14.Java多线程的六种状态
15.Java实现多线程的三种方式
16.线程的生命周期包括哪几个阶段?
17.有三个线程 t1,t2,t3,怎么确保它们按顺序执行?
18.JUC 之 Condition 使用 + 原理分析
19.多线程异步调用实现方式:CompletableFuture
2021-12-07,《JUC 并发工具类》已更新,《并发编程》版块完结
博主写作不易,加个关注呗
求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙
我不能保证所写的内容都正确,但是可以保证不复制、不粘贴。保证每一句话、每一行代码都是亲手敲过的,错误也请指出,望轻喷 Thanks♪(・ω・)ノ



