Java多线程并发–Java并发包(JUC)
前言
前一篇文章中,笔者已经介绍了Java多线程的一些基础知识,但是想要成为一名中高级Java程序员还必须懂得Java并发包(JUC)的知识点,而且JUC现在也是面试中必问的知识点了。
1.什么是Java并发包(JUC)
为了更好的支持多线程并发编程,JDK内部提供了大量实用的API。Java并发包(JUC)就是java.util.concurrent包,该包主要给我们提供多线程控制、线程池和并发容器这三大部分,笔者接下去的讲解也是围绕这三块来展开的。
2.多线程控制
1.同步控制–重入锁
在上一篇文章中已经介绍过了synchronized关键字,这是一种最简单的控制方法,但是synchronized关键字在使用中并不够灵活。JDK在java.util.concurrent.locks包下给我们提供了增强版–重入锁。
重入锁可以完全代替synchronized关键字,并且在JDK1.5早期版本中,重入锁的性能远远高于synchronized关键字,JDK1.6之后,synchronized关键字做了大量的优化,才使得两者的性能差距不大。
重入锁使用java.util.concurrent.locks.ReentrantLock类来实现,该类实现了java.util.concurrent.locks.Lock接口。在这里主要介绍Lock接口中定义的这些方法。下面介绍的方法若没特别说明,就是属于Lock接口的,只要是Lock接口的实现类就可以正常使用。
2.获取锁和释放锁
返回值类型 | 方法 | 说明 |
---|---|---|
void | lock() | 获取锁,如果锁已经被占用,则等待 |
void | unlock() | 释放锁 |
public class ThreadDemo implements Runnable {
private static int count = 0;
private static Lock lock = new ReentrantLock();
@Override
public void run() {
for(int i=0; i<100; i++){
// 获取锁,如果锁已经被占用,则等待
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "线程抢到了锁");
count++;
}finally {
System.out.println(Thread.currentThread().getName() + "线程释放了锁");
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new ThreadDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("count = " + count);
}
}
注意:
- 线程找退出临界区(需要同步的代码块)时,必须要手动释放锁,否则其他线程就没机会获得锁进入临界区。
- ReentrantLock和关键字synchronized一样是可重入的,如果一个线程多次获得锁,在释放时必须释放相同次数。
3.公平锁
运行上面的示例代码,我们可以发现一个现象,同一个线程能连续多次获得到锁从而执行控制台打印。这是因为大多数情况下,锁的申请都是非公平的,JVM在调度时,一个线程会倾向于再次获取已经持有的锁,这种分配是高效的,但是容易出现饥饿现象(线程一直获取不到资源,一直在阻塞)。我们使用关键字synchronized产生的锁就是非公平的。
而公平锁,它会按照时间的先后顺序,保证所有线程都能等到资源执行代码。
方法 | 说明 |
---|---|
ReentrantLock(boolean fair) | 构造方法,fair 为true时,为公平锁,默认为false |
public class ThreadDemo implements Runnable {
private static Lock lock = new ReentrantLock(true);
@Override
public void run() {
for(int i=0; i<100; i++){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "线程抢到了锁");
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new ThreadDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
注意:
- 观察控制台的输出,很明显看到两个线程交替执行,并没想上一个示例那样同一个线程连续很多次获取锁。
- 虽然公平锁能保证线程不会出现饥饿现象,但是要实现公平锁,系统内部必然维护了有序队列,影响程序运行的性能,所以在没有特别需求时,尽量不要使用公平锁。
4.中断响应
在上一篇文章中,已经介绍过想要中断线程可以使用interrupt()和isInterrupted()方法,但是使用关键字synchronized时,如果一个线程在等待锁,只会发生两种情况:
- 获得锁继续执行
- 继续等待直到获得锁
即使我们调用interrupt()方法,也要等待获取锁后才能使用isInterrupted()方法。而重入锁给我们提供了更好的解决方案,即线程在等待锁的过程中也可以被中断。
返回值类型 | 方法 | 说明 |
---|---|---|
void | lockInterruptibly() | 获得锁,但优先响应中断 |
boolean | isHeldByCurrentThread() | 查询此锁是否由当前线程持有。(该方法是属于ReentrantLock类的,Lock接口中没有该方法) |
public class ThreadDemo implements Runnable {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
private int number;
public ThreadDemo(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
if(number == 1){
try {
lock1.lock();
System.out.println(Thread.currentThread().getName() + "线程获取了lock1的锁");
try {
Thread.sleep(500);
}catch (InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + "线程准备获取lock2的锁");
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "线程获取了lock2的锁");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "线程被中断");
}finally {
if(lock2.isHeldByCurrentThread()){
System.out.println(Thread.currentThread().getName() + "线程释放lock2的锁");
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + "线程释放lock1的锁");
lock1.unlock();
}
}else {
try {
lock2.lock();
System.out.println(Thread.currentThread().getName() + "线程获取了lock2的锁");
try {
Thread.sleep(500);
}catch (InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + "线程准备获取lock1的锁");
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "线程获取了lock1的锁");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "线程被中断");
}finally {
if(lock1.isHeldByCurrentThread()){
System.out.println(Thread.currentThread().getName() + "线程释放lock1的锁");
lock1.unlock();
}
System.out.println(Thread.currentThread().getName() + "线程释放lock2的锁");
lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadDemo(1), "t1");
Thread t2 = new Thread(new ThreadDemo(2), "t2");
t1.start();
t2.start();
Thread.sleep(2000);
System.out.println("中断线程:" + t2.getName());
t2.interrupt();
}
}
输出结果
t1线程开始执行。。。
t2线程开始执行。。。
t2线程获取了lock2的锁
t1线程获取了lock1的锁
t1线程准备获取lock2的锁
t2线程准备获取lock1的锁
中断线程:t2
t2线程被中断
t2线程释放lock2的锁
t1线程获取了lock2的锁
t1线程释放lock2的锁
t1线程释放lock1的锁
这个案例是模拟了线程t1和线程t2死锁。线程t1和t2启动后,t1先获取了lock1,然后再去获取lock2。t2先获取了lock2,然后再去获取lock1,这样就非常容易造成死锁现象,t1被阻塞在获取lock2,t2被阻塞在获取lock1。此时若没有外部操作,两个线程会一直处于死锁状态。main方法中执行了t2.interrupt()后,线程t2被中断,进而释放了lock2,使得线程t1可以获取到lock2,能继续执行,线程t2则结束退出。
5.尝试获取锁
上面的案例中我们在外部使用了线程中断,才避免了死锁。除了这个方式,JDK还给我们提供了tryLock()方法尝试获取锁。
返回值类型 | 方法 | 说明 |
---|---|---|
boolean | tryLock() | 尝试获得锁,如果成功,则返回true,失败返回false,该方法不等待,立即返回 |
boolean | tryLock(long timeout, TimeUnit unit) | 在给定时间内尝试获得锁(该方法能被中断) |
public class ThreadDemo implements Runnable {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
private int number;
public ThreadDemo(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
if(number == 1){
try {
lock1.lock();
System.out.println(Thread.currentThread().getName() + "线程获取了lock1的锁");
try {
Thread.sleep(500);
}catch (InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + "线程准备获取lock2的锁");
if(lock2.tryLock()){
System.out.println(Thread.currentThread().getName() + "线程获取了lock2的锁");
}else {
System.out.println(Thread.currentThread().getName() + "线程没有获取到lock2的锁");
}
}finally {
if(lock2.isHeldByCurrentThread()){
System.out.println(Thread.currentThread().getName() + "线程释放lock2的锁");
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + "线程释放lock1的锁");
lock1.unlock();
}
}else {
try {
lock2.lock();
System.out.println(Thread.currentThread().getName() + "线程获取了lock2的锁");
try {
Thread.sleep(500);
}catch (InterruptedException e){ }
System.out.println(Thread.currentThread().getName() + "线程准备获取lock1的锁");
if(lock1.tryLock()){
System.out.println(Thread.currentThread().getName() + "线程获取了lock1的锁");
}else {
System.out.println(Thread.currentThread().getName() + "线程没有获取到lock1的锁");
}
}finally {
if(lock1.isHeldByCurrentThread()){
System.out.println(Thread.currentThread().getName() + "线程释放lock1的锁");
lock1.unlock();
}
System.out.println(Thread.currentThread().getName() + "线程释放lock2的锁");
lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadDemo(1), "t1");
Thread t2 = new Thread(new ThreadDemo(2), "t2");
t1.start();
t2.start();
}
}
输出结果
t2线程开始执行。。。
t2线程获取了lock2的锁
t1线程开始执行。。。
t1线程获取了lock1的锁
t1线程准备获取lock2的锁
t1线程没有获取到lock2的锁
t1线程释放lock1的锁
t2线程准备获取lock1的锁
t2线程获取了lock1的锁
t2线程释放lock1的锁
t2线程释放lock2的锁
6.Condition
Lock接口中剩下最后一个方法newCondition() ,通过这个方法可以获取到一个Condition(条件)对象。
Condition对象的作用与Object类中的 wait 、notify和notifyAll 方法类似,可以说wait 、notify和notifyAll 方法是配合关键字synchronized使用,而Condition对象是配合Lock对象使用。
Condition本身是一个接口。
返回值类型 | 方法 | 说明 |
---|---|---|
void | await() | 导致当前线程等到发信号或 interrupted |
boolean | await(long time, TimeUnit unit) | 使当前线程等待直到发出信号或中断,或指定的等待时间过去 |
long | awaitNanos(long nanosTimeout) | 使当前线程等待直到发出信号或中断,或指定的等待时间过去 |
void | awaitUninterruptibly() | 使当前线程等待直到发出信号 |
boolean | awaitUntil(Date deadline) | 使当前线程等待直到发出信号或中断,或者指定的最后期限过去 |
void | signal() | 唤醒一个等待线程 |
void | signalAll() | 唤醒所有等待线程 |
public class ThreadDemo implements Runnable {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。");
System.out.println(Thread.currentThread().getName() + "线程进入等待");
try {
lock.lock();
condition.await();
System.out.println(Thread.currentThread().getName() + "线程被唤醒,执行任务结束");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadDemo());
t1.start();
Thread.sleep(2000);
lock.lock();
condition.signalAll();
lock.unlock();
}
}
注意:
- Condition对象与Lock对象是绑定的,Condition对象只能在当前Lock对象上操作。
- 和Object类中的 wait 、notify和notifyAll 方法一样,当调用await()方法时,会释放掉当前的锁,被 signal()方法唤醒时会去竞争锁资源。
- signal()方法必须在await()方法之后被调用,不然线程不会被唤醒。
7.读写锁
读写锁(ReadWriteLock)是JDK1.5提供的读写分离锁。假设这样一个场景,一个容器中存了大量的数据,程序对这个容器主要的操作就是在其中获取数据,增删改只是极少量的操作,这时再用重入锁(ReentrantLock)或者关键字synchronized,读写都是串行执行,程序性能是非常低的。而读写分离可以有效的减少锁竞争,提高程序性能。
读写锁访问约束:
- 读-读不互斥:读读之间不阻塞。
- 读-写互斥:读阻塞写,写也阻塞读。
- 写-写互斥:写写之间阻塞。
ReadWriteLock本身是一个接口,只定义了两个抽象方法:
- Lock readLock(): 返回用于阅读的锁
- Lock writeLock(): 返回用于写入的锁
ReadWriteLock接口有个实现类ReentrantReadWriteLock,从这个实现类的名字上也能看出,其返回的读锁和写锁都是可重入的。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
Lock lock = new ReentrantLock();
Thread[] ts = new Thread[20];
for(int i=0; i<20; i++){
ts[i] = new Thread(()->{
ThreadDemo.lockTest(readLock);
// ThreadDemo.lockTest(writeLock);
// ThreadDemo.lockTest(lock);
});
}
long startTime = System.currentTimeMillis();
for (Thread t : ts) {
t.start();
}
for (Thread t : ts) {
t.join();
}
long endTime = System.currentTimeMillis();
System.out.println("共耗时:" + (endTime-startTime));
}
private static void lockTest(Lock lock){
lock.lock();
try {
// 模拟一个耗时操作
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
这个示例中分别对读锁之间、写锁之间、重入锁之间的性能做了测试。当20个线程传入的都是读锁时,由于读读之间不阻塞,耗时基本是500毫秒稍微多一点。而写锁之间和重入锁之间耗时都在10秒以上,其线程是串行执行的。
8.信号量(Semaphore)
无论是synchronized关键字还是重入锁ReentrantLock,一次只能让一个线程访问一个资源。信号量(Semaphore)给我们提供了更强大的多线程控制,它允许同一时间多个线程访问。
返回值类型 | 方法 | 说明 |
---|---|---|
Semaphore(int permits) | 构造方法,permits 为信号量的准入数 | |
emaphore(int permits, boolean fair) | 构造方法,permits 为信号量的准入数,fair 指定是否公平 | |
void | acquire() | 尝试获取一个准入的许可,若无法获取,线程会等待 |
void | acquireUninterruptibly() | 与 acquire() 类似,但不响应中断 |
boolean | tryAcquire() | 尝试获得一个许可,成功返回true,失败返回false,线程不会进行等待 |
boolean | tryAcquire(long timeout, TimeUnit unit) | 在指定时间内尝试获得一个许可 |
void | release() | 释放一个许可 |
public class ThreadDemo implements Runnable {
private static Semaphore semaphore = new Semaphore(5);
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "线程正在执行。。。");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + "线程结束执行");
semaphore.release();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0; i<20; i++){
new Thread(new ThreadDemo()).start();
}
}
}
注意:
- 使用信号量(Semaphore)结束时,务必调用release()方法,释放信号量,如果发生了信号量泄露(申请了但没有释放),那么能进入临界区的线程会越来越少,直到没有线程能访问。
9.倒计数器(CountDownLatch)
倒计数器(CountDownLatch)通常用来控制线程等待,它可以让一个线程等待直到倒计数结束后再执行。生活中这样的场景很多,举个例子。工厂里生产一个产品,先是在各个车间里制造产品的组件,所有组件制造好后再组装起来,这个例子可能不是很恰当。
返回值类型 | 方法 | 说明 |
---|---|---|
CountDownLatch(int count) | 构造方法,count 为倒计数 | |
void | await() throws InterruptedException | 线程进入等待,直到倒计数为0 |
boolean | await(long timeout, TimeUnit unit) | 线程进入等待,直到倒计数为0,或指定的等待时间过去 |
void | countDown() | 倒计数减一 |
long | getCount() | 返回当前计数 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
Random random = new Random();
for(int i=0; i<10; i++){
new Thread(()->{
try {
Thread.sleep(random.nextInt(10) * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程执行结束,倒计数减一");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("主线程线程执行结束");
}
}
10.循环栅栏(CyclicBarrier)
循环栅栏(CyclicBarrier)与倒计数器(CountDownLatch)非常相似,但CyclicBarrier的功能更复杂更强大。CountDownLatch在倒计数结束之后就唤醒线程,它只能使用一次。而CyclicBarrier的这个计数器可以循环多次使用,还可以接受一个Runnable类型的参数,就是完成一次计数后,系统会执行的动作。
返回值类型 | 方法 | 说明 |
---|---|---|
CyclicBarrier(int parties) | 构造方法,parties为指定的等待它的线程 | |
CyclicBarrier(int parties, Runnable barrierAction) | 构造方法,parties为指定的等待它的线程,barrierAction为完成一次计数后系统会执行的动作 | |
int | await() | 等待所有 parties已经在这个障碍上调用了 await |
int | await(long timeout, TimeUnit unit) | 等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去 |
int | getNumberWaiting() | 返回目前正在等待障碍的各方的数量 |
int | getParties() | 返回这个障碍所需的数量 |
boolean | isBroken() | 查询这个障碍是否处于破碎状态 |
void | reset() | 将障碍重置为初始状态 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.err.println("5个线程执行完成,循环完一遍");
}
});
Random random = new Random();
for(int i=0; i<20; i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "线程开始执行。。。。");
try {
Thread.sleep(random.nextInt(10) * 500);
System.err.println(Thread.currentThread().getName() + "线程开始等待");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程继续执行。。。");
}).start();
}
}
}
11.线程阻塞工具类(LockSupport)
LockSupport是一个十分方便的线程阻塞工具,可以在线程内任意位置让线程阻塞。无论是Object类中的wait 、notify和notifyAll 方法还是Condition类的await和signal方法,都需要配合锁才能使用,而且唤醒线程的操作必须在线程睡眠之后,用起来稍微有点麻烦。而LockSupport不需要先获得锁,并且可以在线程睡眠之前给一个唤醒的信号,这样线程就不会进入睡眠状态。
返回值类型 | 方法 | 说明 |
---|---|---|
void | park() | 静态方法,禁止当前线程进行线程调度,除非许可证可用 |
void | unpark(Thread thread) | 为给定的线程提供许可证(如果尚未提供) |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 开始,时间:" + LocalDateTime.now().format(dtf));
LockSupport.park();
System.out.println(Thread.currentThread().getName() + ": 结束,时间:" + LocalDateTime.now().format(dtf));
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(3000);
LockSupport.unpark(t1);
Thread t2 = new Thread(runnable);
t2.start();
LockSupport.unpark(t2);
System.out.println(Thread.currentThread().getName() + ": 结束");
}
}
注意:
- 如果 unpark() 方法执行在 park() 方法之前,park() 方法会立即返回
3.线程池
1.什么是线程池
虽然与进程相比,线程是一种轻量级的工具,但其创建和关闭还是需要花费时间。如果为每个任务都开启一个线程,那么创建和销毁线程将消耗大量时间,并且每一个Java线程都是要一个Java对象,需要占用内存空间,当线程数量过多时,cpu和内存的资源也会被耗尽,过多的Java对象对JVM的垃圾回收也是很大的压力。
为了避免频繁的创建和销毁线程,我们就需要对线程进行复用,这就引出了线程池概念。在线程池中维护了若干个线程,当要使用线程时,可以从线程池中拿一个空闲的线程,当完成工作后,并不将这个线程关闭,而是将线程放回到线程池中,方便下次获取线程。简单的说,就是在使用线程时,不再自己创建线程,而是从线程池中获取线程,使用完后,关闭线程变成向线程池归还线程。
2.线程池工厂(Executors)
Executors(java.util.concurrent.Executors)是一个十分像工厂模式的线程池工具类。通过这个工具类,我们可以获取各种类型的线程池。
返回值类型 | 方法 | 说明 |
---|---|---|
ExecutorService | newFixedThreadPool(int nThreads) | 静态方法,返回一个固定线程数量的线程池 |
ExecutorService | newSingleThreadExecutor() | 静态方法,返回一个只有一个线程的线程池 |
ExecutorService | newCachedThreadPool() | 静态方法,返回一个可根据实际情况调整线程数量的线程池 |
public class ThreadDemo {
public static void main(String[] args) {
// 创建一个大小为5的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
for(int i=0; i<10; i++){
// 执行任务
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在执行。。。");
}
});
}
// 关闭线程池
executor.shutdown();
}
}
注意:
- newFixedThreadPool(int nThreads)创建的线程池中的线程数量始终不变,当有一个新的任务提交时,线程池中若有空线程,则立即执行,若没有,则新的任务会被暂时存在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。
- newSingleThreadExecutor()创建的线程池中的线程只有一个,若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- newCachedThreadPool()创建的线程池中的线程数量不确定,若有空闲线程可以复用,则会优先使用可复用的线程,若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。
3.线程池(ThreadPoolExecutor)
如果去看过Executors源码,就会发现newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool三个方法内部实现都是使用了ThreadPoolExecutor对象,只是由于传入的参数不同,而创建了不同功能的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
接下来,我们来看一下线程池(ThreadPoolExecutor)最重要的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 具体实现省略
}
注意:
- corePoolSize: 指定线程池中的线程数量
- maximumPoolSize: 指定线程池中的最大线程数量
- keepAliveTime: 当线程池线程数量超过corePoolSize 时,多余的空闲线程的存活时间
- unit: keepAliveTime 的单位
- workQueue: 任务队列,存放被提交但尚未被执行的任务
- threadFactory: 线程工厂,用于创建线程,一般默认即可
- handler: 拒绝策略,当任务太多来不及处理时,如何拒绝任务
- ThreadPoolExecutor类虽然还有几个重载的构造方法,但这些构造方法内部实现都是调用上面那个构造方法。
回过头来,再来看Executors类的newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool,不难理解这三个方法创建出来的线程池的特点。
newFixedThreadPool方法中,传入的线程池中的线程数量和线程池中的最大线程数量都是同一个参数,故其线程数量为固定的,任务队列使用了LinkedBlockingQueue无界队列,当频繁提交任务,并且任务执行又不是那么快时,该队列会迅速膨胀,从而耗尽系统资源。
newSingleThreadExecutor方法与newFixedThreadPool方法类似,只不过传入的线程池中的线程数量和线程池中的最大线程数量都为1。
newCachedThreadPool方法中,传入的线程池中的线程数量为0,线程池中的最大线程数量传入的是最大整数,并且其任务队列使用的是SynchronousQueue,SynchronousQueue是一种直接提交的队列,并不会存储任务,它会使得线程池增加线程执行任务。如果同时有大量任务提交时,系统就要创建等量的线程来处理任务,这样也会很快耗尽系统资源。
4.线程池返回结果的任务
在这个类图中可以看到ThreadPoolExecutor类是继承了AbstractExecutorService类,而AbstractExecutorService类又是实现了ExecutorService接口。所以在这里我主要就介绍ExecutorService接口中定义的一些方法。
返回值类型 | 方法 | 说明 |
---|---|---|
void | execute(Runnable command) | 在将来的某个时间执行给定的任务 |
Future | submit(Callable task) | 提交任务以执行,并返回代表任务待处理结果的Future |
Future<?> | submit(Runnable task) | 提交一个可运行的任务执行,并返回一个表示该任务的未来 |
void | shutdown() | 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务 |
上面的代码示例中已经使用过了execute提交任务,而submit的重载方法 submit(Runnable task),与execute提交任务类似,这里也不多做介绍。submit(Callable task)传入的是一个Callable类型的任务,Callable接口内部自定义了一个call() 方法,该方法有一个返回值。
public class ThreadDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// 简单模拟一下任务执行操作
Thread.sleep(1000);
return "我是线程" + Thread.currentThread().getName() + ",我完成了一个任务并返回了一个字符串。";
}
});
try {
// 等待任务完成,获取结果
String s = future.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
// 该异常是任务方法call方法中抛出的异常
e.printStackTrace();
}
executor.shutdown();
}
}
注意:
- 线程池submit提交任务后会返回一个Future类型的对象,通过该对象,可以对当前任务做取消、获取计算结果等多种操作。
- 线程池submit提交的任务类型如果是Runnable,由于Runnable中的run方法并不会返回结果,调用Future对象的get方法,返回的是null。
5.计划任务线程池(ScheduledExecutorService)
ScheduledExecutorService是一个可以设置若干时间后执行任务的线程池,类似JavaScript中的setTimeout函数,提交任务后,它不会立即执行,而是在指定时间执行任务。ScheduledExecutorService是一个接口,继承了ExecutorService接口。
ScheduledExecutorService线程池可以通过Executors中的静态方法newSingleThreadScheduledExecutor和newScheduledThreadPool获取,这两个方法区别就是newSingleThreadScheduledExecutor返回的是只有1个线程的线程池,而newScheduledThreadPool可以指定线程数量。
接下来,我们来看一下ScheduledExecutorService中的一些特有的方法。
返回值类型 | 方法 | 说明 |
---|---|---|
ScheduledFuture | schedule(Callable callable, long delay, TimeUnit unit) | 在给定时间后执行任务 |
ScheduledFuture | schedule(Runnable command, long delay, TimeUnit unit) | 在给定时间后执行任务 |
ScheduledFuture | scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 对任务进行周期性调度,调度频率一定,以上一个任务开始执行时间为起点 |
ScheduledFuture | scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) | 对任务进行周期性调度,以上一个任务结束时间为起点 |
public class ThreadDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// 简单模拟一下任务执行操作
System.out.println(Thread.currentThread().getName() + "----" + LocalDateTime.now());
}
}, 2L, 5L, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + "----" + LocalDateTime.now());
}
}
注意:
- schedule方法提交任务,是在指定时间后,只会对任务进行一次调度。
- schedule方法提交任务提交的任务类型为Callable时,可以通过该方法返回的ScheduledFuture对象获取任务的结果,ScheduledFuture接口是继承Future接口的,故操作和Future对象类似。
- 方法scheduleAtFixedRate和scheduleWithFixedDelay都只能提交Runnable类型的任务。
- 方法scheduleAtFixedRate和scheduleWithFixedDelay的参数中的initialDelay为第一次执行任务的延时时间,delay为循环任务间隔时间,两个参数的单位都是由unit定义的。
- scheduleAtFixedRate方法的间隔时间是以上一个任务开始时间为起点,scheduleWithFixedDelay方法的间隔时间是以上一个任务结束时间为起点。当线程执行的任务比较慢,超过设置的间隔时间时,scheduleAtFixedRate方法调度的任务会在任务结束后立即启动调度执行,而scheduleWithFixedDelay方法调度的任务则会在任务结束后等待间隔时间后再调度任务执行。
6.ForkJoinPool
fork有分岔、分流的意思,join 结合。ForkJoinPool线程池,从它的名字中也能猜到一些它的作用,将一个任务拆分成若干个小任务,并提交给线程池执行,并将各分支的结果合并得到最终的结果。ForkJoinPool线程池从思想上讲比较类似大数据中的分而治之的思想,从编写的代码上来看,可能会比较像递归算法。
ForkJoinPool线程池使用中,最重要的是它的任务对象类ForkJoinTask,ForkJoinTask类是一个抽象类,其有两个重要的子类: RecursiveAction和RecursiveTask。这两个子类也是抽象类,在开发时需要继承其中一个子类,并重写compute方法,compute方法就是任务执行的主要逻辑。RecursiveAction表示无返回值的任务,RecursiveTask表示有返回值的任务。
public class ThreadDemo {
public static void main(String[] args) {
// 创建一个ForkJoinPool线程池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 提交一个任务
ForkJoinTask<Long> submit = forkJoinPool.submit(new MyTask(0, 10000000L));
try {
// 获取最终结果,该方法会阻塞
Long sum = submit.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyTask extends RecursiveTask<Long> {
private final long start;
private final long end;
public MyTask(long start, long end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if((end - start) < 10000){
for(long i=start; i<=end; i++){
sum += i;
}
}else {
long step = (end - start) / 100;
MyTask[] myTasks = new MyTask[100];
long pos = start;
long lastOne;
for(int i=0; i<100; i++){
lastOne = pos + step;
if(lastOne > end){
lastOne = end;
}
myTasks[i] = new MyTask(pos, lastOne);
pos = lastOne + 1;
// 将任务分流执行
myTasks[i].fork();
}
for (MyTask myTask : myTasks) {
// 合并各分支的结果
sum += myTask.join();
}
}
return sum;
}
}
注意:
- 在上面的示例中,可以看到compute方法的编写是十分类似递归方法的,当任务层次划分太多的时候,其也会产生类似递归算法的一些问题。第一,系统内的线程越积越多,导致性能严重下降。第二,若调用的层次太多,可能会导致栈溢出(StackOverflowError)。
4.并发容器
说到Java中线程安全的容器类,第一想到的可能就是Vector、HashTable这些。但在这里,我将给大家介绍的是java.util.concurrent包下的并发容器。
1.ConcurrentHashMap
ConcurrentHashMap,可以把它理解为一个高效的、线程安全的HashMap,用法也与HashMap一致,不同的是ConcurrentHashMap中的key和value都不能为null。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 如果key或者value为null,则抛出空指针异常
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 如果节点数组对象为null,或者其长度为0,初始化节点数组
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 将f设值为当前key该放入的那个桶位,如果该桶位为空,则根据CAS算法将现在的值放进去,成功则退出循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
// 如果当前桶位中为转发节点
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 对当前桶位中的链表或红黑树加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
这段代码是ConcurrentHashMap中的源码,是put放发的主要逻辑,ConcurrentHashMap 内部是在数据写入链表的时候加锁,其不会影响访问和操作ConcurrentHashMap其他链表数据。所以如果数据分布够均匀,那么最高能有ConcurrentHashMap中桶位个数的线程无冲突访问ConcurrentHashMap对象。
2.CopyOnWriteArrayList
CopyOnWriteArrayList是一个性能非常好的ArrayList,在读多写少的环境中,其性能远远高于Vector。
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这是CopyOnWriteArrayList中的部分源码,CopyOnWriteArrayList在读取数据时采用了无锁设计,在写入操作时,会进行一次自我复制,将修改的内容写入副本中,写完之后,再用修改完的副本代替原来的数据。所以CopyOnWriteArrayList只有写-写互斥,读-读 和 读-写 都不互斥。
3.ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个高性能的并发队列,使用链表实现,可以当做线程安全的LinkedList。ConcurrentLinkedQueue底层使用了CAS保证数据的安全。
4.BlockingQueue
BlockingQueue 是一个接口。该接口中有两个重要的方法:put和take。put方法是将元素压入队列末尾,如果队列满了,它会一直等待,直到队列中有空闲的位置。take方法是从队列的头部获得一个元素,如果队列为空,该方法会等待,直到队列内有可用的元素。正是这样的操作BlockingQueue保证了其读-写不互斥。
BlockingQueue 有两个比较重要的实现ArrayBlockingQueue、LinkedBlockingQueue。ArrayBlockingQueue基于数组实现的,队列中可容纳的最大元素需要在队列创建时指定,适合做有界队列。而LinkedBlockingQueue基于链表实现,适合做无界队列,或者边界值非常大的队列。