JUC
J(java)U(util)C(concurrent)其实就是指上图的三个包。
1 Volatile
https://blog.csdn.net/lzcaqde/article/details/80868854
jvm提供的轻量级同步机制,它有三个特性:
- 保证可见性
- 禁止指令重排
- 不保证原子性
1.1 JMM(Java Memory Model)
java内存模型,一个非实际存在的抽象概念,是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
- 加锁前,线程从主存中读取最新值到工作内存
- 解锁前,线程将自己的工作内存中的共享变量值写会主内存
- 加锁解锁,是对同一把锁
JVM运行程序的实体是线程,每个线程进行创建时,JVM都会为其创建该线程独有的工作内存(栈空间)。当线程操作内存中的变量时,需要先将其拷贝到自己的空间,整一个副本,然后在自己的工作内存中对其一顿操作,之后,将副本写回主内存。
从上述描述,即可发现一个问题:在多线程并发的情况下,由于存在副本和副本写回这样的步骤,势必会出现类似脏读、幻读、不可重复读的问题。
比如:线程1、线程2将数据进行拷贝到自己的工作内存后,线程1进行数据修改并写回主内存,但是线程2还是使用原来的数据
所以,只要有一个线程写回主存,那么应该通知其他线程,告诉它们:你们持有的此变量副本数据已经过期,赶紧从主内存读取,更新一下,别用旧的数据啦~~~
这种效果:即所谓的可见性
1.2 验证Volatile的可见性
/**
* 资源类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
class MyData {
int num = 2;
volatile int num2 = 2;
public void addNum() {
num = 10;
}
public void addNum2() {
num2 = 10;
}
public void plus() {
num2++;
}
}
public class VolatileDemo {
public static void demo1() {
// 线程要操作的资源类
MyData myData = new MyData();
System.out.println("main线程开始");
new Thread(()->{
System.out.println("t1线程开始");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addNum();
System.out.println("t1线程结束");
}, "t1").start();
while (myData.getNum() == 2);
System.out.println("main线程结束");
}
}
分析一波:
一共两个线程(不考虑gc线程):main和t1线程
易知,main线程会被卡住,倘若t1线程修改num为10并刷回主存后,main线程能够感知这一变化,那么main最后应该能跳出循环。事实上,由于num
没有volatile
修饰,即便num
被修改,main也无法感知到。
public static void demo2() {
MyData myData = new MyData();
System.out.println("main线程开始");
new Thread(()->{
System.out.println("t1线程开始");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addNum2();
System.out.println("t1线程结束");
}, "t1").start();
while (myData.getNum2() == 2);
System.out.println("main线程结束");
}
demo2修改volatile
修饰的变量num2
,测试发现3s后,main线程结束,综合demo1和demo2,可以说明volatile
保证了可见性
1.3 验证volatile不保证原子性
public static void demo3() {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
myData.plus();
}
}, "t" + i).start();
}
// 当上面的20个线程执行结束后,此时活跃的线程数正好是2(gc和main),main线程可以输出结果了
while(Thread.activeCount() > 2) Thread.yield();
System.out.println(myData.getNum2());
}
上面的循环,创建20个线程,每个线程对num2进行1000次加1操作,倘若,volatile能够保证原子性,那么经过2w次加1操作后,num2应该从初始值2,变更为20002
通过测试发现,最后的结果不是20002
那为什么会出现这样的情况?
①Thread1和Thread2得到num2
副本,在自己的工作内存进行自增操作,num2
都为3
②Thread1将副本写回主内存的时候,CPU调度,Thread1挂起,Thread2开始将自己的副本写回主内存,此时主内存的num2
变为3
③Thread2结束,Thread1写回操作继续,num2
再一次被赋值为3
这种情况,多次发生,导致最后获得的num2
不为期望值20002(极其少数情况,正好是期望值)
通过字节码文件可以看到,num++,被拆分成了3条指令,自增是非原子性操作
- GETFIELD:拿到原始的num
- IADD:进行自增操作
- PUTFIELD:新的结果写回
如何保证原子性?????
- 将addPlus方法通过Synchronized关键字变成同步方法
- 使用JUC中的AtomicInteger,利用了CAS原理
class MyData {
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
1.4 指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序
单线程环境下,确保程序最终执行结果和代码顺序执行的结果一致
处理器在进行重排序时必须要考虑 指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性无法确定,结果无法预测。
public void mySort() {
int x = 11;①
int y = 12;②
x = x + 5;③
y = x * x;④
}
上面的代码执行顺序的可能性:
- ①②③④
- ①②④③
- ②①③④
- ②①④③
- ①③②④
针对上面的代码,指令重排后可能会影响到y的值
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
2 CAS(Compare and Set)
2.1 概述
结合JMM线程的工作内存,主内存,副本之类的知识点。CAS主要对副本写回主内存进行了限定要求。
主存有一个变量为num,其值为5,称之为A,A指代主存num的值
Thread1从主内存拷贝副本到自己的工作内存,此时的副本称之为B,B指代副本num = 5
Thread1将num修改为2019
,此时称之为C,C指代副本num = 2019
Thread1将修改过的num写回主内存时,做如下事情:
将B与A进行比较,如果相等,说明在我Thread1一顿猛操作的期间,没有第二个线程动过num,所以我Thread1可以大胆的将新num,也就是C,写回内存;
如果不相等,说明在我Thread1操作期间,有人先我一步动过了num,把它给改变了,那我之前的一顿操作都是在旧数据的基础上干的,其实就是白干了,所以无法写回内存。
public static void demo1() {
// 主内存的值
AtomicInteger atomicInteger = new AtomicInteger(5);
// 修改成功返回true
atomicInteger.compareAndSet(5, 2019);
// 结果是2019
System.out.println(atomicInteger.get());
// 修改失败返回false
atomicInteger.compareAndSet(5, 1024);
// 结果是2019
System.out.println(atomicInteger.get());
}
// 源代码
public final boolean compareAndSet(int expect, int update) {
// this:当前对象
// valueOffset:内存偏移量
// expect:期望值,也就是上述的B
// update:修改后的值,也就是上述的C
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CAS是一条CPU并发原语,功能是判断内存某个位置的值是否为预期值,是则更新,这个过程是原子的。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件
的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于OS用语范畴,是由若干条指令组成,用语完成某个功能的一个 过程,并且原语的执行必须是连续不能被中断的,所以CAS不会造成数据不一致问题。
2.2 Unsafe类
CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存。Java中CAS操作依赖于Unsafe类中的方法。
注意UnSafe类中的方法都是native修饰,也就是说Unsafe类中的方法都直接调用OS底层资源执行响应任务
2.3 自旋锁
// Unsafe类中自旋锁的示例
@HotSpotIntrinsicCandidate
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
// 从内存偏移量offset的位置获取对象o的数据,相当于副本
v = getIntVolatile(o, offset);
// 如果期望值与主存中的真实值不同会一直卡在这,自旋
} while (!weakCompareAndSetInt(o, offset, v, newValue));
return v;
}
@HotSpotIntrinsicCandidate
public native int getIntVolatile(Object o, long offset);
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
return compareAndSetInt(o, offset, expected, x);
}
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
@HotSpotIntrinsicCandidate JDK9开始出现的注解,该注解表示此方法在HotSpot虚拟机中可能另有更加高效的实现
2.4 CAS的问题
- 一直卡while,CPU负担不小
- ABA问题
- CAS针对一个共享变量,多个不行,只能上锁
2.5 ABA
在概述中,读完后其实会有一个问题:期望值与主存中的值一致,就一定能够说明没有其他线程动过这个数据吗?
这是不能保证的,Thread1的期望值是A,它再写回主存之前,先比较主存的值是否是A。在比较之前,完全可以有另外的线程对该变量一通操作,最后重新改为A,Thread1就会误认为期间无其他线程动过数据。
这就是所谓的ABA问题。
如何解决ABA问题?
时间戳原子引用,在比较期望值的同时,也会比较时间戳(版本号),类比数据库中的乐观锁。每次修改数据,数据的版本号都会发生变化,某一线程修改数据,不仅期望值要求一样,期望的版本号也要一致。这样就解决了ABA问题
3 阻塞队列
BlockingQueue接口实现类 | 说明 |
---|---|
ArrayBlockingQueue | 基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序 |
LinkedBlockingQueue | 底层链表,有界(默认Integer.MAX_VALUE)吞吐量通常高于ArrayBlockingQueue |
SynchronousQueue | 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的延迟无界阻塞队列 |
LinkedTransferQueue | 由链表实现的无界阻塞队列 |
LinkedBlockingDeque | 由链表实现的双向阻塞队列 |
3.1 阻塞队列的好处
在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足会被自动唤醒。
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,是什么时候需要唤醒线程,因为这一切BlockingQueue都包办了。
在concurrent包发布之前,多线程下,我们每个程序员需要自己控制这些细节,尤其还需要兼顾效率和线程安全,而这会给程序带来不小的复杂度
3.2 BlockingQueue的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove(e) | poll() | take() | poll(time, unit) |
检查 | element() | peek() |
满 | 空 | |
---|---|---|
抛出异常 | add会IllegalStateException: Queue full | remove会NoSuchElementException element会NoSuchElementException |
特殊值 | offer会false | poll、peek会null |
一直阻塞 | put死等 | take死等 |
超时退出 | offer有限等待 | poll有限等待 |
上面这些东西,可以看BlockingQueue的源码,注释写的很明白
【测试代码:】
/**
* 抛异常组:add、remove、element
*
* @author: silverbeats
* @date: 2021/6/18 22:32
*/
public void throwExceptionGroupDemo() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// 查看队首
// 对列如果为空,则会java.util.NoSuchElementException
// System.out.println(blockingQueue.element());
System.out.println(blockingQueue.add("a")); // true
System.out.println(blockingQueue.add("b")); // true
System.out.println(blockingQueue.add("c")); // true
// 阻塞队列满,抛异常 java.lang.IllegalStateException: Queue full
// System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.remove()); // a
System.out.println(blockingQueue.remove()); // b
System.out.println(blockingQueue.remove()); // c
// System.out.println(blockingQueue.remove()); // 阻塞队列空,继续出会抛异常java.util.NoSuchElementException
}
/**
* 出错返回特殊值的组:offer,poll,peek
* 就是不报错版,出问题分别返回:false,null,null
*
* @author: silverbeats
* @date: 2021/6/18 22:42
*/
public void giveSpecialGroupDemo() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// 查看队首,对列如果为空,则会返回null
System.out.println("队列为空,查看队首的结果是:" + blockingQueue.peek());
System.out.println(blockingQueue.offer("a")); // true
System.out.println(blockingQueue.offer("b")); // true
System.out.println(blockingQueue.offer("c")); // true
// 阻塞队列满,继续添加返回false
System.out.println("超出容量了,添加第四个的结果是:" + blockingQueue.offer("d"));
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println("队列已经为空,继续移除的返回结果是:" + blockingQueue.poll()); // 阻塞队列空,返回null
}
/**
* put、take
* 满了之后添加,永远等待,什么时候有位置了,什么时候添加进入,死等
* take同理,空了之后,死等,有了再取
*
* @author: silverbeats
* @date: 2021/6/18 22:59
*/
public void blockGroupDemo() {
BlockingQueue<Character> blockingQueue = new ArrayBlockingQueue<>(3);
new Thread(() -> {
// 添加abcd,d会一直等待
for (int i = 0; i < 4; i++) {
try {
blockingQueue.put((char) ('a' + i));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println((char) ('a' + i) + "添加好了");
}
}, "t" + 1).start();
new Thread(() -> {
try {
// t2睡眠2秒后开始取,
// 确保t1已经放入abc,并且d在等待空位置
TimeUnit.SECONDS.sleep(2);
for (int i = 0; i < 4; i++) {
Character item = blockingQueue.take();
System.out.println("已经取出来" + item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t" + 2).start();
}
/**
* 和上面的区别就是,有时间限制的等待,不会死等
* @author: silverbeats
* @date: 2021/6/18 23:08
*/
public void timeLimitBlockGroup() throws InterruptedException {
BlockingQueue<Character> blockingQueue = new ArrayBlockingQueue<>(3);
// 前三个插入时,因为队列还有空间,不会等待2s
System.out.println(blockingQueue.offer('a', 2l, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer('a', 2l, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer('a', 2l, TimeUnit.SECONDS));
// 此时队列满了,只等待2秒,2秒后插入不进去,返回false
System.out.println(blockingQueue.offer('a', 2l, TimeUnit.SECONDS));
}
4 线程池
4.1 概述
【为什么使用池化技术?】
在学习面向对象OOP时,肯定听过一句话,java皆对象,要什么对象就new什么对象。
之后学习spring时,发现通过DI依赖注入后,便不再手动new对象了。
同理,在之前javase学习Thread时,手动new Thread,麻烦,另外gc也需要去不断收回,耗性能。
那么,我们能不能将所需要的线程提前创建好,并且可以反复利用呢?
线程池来了。
【简介】
线程池的工作主要是控制运行线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等待其他线程执行完毕。
【特点优势】
主要特点是:线程复用;控制最大并发数;管理线程
- 降低资源消耗:通过复用已创建的线程降低线程的创建和销毁造成的消耗
- 提高响应速度:任务到达时,任务可以不需要等待线程创建,即可立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,会消耗系统资源,降低系统稳定性,使用线程池可以进行统一分配管理,调优和监控
4.2 三大方法
可供使用的线程池种类 | 说明 |
---|---|
Executors.newCachedThreadPool() | 一池多线程带缓存,执行很多短期异步的小程序或者负载较轻的服务器 |
Executors.newFixedThreadPool() | 一池固定线程,执行长期任务,性能好 |
Executors.newSingleThreadScheduledExecutor() | 一池一线程,一个任务一个任务执行的场景 |
Executors这个类中,定义Executor、ExecutorService、ScheduledExecutorService、ThreadFactory和Callable类的工厂和实用方法。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4.3 七大参数
- corePoolSize:常驻线程池的核心线程数 - maximumPoolSize:最大线程数 - keepAliveTime:多余的空闲线程存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到KeepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止 - unit:存活时间的单位 - workQueue:底层使用的阻塞队列,被提交但是还未执行的任务会保存在这里 - threadFactory:线程工厂,表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可 - handler:拒绝策略(有4种),当队列满了,并且工作线程大于等于线程池的最大线程数,应该如何处理多出来的线程4.4 四种拒绝策略
- AbortPolicy(默认的拒绝策略):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不抛弃任务,也不会抛异常,而是将某些任务回退给调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后将当前任务加入队列中尝试再次提交
- DiscardPolicy:直接丢弃任务,不处理不抛异常。如果任务允许丢失,这是最好的一种
4.5 线程池工作原理
- 在创建了线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个任务请求时,线程池做如下判断:
- 如果正在运行的线程数量小于corePoolSize,马上创建线程运行这个任务
- 如果正在运行的线程数量大于等于corePoolSize,将这个任务放入队列中
- 如果队列满了并且运行的线程数小于maximumPoolSize,创建非核心线程立即运行这个任务(这里需要注意,不是运行队列中的任务,而是当前任务,意思是说存在插队)
- 如果队列满了,也无法扩容了,线程池启动饱和拒绝策略(一共4种)
- 当一个线程完成任务时,会从阻塞队列中进行获取下一个任务
- 当一个线程无事可做,超过KeepAliveTime时,线程池会判断:
- 当前运行的线程数大于corePoolSize,这个线程会被停掉(缩容)
- 所以线程池所以任务完成后,它最终会缩容到corePoolSize
4.6 如何创建线程池
虽然Executors很强大,但是禁止使用Executors创建线程池,阿里的开发规范明确表示禁止使用Executors创建线程池,推荐自行new ThreadPoolExecutor()
,根据实际情况传7大参数,创建线程池。
禁止使用Executors原因是出在上述三大方法中创建ThreadPoolExecutor对象的默认参数上:
- FixedThreadPool和SingleThreadPool底层使用LinkedBlockingQueue,最大长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
- CachedThreadPool和ScheduledThreadPool,默认的maximumPoolSize是Integer.MAX_VALUE,也会有可能导致OOM
4.7 线程池如何合理配置线程数
- CPU密集型:该任务需要大量的运算,没有阻塞,CPU一直全速运行。CPU密集任务只有在多核CPU上才可能通过多线程得到加速。CPU密集型任务配置尽可能少的线程数量,线程池大小的一般公式:CPU核数+1个线程
- IO密集型:
- 由于IO密集型任务线程不是一直在执行的任务,则应配置尽可能多的线程,比如CPU核数*2
- IO密集型,存在大量阻塞,在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力,所以在IO密集型任务中使用多线程可以大大加速程序运行。参考公式:CPU核数/(1-阻塞系数),阻塞系数∈[0.8, 0.9]
5 锁
5.1 fair和Nofair,公平与非公平锁
- 公平fair锁:遵守FIFS,先来先服务,按照线程的申请顺序
- 非公平Nofair:多个线程获取锁的顺序不是严格按照申请的顺序,存在插队现象,在高并发的环境下,可能造成优先级反转或者饥饿现象,synchronized是一种非公平锁
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
公平锁的先来先服务,保证公平,同样也是致命的问题。
比如买奶茶时,你买一杯,你前面那位买100杯,你的体验会特别的糟糕,现实中,你可以与前面的人商量让店员先给你做,然而公平锁是不允许这样交换的,就是先来先得。
5.2 可重入锁(递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码
同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
同步方法1内部调用同步方法2,当线程获取同步方法1的锁,它也会获取方法2的锁。Synchronized和ReentrantLock是典型的可重入锁,可重入锁最大的作用是避免死锁
5.3 独占锁(写锁),共享锁(读锁)
独占锁:只能被一个线程占用,对ReentrantLock和Synchronized而言都是独占锁
共享锁:可以被多个线程所持有
对ReentrantReadWriteLock而言,读锁为共享,写锁为独占锁
读锁的共享锁可以保证并发读时非常高效的,读写、写写的过程是互斥的
5.4 自旋锁
是指尝试获取的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处就是减少线程上下文切换的消耗,缺点是循环会消耗CPU
6 生产者消费者问题
6.1 传统版sync-wait-notify
public class ProduceConsumer1 {
// 存储资源的容器
private Queue<Person> container = new LinkedList<>();
// 容器存储容量上限
private final int CAPACITY = 3;
// 消费者调用的方法
public synchronized Person consumer() {
String tName = Thread.currentThread().getName();
while (container.size() <= 0) {
try {
System.out.println(tName + "在等待,因为无资源可用");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(tName + "进行消费");
Person res = container.poll();
notifyAll();
return res;
}
// 生产者调用的方法
public synchronized boolean producer(Person person) {
String tName = Thread.currentThread().getName();
while (container.size() >= CAPACITY) {
try {
System.out.println(tName + "在等待,因为无空间存放");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(tName + "进行生产");
boolean res = container.add(person);
notifyAll();
return res;
}
}
6.2 lock
public class ProduceConsumer2 {
// 存储资源的容器
private Queue<Person> container = new LinkedList<>();
// 容器存储容量上限
private final int CAPACITY = 3;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Person consumer() {
String tName = Thread.currentThread().getName();
Person res = null;
lock.lock();
try {
// 判断是否能够消费
while (container.size() == 0) {
System.out.println(tName + "正在等待产品");
condition.await();
}
// 进行消费
res = container.poll();
System.out.println(tName + "成功消费");
// 通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return res;
}
public boolean producer(Person person) {
String tName = Thread.currentThread().getName();
boolean res = false;
lock.lock();
try {
// 判断是否有空间继续生产
while (container.size() == CAPACITY) {
System.out.println(tName + "等待消费者进行消费");
condition.await();
}
res = container.add(person);
System.out.println(tName + "生产成功");
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return res;
}
}`
6.3 阻塞队列
public class ProduceConsumer3 {
// 默认开启,进行生产+消费
private volatile boolean FLAG = true;
private AtomicInteger atomicInteger = new AtomicInteger();
BlockingQueue<String> blockingQueue;
public Source3(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
public void producer() throws Exception {
String data;
boolean retValue;
String tName = Thread.currentThread().getName();
while (FLAG) {
data = atomicInteger.incrementAndGet() + "";
retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (retValue) {
System.out.println(String.format("%s将%s插入队列成功", tName, data));
} else {
System.out.println(String.format("%s将%s插入队列失败", tName, data));
}
// 1s生产一个
TimeUnit.SECONDS.sleep(1);
}
System.out.println(tName + "线程停止,flag变成false,生产结束");
}
public void consumer() throws Exception {
String data;
String tName = Thread.currentThread().getName();
while (FLAG) {
data = blockingQueue.poll(3L, TimeUnit.SECONDS);
if (Objects.isNull(data) || "".equalsIgnoreCase(data)) {
FLAG = false;
System.out.println(tName + "线程停止,3s超时未获取到,消费结束");
return;
}
System.out.println(String.format("%s消费%s成功", tName, data));
// 2s取一个
TimeUnit.SECONDS.sleep(2);
}
}
public void stop() throws Exception {
FLAG = false;
}
}