进程与线程
进程是程序的一次执行过程,是系统运行程序的基本单位。每个进程都有自己的内存空间和系统资源,它是系统进行资源分配和调度的一个独立单位。线程与进程相似,一个进程在其执行的过程中可以产生多个线程,与进程不同的是多个线程共享一块内存空间,引入线程的目的是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以便于系统管理,因此线程也被称为轻量级进程。
线程的6个基本状态:
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。java线程状态变迁如下图所示:
线程创建之后将处于初始(NEW)状态,调用start()
方法后开始运行,这时候线程处于就绪(READY)状态。就绪状态的线程获得了cpu时间片(timeslice)后就处于运行(RUNNING)状态。
操作系统隐藏jvm中的READY状态和RUNNING状态,它只看到RUNNABLE状态。
当线程执行 wait()
方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
线程属性
线程优先级
默认情况下,一个线程继承它的父线程的优先级,可以将优先级设置为在MIN_PRIORITY(在Thread类中定义为1)与MAX_PRIORITY(定义为10)之间的任何值。NORM_PRIORITY被定义为5。
void setPriority(int newPriority) :设置线程的优先级,优先级处于Thread.MIN_PRIORITY与Thread.MAX_PRIORITY之间。一般使用Thread.NORM_PRIORITY优先级。
守护线程
守护线程的唯一用途是为其他线程提供服务,当只剩下守护线程时,虚拟机自动退出。
void setDaemon(boolean isDaemon) :标识该线程为守护线程或用户线程,这一方法必须在线程启动之前调用。
多线程的创建
创建多线程有两种方法:
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法(也可采用匿名内部类方式)
Thread类
实现多线程本质上都是由Thread类来进行操作的,多线程执行时,在栈内存中每一个执行线程都有一片自己所属的栈内存空间来进行方法的压栈和弹栈。常用构造方法:
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。
而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
常用方法:
public String getName() :获取当前线程名称。
public void setName() :设置当前线程的名称。
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() :此线程要执行的任务在此处定义代码。
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
public static void yield() :让别的线程先执行,但不确保真正让出。
public void join() :等待该线程死亡才执行别的线程。
public void interrupt() :中断这个线程,由被通知的线程自己终止,不会真正停止一个线程。
public static boolean interrupted() :测试当前线程是否被中断,并将当前线程的中断状态重置为false。
public boolean isInterrupted() :测试线程是否被中断。
Runnable接口
采用Runnable接口创建多线程是非常常见的一种,步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
的线程对象。 - 调用线程对象的start()方法来启动线程。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用
java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进
程。
线程同步
在多线程环境下,线程是交替执行的。当使用多个线程执行包含共享变量的代码块或者一些非原子操作(组合操作)时,可能会出现多个线程修改对象状态导致对象状态不一致的情况,导致发生线程安全问题。
锁机制
有两种机制防止代码块受并发访问的干扰:
- Synchronized关键字
- 显示Lock锁
Synchronized关键字
Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。synchronized同时保证线程的原子性(被保护代码块是一次被执行的,没有任何线程会同时访问)和可见性(当执行完synchronized之后,修改后的变量对其他的线程是可见的)。
synchronized用法:
- 修饰静态方法,获取到的锁对象是类的字节码文件对象
public static synchronized void method(){
产生线程安全问题的代码块
}
- 修饰普通方法,用到的锁是对象锁
public synchronized void method(){
产生线程安全问题的代码块
}
- 修饰代码块,锁对象是类的实例化对象
synchronized(同步锁){
需要同步操作的代码
}
同步锁的锁对象可以是任意类型(每个对象都有一个内置锁)。
synchronized内部条件锁只有一个相关条件。Object定义了3个final方法,wait()
方法添加一个线程到等待集中,notifyAll()
和notify()
方法解除等待线程的阻塞状态。
Lock锁
java.util.concurrent.locks.lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有。常见的Lock有ReentrantLock和ReentrantReadWriteLock。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁public void unlock()
:释放同步锁
AQS(AbstractQueueSynshronized)
通常地,java.util.concurrent.locks.lock
下的AbstractQueueSynchronized简称为AQS。AQS是一个定义了一套多线程访问共享资源的同步框架,在Lock包中的相关锁(常用的有ReentrantLock、ReadWriteLock)都是基于AQS来构建。
**AQS原理:**如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS用CLH队列将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性。0代表锁没有被占用=,大于0代表有线程持有当前锁
其对state的访问包括三种方法:getState(),setState(),compareAndSetState()。其中,compareAndSetState()是原子操作,底层是CAS算法实现(乐观锁)。
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS框架包含两种可供选择的实现方式:独占(Exclusive)和共享(Share)。由于不同自定义同步器征用共享资源的方式不同,自定义同步器实现时只需实现共享资源state的获取与释放方式即可,而不需要考虑队列的维护。
Exclusive: 只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁。
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,先通过两次CAS操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
Share: 多个线程可同时执行,如ReentrantReadWriteLock。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
Semaphore(信号量)-允许多个线程同时访问
synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
执行acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。
CountDownLatch(倒计时器)
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完成后再执行。
CountDownLatch的三种典型用法:
- 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化位n:
new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1countdownlatch.countDown()
,当计数器的值变为0时,在CountDownLatch上wait()
的线程就会被唤醒。 - 实现多个线程开始执行任务的最大并行性。强调的是多个线程在某一时刻同时开始执行,做法是初始化一个共享的CountDownLatch对象,将其计数器初始化为1:
new CountDownLatch(1)
,多个线程在开始执行任务前首先countdownlatch.await()
,当主线程调用countdownlatch.countDown()
时,计数器变为0,多个线程同时被唤醒。 - 死锁检测:可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
CountDownLatch使用实例:
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成,通知CountDownLatch对象将构造函数中初始化的count值减1。
}
});
}
countDownLatch.await();//阻塞主线程,直到其他线程完成各自的任务,await()唤醒。
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
CountDownLatch的不足:
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能被再次使用。
CyclicBarrier(循环栅栏)
CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch更加复杂和强大。主要应用场景和CountDownLatch类似。它要做的事情是,让一组线程到达一个同步点时被阻塞,直到最后一个线程到达同步点,栅栏才会开门,所有被阻塞的线程恢复工作。
构造函数:
public CyclicBarrier(int parties) :parties表示栅栏拦截的线程数量
public CyclicBarrier(int parties, Runnable barrierAction) :在线程到达栅栏时,优先执行barrierAction
**应用场景:**CyclicBarrier可用于多线程计算数据,最后合并计算结果的应用场景。
使用示例:
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
CyclicBarrier内部通过一个count变量作为计数器,count的初始值为parties属性的初始值,每当一个线程到了栅栏这里就将计数器减一。如果count为0了,就尝试执行任务
CyclicBarrier和CountDownLatch的区别
CountDownLatch是只能使用一次的计数器,而CyclicBarrier的计数器提供reset功能,可以多次使用。CountDownLatch计数器线程完成一个记录一个,只不过技术不是递增而是递减;CyclicBarrier更像是一个阀门,需要所有线程都到达阀门才能打开,然后继续执行。
ReentrantLock
ReentrantLock基于AQS实现,是一个可重入的互斥锁,又称为独占锁。可重入意为一个持有锁的线程,可以对资源重复加锁而不会阻塞。同时ReentrantLock还定义了公平与非公平策略。
构造方法:
public ReentrantLock() :创建一个默认的非公平锁对象
public ReentrantLock(boolean fair) :根据给定的公平策略创建一个锁对象
非公平锁的lock方法
static final class NonfairSync extends Sync {
final void lock() {
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里没有对阻塞队列进行判断
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁的lock方法
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁和非公平锁的区别:
- 非公平锁在调用lock后,首先就会调用CAS进行一次抢锁,如果这个时候恰巧锁没有被占用就直接获取锁返回。
- 非公平锁在CAS抢锁失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁这个时候被释放了(state==0),非公平锁会直接CAS抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,排到CLH队列后面。
如果非公平锁两次CAS失败,都要进入到阻塞队列等待唤醒。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。
ReentrantReadWriteLock
ReentrantReadWriteLock基于AQS实现的可重入读写锁,将state变量分割成两个部分,高16位表示读,低16位表示写。采用了Exclusive和Share两种实现方式组合将读写分离,在读取数据的时候,可以多个线程同时进入到临界区,在写数据的时候,无论是读线程还是写线程都是互斥的。读锁不支持条件对象,写锁支持一个条件对象。
锁降级:
- 线程获取写入锁后可以获取读取锁,然后释放写入锁。从写入锁变成读取锁,从而实现锁降级的特性。
- 线程获取读取锁是不能升级为写入锁的,需要释放所有读取锁,才可获取写锁。
ReentrantReadWriteLock比ReentrantLock锁多了两个内部类(都是Lock实现)来维护读锁和写锁:ReadLock、WriteLock。在获取锁的时候比ReentrantLock多了一步判断高/低16位来看是读锁还是写锁。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
条件对象Condition
通常,线程进入临界区却发现在某一条件满足之后它才能执行。这时,就要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。Condition是同步器AQS的内部类,每个Condition对象都包含着一个队列(等待队列)。
等待队列:
等待队列是一个FIFO的队列,队列的每一个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了await()方法,该线程就会释放锁、构造成节点进入等待队列并进入等待状态。当一个持有锁的线程调用signal()时,会从等待队列的队首开始尝试对队首节点执行唤醒操作,signalAll()则对所有节点依次执行唤醒操作。
消费生产者模型
public class ConditionTest {
public static void main(String[] args) {
// 仓库
Depot depot = new Depot(100);
// 消费者
Consumer consumer = new Consumer(depot);
// 生产者
Produce produce = new Produce(depot);
produce.produceThing(5);
consumer.consumerThing(5);
produce.produceThing(2);
consumer.consumerThing(5);
produce.produceThing(3);
}
}
class Depot {
private int capacity;
private int size;
private Lock lock;
private Condition consumerCond;
private Condition produceCond;
public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
this.lock = new ReentrantLock();
this.consumerCond = lock.newCondition();
this.produceCond = lock.newCondition();
}
public void produce(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size >= capacity) {
produceCond.await();
}
int produce = (left+size) > capacity ? (capacity-size) : left;
size += produce;
left -= produce;
System.out.println(Thread.currentThread().getName() + ", ProduceVal=" + val + ", produce=" + produce + ", size=" + size);
consumerCond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumer(int val) {
lock.lock();
try {
int left = val;
while (left > 0) {
while (size <= 0) {
consumerCond.await();
}
int consumer = (size <= left) ? size : left;
size -= consumer;
left -= consumer;
System.out.println(Thread.currentThread().getName() + ", ConsumerVal=" + val + ", consumer=" + consumer + ", size=" + size);
produceCond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class Consumer {
private Depot depot;
public Consumer(Depot depot) {
this.depot = depot;
}
public void consumerThing(final int amount) {
new Thread(new Runnable() {
public void run() {
depot.consumer(amount);
}
}).start();
}
}
class Produce {
private Depot depot;
public Produce(Depot depot) {
this.depot = depot;
}
public void produceThing(final int amount) {
new Thread(new Runnable() {
public void run() {
depot.produce(amount);
}
}).start();
}
}
synchornized和ReentrantLock的区别
- 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器自增1,所以要等到锁的计数器下降为0才能释放锁。
- synchronized是依赖于JVM实现的,而ReentrantLock依赖于API。
- ReentrantLock提供了一种能够中断等待锁的线程的机制,还可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。
- ReentrantLock可以结合Condition绑定多个条件对象,而synchronized关键字只有一个内部条件对象。
volatile关键字
volatile关键字是线程同步的轻量级实现,只能用于变量。volatile用来保证数据对线程的可见性,而不能保证原子性。
java内存模型:
在jdk1.2之前,java的内存模型实现总是从主存(即共享内存)读取变量,而在当前的java内存模型下,线程可以把变量保存到本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。volatile关键字的主要作用就是保证变量的可见性还有一个作用是防止指令重排序。
例如,假定一个对象有一个布尔标记done,它的值被一个线程设置却被另一个线程查询,你可以使用锁:
private boolean done ;
public synchronized boolean isDone() { return done ; }
public synchronized void setDone() { done = true ; }
如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。这种情况下,将域声明为volatile是合理的:
private volatile boolean done ;
public boolean isDone() { return done ; }
public void setDone() { done = true ; }
final变量
除了使用锁或volatile修饰符,还有一种情况可以安全地访问一个共享域,即将这个域声明为final,以下声明使其他线程在构造函数完成构造之后才看到这个accounts变量。
final Map<String,Double> accounts = new HashMap<>();
final仅仅是不能修改该变量的引用,但是引用里边的数据是可以修改的,如HashMap的add、remove等操作。
ThreadLocal线程局部变量
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。引入ThreadLocal可以提供线程内的局部变量,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他的线程的局部变量进行冲突,实现了线程的数据隔离。
- set
public class ThreadLocal<T> {
static class ThreadLocalMap {...} //ThreadLocalMap是ThreadLocal的静态内部类
...
public void set(T value) {
Thread t = Thread.currentThread(); //拿到当前线程
ThreadLocalMap map = getMap(t); //取出线程维护的ThreadLocalMap
if (map != null)
map.set(this, value); //ThreadLocalMap的key为当前ThreadLocal对象,value就是我们需要存储的变量
else
createMap(t, value); //该线程第一次使用ThreadLocal.set时创建ThreadLocalMap对象,并赋值。
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
...
}
- get
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //从当前线程取出ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //以当前ThreadLocal对象为key取出ThreadLocalMap.Entry
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //如果这个ThreadLocal对象没有赋值直接get,会给它赋值为null并返回。
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
...
}
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储,key是ThreadLocal对象,值是传递进来的Object对象。ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动调用remove()删除对应key就会导致垃圾回收的时候key被清理掉而value不会被清理掉出现key为null的Entry造成内存泄漏。
ThreadLocal与Thread同步机制的比较
- 同步机制采用了以时间换空间方式,通过对象锁保证在同一个时间对于同一个实例对象,只有一个线程访问。
- ThreadLocal采用以空间换时间方式,为每一个线程都提供一份变量,各线程同时间访问互不影响。
Atomic原子类
java.util.concurrent.atomic
包中有很多类使用了高级的机器级指令(而不是使用锁)来保证其他操作的原子性。
原子操作包为我们提供了四类原子操作:
原子更新基本类型
- AtomicInteger :整型数据的原子操作
- AtomicLong :长整型数据的原子操作
- AtomicBoolean :布尔数据的原子操作
原子更新数组里的某个元素
- AtomicIntegerArray :原子操作整型数组
- AtomicLongArray :原子操作长整型数组
- AtomicReferenceArray :原子操作对象引用数组
原子更新引用类型
- AtomicReference :引用类型
- AtomicStampedReference :带有版本号的引用类型
- AtomicMarkableReference :带有标记位的引用类型
原子更新对象的属性
- AtomicIntegerFieldUpdater:对象的属性是整型
- AtomicLongFieldUpdater:对象的属性是长整型
- AtomicReferenceFieldUpdater:对象的属性是引用类型
CAS(Compare And Swap)
CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写数据时由于执行顺序不确定以及中断的不可预知性产生的数据不一致问题。
CAS有3个操作数:
- 内存值V
- 旧的预期值A
- 要修改的新值B
当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值(预期值A和内存值V相同,将内存值V修改为新值B),而其他线程失败后或者重试(自旋),或者什么也不做结束线程。失败重试的线程发现内存值和预期值不相等,将预期值更新为最新的内存值,再次CAS将内存值V修改为新值B。
ABA问题:
当有一个线程把内存值V从A改成了B再改回到A时,另一个线程比较内存值V和预期值A发现相等,就会认为内存值V没有被改变,从而发生ABA问题。解决ABA问题可以使用AtomicStampedReference类:其中的compareAndSet
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
基本数据类型原子类的优势
多线程环境不使用原子类保证线程安全
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
多线程环境使用原子类保保证线程安全
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
AtomicInteger类
常用方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
常用方法使用:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
int temvalue = 0;
AtomicInteger i = new AtomicInteger(0);
temvalue = i.getAndSet(3);
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3
temvalue = i.getAndIncrement();
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4
temvalue = i.getAndAdd(5);
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9
}
}
AtomicInteger类主要利用CAS+volatile+native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
AtomicIntegerArray
常用方法:
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
常用方法使用:
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
int temvalue = 0;
int[] nums = { 1, 2, 3, 4, 5, 6 };
AtomicIntegerArray i = new AtomicIntegerArray(nums);
for (int j = 0; j < nums.length; j++) {
System.out.println(i.get(j));
}
temvalue = i.getAndSet(0, 2);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndIncrement(0);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndAdd(0, 5);
System.out.println("temvalue:" + temvalue + "; i:" + i);
}
}
死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
如何避免死锁
- 破坏互斥对象 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
并发容器
ConcurrentHashMap
jdk1.7实现
JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,对整个桶数组进行了分割分段形成Segement数组,将数据分为一段一段的存储。每一段数据有一把锁。当一个线程访问其中一段数据时其他段的数据也能被其他线程访问,不会存在锁竞争,提高并发访问率。
jdk1.8实现
JDK1.8的ConcurrentHashMap取消了Segement分段锁,采用CAS和Synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑树。synchronized只锁定当前链表或红黑树的首节点,只要hash不冲突就不会产生并发,大为提升效率。
ConcurrentHashMap的key/value不能为null。
CopyOnWriteArrayList
CopyOnWriteArrayList与读写锁ReentrantReadWriteLock的思想非常类似,为了将读取的性能发挥到极致,CopyOnWriteArrayList读取是完全不用加锁的,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。
**CopyOnWriteArrayList实现原理:**所有可变操作(add,set等)都是通过创建底层数组的副本来实现的,当需要修改数据的时候不在原有数组上修改,而是复制原有数组数据进行修改再将原来内存指针指向新的内存。
读取操作没有任何同步控制和锁操作,内部array不会发生修改,只会被另外一个array替换,因此可以保证线程安全。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
CopyOnWriteArrayList写入操作add()方法在添加数据的时候加了锁,保证了同步,避免多线程写的时候会copy出多个副本来。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();//释放锁
}
}
ConcurrentLinkedQueue
Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue。阻塞队列可以通过加锁来实现,非阻塞队列可以通过CAS操作实现。
ConcurrentLinkedQueue底层使用链表数据结构,是在高并发环境中性能最好的队列,主要使用CAS非阻塞算法来实现线程安全。
BlockingQueue
BlockingQueue提供了可阻塞的插入和移除的方法,被广泛使用在“生产者-消费者”模型中。当队列容器已满生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直到队列非空为止。
BlockingQueue是一个接口,继承自Queue,其实现类可以作为Queue的实现来使用,而Queue又继承自Collection接口。
**相关实现类:**ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue。
ArrayBlockingQueue
ArrayBlockingQueue是BlockingQueue接口的有界队列实现类,底层使用数组来实现。一旦创建其容量不能改变,采用可重入锁控制并发线程。
LinkedBlockingQueue
LinkedBlockingQueue是底层基于单向链表实现的阻塞队列,可以当作有/无界队列来使用,同时满足FIFO的特性。
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无阻塞队列,默认情况下元素采用自然排序进行排序,也可以通过自定义类实现Comparable接口重写compareTo()方法来指定元素排序规则,或者初始化通过构造方法参数Comparator来指定排序规则。
PriorityBlockingQueue并发控制采用的是ReentrantLock,只能指定初始队列的大小,后面插入元素的时候空间不够的话会自动扩容。简单来说,它就是PriorityQueue的线程安全版本,不可以插入null值。同时,插入队列的对象必须是可比较大小的(Comparable)。
线程池
线程池提供了一种限制和管理资源,每个线程池还维护一些基本统计信息,如已完成任务的数量。
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Executor框架
Executor框架是Java1.5之后引进的,通过Executor来启动线程比使用Thread的start方法更好。除了更易管理、效率更好外,还能避免this逃逸问题。
this逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发错误。
Executor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,让并发编程变得更加简单。
Executor框架结构
任务(Runnable/Callable)
执行任务需要实现了Runnable或Callable接口。Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。
tips:可以简单认为Callable是Runnable的扩展,Runnable没有返回值也不能抛出受检查的异常。当我们任务需要返回值或抛出受检查的异常时,可以用Callable。
任务的执行(Executor)
任务执行机制的核心接口为Executor,ExecutorService继承自Executor接口,其关键实现类有ThreadPoolExecutor和ScheduledThreadPoolExecutor。实际使用中,以ThreadPoolExecutor使用频率最高。
异步计算的结果(Future)
Future接口以及Future接口的实现类FutureTask都可以代表异步计算的结果。
当ThreadPoolExecutor或ScheduledThreadPoolExecutor执行submit()方法会返回一个FutureTask对象。
Future一般认为是Callable的返回值,其实代表的是任务的生命周期。
使用示意图
- 主线程首先创建实现Runnable或Callable接口的对象。
- 把对象交给ExecutorService执行:ExecutorService.execute(Runnable command);或者把对象提交给ExecutorService执行:ExecutorService.submit(Runnable task)/ExecutorService.submit(Callable < T > task)。
- 如果执行ExecutorService.submit()方法,ExecutorService将返回一个实现Future接口的对象。由于返回的FutureTask对象实现了Runnable接口,所以我们也可以创建FutureTask然后直接交给ExecutorService执行。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
submit()方法用于提交需要返回值的任务,返回的Future对象可以判断任务是否执行成功。
- 最后,主线程可以执行FutureTask的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。主线程也可执行FutureTask.cancel(boolean myInterruptIfRunning)来取消任务的执行。
ThreadPoolExecutor
线程池实现类ThreadPoolExecutor是Executor框架最核心的类。
ThreadPoolExecutor核心构造方法:
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数分析:
- corePoolSize:核心线程线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到线程就会被存放在队列中。
- keepActiveTime:当线程池中的线程数量大于corePoolSize的时候,如果没有新的任务提交,核心线程外的线程不会立即销毁而是等待直到等待时间超过了keepActiveTime才会被回收销毁。
- unit:keepActiveTime参数的时间单位。
- threadFactory:executor创建新线程的时候用到线程工厂。
- handler:饱和策略。
使用实例代码1:(Runnable+ThreadPoolExecutor)
MyRunnable.java
import java.util.Date;
/**
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @author shuang.kou
*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
ThreadPoolExecutorDemo.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
isTerminated :当调用shutdown()方法后,并且所有提交的任务完成后返回为true。
使用实例代码2:(Callable+ThreadPoolExecutor)
MyCallable.java
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
//返回执行当前 Callable 的线程名字
return Thread.currentThread().getName();
}
}
CallableDemo.java
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CallableDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
List<Future<String>> futureList = new ArrayList<>();
Callable<String> callable = new MyCallable();
for (int i = 0; i < 10; i++) {
//提交任务到线程池
Future<String> future = executor.submit(callable);
//将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
futureList.add(future);
}
for (Future<String> fut : futureList) {
try {
System.out.println(new Date() + "::" + fut.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//关闭线程池
executor.shutdown();
}
}
output:
Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
原理分析:
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。 ScheduledThreadPoolExecutor使用的任务队列DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTask的time变量小的先执行) ,如果执行所需时间相同则先提交的任务先被执行(ScheduledFutureTask的squenceNumber变量小的先执行)。
运行机制:
- 当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFuture接口的ScheduledFutureTask。
- 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下修改:
- 使用DelayQueue作为任务队列;
- 获取任务的方式不同;
- 执行周期任务后,增加了额外的处理;
执行周期任务的步骤:
- 线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前系统的时间;
- 线程1执行这个ScheduledFutureTask;
- 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间;
- 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add());
FixedThreadPool
FixedThreadPool
被称为可重用固定线程数的线程池。
/**
* 创建一个可重用固定数量线程的线程池
*/
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
FixedThreadPool的corePoolSize和maximum都被设置为nThreads。
execute()方法示意图:
- 如果当前运行线程数小于corePoolSize,有新任务的话就创建线程来执行任务;
- 当前运行线程数等于corePoolSize,新任务将加入LinkedBlockingQueue;
- 线程池中的线程执行完手头的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;
SingleThreadExecutor
SingleThreadExecutor是一个只有一个线程的线程池,其corePoolSize和maximumPoolSize都被设置为1,其他参数和FixedThreadPool相同。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
运行示意图:
- 当前运行线程数少于corePoolSize(为0),则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程,将任务加入LinkedBlockingQueue;
- 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;
为什么不推荐FixedThreadPool和SingleThreadExecutor?
答:两者都使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE),运行中的线程池不会拒绝任务,在任务比较多的时候可能导致内存溢出(OOM)。
CachedThreadPool
CachedThreadPool是一个会根据需要创建新线程的线程池。CachedThreadPool的corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,即它是无界的。如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程,极端情况会导致cpu和内存资源耗尽。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
执行示意图:
- 首先执行SynchronousQueue.offer(Runnable task),提交任务到任务队列。如果当前maximumPool中有线程正在执行SynchronousQueue.poll(keepActiveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行步骤2;
- 当初始maximumPool为空,或者maximumPool中没有空闲线程,将没有线程执行poll操作。这种情况下,步骤1失败,此时CachedThreadPool会创建线程执行任务,execute方法执行完成;