本文内容摘自:Java并发编程之美、深入理解Java虚拟机、Java并发编程的艺术
什么是线程?
操作系统在运行一个程序时会为其创建一个进程,在一个进程里可以创建多个线程,而操作系统调度的最小单元是线程,也叫轻量级进程(是内核线程的接口,一条Java线程就映射到一条轻量级进程之中);
操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以说线程是CPU分配的基本单元
在Java中我们启动main函数时其实就是启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程;
Java线程的调度:协同式线程调度(一个线程执行完了通知另一个线程来执行)和抢占式线程调度(每个线程由系统来分配执行时间,线程的切换不由线程本身决定,Java采用这种方式)
什么是线程安全?
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全的实现方法?
1.互斥同步:多线程并发访问共享数据时,保证数据在同一时刻只被一个线程使用,(也叫阻塞同步,悲观的)常见例Sychronized关键字
2.非阻塞同步:基于冲突检测的乐观并发策略,即先进行操作,如果没有其他线程争用共享数据则成功,否则就产生冲突,再采取其他补偿措施(常见的就是冲突后不断重试);不需要挂起线程,但是其不断尝试会占用CPU资源常见例CAS操作
补充:CAS操作(由于硬件的支持,CAS操作是原子性的)CAS指令需要三个操作数,分别是内存位置V,旧的预期值A和新值B;CAS操作执行时,当且仅当V符合旧预期值A时,处理器用新值B更行V的值,否则它就不执行更新,但无论是否更新了V的值,都会返回V的旧值。
3.无同步方案:如果一个方法不涉及共享数据,他就不会产生线程安全问题;举两个例子:可重入代码,随时可以中断再运行不涉及公共资源;线程本地存储,把共享数据的可见范围限制在同一个线程之内
public class MyTest {
public static void main(String[] args) throws InterruptedException {
//方式一
new MyThread1("good ").start();//启动线程
new MyThread1("bad ").start();
//方式二
new Thread(new MyThread2("nice ")).start();
new Thread(new MyThread2("haha ")).start();
//方式三
FutureTask<String> task = new FutureTask<>(new MyThread3());
new Thread(task).start();
try {
System.out.println(task.get());//等待任务执行完毕并返回结果
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//继承Thread类重写run方法创建线程
static class MyThread1 extends Thread {
private String message;
public MyThread1(String message) {
this.message = message;
}
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(message);
}
}
}
//实现Runnable接口创建线程
static class MyThread2 implements Runnable {
private String message;
public MyThread2(String message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(message);
}
}
}
static class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
return "hello !";
}
}
}
实现接口会更好一些,因为:
注意:wait和notify方法都是线程调用的共享变量的方法,而不是线程的方法
线程等待与线程阻塞的区别?
两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外,而进入 waiting 状态是在同步代码之内(然后马上退出同步)。
注:若调用wait()方法的线程未事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒),即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒;以下代码是经典的调用共享变量 wait()方法的实例:
//模拟生产者/消费者场景
private static volatile LinkedList<Integer> queue = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
Thread product = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == 10) {
//挂起当前线程,并释放通过同步块获取的queue上的锁
// 消费者线程可以获取该锁来获取队列里面的元素
queue.wait();
}
//空闲则生成元素,并通知消费者线程
queue.push(2);
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (queue) {
//消费队列空
while (queue.size() == 0) {
//挂起当前线程,并释放通过同步块获取的queue上的锁
// 生产者线程可以获取该锁来往队列中增加元素
queue.wait();
}
//消费元素并通知唤醒生产者线程
queue.pop();
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (resourceA) {
System.out.println("A get resourceA lock");
System.out.println("A wait...");
resourceA.wait();
System.out.println("A end wait");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (resourceA) {
System.out.println("B get resourceA lock");
System.out.println("B wait...");
resourceA.wait();
System.out.println("B end wait");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread C = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("C get resourceA lock");
resourceA.notify();
}
}
});
B.start();
A.start();
Thread.sleep(1000);//main线程休眠1s
C.start();
/*运行结果
B get resourceA lock
B wait...
A get resourceA lock
A wait...
C get resourceA lock
B end wait
并且JVM会继续运行,因为线程A还在阻塞...
*/
前面介绍的等待通知方法是object类中的方法,而join()方法则是Thread类直接提供的,是无参数且返回值为void的方法。
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("child threadone over");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("child threadTwo over");
}
});
threadOne.start();
threadTwo.start();
// threadOne.join();
// threadTwo.join();
//加入join方法,会输出child threadone/Two over之后再输出main thread over;否则会先输出main thread over
System.out.println("main thread over");
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("child thread is in sleep");
Thread.sleep(10000);
System.out.println("child threadOne is awake!");
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
threadOne.start();
Thread.sleep(2000);
threadOne.interrupt();
Thread.sleep(2000);
System.out.println("main thread is over");
//此例可知在一个线程处于睡眠状态时,另一个线程中断了它,会在调用sleep方法处抛出异常并返回;然后另一个线程继续执行
注:另外需要注意的是,如果在调用 Thread.sleep(long millis)时为 millis 参数传递了一个 负数,则抛出IllegalArgumentException 异常
Java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
//根据中断标志判断线程是否终止的例子
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread()+" hello");
}
}
});
threadOne.start();
Thread.sleep(1000);
System.out.println("main thread interrupt thread");
threadOne.interrupt();
threadOne.join();
System.out.println("main thread is over");
interrupt()方法也属于Thread类的方法,主要是一个线程调用别的线程来提醒那个线程,(跟真正的中断后保存断电处理从而处理中断不太一样)
CPU同一时刻只能被一个线程使用,为了给用户感觉多个线程在同时执行,CPU资源的分配采用了时间片轮转的策略,即给每个线程分配一个时间片,线程在时间片内占用CPU资源执行任务;但是,在切换之前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文的切换!
//形成死锁
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread()+" get resourceA");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("waiting get resourceB");
//持有资源A了还要继续请求持有资源B
synchronized (resourceB){
System.out.println(Thread.currentThread()+" get resourceB");
}
}
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread()+" get resourceB");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("waiting get resourceA");
synchronized (resourceA){
System.out.println(Thread.currentThread()+" get resourceA");
}
}
}
});
threadOne.start();
threadTwo.start();
}
同步的措施一般是加锁,那么有没有一种方式可以做到,在创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本;即创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存!
本质:在每个线程内部都有一个名为threadLocals的成员变量, 该变量的类型为HashMap, 其中key为我们定义的ThreadLocal变量的this引用 , value 则为我 们使用set方法设置的值。 每个线程的本地变量存放在线程自己的内存变量threadLocals中, 如果当前线程一直不消亡, 那么这些本地变量会一直存在, 所以可能会造成内存溢出 , 因此使用完毕后要记得调用ThreadLocal的remove 方法删除对应线程的threadLocals中的本地变量。
//同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。
public static ThreadLocal<String> threadlocal = new ThreadLocal<>();//创建线程变量
public static void main(String[] args) throws InterruptedException {
threadlocal.set("hello !");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread: " + threadlocal.get());//子线程输出线程变量的值
}
});
thread.start();
System.out.println("main thread:"+threadlocal.get());//主线程(也是子线程的父线程)输出线程变量的值
}
/*运行结果
main thread:hello !
thread: null
*/
Java内存模型规定,将所有的变量都放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存中的变量。
上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。 对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用 volatile关键字;
所谓原子性 操作,是指执行一系列操作时,这些操作要么全部执行完,要么不执行,不存在只执行其中一部分的情况;
在Java中 , 锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就 是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。 Java提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题, 这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读改写等的原子性问题。 CAS 即Compare and Swap,其是JDK提供的非阻塞原子性操作, 它通过硬件保证了比较更新操作的原子性
JDK的rt.jar包中的Unasfe类提供了硬件级别的原子性操作!!!
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序;这里先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下 一定会输出 4 ? 答案是不一定,由于代码 (1 ) ( 2 ) ( 3 ) (4)之间不存在依赖关系,所以写线程的代码 (3) (4可能被重排序为先执行(4)再执行(3) ;那么执行(4)后, 读线程可能已经执行了(1)操作, 并且在(3)执行前开始执行(2)操作, 这时候输出结果为0而不是 4。
private static int num = 0;
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Read read = new Read();
read.start();
Write write = new Write();
write.start();
Thread.sleep(10);
read.interrupt();
System.out.println("main exit");
}
public static class Read extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (ready) //(1)
System.out.println(num+num);//(2)
System.out.println("read thread......");
}
}
}
public static class Write extends Thread {
@Override
public void run() {
num=2;//(3)
ready=true;//(4)
System.out.println("write thread set over......");
}
}
当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个 Cache 行大 小的内存复制到 Cache中 。 由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中 。 当多个线程同时修改一个缓存行里面的多个变量时, 由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享!!!
乐观锁和悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。实例sychronized关键字
乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测!实例CAS 操作
公平锁和非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得。【在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销】
独占锁和共享锁
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁!!!
独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读 操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线 程必须等待当前线程释放锁才能进行读取;
共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
可重入锁
当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线 程再次获取它自己己经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可 重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严格来说是有限次数)地进入被该锁锁住的代码;
自旋锁
当前线程在获取锁时,如果发现锁已经被其他线程占有, 它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可 以使用 -XX:PreB lockS pinsh 参数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁。 如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。 由此看来自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了
以下主要讲述Java中锁的实现:
首先,锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
当需要阻塞和唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作,而LockSupport也成为构建同步组件的基础工具;
LockSupport定义了一组以park开头的方法用来阻塞当前线程 ,以及unpark方法来唤醒一个被阻塞的线程!
任意一个Java对象都拥有一组监视器方法,主要包括wait、notify等方法,这些方法与sychronized同步关键字配合,可以实现等待/通知模式;condition接口也提供了类似Object的监视器方法,与lock配合可以实现等待/通知模式!
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前 获取到Condition对象关联的锁;Condition对象是由Lock对象(调用了Lock对象的newCondition方法)创建出来的,即Condition对象是依赖Lock对象的。以下代码实现一个有解队列(类似于生产者消费者模式):
public class BoundedQueue<T> {
private Object[] items;
//添加的下标,删除的下标和数组当前数量
private int addindex, removeIndex, count;
private Lock lock = new ReentrantLock();//独占锁
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
//添加一个元素,如果数组满,则添加线程进入等待状态,直到有“空位”
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length)//如果数组满,则添加线程进入等待状态
notFull.await();
items[addindex] = t;
if (++addindex == items.length)
addindex = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
//由头部删除一个元素,如果数组空,则线程进入等待状态,直到有添加元素
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,从而使得Entry的next结点永不为空产生死循环获取Entry;故HashMap是线程不安全的!
HashTable容器使用sychronized来保证线程安全,无论读写都需要竞争同一个锁,效率低下!
假如容器里有多把锁,每一把锁用于锁容器中一部分数据,那么当多线程访问容器里不同数据时,线程间不会存在锁竞争,从而有效提升高并发访问效率这就是ConcurrentHashMap使用的锁分段技术。
ConcurrentLinkedQueue容器
是一个基于链接结点的无界线程安全队列,它采用先进先出的规则对结点进行排序,并且采用CAS来实现。
Java中的阻塞队列
阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
阻塞队列(blockingQueue)是一个支持两个附加操作的队列:
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每一个小任务结果后得到大任务结果的框架。
工作窃取算法:一个比较大的任务分割为若干不相关的子任务,为减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建单独的线程来执行;线程负责A队列里的任务 线程B负责B队列里的任务,当A队列里的任务被线程A执行完而B队列里还有任务,于是线程A去B队列的队尾窃取一个任务来执行!(实现窃取算法一般是要用双端队列的)
框架使用:
我们要使用ForkJoin框架
第一步,创建一个ForkJoin任务,在提供在任务中Fork()和Join()操作的机制;通常情况下我们无需直接继承ForkJoinTask类,只需继承他的子类;
RecursiveAction:用于没有返回结果的任务
RecursiveTask:用于有返回结果的任务
第二步,ForkJoinTask需要通过ForkJoinPool来执行;
使用Fork/Join框架的代码示例:
import java.util.concurrent.*;
public class ForkJoin extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;//阈值
private int start;
private int end;
public ForkJoin(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;//如果任务大于阈值,就分裂成两个任务计算
ForkJoin leftTask = new ForkJoin(start, middle);
ForkJoin rightTask = new ForkJoin(middle + 1, end);
leftTask.fork();
rightTask.fork();//执行子任务,又会进入compute方法
Integer leftResult = leftTask.join();
Integer rightResult = rightTask.join();//等待子任务执行完成并得到其结果
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoin task = new ForkJoin(1, 4);//生成一个计算任务,负责计算1+2+3+4
Future<Integer> result = forkJoinPool.submit(task);//执行一个任务
try {
System.out.println(result.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
/*运行结果为:10*/
java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更行一个变量的方式。
Atomic包一共提供了13个类,属于4中类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段);其基本都是使用Unsafe类实现的包装类。
介绍AtomicInteger的常用方法,其他两种的常用方法类似:
int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果;
boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值;
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值;
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
//AtomicInteger的使用
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(1);
System.out.println(a.getAndIncrement());
System.out.println(a.get());
}
/*运行结果:
1
2*/
public final int getAndincrement(){//这是getAndincrement方法的源码
for(;;){
int current=get();
int next=current+1;
if(compareAndSet(current,next))
return current;
}
}
在开发过程中,合理使用线程池会带来3个好处:
第一,降低资源消耗;通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
第二,提高响应速度;当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三,提高线程的可管理性;线程池可以进行统一分配、调优和监控。
/**
* 用给定的初始参数创建一个新的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;
}
ThreadPoolExecutor 3 个最重要的参数:
ThreadPoolExecutor其他常见参数:
为了搞懂线程池的原理,我们需要首先分析一下 execute方法:
// 存放线程池的运行状态 (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);
}
- 简介
Executor框架是Java5之后引进的,在Java5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理效率更高外,还有关键一点:有助于避免this逃逸问题;
Executor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,让 并发编程变得更加简单。
ExecutorService.execute(Runnable command)其中execute()方法用于提交不需要返回值的任务;
ExecutorService.submit(Runnable task)其中submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象。
通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
ThreadPoolExecutor使用示例
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@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());
}
@Override
public String toString() {
return this.command;
}
}
/*************分割线*****************/
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
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) {
ExecutorService 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对象(WorkThread类实现了Runnable接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
运行结果:
pool-1-thread-3start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-4start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-1start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-5start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-2start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-2end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-3end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-4end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-5end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-5start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-1end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-4start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-3start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-2start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-1start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-2end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-3end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-1end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-4end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-5end .time= Wed Mar 04 16:02:19 CST 2020
Finished all threads
分析:我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段,Exchanger工具类则提供了在线程间交换数据的一种手段。
用来控制一个或者多个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
/*运行结果
run..run..run..run..run..run..run..run..run..run..end
*/
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
运行结果:before…before…before…before…before…before…before…before…before…before…after…after…after…after…after…after…after…after…after…after…
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
/*运行结果:
2 1 2 2 2 2 2 1 2 2
*/