概要
放眼望去,java.util.concurrent
包下类大致包括:atomic 原子类、锁、并发集合、线程池、工具类。我们挑重要的了解一下。
Atomic 原子类
Java针对并发编程已经有了各种锁,为什么还需要原子类?原子类一定有些特别的应用场景?
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的递增或者递减方案,这个方案一般需要满足以下要求:
1、 简单:操作简单,底层实现简单
2、 高效:占用资源少,操作速度快
3、 安全:在高并发和多线程环境下要保证数据的正确性
对于是需要简单的递增或者递减的需求场景,使用 synchronized 关键字和 lock 固然可以实现,但代码写的会略显冗余,且性能会有影响,此时用原子类更加方便。
Atomic 类的原理
通过 CAS 实现线程安全访问。CAS 可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。但并发量很大的话,CPU 会花费大量的时间在试错上面。如果并发量小的情况,这些消耗可以忽略不计。因此 Atomic 类会因为并发度太高而性能变差。
AtomicInteger
在Java中,++i
和i++
操作并不是线程安全的,因为他们不是原子操作。在并发场景下,数值加减操作有线程不安全,而 synchronized 这种锁又大大降低了并发效率。这时候就可以考虑使用AtomicInteger
。AtomicInteger
,一个提供原子操作的 Integer 的类。
我们先来看看AtomicInteger给我们提供了什么接口:
int getAndIncrement(); // 获取当前值,然后自加,相当于i++
int getAndDecrement(); // 获取当前值,然后自减,相当于i--
int incrementAndGet(); // 自加1后并返回,相当于++i
int decrementAndGet(); // 自减1后并返回,相当于--i
int getAndAdd(int delta); // 获取当前值,并加上预期值
int getAndSet(int newValue); // 获取当前值,并设置新值
AtomicLong
AtomicLong
也是在高并发下对单一变量进行 CAS 操作,从而保证其原子性。
LongAdder
LongAdder
类继承了Striped64
类,LongAdder
是一种以空间换时间的解决方案。其内部维护了一个long
类型的base
变量,和一个cell
数组,当线程写base
有冲突时,将其写入数组的一个cell
中。将base
和所有cell
中的值求和就得到最终LongAdder
的值了。
- 在高并发的场景,
AtomicAdder
比AtomicLong
更高效。代价是更高的空间消耗。 - 在并发度不高的情况下,
AtomicAdder
和AtomicLong
性能差不多。
LongAdder
可以作为数据库主键生成器。
Atomic 类总结
并发度不高用 AtomicInteger
、AtomicLong
,并发度高用LongAdder
。
锁
AQS
AbstractQueuedSynchronizer(AQS)
提供了一套可用于实现锁同步机制的框架。AQS
通过一个FIFO
队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。AQS
根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition
结构提供了wait/signal
等待唤醒机制。在JUC
中,诸如ReentrantLock
、CountDownLatch
等都基于AQS
实现。·
ReentrantLock
ReentrantLock
和synchronized
的作用是相同的,它们的比较:
- 它们都是可重入锁。
synchronized
是Java语言层面提供的语法,不需要考虑异常,且会自动释放锁,而ReentrantLock
是Java代码实现的锁,必须先获取锁,然后在finally
中正确释放锁。 ReentrantLock
可以尝试获取锁,超时还获取不到锁就可以处理别的事情,如tryLock(long timeout, TimeUnit unit)
;而synchronized
不能,只能一直等待。所以,使用ReentrantLock
比直接使用synchronized
更安全,线程在tryLock()
失败的时候不会导致死锁。- 可中断性:
synchronized
锁是不可中断的,无法响应中断请求。ReentrantLock
支持中断,可以响应中断请求。 - 锁的公平性:
synchronized
关键字是非公平锁,即不保证等待锁的线程获取锁的先后顺序。ReentrantLock
可以实现公平锁和非公平锁,默认是非公平锁,但可以通过构造方法来实现公平锁。公平锁:公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。
ReentrantLock 使用 Condition 完成等待和唤醒线程的功能
synchronized
可以配合wait
和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock
我们怎么编写wait
和notify
的功能呢?
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
可见,使用Condition
时,引用的Condition
对象必须从Lock
实例的newCondition()
返回,这样才能获得一个绑定了Lock
实例的Condition
实例。
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
await()
会释放当前锁,进入等待状态;signal()
会唤醒某个等待线程;signalAll()
会唤醒所有等待线程;- 唤醒线程从
await()
返回后需要重新获得锁。
ReadWriteLock
因为synchronized
和ReentrantLock
这两种锁都是独占锁,每次只允许一个线程执行临界区代码,所以它们比较重量级的锁。有些读多写少的场景,只用保证写的时候只有一个线程,而读的时候可以多个线程同时读。这时候读写锁派上用场了。
ReadWriteLock
的特点:
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)
ReadWriteLock
大大提高了并发读的执行效率。
注意:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
使用举例:
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}