本文针对JUC包中的常用类进行一个简单整理,讲解为什么用、是什么和怎么用,但不会涉及背后的原理。我一贯认为,先使用有了直观感受之后再探究原理会事半功倍。本文涉及到的类如下:
- 线程局部变量,ThreadLocal
- 并发随机数生成器,ThreadLocalRandom
- 原子操作类,AtomicInteger等
- 常用的锁
- 可重入的独占锁,ReentrantLock
- 可重入的读写锁,ReentrantReadWriteLock
- 同步器
- 和锁配合实现wait/notify模式,Condition
- 控制并发线程数,Semaphore
- 等待多线程完成,CountDownLatch
- 同步屏障,CyclicBarrier
- 并发List
- 线程安全的ArrayList,CopyOnWriteArrayList
- 并发队列
- 线程安全的无界非阻塞队列,ConcurrentLinkedQueue
- 独占锁方式实现的阻塞队列,LinkedBlockingQueue
- 有界数组方式实现的阻塞队列,ArrayBlockingQueue
- 带优先级的无界阻塞队列,PriorityBlockingQueue
- 无界阻塞延迟队列,DelayQueue
- 并发Map
- 哈希表实现,ConcurrentHashMap
- 跳表实现,ConcurrentSkipListMap
- 线程池
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
1. ThreadLocal
ThreadLocal的设计原理是:对于多个线程并发访问的共享变量,ThreadLocal可以提供线程本地变量,访问ThreadLocal变量的每个线程都会有这个变量的一个本地副本,当线程操作这个变量时,实则是操作的自己本地内存里的变量,从而在根本上解决了线程安全问题。
ThreadLocal的常用方法有get()、set()和remove(),在下面的代码中,使用两个线程分别访问提供一个ThreadLocal变量,从输出结果可知两个线程中的变量是独立存在的。
public class ThreadLocalTest {
private static ThreadLocal local = new ThreadLocal<>();
public static void main(String[] args) {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
local.set("Thread One Variable");
System.out.println("threadOne: " + local.get());
local.remove();
System.out.println("threadOne: " + local.get());
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
local.set("Thread Two Variable");
System.out.println("threadTwo: " + local.get());
local.remove();
System.out.println("threadTwo: " +local.get());
}
});
one.start();
two.start();
}
}
程序输出:
threadOne: Thread One Variable
threadOne: null
threadTwo: Thread Two Variable
threadTwo: null
Process finished with exit code 0
2. ThreadLocalRandom
Random类随机数的产生是用一个种子变量计算出的,在多线程环境下对这个种子变量的竞争会降低并发性能,因此,ThreadLocalRandom应运而生。顾名思义,ThreadLocalRandom的原理是:让每一个线程都拥有自己的种子变量,则每个线程更新随机数时都根据自己老的种子计算新的种子,并使用新的种子计算随机数,这样就不会存在竞争问题了,可以极大提高并发性能。
使用ThreadLocalRandom时在每个线程里用ThreadLocalRandom.current()
获取ThreadLocalRandom实例并使用即可,在下面的代码中,使用ThreadLocalRandom和Random各生成一千万个整数,ThreadLocalRandom耗时30ms,Random耗时507ms,显然ThreadLocalRandom在多线程环境下效率更高。
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
public class TheadLocalRandomTest {
private static Random r = new Random();
private static CountDownLatch randomLatch = new CountDownLatch(10);
private static CountDownLatch randomLocalLatch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
useRandom();
useThreadLocalRandom();
}
private static void useThreadLocalRandom() throws InterruptedException {
long start = System.currentTimeMillis();
ThreadLocalRandomTestThread[] rs = new ThreadLocalRandomTestThread[10];
for (int i = 0; i < rs.length; i++) {
rs[i] = new ThreadLocalRandomTestThread();
new Thread(rs[i]).start();
}
randomLocalLatch.await();
long end = System.currentTimeMillis();
System.out.println("Use ThreadLocalRandom to generate 10000000 integers : " + (end - start) + " ms" );
}
private static void useRandom() throws InterruptedException {
long start = System.currentTimeMillis();
RandomTestThread[] rs = new RandomTestThread[10];
for (int i = 0; i < rs.length; i++) {
rs[i] = new RandomTestThread();
new Thread(rs[i]).start();
}
randomLatch.await();
long end = System.currentTimeMillis();
System.out.println("Use Random to generate 10000000 integers : " + (end - start) + " ms" );
}
static class ThreadLocalRandomTestThread implements Runnable {
@Override
public void run() {
ThreadLocalRandom localRandom = ThreadLocalRandom.current();
for (int i = 0; i < 1000000; i++) {
localRandom.nextInt();
}
randomLocalLatch.countDown();
}
}
static class RandomTestThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
r.nextInt();
}
randomLatch.countDown();
}
}
}
程序输出:
Use Random to generate 10000000 integers : 507 ms
Use ThreadLocalRandom to generate 10000000 integers : 30 ms
Process finished with exit code 0
3. 原子操作类
当多个线程同时更新同一个变量时,可能会得到期望之外的值,例如变量i = 1,A线程更新i = i + 1,B线程更新i = i + 1,经过A和B操作完后结果可能不是期望的3,而是可能为2,因为A和B线程在更新变量i的时候拿到的i可能都是1。为了解决此类线程不安全的更新操作,我们通常会用synchronized关键字,但其属于重量级锁,性能较低,JUC包中的Atomic包的原子操作类提供了一种用法简单、性能高效又线程安全的变量更新方式。
对于基本类型的原子方式更新,Atomic包提供了三个类:AtomicBoolean、AtomicInteger、AtomicLong。它们的接口方法几乎一致,我们以AtomicInteger为例。
AtomicInteger的常用方法有addAndGet
等,这些API望文生义即可,无需过多解释。在下面的代码中,使用1000个线程对一个整数分别累加1000次,期望结果应当是100000,这个整数分别用普通整型变量、synchronized加锁保护和AtomicInteger变量实现,最后我们观察累加结果和时间消耗。由下面结果可见,使用普通整型变量的结果必定会小于1000000,但时间消耗是最小的;使用synchronized加锁保护和AtomicInteger变量的方式其累加结果是正确的,且synchronized比AtomicInteger变量更耗时。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
private static Integer a = new Integer(0);
private static Integer b = new Integer(0);
private static AtomicInteger c = new AtomicInteger(0);
private static final int THREAD_NUM = 1000;
private static CountDownLatch latchOne = new CountDownLatch(THREAD_NUM);
private static CountDownLatch latchTwo = new CountDownLatch(THREAD_NUM);
private static CountDownLatch latchThree = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) throws InterruptedException {
useThreadTwo();
useThreadOne();
useThreadThree();
}
private static void useThreadOne() throws InterruptedException {
long start = System.currentTimeMillis();
ThreadOne threadOne = new ThreadOne();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(threadOne).start();
}
latchOne.await();
long end = System.currentTimeMillis();
System.out.println("normal: a => time = " + (end - start) + " ms" + ", result = " + a);
}
static class ThreadOne implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
a++;
}
latchOne.countDown();
}
}
private static void useThreadTwo() throws InterruptedException {
long start = System.currentTimeMillis();
ThreadTwo threadTwo = new ThreadTwo();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(threadTwo).start();
}
latchTwo.await();
long end = System.currentTimeMillis();
System.out.println("synchronized: b => time = " + (end - start) + " ms" + ", result = " + b);
}
static class ThreadTwo implements Runnable {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 1000; i++) {
b++;
}
latchTwo.countDown();
}
}
}
private static void useThreadThree() throws InterruptedException {
long start = System.currentTimeMillis();
ThreadThree threadThree = new ThreadThree();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(threadThree).start();
}
latchThree.await();
long end = System.currentTimeMillis();
System.out.println("AtomicInteger: c => time = " + (end - start) + " ms" + ", result = " + c);
}
static class ThreadThree implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
c.incrementAndGet();
}
latchThree.countDown();
}
}
}
程序输出:
synchronized: b => time = 644 ms, result = 1000000
normal: a => time = 331 ms, result = 943204
AtomicInteger: c => time = 408 ms, result = 1000000
Process finished with exit code 0
4. 常用的锁
Java的synchronized关键字已经给我们提供了锁的功能,synchronized的特点是使用简单,一切交给JVM处理不需要显示释放锁;而Lock需要在finally里释放锁,否则会产生异常。Lock并不是对synchronized的替代,而是提供了更高级的功能,当你需要控制可重入、公平锁、可限时等功能。
4.1 ReentrantLock
ReentrantLock是可重入锁,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。下面代码中,使用1000个线程对变量分别累加10,并通过ReentrantLock控制来确保输出正确的结果10000。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private static ReentrantLock lock = new ReentrantLock();
private static int i = 0;
private static final int THREAD_NUM = 1000;
private static CountDownLatch cd = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < THREAD_NUM; j++) {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
for (int k = 0; k < 10; k++) {
i++;
}
} finally {
lock.unlock();
}
cd.countDown();
}
}).start();
}
cd.await();
System.out.println("i = " + i);
}
}
程序输出:
i = 10000
Process finished with exit code 0
4.2 ReentrantReadWriteLock
ReentrantLock是独占锁,每时每刻只能有一个线程可以获取该锁,但实际场景中会有读多写少的场景,此时使用ReentrantLock就会影响性能,因为读线程和读线程之间不会引起线程安全问题,于是,ReentrantReadWriteLock应运而生,ReentrantReadWriteLock采取读写分离的策略,允许多个线程可以同时获取读锁。
通过ReentrantReadWriteLocks的实例对象可以获取ReadLock和WriteLock,在下面代码中,分别用ReentrantReadWriteLock和ReentrantLock实现了List,并各自启用1000个线程来执行查询操作,观察结果可发现ReentrantReadWriteLock的时间耗费远低于ReentrantLock。
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockTest {
private static ReentrantLockList lockList = new ReentrantLockList();
private static ReentrantReadWriteLockList readWriteLockList = new ReentrantReadWriteLockList();
private static final int THREAD_NUM = 1000;
private static CountDownLatch lockDownLatch = new CountDownLatch(THREAD_NUM);
private static CountDownLatch readWriteLockDownLatch = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) throws InterruptedException {
lockList.add(0);
readWriteLockList.add(0);
useReentrantLockList();
useReentrantReadWriteLockList();
}
private static void useReentrantReadWriteLockList() throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriteLockList.get(0);
readWriteLockDownLatch.countDown();
}
}).start();
}
readWriteLockDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("ReentrantReadWriteLock => time = " + (end - start) + " ms");
}
private static void useReentrantLockList() throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lockList.get(0);
lockDownLatch.countDown();
}
}).start();
}
lockDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("ReentrantLockList => time = " + (end - start) + " ms");
}
}
class ReentrantLockList {
private ArrayList array = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
public void add(int e) {
lock.lock();
try {
array.add(e);
} finally {
lock.unlock();
}
}
public int get(int index) {
lock.lock();
try {
Thread.sleep(10);
return array.get(index);
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
} finally {
lock.unlock();
}
}
}
class ReentrantReadWriteLockList {
private ArrayList array = new ArrayList<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
public void add(int e) {
writeLock.lock();
try {
array.add(e);
} finally {
writeLock.unlock();
}
}
public int get(int index) {
readLock.lock();
try {
Thread.sleep(10);
return array.get(index);
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
} finally {
readLock.unlock();
}
}
}
程序输出:
ReentrantLockList => time = 10951 ms
ReentrantReadWriteLock => time = 184 ms
Process finished with exit code 0
5. 同步器
5.1 Condition
使用Lock可以实现临界区的互斥访问,但想实现线程间的等待通知模式,还需要Condition的辅助。Condition对象由Lock对象创建,Condition对象定义了await和signal两种方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。当调用await方法时,当前线程会释放锁并在此等待,而其它线程调用Condition的signal方法,当前线程才从await方法返回,并在返回前已经获取了锁。
获取一个Condition必须通过Lock的newCondition()方法。下面使用Lock和Condition实现生产者消费者模式:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
private static final int SIZE = 10;
private static List items = new ArrayList<>();
private static ReentrantLock lock = new ReentrantLock();
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();
public static void main(String[] args) {
Thread producer = new Thread(new Producer());
Thread consumer = new Thread(new Consumer());
producer.start();
consumer.start();
}
static class Producer implements Runnable {
private void produce() {
lock.lock();
try {
if (items.size() == SIZE) {
System.out.println("Producer start await ...");
notFull.await();
}
items.add(ThreadLocalRandom.current().nextInt(100));
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
while (true) {
produce();
}
}
}
static class Consumer implements Runnable {
private void consume() {
lock.lock();
try {
if (items.size() == 0) {
System.out.println("Consumer start await ...");
notEmpty.await();
}
items.remove(0);
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
while (true) {
consume();
}
}
}
}
5.2 Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,为多线程协作提供了更为强大的控制方法,是对锁的一种扩展。锁每一次只允许一个线程访问某一个资源,而信号量却可以指定多个线程同时访问某一个资源。
信号量在初始化时指定信号量的准入数,并通过acquire和release两个方法来控制线程的数量,示例见下面代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest implements Runnable{
private Semaphore semaphore = new Semaphore(5);
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(20);
SemaphoreTest demo = new SemaphoreTest();
for (int i = 0; i < 20; i++) {
exec.submit(demo);
}
exec.shutdown();
}
@Override
public void run() {
try {
semaphore.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + ":done!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
5.3 CountDownLatch
CountDownLatch适用于这样的场景:当需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。
CountDownLatch的常用方法为:初始化时设置计数器的大小,countDown方法使计数器减一,await方法使得当前线程在计数器不为0时等待。前文中的代码已多次使用过CountDownLatch,故不再赘述代码。
5.4 CyclicBarrier
CountDownLatch的缺点在于其计数器是一次性的,也就是当其计数器变为0后,再调用await和countDown方法都会立即返回,从而失去了线程同步的作用。CyclicBarrier提供了计数器可重置的功能,它的作用就是会让所有线程都等待完成后才会继续下一步行动。
CyclicBarrier的常用方法为:初始化时设置计数器大小,调用await方法时使得计数器减1并当计数器不为0时进行等待,当计数器为0时,放行所有线程并重置计数器,示例代码如下:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
private static final int NUM = 3;
private static CyclicBarrier barrier = new CyclicBarrier(NUM);
public static void main(String[] args) {
for (int i = 0; i < NUM; i++) {
new Thread(new Task()).start();
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getId() + " reach barrier A");
barrier.await();
System.out.println(Thread.currentThread().getId() + " leave barrier A");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + " reach barrier B");
barrier.await();
System.out.println(Thread.currentThread().getId() + " leave barrier B");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
程序输出:
10 reach barrier A
11 reach barrier A
12 reach barrier A
12 leave barrier A
11 leave barrier A
10 leave barrier A
12 reach barrier B
11 reach barrier B
10 reach barrier B
10 leave barrier B
12 leave barrier B
11 leave barrier B
Process finished with exit code 0
6. 并发List
并发包中的并发List只有CopyOnWriteArrayList,顾名思义,当对其修改时,会使用写时复制策略,即对其进行的修改操作都是在底层一个复制的数组(快照)上进行的。
显然,写时复制策略会导致弱一致性问题,也就是说,对List的增删改是需要在一个新数组上执行的,而对List的查是在老数组上执行的。当返回List的迭代器进行遍历时,List的增删改都将对该迭代器不可见,这就是弱一致性问题。
下面这段代码中,在主线程中对CopyOnWriteArrayList添加了五个数,并启用了一个新线程删除了CopyOnWriteArrayList的三个数,但最后的结果输出仍然是五个数,这就说明了弱一致性问题。
package juc;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListTest {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
new Thread(new Runnable() {
@Override
public void run() {
list.remove(0);
list.remove(1);
list.remove(2);
}
}).start();
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
程序输出:
1
2
3
4
5
Process finished with exit code 0
7. 并发队列
总的来说,并发队列可分为阻塞队列和非阻塞队列,前者使用锁实现,后者使用CAS非阻塞算法实现;也可以分为有界和无界,前者使用数组实现,后者使用链表实现。
由于并发队列使用方法和普通队列一样,如无需要验证的特殊性质,将不再提供代码。
7.1 ConcurrentLinkedQueue
无界非阻塞队列,底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。由于使用非阻塞CAS算法,没有加锁,所以在计算size时有可能进行了offer、poll等操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用。
7.2 LinkedBlockingQueue
无界阻塞队列,底层数据结构使用单向链表实现,对于入队和出队操作使用独占锁来实现线程安全。
7.3 ArrayBlockingQueue
有界阻塞队列,底层数据结构使用数组实现,对于入队和出队操作使用独占锁来实现线程安全。
7.4 PriorityBlockingQueue
无界阻塞队列,带优先级,底层使用堆实现,但数组可扩容,对于入队和出队操作使用独占锁来实现线程安全。
7.5 DelayQueue
无界阻塞队列,元素带过期时间,队列头元素是最快要过期的元素,当从队列获取元素时,只有过期元素才会出队列。其内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。
8. 并发Map
8.1 ConcurrentHashMap
HashMap不是并发安全的,在多线程环境下容易造成死循环问题;并发安全的HashTable又是效率低下的,因此,ConcurrentHashMap应运而生。ConcurrentHashMap采用锁分段技术,在保证安全的情况下也提高了效率。
参考文献
【1】《Java并发编程之美》翟陆续等著,电子工业出版社。
【2】《Java并发编程的艺术》方腾飞等著,机械工业出版社。
【3】《实战Java高并发程序设计(第2版)》葛一鸣著,电子工业出版社。
每日学习笔记,写于2020-04-18 星期六