Java并发编程、JUC(java.util.concurrent包)
参考:JUC详解
概念辨析
进程、线程、管程
进程
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
它是操作系统动态执行的基本单元,是操作系统结构的基础。
在传统的操作系统中,进程既是系统资源分配和调度的基本单元,也是程序执行的基本单元。
在当代面向线程设计的计算机结构中,进程是线程的容器。
程序是指令、数据及其组织形式的描述,进程是正在运行的程序的实例。
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程则是执行程序的一次过程,它是一个动态的概念。是系统资源分配的单位。
线程
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发执行多个线程,每条线程并行执行不同的任务。
线程是独立调度和分派的基本单位。
线程是系统分配处理器时间资源的基本单元,或者说进程内独立执行的一个单元执行流。是程序执行的最小单位。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计算器pc,线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象,这就是的线程间通信更简便、高效。
关系:
- 每个进程只有一个方法区和堆,每一个线程都有一个虚拟机栈和一个程序计数器,多个线程共用一个方法区和堆。
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 同一进程中的多条线程将共享该进程中的全部系统资源:计算资源、内存资源。
- 一个进程可以有很多线程,每条线程并行执行不同的任务。可并发执行。
线程的状态
新建(NEW)、准备就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、等待超时(TIMED_WAITING)、终止(TERMINATED)。
管程
管程:管程是一种程序结构,又称为监视器。结构内的多个子程序形成的工作线程互斥访问的共享资源。它提供了一种机制,线程可以临时放弃互斥访问,等待条件满足时才重新获取执行权。
JVM中同步是基于进入和退出**管程对象(Monitor)**实现的,每个对象都会有一个管程对象,管程会随着Java对象一同创建和销毁。
线程执行首先要持有管程对象,然后才能执行方法,当方法完成后会释放管程,方法在执行时会持有管程,其他线程无法再获取同一个管程。
串行、并行、并发
串行:即表示所有任务按照先后顺序依次进行。串行每次只能取并且处理一个任务。
并行:同一时刻,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。并行的效率从代码层次上强依赖于多进程/多线程,从硬件角度上则依赖于多核CPU。
并发:同一个时间段内,两个或多个程序交替使用系统计算资源,使得所有程序可以同时运行(宏观上是同时,微观上仍是顺序执行)。这里的“同时运行”表示的不是真的同一时刻有多个线程同时运行的现象,而是看起来多个线程同时运行。单核CPU任意时刻只能运行一个线程。并发是同一时刻多个线程访问同一个资源。
wait和sleep
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
- sleep不会释放锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 它们都可以被interrupted方法中断。
用户线程和守护线程
用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,用来服务于用户线程;不需要上层逻辑介入,当然我们也可以手动创建一个守护线程。(用白话来说:就是守护着用户线程,当用户线程死亡,守护线程也会随之死亡)。
同步和异步
volatile关键字
volatile关键字原理的使用介绍和底层原理解析和使用实例
volatile的作用及原理
volatile 是 Java 中的关键字,是一个变量修饰符,被用来修饰会被不同线程访问和修改的变量,是java虚拟机提供的轻量级的同步机制。
三大特性:
- 保证线程间可见性;
- 进制指令重排:保证线程对volatile变量读写操作的有序性;
- 禁止编译器优化;
- 不保证线程对volatile变量的读写操作的原子性
什么是指令重排:程序的执行顺序可能不是按照你的书写顺序去执行!
源代码 —> 编译器优化的重排 —>指令并行也可能会重排 —>内存系统也会重排 -->执行
为什么volatile可以禁止指令重排?
依赖内存屏障和缓存一致性协议。具体原理待补充~
JMM内存模型
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。
但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。
此时,就需要上述的JMM规定来保证多个线程操作的安全性。
四组内存交互操作
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量
实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,
必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
synchronized关键字
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
-
修饰某一处代码块,被修饰的代码块称为同步语句块。作用范围就是{}之间。作用的对象是调用这个代码块的对象。
-
修饰在普通方法上,被修饰的方法就称为同步方法。作用范围则是整个方法。作用的对象则是调用这个方法的对象。
注:synchronized 关键字不能被继承,如果父类中某方法使用了synchronized 关键字,字类又正巧覆盖了,此时,字类默认情况下是不同步的,必须显示的在子类的方法上加上才可。当然,如果在字类中调用父类中的同步方法,这样虽然字类并没有同步方法,但子类调用父类的同步方法,子类方法也相当同步了。 -
修饰某个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。锁是当前类的Class对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized关键字8锁问题
Lock锁(重点)
公平锁、非公平锁
公平锁:按等待获取锁的线程的等待时间进行获取,先来后到,一定要排队执行。
非公平锁:可以插队。
悲观锁、乐观锁
可重入锁(递归锁)、自旋锁、读写锁
可重入锁:又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而死锁。
自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。
JUC介绍
【并发编程】JUC并发编程(彻底搞懂JUC)
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。包括:
java.util.concurrent包
java.util.concurrent.atomic包
java.util.concurrent.locks包
JUC框架结构
JUC框架的底层在Java代码里是Unsafe,而Unsafe是底层Jvm的实现。有了Unsafe的支持实现了一些了支持原子型操作的Atomic类,然后上层才有了我们熟知的AQS,和LockSupport等类。有了这些之后才是各种读写锁各种线程通信以及同步工具的实现类。
JUC中的锁Lock
Lock接口
JUC中的Lock是一个接口,接口形式如下:
public interface Lock {void lock(); //获得锁。/**除非当前线程被中断,否则获取锁。如果可用,则获取锁并立即返回。如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下两种情况之一:锁被当前线程获取; 要么其他一些线程中断当前线程,支持中断获取锁。如果当前线程:在进入此方法时设置其中断状态; 要么获取锁时中断,支持中断获取锁,*/void lockInterruptibly() throws InterruptedException; /**仅在调用时空闲时才获取锁。如果可用,则获取锁并立即返回值为true 。 如果锁不可用,则此方法将立即返回false值。*/boolean tryLock();//比上面多一个等待时间 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 解锁void unlock(); //返回绑定到此Lock实例的新Condition实例。Condition newCondition();
}
对常用接口的解释:
lock()
是最常用的方法之一,作用就是获取锁,如果锁已经被其他线程获得,则当前线程将被禁用以进行线程调度,并处于休眠状态,等待,直到获取锁。
如果使用到了lock的话,那么必须去主动释放锁,就算发生了异常,也需要我们主动释放锁,因为lock并不会像synchronized一样被自动释放。所以使用lock的话,必须是在try{}catch(){}中进行,并将释放锁的代码放在finally{}中,以确保锁一定会被释放,以防止死锁现象的发生。
unlock()
作用就是主动释放锁。
newCondition()
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式,Lock锁的newContition()方法返回 Condition 对象,Condition类也可以实现等待/通知模式。
用notify()通知时,JVM会随机唤醒某个等待的线程,使用Condition类可以进行选择性通知,Condition比较常用的两个方法:
**await():**会使当前线程进入到此condition对象的等待队列中等待,同时会释放已经获得的锁,当等到其他线程调用signal()方法时,此时这个沉睡线程会重新获得锁并继续执行代码(在哪里沉睡就在哪里唤醒)。
**signal():**用于唤醒一个等待的线程。
注意:在调用 Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用 await()后线程会释放这个锁,在调用singal()方法后会从当前Condition对象的等待队列中,唤醒一个线程,后被唤醒的线程开始尝试去获得锁,一旦成功获得锁就继续往下执行。
Lock接口的实现类
Lock接口的实现类:ReentrantLock(可重入锁)
ReentrantLock(可重入锁)
ReentrantLock(可重入锁)的方法:
加锁:lock()
尝试获取锁:tryLock()
解锁:unlock
ReadWriteLock接口(读写锁)
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {// 获取读锁Lock readLock();// 获取写锁Lock writeLock();
}
读写锁实现类:ReentrantReadWriteLock
ReentrantReadWriteLock.ReadLock = reentrantReadWriteLock.readLock(); // 读锁,共享锁
ReentrantReadWriteLock.WriteLock = = reentrantReadWriteLock.writeLock(); // 写锁,独占锁
synchronized关键字和锁的对比
- Synchronized是java内置的一个关键字,Lock是工具包下java提供的一个接口
- Synchronized无法判断锁的状态,Lock可以判断是否获取到了锁
- Synchronized会自动释放锁,Lock必须要手动释放锁,否则会产生死锁
1、当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用 (不需要手动释放锁)2、若线程执行发生异常,jvm会让线程释放锁。 - Synchronized遇到线程阻塞时会一直等待,Lock锁不一定会等待下去
- Synchronized是已设置好的可重入锁、不可中断、非公平锁;Lock是可以手动设置的可重入锁、中断(是否),公平(是否)
- 正是由于Synchronized的全自动性,对于少量代码时,Synchronized更适用,Lock适用于大量代码内容
JUC中的辅助工具类(tools包)
CoundownLatch(闭锁,减法计数器)
CountDownLatch是一个同步辅助类,允许一个或多个线程等待,一直到其他线程执行的操作完成后再执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当有一个线程执行完毕后,然后通过 countDown 方法来让计数器的值-1,当计数器的值为0时,表示所有线程都执行完毕,然后继续执行 await 方法 之后的语句,即在锁上等待的线程就可以恢复工作了。
CountDownLatch中主要有两个方法:
- countDown:
递减锁存器的计数,如果计数达到零,则释放所有等待的线程。
如果当前计数大于零,则递减。 如果新计数为零,则为线程调度目的重新启用所有等待线程。
如果当前计数为零,则什么也不会发生。 - await:
使当前线程等待直到闩锁倒计时为零,除非线程被中断。
如果当前计数为零,则此方法立即返回。即await 方法阻塞的线程会被唤醒,继续执行。
如果当前计数大于零,则当前线程出于线程调度目的而被禁用并处于休眠状态。
Semaphore(信号量)
Semaphore:信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数。
是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。
线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。
线程可以通过release()来释放它所持有的信号量许可。
semaphore.acquire() --> 从信号量中获取许可证,获取不到,等待
semaphore.release() --> 释放许可证到信号量中
CyclicBarrier(栅栏,加法计数器)
一个同步辅助类,允许一组线程互相等待,直到所有线程到达某个公共屏障点,并且在释放等待线程后可以重用。
加法计数器,当任务数量达到指定数量时,执行后续任务,否则处于阻塞状态。
public class CyclicBarrier {private int dowait(boolean timed, long nanos); // 供await方法调用 判断是否达到条件 可以往下执行吗//创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待时触发,每执行一次CyclicBarrier就累加1,达到了parties,就会触发barrierAction的执行public CyclicBarrier(int parties, Runnable barrierAction) ;//创建一个新的CyclicBarrier ,参数就是目标障碍数,它将在给定数量的参与方(线程)等待时触发,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句public CyclicBarrier(int parties) //返回触发此障碍所需的参与方数量。public int getParties()//等待,直到所有各方都在此屏障上调用了await 。// 如果当前线程不是最后一个到达的线程,那么它会出于线程调度目的而被禁用并处于休眠状态.直到所有线程都调用了或者被中断亦或者发生异常中断退出public int await()// 基本同上 多了个等待时间 等待时间内所有线程没有完成,将会抛出一个超时异常public int await(long timeout, TimeUnit unit)//将障碍重置为其初始状态。 public void reset()
}
cyclicBarrier.await() --> 表示本线程到达屏障点,并等待任务数量达到指定数量。
FutureTask(异步任务)
JUC中的并发安全容器类
阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
ConcurrentLinkedQueue
Deque
ArrayDeque
LinkedList
CopyOnWriteArrayList
背景:并发修改触发异常。多个线程向List集合中插入数据时,java.util.ConcurrentModificationException 并发修改异常。
解决方法:
- 使用Vector
- 使用Collections.synchronizedList将不安全的容器转换为安全容器
- 使用JUC包下的CopyOnWriteArrayList。
CopyOnWrite 写入时复制,cow计算机程序设计领域的一种优化策略。
相较于Vector的优势,CopyOnWriteArrayList使用lock锁的方式,效率更高
读写分离。
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
CopyOnWriteArraySet
与上类似。
ConcurrentHashMap
与上类似。
ConcurrentSkipListSet
JUC中关于并发操作的接口
Future接口
Future 接口位于java.util.concurrent包下。
Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。
public interface Future<V> {boolean cancel(boolean mayInterruptIfRunning); //尝试取消此任务的执行。boolean isCancelled();//如果此任务在正常完成之前被取消,则返回true boolean isDone(); //如果此任务完成,则返回true 。 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true V get() throws InterruptedException, ExecutionException; //获得任务计算结果V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;//可等待多少时间去获得任务计算结果
}
Future和Runnable接口的实现类:FutureTask
位于 java.util.concurrent包下。
FutureTask实现了 Runnable 和 Future接口,并方便地将两种功能组合在一起。并且通过构造函数使得可以通过Callable来创建FutureTask,进而提供给Thread来创建线程。
FutureTask有以下三种状态:
- 未启动状态:还未执行run()方法。
- 已启动状态:已经在执行run()方法。
- 完成状态:已经执行完run()方法,或者被取消了,亦或者方法中发生异常而导致中断结束。
应用场景:
在主线程执行那种比较耗时的操作时,但同时又不能去阻塞主线程时,就可以将这样的任务交给FutureTask对象在后台完成,然后等之后主线程需要的时候,就可以直接get()来获得返回数据或者通过isDone()来获得任务的状态。
一般FutureTask多应用于耗时的计算,这样主线程就可以把一个耗时的任务交给FutureTask,然后等到完成自己的任务后,再去获取计算结果。
注意事项:
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。
get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
因为只会计算一次,因此通常get方法放到最后。
CompletableFuture类
//没有返回值得异步回调CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("异步回调方法。。。");});//先打印外部输出System.out.println("1111");//阻塞 待任务中程序执行完毕future.get();//供给型接口 有返回值的异步输出CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("有返回值型接口。。");int a=10/0;return 1024;});
Callable接口
Callable 接口位于java.util.concurrent包下,与Runnable接口对比,可以将执行线程执行的结果返回给调用线程。
@FunctionalInterface
public interface Callable<V> {V call() throws Exception; // 计算结果,如果无法计算则抛出异常。
}
Callable 类似于Runnable 接口,但Runnable 接口中的run()方法不会返回结果,并且也无法抛出经过检查的异常,但是Callable中call()方法能够返回计算结果,并且也能够抛出经过检查的异常。
通过实现Callable接口创建线程详细步骤:
- 创建实现Callable接口的类SomeCallable
- 创建一个类对象:Callable oneCallable = new SomeCallable();
- 由Callable创建一个FutureTask对象:FutureTask futureTask= new FutureTask(oneCallable);
注释:FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。 - 由FutureTask创建一个Thread对象:Thread oneThread = new Thread(futureTask);
- 启动线程:oneThread.start();
Executor接口
是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService。
ThreadPoolExecutor
JUC中的原子变量(atomic包)
是JDK提供的一组原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。
AtomicBoolean
AtomicInteger
AtomicReference
JUC中的锁(locks包)
是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁。
ReentrantLock(独占锁)
它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
ReentrantReadWriteLock
它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
LockSupport
它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
线程、线程池
创建线程
【java】Java并发编程–Java实现多线程的4种方式
线程池
为什么使用线程池技术
程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段
池化技术:提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。
线程池的工作原理::控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
创建线程池
三大方式、七大参数、四种拒绝策略
三大创建方式(实际开发中均不建议使用)
单一线程线程池:ExecutorService threadPool = Executors.newSingleThreadExecutor()
固定数量的线程池:ExecutorService threadPool = Executors.newFixedThreadPool(num)
可伸缩大小的线程池: ExecutorService threadPool = Executors.newCachedThreadPool()
线程池执行任务:threadPool.execute(Runnable runnable)支持使用lambda表达式;
七大参数
以上三种创建线程池的方法均会调用到的构造函数和参数释义如下:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数int maximumPoolSize, // 最大线程数long keepAliveTime, // 超时等待的时间TimeUnit unit, // 超时等待的时间单位BlockingQueue<Runnable> workQueue, // 待执行任务的队列ThreadFactory threadFactory, // 线程的创建工厂,默认的Executors.defaultThreadFactory()RejectedExecutionHandler handler // 拒绝策略){}
实际开发中,不要使用工具类提供的三种创建方式,而是自定义线程池,阿里巴巴代码规约中有明确说明:
8. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
四种拒绝策略
当待执行任务数大于线程池的最大容量时,线程池的拒绝策略:
ThreadPoolExecutor.AbortPolicy :抛出异常提示
ThreadPoolExecutor.CallerRunsPolicy:此任务从哪个线程进来的,会被退回到哪个线程执行,如main方法进入,此任务交由main()线程执行
ThreadPoolExecutor.DiscardPolicy:任务会被忽略
ThreadPoolExecutor.DiscardOldestPolicy :与线程池中已有的任务进行竞争
如何定义最大线程数量
分为两种方式:
CPU密集型:获取当前电脑的最大线程数量,作为最大线程数量的参数即可。
Runtime.getRuntime().availableProcessors()
IO密集型:当程序中调用IO资源较多时,最大线程数量应大于IO任务数量,最好为2倍。 如IO任务数量为15时,最大线程数量定义为30。
Java并发编程常考问题
Java多线程常见面试题汇总:JUC详解
JUC常考问题:JUC实战必备-全是精华
参考
JUC详解 | JUC概述