前言
本文全面覆盖并发相关知识点,建立完整的知识体系。
基础
本章节包含并发概念,缓存,线程,Java内存模型
并发概念
并发指多个线程同时执行程序的能力。在单核处理器中,多个线程交替占用内存空间实现并发。在多核处理器中,每个线程可以分配到独立的处理核,达到真正的同时运行。需要注意的是在并发中,多个线程是同时存在的,每个线程都处于执行的某个状态。
并发与高并发区别:
并发主要专注于保证线程安全与每个线程对资源的合理使用。高并发专注于解决程序或服务器同时处理多种请求的能力,主要目的在于提高程序的性能。
CPU 多级缓存
图中所示为高速缓存。高速通道(bus)连接了高速缓存与CPU核心来达到高速效果。随着系统的复杂化,高速缓存与主内存(main memory)的速度差异越来越大,促使了不同级别缓存的出现。比如:L1,L2,L3 caches
缓存有不命中的风险,它的存在意义来自于数据的时间和空间局部性:当某个数据被访问时,不久的将来可能被再次访问。当某个数据被访问时,它的相邻数据也可能被访问。
CPU 多级缓存一致性 (MESI)
MESI协议保证多个CPU缓存之间缓存数据的一致性
NESI是四种状态的缩写:
M: 被当前cpu修改过,与主内存不一致
E: 未被当前cpu修改过,与主内存一致
S: 共享状态,多个cpu可以同时访问
I: 无效状态:Invalid
CPU缓存的四种操作:
local read: 读取本地缓存数据
local write: 写入本地缓存
remote read: 讲内存中数据读取出来
remote write: 写回主内存
CPU 多级缓存 - 乱序执行优化
处理器为提高运算速度会作出违背代码原有顺序的优化。
如果我们不做任何防护措施,并发会有严重后果。
Java内存模型
为了屏蔽不同硬件与操作系统内存访问的差异来让Java程序在不同的场景实现相同的并发效果,JVM虚拟机定义了Jave Memory Model的规范。JMM规范了虚拟机与计算机内存的协同工作,比如规定了一个线程在何时和何种方式可以共享其他线程上的值。
Heap: Java堆是运行时的数据区。堆的大小是程序运行中动态分配的,最后由gc来统一回收。因为其动态性,堆的存储速度很慢。
Stack: 栈的存储速度比堆要快不少,仅次于计算机里的寄存器(register)。栈中的数据是可以共享的,但是栈中的数据大小与生存期必须提前确定。
Java内存模型要求类的方法与本地变量存储在栈上面,对象存储在堆上面。当两个本地变量同时访问堆中的静态变量的方法时候,每个线程获得一份成员方法的拷贝。
JMM 寄存器 (register)
CPU在寄存器(register)上的执行速度远大于在主内存(main memory)上的执行速度。尽管CPU访问register的速度很快,但是还是比访问计数器要慢一些。
Program Counter Register (计数器) PC计数器为线程私有,指向下一条命令的地址,也就是下一条即将执行的指令代码,由执行引擎来读取。
线程与主内存的抽象关系
JVM中的Thread Stack和Heap都储存在主内存中,部分可能储存在寄存器register或者cache中。线程的共享变量也储存在主内存中。每个线程都有一个私有的本地内存,涵盖了缓存,写缓存区,寄存器,已经其他硬件或者编译器优化。
线程私有的本地内存并不真实存在,只是对主内存物理内存的划分和定义
Java 内存模型 - 同步操作
同步操作:
- Lock: 作用于主内存,上锁。
- Unlock: 作用于内存,解锁。
- Read: 将主内存读入工作内存
- Load:作用于把主内存值放入工作副本中
- Use:把工作内存中变量传递给执行引擎
- Assign:把执行引擎的值传给工作内存
- Store:把工作内存传入主内存
- Write:写入主内存
部分规范
- Read Load 必须同时执行
- Store Write 必须同时执行
- Assign使用后必须同步给主内存
- 不允许线程在没有Assign的情况下同步主内存
- 一个新的变量只能从主内存中诞生
- 如果同线程Lock多次,需相同数量Unlock来解锁
- 执行Lock操作会清空工作内存中此变量的值
- Unlock不能解锁没上锁变量,或者非本线程锁
- 在Unlock前必须同步数据回主内存
部分并发测试工具
Apache Bench
Apache JMeter
Postman
Java 并发模拟
@NotThreadSafe
public class ConcurrencyTest {
public static int count = 0;
public static int clientTotal = 5000;
public static int threadTotal = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i=0; i<clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
System.out.println(e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(count);
}
public static void add() {
count++;
}
}
CountDownLatch 源码中解释如下:
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
Semaphore 源码中解释如下:
Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource
代码中,countDownLatch设置了初始值为5000,也就是总共需要执行的add()次数。每次执行调用countDown()直到为0,关闭线程池并打印最终结果。每次执行过程中使用Semaphore.acquire() 和Semaphore.release()来加锁解锁。
线程安全性
此章节讲解线程安全的三大特性
安全性概念
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程如何进行交替执行,并且在主调代码中不需要任何和歪的同步或者协调,这个类都能表现出正确的行为,这个类就是线程安全的。
- 原子性:提供了互斥访问,同一时刻只能有一个线程对它操作。
- 可见性:一个线程对祝内存的修改可以及时被其他线程观察到。
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
原子性 - Atomic包
java.util.concurrent.atomic 中提供了很多atomic类,都是通过CAS来完成原子性的。
AtomicInteger
首先来看下AtmoicXXX 系列用法:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
//count.getAndIncrement();
再来看下incrementAndGet的方法:
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
//private static final Unsafe U = Unsafe.getUnsafe()
我们可以发现此方法调用了U也就是unsafe这个类
继续来看下这个类中的getAndAddInt的用法:
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
这个方法里面又调用了weakCompareAndSetInt,返回
return compareAndSetInt();
最后来看下核心函数:compareAndSetInt
public final native boolean compareAndSetInt();
native是Java底层实现的,总而言之CAS在收到修改请求后,会在修改前查看请求的数据有没有被别的线程修改。每次while loop 中getLongVolatile拿到的值都会在修改操作之前再次确认一下来保证其数据的原子性
LongAdder
JVM允许将64位的数据拆成两个32位的数据进行计算。Java Atomic 包底下的 LongAdder 可以存储一个 Int 类型数组,所有线程对数组进行操作最后将数组合并得到最终结果。通过不同 cell的分散热点,此类在AtomicLong的基础上增加了并行效率。
缺陷:如果在同行时候有并发更新,可能有数据误差。
AtomicReference
看一下案例代码:
public class AtomicExample {
private static AtomicReference<Integer> count =
new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0,2); //2
count.compareAndSet(0,1); //skip
count.compareAndSet(1,3); //skip
count.compareAndSet(2,4); //4
count.compareAndSet(3,5); //skip
System.out.println(count.get());
}
}
这个类可以调用compareAndSet来设定自定义的CAS规则
AtomicXXXFieldUpdater
看一下案例代码:
public class AtomicExample2 {
@Getter
public volatile int count = 100;
private static AtomicExample2 example2 = new AtomicExample2();
private static AtomicIntegerFieldUpdater<AtomicExample2> updater =
AtomicIntegerFieldUpdater.newUpdater(AtomicExample2.class, "count");
public static void main(String[] args) {
if (updater.compareAndSet(example2, 100, 120)) {
System.out.println("update succeed");
}
}
}
被更改的值必须使用volatile关键字,之后会详细说明。
AtomicStampReference
此类主要为了解决 CAS 中的 ABA 问题。
ABA: 当某个线程把值A改成B再改回A的操作。此时其他线程调用CAS算法不会发现当前线程已经修改过了内存中的值。
解决方案:给每一次修改操作增加版本号,来看一下AtomicStampReference:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
由此可知,expectedStamp 就是版本控制变量。
AtomicLongArray
此类维护的是一个数组,数组可以进行选择性的更新。对应某个index的修改也具备原子性。
AtomicBoolean
private static AtomicBoolean status;
status.compareAndSet(false,true);
原子性 - 锁
除了Atomic包之类,锁也可以保证程序的原子性。
- synchronized: 依赖JVM的锁
- Lock: 代码层面锁,依赖CPU命令 (ReetrantLock)
synchronized
修饰代码块:大括号内,作用于调用的对象。
修饰方法:整个方法,作用于调用的对象。
修饰静态方法:作用于所有对象
修饰类:括号里,作用于所有对象
// 修饰一个代码块
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修饰一个方法
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
下面看一下模拟多线程的结果:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test2(1);
});
executorService.execute(() -> {
example2.test2(2);
});
}
我们会发现,修饰同步代码块和直接修饰整个方法的结果相同。两个test方法都只保证了某个线程执行循环是有序的,不保证线程间的执行顺序。当类为静态类的时候,这时候一个线程执行完了才会另一个线程执行
注意:当子类继承带有
synchronized关键字的父类时,子类中调用这些方法不具备锁的能力。(关键字不属于方法声明的一部分)
synchronized vs Lock
synchronized 为不可中断锁,适合竞争不激烈,可读性好。Lock 为可中断锁,多样化同步,竞争激烈时能维持常态。
可见性
一个线程对主内存的修改可以及时被其他线程接收。
导致共享变量线程间不可见原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 更新后共享变量没有及时更新
synchronized
JMM 对 synchronized的两条规定:
- 线程解锁前,必须将共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
volatile
实际在代码中,直接将变量加上volatile并不能保证线程安全。下面代码中,count++在底层实际执行了三步骤:
volatile count = 0;
count++;
// 1. read count
// 2. +1
// 3. store count
假设两个线程同时执行操作,volatile保证两个线程能读到内存中的值,但是两个线程的写入操作可能同时执行,导致其中一个线程的执行无效。
volatile 使用
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while (!inited) {
sleep();
}
doSomethingWithConfig(context);
volatile特别适合作为状态标记量
有序性
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
happens-before 原则
不需要通过任何手段就可以得到保证的有序性。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则: 一个unlock操作先行发生于后面对同一个锁的lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,操作A先行于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断贵的:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件发生。
- 线程中断规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值检测是否终结。
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
安全发布对象
发布对象:使一个对象能够被当前范围之外的代码所使用
对象溢出:错误的发布,对象构造完成前被其他线程看见
安全发布的四种方法
- 静态初始化函数中初始化对象引用
- 初始化对象保存到
volatile或者AtomicReference - 将对象的引用保存到某个正确构造对象的final中
- 将对象引用保存到有锁的保护域中
单例写法 Singleton
先来看一个@NotThreadSafe的懒汉模式
@NotThreadSafe
public class SingletonExample1 {
// 私有构造函数
private SingletonExample1() {}
// 单例对象
private static SingletonExample1 instance = null;
// 静态的工厂方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
两个线程可能同时判断对象没有被初始化,导致最后返回的构造器的对象错误。(虽然在这个例子中,没啥影响)
再来看一下ThreadSafe的饿汉模式
@ThreadSafe
public class SingletonExample2 {
// 私有构造函数
private SingletonExample2() {}
// 单例对象
private static SingletonExample2 instance = new SingletonExample2();
// 静态的工厂方法
public static SingletonExample2 getInstance() {
return instance;
}
}
虽然线程安全,饿汉模式保证了类的加载。如果类的加载损耗很大但是却没有被实力使用,会造成资源浪费。
所以我们来看一下@ThreadSafe的懒汉模式
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有构造函数
private SingletonExample3() { }
// 单例对象
private static SingletonExample3 instance = null;
// 静态的工厂方法
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
使用synchronized保证只有一个线程可以使用getInstance方法,但是性能开销会很大。
再来看一下NotThreadSafe的双重同步锁懒汉模式
@NotThreadSafe
public class SingletonExample4 {
// 私有构造函数
private SingletonExample4() { }
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// JVM和cpu优化,发生了指令重排
// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象
// 单例对象
private static SingletonExample4 instance = null;
// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}
双重检测机制并不能保证线程安全,因为此方法可能会返回一个还没有被构造完的实例对象。具体来说JVM的重排序可能导致instance=memory优先执行给对象制定了内存空间,让instance != null 但是对象的构造ctorInstance还来不及调用。
线程安全策略
本章节包含不可变对象,线程封闭,线程不安全的类与写法,以及同步并发容器的介绍。
不可变对象
不可变对象满足了发布安全策略因为只要发布他们就一定是安全的。不可变对象需要满足以下条件:
- 对象创建以后其状态不能修改
- 对象所有的field都是final类型
- 对象是正确创建的
final关键字
final可以修饰类,方法,和变量
- 修饰类:类不能被继承
- 修饰方法:1.方法锁定 2.效率(早起会优化)
- 修饰变量:基础数据变量,引用变量
集合框架转换不可变对象
- Collections.unmodifiableXXX
- Guava: ImmutableXXX
这两个方法提供了针对集合框架实现类中:Collection, List, Set, Map, 等等的不可变对象方法。
下面来看一下UnmodifiableMap的实现
private final Map<? extends K, ? extends V> m;
public boolean containsKey(Object key) {
return m.containsKey(key);
}
public V remove(Object key) {
throw new UnsupportedOperationException();
}
其实底层就是存储了一个HashMap的实例来调用了常规的MapAPI,当调用修改函数时候,手动抛出异常
线程封闭
- Ad-hoc 线程封闭:程序控制实现,糟糕,忽略
- 堆栈封闭:局部变量,无并发问题。
- ThreadLocal 线程封闭:特别好的封闭
ThreadLocal
在httpRequest中,假设用户传递进入一个值,不使用ThreadLocal的话需要把传入对象一层层的传递到数据库层。代码复杂。使用此类可以在接口操作前取出信息,又能保证线程安全(通过从类中拿值)
RequestHolder 存储该线程的值
public class RequestHolder {
private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();
public static void add(Long id) {
requestHolder.set(id);
}
public static Long getId() {
return requestHolder.get();
}
public static void remove() {
requestHolder.remove();
}
}
HttpFilter过滤http请求
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath());
RequestHolder.add(Thread.currentThread().getId());
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
HttpInterceptor
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
log.info("afterCompletion");
return;
}
}
afterCompletion表示httpRequest被执行完毕,我们需要手动删除ThreadLocal避免内存泄漏。
ThreadLocal 与内存泄漏
线程不安全类与写法
- StringBuilder -> StringBuffer
- SimpleDataFormat -> JodaTime.DataTimeFormatter
- ArrayList, HashSet, HashMap 等集合
- if(condition(a)) {handle(a);}
同步容器
开发中任何线程不安全的类与写法都需要程序员今天同步处理。Java中提供了同步容器:
- List -> Vector, Stack
- Map -> HashTable (key,value != null)
- Collections.synchronizedXXX
需要注意的是,同步容器不能保证线程安全:
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread thread1 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
};
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
};
thread1.start();
thread2.start();
}
上面代码虽然get和remove都保证了原子性,但是两个线程的交替执行可能导致get(i)与remove(i)同时执行,导致数组越界或者空指针异常。
同步容器的遍历器有时候也不能保证线程安全:
// java.util.ConcurrentModificationException
private static void test1(Vector<Integer> v1) { // foreach
for(Integer i : v1) {
if (i.equals(3)) {
v1.remove(i);
}
}
}
// java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1) { // iterator
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i.equals(3)) {
v1.remove(i);
}
}
}
// success
private static void test3(Vector<Integer> v1) { // for
for (int i = 0; i < v1.size(); i++) {
if (v1.get(i).equals(3)) {
v1.remove(i);
}
}
}
建议不要在遍历的过程中进行更新操作。
并发容器
Java.Util.Concurrent 实现了并发容器
- ArrayList -> CopyOnWriteArrayList
- HashSet -> CopyOnWriteArraySet
- TreeSet -> ConcurrentSkipListSet
- HashMap -> ConcurrentHashMap
- TreeMap -> ConcurrentSkipListMap
CopyOnWriteArrayList
- 写操作时复制旧数组,完成写操作然后读入。
- 在锁的保护下达到线程安全。
- 因为要频繁开辟新内存,导致young/full gc
- 保证数据一致性,不能保证实时性。
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
需要注意的是,JDK中锁不再使用ReentrantLock
/**
* The lock protecting all mutators.
* (We have a mild preference
* for builtin monitors over ReentrantLock
* when either will do.)
*/
final transient Object lock = new Object();
CopyOnWriteArraySet
- 底层实现为
CopyOnWriteArrayList - 开销较大
- 迭代器不支持可变的
remove操作
ConcurrentSkipListSet
- JDK6新增的类,支持自然排序
- 基于Map集合,多线程都是线程安全的
addAll等不能保证整个操作的原子性
ConcurrentHashMap
- 针对读操作做了大量的优化
- 高并发有特别好的表现
ConcurrentSkipListMap
- 使用
SkipList跳表结构实现 - key是有序的
- 它支持更高的并发,存储实际和线程无关
- 在非多线程情况下使用ConcurrentHashMap
什么是SkipList?
J.U.C
AQS - J.U.C 核心
全称 AbstractQueuedSynchronizer
- 使用Node实现FIFO队列,可以用于构造锁或者其他同步装置的基础框架。
- 利用
init类表示状态 - 使用方法是继承AQS
- 子类需要通过实现
acquire和release方法来操纵状态 - 可以同时实现排它锁和共享锁模式(E,M)
AQS 实现思路
- AQS 内部维持了一个CRS队列来管理锁
- 线程会尝试获得锁。如果失败,线程会包装成Node节点加入到同步队列。然后不断的循环尝试获取锁。只有Head的直接后径会尝试获得锁。
- 我们所用的同步组件,都是基于AQS的实现完成的。
AQS 同步组件
- CountDownLatch
- Semaphore
- CyclicBarrier
- ReentrantLock
- Condition
- FutureTask
- ......
CountDownLatch
此类为同步辅助类,拥有阻塞当前线程的功能。该计数器的操作为原子操作。当计数为0,等待线程会被释放。需要注意的是只有一个线程会被堵塞,也就是一个线程会被释放,继续执行。
在指定时间完成CountDownLatch:
countDownLatch.await
(10, TimeUnit.MILLISECONDS);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(10); //false!!!
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
log.error("exception", e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.MILLISECONDS);
exec.shutdown();
需要注意的是,时间限制比如sleep必须在并发方法中调用。时间限制记录的不是整个循环语句的时间,而是方法调用时间。shutdown()操作不会立刻关系所有线程,而是让没有执行完的线程继续执行,所以await(10)还是会输出部分结果。
Semaphore
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
semaphore.acquire(); // 获取一个许可
test(threadNum);
semaphore.release(); // 释放一个许可
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
非常简单,只需要使用acquire和release来加锁解锁。下面来看一下获取多个锁的操作:
try {
if (semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { // 尝试获取一个许可
test(threadNum);
semaphore.release(); // 释放一个许可
}
} catch (Exception e) {
log.error("exception", e);
}
尝试拿到多个锁,如果没有拿到,等到一定时间再丢弃。如果不加时间限制,那么没拿到锁就会直接丢弃当前线程。
CyclicBarrier
此类也是个同步辅助类,可以使多个线程互相等待,才能各自执行下面的操作。注意此类是从
0开始向上累加。由于此类在释放等待线程后可以重用,所以是循环屏障
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
log.info("callback is running");
});
可以设置此类让它获得线程时优先执行callback中的指令,再继续方法执行。
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
try {
barrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.warn("BarrierException", e);
}
log.info("{} continue", threadNum);
}
最后,如果需要给当前barrier设置最高等待时间,一定要抓住所有可能的异常抛出来保证后续程序能够完整执行。
ReentrantLock 与锁
ReentrantLock和synchronized区别:
- 可重入性相差不大,都是通过计数器来获得锁,计数器为0的时候才能释放锁。
- 锁的实现:synchronized依赖JVM,ReentrantLock是依赖JDK的。
- 性能的区别:在synchronized引入了轻量锁,偏向锁后,两者性能差不多。
- 功能的区别: synchronized简单一些,ReentrantLock灵活性更好。
ReentrantLock 独有的功能
- 可指定是公平锁还是非公平锁
- 提供了
Condition类,实现分组唤醒。(synchronized要不随机唤醒,要不全部唤醒) - 提供能够中断等待锁的线程机制:
lock.lockInterruptibly() - 公平锁:先等待的线程先获得锁
ReentrantLock 部分方法:
ReentrantReadWriteLock
看一下代码实例,有点长:
public class LockExample3 {
private final Map<String, Data> map = new TreeMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public Data get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public Set<String> getAllKeys() {
readLock.lock();
try {
return map.keySet();
} finally {
readLock.unlock();
}
}
public Data put(String key, Data value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
readLock.unlock();
}
}
class Data {
}
}
此类实现的是悲观锁,当writeLock.lock()执行的时候,不允许其他线程还有readLock.lock()在执行。所以当读取很多写入很少的时候,此类容易让线程饥饿。(写操作一直等待读取操作完成,但是一直等不到)
StampedLock
此类有三种操作:写,读,乐观读
此锁的状态是由版本和模式两个部分组成。
锁的获取方法返回一个数字作为票据。
乐观读: 乐观的认为读取和写入的同时发生的情况很少。不用悲观的使用读取锁定
下面看一下乐观锁的源码案例:
//下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
下面看一下悲观锁的源码案例:
//下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
if (ws != 0L) { //这是确认转为写锁是否成功
stamp = ws; //如果成功 替换票据
x = newX; //进行状态改变
y = newY; //进行状态改变
break;
} else { //如果不能成功转换为写锁
sl.unlockRead(stamp); //我们显式释放读锁
stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
}
}
} finally {
sl.unlock(stamp); //释放读锁或写锁
}
}
ReentrantLock + Condition
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
try {
reentrantLock.lock();
log.info("wait signal"); // 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal"); // 4
reentrantLock.unlock();
}).start();
new Thread(() -> {
reentrantLock.lock();
log.info("get lock"); // 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
log.info("send signal ~ "); // 3
reentrantLock.unlock();
}).start();
}
下图是之前讲到的AQS队列,使用condition().await()会使当前线程加入到Condition queue里面。当condition().signalAll();执行时候,所有Condition里面的线程回到Sync queue里面来。当前线程unlock后,按照顺序唤醒其他线程。
锁的小结
当只有少量线程时,
synchronized很适合。如果线程不少但是增加可预估,ReentrantLock是个非常通用的锁。其他不可测情况用StampedLock
FutureTask
J.U.C 里面非 AQS 的子类。传统的Runnable实现线程的缺陷:执行完任务之后无法获取执行结果。从JDK5开始通过FutureTask可以收到任务执行的结果。
Callable 与 Runnable 接口对比:
- Runnable 简单粗暴,实现
run() - Callable 是个范型接口,可以有返回值
Future
- 也是一个接口
- 它可以取消任务,查询任务状态,获取结果,等等。可以监视目标线程调用
call的情况。也就是可以得到其他线程的返回值。
FutureTask
- 父类为
RunnableFuture - 父类继承了
Runnable和Future - 最终执行
Callable类型动作
下面是Future的演示
public class FutureExample {
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(new MyCallable());
log.info("do something in main");
Thread.sleep(1000);
String result = future.get();
log.info("result:{}", result);
}
}
submit提交Callable任务得到返回值
当调用future.get()发现返回值还没有计算结束时候,线程会堵塞直到收到返回值。
再来看一下FutureTask
public class FutureTaskExample {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
});
new Thread(futureTask).start();
log.info("do something in main");
Thread.sleep(1000);
String result = futureTask.get();
log.info("result:{}", result);
}
}
将futureTask传入Thread构造器可以直接调用start()执行当前线程,也可以使用futureTask来得到执行结果。就很nice。
Fork/Join 框架
JDK7提供的用于并行执行任务的框架。用于拆分大任务成小任务执行,最后汇总的框架。
工作窃取算法:
如果能将任务分成互不影响的小任务,那么假设一个线程已经执行完了它的全部任务,它可以从底部开始偷走别的线程的任务来提高程序效率。
缺点:当线程中只有一个任务,竞争依然会出现。创建多个线程进行小任务拆分也会有内存开销。
Fork/Join局限性
- 只能使用
Fork和Join进行同步机制,比如sleep - 不能进行I/O操作
- 不能抛弃异常,必须代码处理异常
最后来看下代码:
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
public static final int threshold = 2;
private int start;
private int end;
public ForkJoinTaskExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//如果任务足够小就计算任务
boolean canCompute = (end - start) <= threshold;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果任务大于阈值,就分裂成两个子任务计算
int middle = (start + end) / 2;
ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待任务执行结束合并其结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
// 合并子任务
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();
//生成一个计算任务,计算1+2+3+4
ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
//执行一个任务
Future<Integer> result = forkjoinPool.submit(task);
try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}
BlockingQueue
队列堵塞两种情况
- 队列满时入队列
- 队列空时出队列
堵塞队列是线程安全的,主要用于消费者与生产者环境
ArrayBlockingQueue
- 容量有限,FIFO方式插入写入数据 DelayQueue
- 必须实现
delete接口 - 定时关闭连接,缓存对象,超时处理都可以使用
- 底层是PriorityQueue + Lock LinkedBlockingQueue
- 可以指定容量
- 内部是链表。 PriorityBlockingQueue
- 带优先级的阻塞队列,没有边际
- 有排序规则,允许插入
NULL - 插入对象必须实现
Comparable接口 SynchronousQueue - 只能存一个元素,同步队列