小黄是根据尚硅谷的视频,对照了学习JUC,尚硅谷YYDS!!!
贴一下课程连接(绝无广告之意) 尚硅谷2021JUC视频
JUC概述
什么是JUC?
在 Java 中,线程部分是一个重点,本篇文章说的 JUC 也是关于线程的。JUC 就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK 1.5 开始出现的。
进程与线程
进程
通俗来讲,进程可以理解为我们电脑中正在运行的软件,如电脑中运行了WeChat,那么WeChat在我们电脑中就是一个进程
线程
线程是程序执行的最小单位,可以说线程是运行在一个进程之内的,一个进程可以并发多个线程,每个线程执行不同的任务
线程的状态
线程的状态分为6种
- NEW:新建
- RUNNABLE:准备就绪
- BLOCKED:阻塞
- WAITING:不见不散(等待状态,我一直等到你来)
- TIMED_WAITING:过时不候(等待状态,约定事件,你不来我就走了)
- TERMINATED:终结
wait和sleep的区别
- sleep是Thread类中的方法,wait是Object中的方法,任何对象都可以执行wait方法
- sleep不会释放锁,也不会占用锁,wait方法会释放锁,但前提是当前方法必须占有锁,也就是在synchronized中
- 它们都可以被 interrupted 方法中断。
并发和并行
串行模式
串行模式有点类似于我们吃羊肉串,正常情况下都是从上往下一口一口吃,在串行模式中,所有的任务排着一条队伍,只有上一个任务执行完毕,才可以接着执行下一个任务
并行模式
我们获取了一批任务,将这一批任务分配给不同的人去执行,执行完成之后再汇总。也就是说当我们收到任务的时候,会将任务队列切成多个小队列,同时执行小队列。在代码层面依赖多线程,在硬件层面依赖多核CPU,现在市面上大部分电脑都是多核CPU的
并发模式
并发模式指的是同一时间多个程序可以同时运行的现象
总结
并行:多项工作一起执行,完成之后再汇总
并发:同一时刻,运行多个线程访问同一个资源,例如抢票、秒杀
管程
管程可以理解为锁,同一时间内,保证只有一个线程在管程中运行
执行线程首先要持有管程对象,然后才能执行方法,当方法执行完成之后会释放管程,方法在执行的时候会持有管程,其他线程无法再获取同一个管程
守护线程、用户线程
用户线程
平时用到的普通线程,用户自定义线程,主程序执行完毕后,程序中如果又用户现场,那么主程序会处于等待状态
public static void main(String[] args) {
Thread aa = new Thread(() -> {
System.out.println("线程名称:" + Thread.currentThread().getName() + "是否是守护线程:" + Thread.currentThread().isDaemon());
while (true){
//模拟线程永不结束
}
}, "aa");
aa.start();
System.out.println(Thread.currentThread().getName() + ":: over");
}
我们可以看到主线程一直处于等待状态
守护线程
守护线程是系统的线程,比如垃圾回收,主程序中执行完毕并且程序中没有用户线程时,守护线程也会结束
public static void main(String[] args) {
Thread aa = new Thread(() -> {
System.out.println("线程名称:" + Thread.currentThread().getName() + "是否是守护线程:" + Thread.currentThread().isDaemon());
while (true){
//模拟线程永不结束
}
}, "aa");
//设置该线程为守护线程,注意要在start()之前
aa.setDaemon(true);
aa.start();
System.out.println(Thread.currentThread().getName() + ":: over");
}
可以看到主线程执行完毕之后,直接关闭了
线程的通信
小黄当初第一次面试的时候,面试官问了我一个问题,在三个线程中,如何保证C线程在B线程执行完之后执行,B线程在A线程执行完之后执行?
小黄那时候没有答出来,果然基础不牢,地动山摇。如此简单的一道送分题,在面试过程中应该是要能答得上来的,至于是怎么保证,咱们来往下看
案例描述
我们先从最简单的开始,有两个线程,一个公共参数number初始值为0,当number为0时,我们执行A线程,让number+1,当number为1时,执行B线程让number-1,循环10次。
解决方案
涉及到多线程问题,我们肯定需要上锁,否则会出现数据不统一的问题
使用synchronized
synchronized是Java中原生的一个修饰符,被synchronized修饰的方法在执行时都会上锁
class ChangeNumber{
private int number = 0;
public synchronized void incr() throws InterruptedException {
//如果number不是0,等待
if (number != 0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "操作值加一:" + number);
//通知其他线程
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
if (number != 1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "操作值减一:" + number);
this.notifyAll();
}
}
public class SycnDemo01 {
public static void main(String[] args) {
ChangeNumber changeNumber = new ChangeNumber();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
changeNumber.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
changeNumber.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
}
}
我们来看一下上述的案例实际执行起来有什么问题,当我将线程扩展到两个加法,两个减法时,执行多次会发现结果不是010101,是什么原因造成的呢?
这里我们要讲一下wait()的原理,wait方法是在哪里等待就在哪里唤醒,简而言之就是AA线程第一次执行wait命令后,当其他线程执行唤醒命令,而A正好抢到了,这时候A会接下去执行,也就是说不需要执行if条件判断,解决办法也很简单,我们使用while来代替if即可
使用Lock
Lock也是一种上锁的办法,相较于synchronized的区别在于,synchronized在执行方法时抛出异常会自动释放锁,而Lock需要手动释放锁
ReentrantLock
可重入锁,这里我们先学习可重入锁,可重入锁有点类似于上厕所,一个人进去后另外一个人必须要等他出来才可以进去
class LockChangeNumber{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void incr(){
lock.lock();
try {
while (number != 0){
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "操作值加一:" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr(){
lock.lock();
try {
while (number != 1){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "操作值减一:" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class LockDemo01 {
public static void main(String[] args) {
LockChangeNumber lockChangeNumber = new LockChangeNumber();
new Thread(()->{
for (int i = 0; i < 10; i++) {
lockChangeNumber.incr();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
lockChangeNumber.decr();
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
lockChangeNumber.incr();
}
},"CC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
lockChangeNumber.decr();
}
},"DD").start();
}
}
定制线程的通信
上面讲了那么多,我们还没有解决小黄在面试中遇到的问题,现在我们在来分析一个案例,通过这个案例,解决面试中的问题
案例分析
有A、B、C三个线程,要求A线程打印一行信息,打印完成之后B线程打印两行信息,紧接着C线程打印三行信息,如此循环十遍。
解决方案
拿到这个问题时,解决方案其实有很多种,小黄选择使用一个标志来解决问题,定义一个flag为1,当flag为1时执行A线程,当flag为2时执行B线程,当flag为3时执行C线程,指定线程被唤醒我们使用lock来解决
class Share{
private int flag = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print1(int turn){
lock.lock();
try {
while (flag != 1){
c1.await();
}
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName() + "执行打印:" + i + ":轮次:" + turn);
}
flag = 2;
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print2(int turn){
lock.lock();
try {
while (flag != 2){
c2.await();
}
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "执行打印:" + i + ":轮次:" + turn);
}
flag = 3;
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print3(int turn){
lock.lock();
try {
while (flag != 3){
c3.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "执行打印:" + i + ":轮次:" + turn);
}
flag = 1;
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class LockDemo02 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
share.print1(i);
}
},"AA").start();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
share.print2(i);
}
},"BB").start();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
share.print3(i);
}
},"CC").start();
}
}
集合线程安全问题
我们循环创建10个线程,执行对集合的写操作以及读操作
逻辑代码
以list集合举例
for (int i = 0; i < 30; i++) {
new Thread(()->{
//写入
list.add(UUID.randomUUID().toString().substring(0,8));
//读取
System.out.println(list);
},String.valueOf(i)).start();
}
List集合
当我们创建ArrayList时,执行上述代码会包并发修改异常java.util.ConcurrentModificationException
解决方案1:使用Vector
List list = new Vector();
解决方案2:使用collections工具类
List list = Collections.synchronizedList(new ArrayList<>());
解决方案3:使用CopyOnWriteArrayList
此方法使用写时复制技术,通过下面这张图来了解一下写时复制技术
List list = new CopyOnWriteArrayList();
set集合
也执行上述代码,同样也会报并发修改异常
解决方案:使用CopyOnWriteArraySet
Set set = new CopyOnWriteArraySet();
map集合
也执行上述代码,同样也会报并发修改异常
解决方案:使用ConcurrentHashMap
Map<String,String> map = new ConcurrentHashMap<>();
多线程锁
synchronized锁的范围
同步方法
synchronized是自动加锁解锁,在使用如下同步方法上锁时,锁的对象为new出来的phone
class Phone{
public synchronized void add(){
System.out.println("========");
}
}
public class SyncDemo02 {
public static void main(String[] args) {
Phone phone = new Phone();
}
}
静态同步方法
当同步方法被static修饰时,锁的对象为Phone,就是这个类的对象
class Phone{
public static synchronized void add(){
System.out.println("========");
}
}
public class SyncDemo02 {
public static void main(String[] args) {
Phone phone = new Phone();
}
}
同步代码块
同步代码块比较好理解,synchronized ()括号中的对象就是锁的对象
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println("=======");
}
},"aa").start();
}
公平锁与非公平锁
这要就要讲到一个卖票的案例,3个窗口同时出售门票,共30张
代码实现
class LockTicket{
private int ticket = 30;
//创建可重入锁
ReentrantLock lock = new ReentrantLock();
public void saleTicket(){
//上锁
lock.lock();
try {
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出:" + (ticket--) + ",余票:" + ticket);
}
}finally {
lock.unlock();
}
}
}
public class LockSaleTicket {
public static void main(String[] args) {
LockTicket lockTicket = new LockTicket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
lockTicket.saleTicket();
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
lockTicket.saleTicket();
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
lockTicket.saleTicket();
}
},"CC").start();
}
}
分析
执行代码我们发现,有时候这30张票全都是同一个线程卖出的,这就叫非公平锁,所有的票都由一个窗口售出了,其他窗口等于不买票。而公平锁就是尽量让所有的窗口都可以卖票
我们来看一下可重入锁的构造方法,默认的构造方法是创建一个非公平锁,在有参构造中,我们可以传入true来创建公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
比较
- 公平锁:执行代码效率会降低
- 非公平锁:效率高,可能会无法实现公平的情况
可重入锁
无论是使用synchronized还是lock创建的锁,都是可重入锁。我们可以从字面上拆解他的意思,可重入锁:可以重复进入的锁。
synchronized演示
在同步方法中调用锁,都是调用的该对象,我们在执行add方法中嵌套了add方法,也就是递归的形式,执行代码报了一个栈溢出的异常,其实我们不需要管异常是什么,可以反向推理一下,如果这个锁不可以重复使用,当第一次调用add方法时,还没有解锁,第二次又调用了add方法,应该是堵塞状态才对
class Iphone{
public synchronized void add(){
add();
}
}
public class SyncDemo03 {
public static void main(String[] args) {
Iphone iphone = new Iphone();
new Thread(()->{
iphone.add();
},"aa").start();
}
}
死锁
当两个或两个以上线程在执行过程中,因为争夺资源而造成一种互相等待的线程称之为死锁
如图所示,线程A获得了锁a,线程B获得了锁b,两者都还没有释放锁,而A线程又需要获取锁b,B线程有需要获取锁a,这就是一种死锁的情况
代码实现
public class SycnDemo04 {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName() + "获取了锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName() + "获取了锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName() + "获取了锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(Thread.currentThread().getName() + "获取了锁a");
}
}
},"B").start();
}
}
当我们执行以上代码,输出如下,并且程序一直处于等待状态
A获取了锁a
B获取了锁b
判断等待原因
当程序出现等待时,并不是所有情况都是因为死锁造成的,比如一个死循环也会出现等待的情况,这时候我们要用命令来判断以下等待的原因
在Java控制台执行以下代码,jps -l
可以理解为Linux中的ps -ef
,可以查看当前运行的信息
YellowStar@DESKTOP-MRK9589 MINGW32 /d/Desktop/study/JUC
$ jps -l
10864
2784 sun.tools.jps.Jps
7840 org.jetbrains.jps.cmdline.Launcher
2504 sycn.SycnDemo04
可以看到sycn.SycnDemo04的编号为2504,紧接着我们再次执行以下代码
YellowStar@DESKTOP-MRK9589 MINGW32 /d/Desktop/study/JUC
$ jstack 2504
Callable接口
概述
有一道非常经典的面试题:创建线程的方式有哪些?这道题的答案可以有三个也可以有四个
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 从线程池中抽取(面试中一般都只需要回答前三种即可)
既然有三种创建线程的方式,那这三种有什么区别呢?
继承Thread类
我们都知道在Java中是单继承多实现的,那么继承就显得非常之宝贵,所以我们一般不建议使用此方法来创建线程
实现Runnable接口
这个方法完美的替代了继承的方法,但唯一的缺点是没有返回值,如果不需要返回值可以使用此方法来创建线程
实现Callable接口
这是JDK1.5中出现的,不仅可以创建线程,还可以获取线程中方法的返回值
实现
在使用Callable接口创建线程之前,我们首先需要认识以下FutureTask(未来任务)
在使用Runnable创建线程我们可以通过new一个Thread类直接创建
new Thread(Runnable,"aa").start;
但在Thread的构造方法中并没有Callable的选项,我们需要通过FutureTask来创建,这里要多提一句FutureTask的原理
在FutureTask中单个任务只执行一次
class MyThread implements Callable{
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + " come in ");
return 200;
}
}
public class CallableDemo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个未来任务
FutureTask futureTask1 = new FutureTask(new MyThread());
//创建一个线程
new Thread(futureTask1,"AA").start();
System.out.println(futureTask1.get());
}
}
在JDK1.8以后,咱们也可以使用lambda表达式来实现
public class CallableDemo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + " come in ");
return 500;
});
System.out.println(futureTask2.get());
new Thread(futureTask2,"BB").start();
}
}
JUC辅助类
再此章节中将为大家介绍常用的三个辅助类
CountDownLatch
此类有些类似于计数器,提前设定一个标准i,每执行一个线程i–,当i为0时执行后续代码
举个栗子:班上有6个同学,班长必须等同学们走完了才可以关门
//6个同学,全部走完,班长关门
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//设定6个同学
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "号同学已离开教室");
//可以理解为同学--
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等待
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长关门");
}
}
CyclicBarrier
跟上述的辅助类正好相反,可以看作是一个累加的过程,当达到一定数量时,执行规定的线程
举个栗子:集齐7颗龙珠后可以召唤神龙许愿
//集齐7颗龙珠召唤神龙
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "星球已被找到");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
Semaphore
有点像发放许可证的样子,获得许可证的线程可以执行,没有获得许可证的线程处于等待状态。这里大家可以理解为一个锁,只不过这个锁可以规定一次有多少人可以进入,人满时外面的人只能等待
举个例子:一共有6辆车,停三个停车位
//6辆车停3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
//发放许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + "开走了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//收回许可
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
读写锁
概述
在Java中有很多锁的类型,例如悲观锁和乐观锁,表锁和行锁,以及我们这里所要讲的读锁和写锁等。
读锁:顾名思义就是该线程用来读取数据的锁
写锁:顾名思义就是该线程用来修改数据的锁
这两种锁都有可能发生死锁的情况
案例
我们创建一个方法用于写入数据,另一个方法用于读取数据,想得到的结果应该是写入完成之后才能读锁,而通过以下简简单单的代码,无法实现这个效果,输出的结果会紊乱
//创建资源类
class MyCache{
//创建一个可变的map
private volatile Map<String,Object> map = new HashMap<>();
public void writeMap(String key,Object value) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "正在写入锁");
map.put(key,value);
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "写入完成");
}
public Object ReadMap(String key) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "正在读锁:" + map.get(key));
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "读完咯");
return map.get(key);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
try {
myCache.writeMap("num" + num,"num" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
try {
myCache.ReadMap("num" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
我们使用读写锁来修改资源类
//创建资源类
class MyCache{
//创建一个可变的map
private volatile Map<String,Object> map = new HashMap<>();
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void writeMap(String key,Object value) {
//写锁上锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写入锁");
map.put(key,value);
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "写入完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
public Object ReadMap(String key){
//读锁上锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读锁:" + map.get(key));
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "读完咯");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//读锁解锁
rwLock.readLock().unlock();
}
return map.get(key);
}
}
输出结果如下,可以看到我们写入的时候也不会出现一个线程没有写完另外一个线程进行写的操作
0正在写入锁
0写入完成
1正在写入锁
1写入完成
2正在写入锁
2写入完成
3正在写入锁
3写入完成
4正在写入锁
4写入完成
0正在读锁:num0
1正在读锁:num1
2正在读锁:num2
4正在读锁:num4
3正在读锁:num3
0读完咯
1读完咯
3读完咯
4读完咯
2读完咯
写锁的降级
这个很好理解,在我们进行写锁之后,并没有释放锁,但我们可以执行读锁的操作,我们称之为写锁的降级
反过来,读锁还未释放时,无法执行写锁,所以读锁的升级是不存在的
以下这个例子很好的反馈了上面所说的话,当执行以下代码时时可以正常的输出以及结束程序的,如果将读锁上锁提前到写锁上锁之前,程序一直卡在读锁上锁之后
public class LockDemo04 {
public static void main(String[] args) {
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = rwlock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = rwlock.readLock();
//写锁上锁
writeLock.lock();
System.out.println("写锁上锁");
//读锁上锁
readLock.lock();
System.out.println("读锁上锁");
//写锁释放
writeLock.unlock();
//读锁释放
readLock.unlock();
}
}
阻塞队列
概述
在讲阻塞队列前,我们先通过以下这张图来了解一下队列与栈的区别
而阻塞队列也是队列的其中一种,不过是比较特殊的队列。可以设定队列的容量,当容器中的容量达到峰值时,往队列里加数据的线程会处于阻塞状态;而当容器中的容量为空时,从队列里拿数据的线程会处于堵塞状态,并且这两种状态是自动化完成的。
核心方法介绍
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
案例
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
//第一组
System.out.println(queue.add("a"));//true
System.out.println(queue.add("b"));//true
System.out.println(queue.add("c"));//true
// System.out.println(queue.add("d")); 异常:java.lang.IllegalStateException: Queue full
System.out.println(queue.remove());//a
System.out.println(queue.remove());//b
System.out.println(queue.remove());//c
// System.out.println(queue.remove()); 异常:java.util.NoSuchElementException
//第二组
System.out.println(queue.offer("a"));//true
System.out.println(queue.offer("b"));//true
System.out.println(queue.offer("c"));//true
System.out.println(queue.offer("d"));//false
System.out.println(queue.poll());//a
System.out.println(queue.poll());//b
System.out.println(queue.poll());//c
System.out.println(queue.poll());//null
//第三组
queue.put("a");
queue.put("b");
queue.put("c");
// queue.put("d");//处于等待状态
System.out.println(queue.take());//a
System.out.println(queue.take());//b
System.out.println(queue.take());//c
// queue.take();//处于等待状态
//第四组
queue.offer("a");
queue.offer("b");
queue.offer("c");
// queue.offer("d",3L, TimeUnit.SECONDS); //处于等待状态,持续3秒
System.out.println(queue.poll());//a
System.out.println(queue.poll());//b
System.out.println(queue.poll());//c
// queue.poll(3L,TimeUnit.SECONDS); //处于等待状态,持续3秒
}
线程池
概述
线程池的优势
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的特点
-
降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
-
提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
-
提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的使用方式
一池N线程
一池N线程的概念理解起来非常简单,创建时设置线程数,假设设置5个线程,那么通过线程池创建不同的线程也只能创建5个
public static void main(String[] args) {
//一池N线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 20; i++) {
threadPool1.execute(()->{
System.out.println(Thread.currentThread().getName() + " 正在执行");
});
}
} finally {
threadPool1.shutdown();
}
}
一池一线程
一池一线程就是说这个线程池中只有一条线程可被调用
public static void main(String[] args) {
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 20; i++) {
threadPool2.execute(()->{
System.out.println(Thread.currentThread().getName() + " 正在执行");
});
}
} finally {
threadPool1.shutdown();
}
}
可扩容线程池
功能非常强大,当线程数量大于初始线程池容量时,自动扩容
public static void main(String[] args) {
//一池N线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//可扩容线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 20; i++) {
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName() + " 正在执行");
});
}
} finally {
threadPool1.shutdown();
}
}
原理
无论上述哪种线程池,他的底层原理都是new ThreadPoolExecutor
7个参数的介绍
7个参数指的是创建线程池所需要的7个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:正常情况下的线程数量。像是银行,一共有10个窗口,但大部分时间只开放五个窗口
- maximumPoolSize:最大可创建的线程数量。银行一个有10个窗口,最多也只能开放10个窗口
- keepAliveTime:当线程数量超过正常线程数量时,会创建新的线程,新的线程执行完之后空闲的等待时间,超过等待时间就关闭线程。
- unit:等待时间的单位
- workQueue:阻塞队列
- threadFactory:线程工厂
- handler:拒绝策略。当线程数超过最大线程数量时执行拒绝策略,直接拒绝线程
工作流程和拒绝策略
工作流程
这里我们假定创建一个线程,正常情况下线程数量是3个,最大线程数量6个,阻塞队列容量3
- 当线程数量小于等于3时:正常使用即可
- 当线程数量大于3小于等于6时:前三个线程可以处理,后面线程存在阻塞队列中等待前面的线程处理完毕
- 当线程数量大于6小于等于9时:前6个如上所述,后面线程会创建新的线程来执行
- 当线程数量大于9时:执行拒绝策略,直接拒绝操作线程
拒绝策略
- ThreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
自定义线程池
阿里巴巴开发手册明确规定禁止使用Executors创建的线程池,因为他的最大线程数量非常之大,容易对系统造成伤害。所以在实际开发过程中,我们都会使用自定义线程池的方式来创建线程池
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
6,
3L,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(3)
);
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " 正在执行");
});
}
} finally {
threadPool.shutdown();
}
}
分支合并框架
概述
简单来说就是将复杂的东西一分为二,二分为四…最后每一个步骤都得到结果之后再汇集起来得到一个总和。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0gLGCRG0-1646833296247)(C:\Users\YellowStar\AppData\Roaming\Typora\typora-user-images\image-20220309213721705.png)]
案例
实现分支合并我们需要使用ForkJoinTask类
我们实现这么一个小案例:从1加到100,使用二分法,如果相差数字小于等于10即可相加,如果大于10,继续分
class MyTask extends RecursiveTask<Integer>{
private static int VALUE = 10;
private int begin;
private int end;
public MyTask(int begin,int end){
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
int result = 0;
if ((end - begin) <= 10){
//直接相加
for (int i = begin; i <= end; i++) {
result += i;
}
}else {
//继续拆分
int middle = (begin + end) / 2;
//左边拆分
MyTask myTask1 = new MyTask(begin,middle);
//右边拆分
MyTask myTask2 = new MyTask(middle+1,end);
myTask1.fork();
myTask2.fork();
result = myTask1.join() + myTask2.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyTask myTask = new MyTask(1,100);
//创建合并分支池
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);
System.out.println(submit.get());
}
}
异步回调
简单讲两个常用的异步回调:没有返回值的异步回调,有返回值的异步回调
public class completableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的异步回调
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName() + "---正在执行1");
});
completableFuture1.get();
//有返回值的异步回调
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "---正在执行2");
//int i = 1/0;
return 1024;
});
completableFuture2.whenComplete((u,t)->{
System.out.println("--u=" + u); //u会输出返回值
System.out.println("--t=" + t); //如果异步回调方法中出现异常,t会将其输出
}).get();
}
}