1、关于 wait()
方法虚假唤醒的问题
wait()
方法一个特点就是在哪里沉睡就会在哪里醒来,所以,如果wait()
方法没加在循环里,就会出现一个只有第一次判断生效,第二次则不进行判断,直接往下执行造成虚假唤醒的情况,从而导致数据出错。下图中,把 if
改为 while
解决虚假唤醒问题
2、线程的定制化通信
// 提供功能
class Th {
private int flag = 1;
ReentrantLock lock = new ReentrantLock();
// 提供三个独立的Condition
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
c1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i);
}
flag = 2;
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i);
}
flag = 3;
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i);
}
flag = 1;
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class dd {
public static void main(String[] args) {
Th th = new Th();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
th.print5();
}
}, "C1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
th.print10();
}
}, "C2").start();
new Thread(() -> {
for (int i = 0; i < 15; i++) {
th.print15();
}
}, "C3").start();
}
}
3、关于ArrayList()
线程不安全问题的解决方法
线程不安全条件下的并发操作
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
arrayList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(arrayList);
},i+"").start();
}
}
经测试,不能再 @Test
下测试,不会有任何输出。通过上述测试,会报 java.util.ConcurrentModificationException
异常
解决方案一:使用 Vector
。List的古老实现类,JDK1.0引入
public static void main(String[] args) {
List<String> arrayList = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
arrayList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(arrayList);
}, i + "").start();
}
}
解决方案二:使用 Collections
工具类
public static void main(String[] args) {
List<String> arrayList = Collections.synchronizedList(new ArrayList<String>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
arrayList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(arrayList);
}, i + "").start();
}
}
解决方案三:CopyOnWriteArrayList
写时复制技术
写时复制技术:CopyOnWrite
容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[] 添加,而是先复制一份当前容器,然后复制的容器中添加元素,添加完之后,再覆盖原来的Object[]数组。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁。因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离思想,读和写不同的容器
原理图:
public static void main(String[] args) {
CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
arrayList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(arrayList);
}, i + "").start();
}
}
源码:
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();
}
}
4、HashSet()
的线程不安全问题
public static void main(String[] args) {
HashSet<String> hashSet = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
hashSet.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(hashSet);
}, i + "").start();
}
}
报错:java.util.ConcurrentModificationException
并发修改异常
解决方案一:使用 Collections
工具类
public static void main(String[] args) {
Set<String> hashSet = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
hashSet.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(hashSet);
}, i + "").start();
}
}
解决方案二:CopyOnWriteArraySet
方法
public static void main(String[] args) {
CopyOnWriteArraySet<String> hashSet = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
hashSet.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(hashSet);
}, i + "").start();
}
}
5、关于 HashMap
线程不安全的问题
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(() -> {
hashMap.put(key, UUID.randomUUID().toString().substring(0, 8));
System.out.println(hashMap);
}, i + "").start();
}
}
报错:java.util.ConcurrentModificationException
并发修改异常
解决方案: 使用ConcurrentHashMap
public static void main(String[] args) {
ConcurrentHashMap<String, String> hashMap = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(() -> {
hashMap.put(key, UUID.randomUUID().toString().substring(0, 8));
System.out.println(hashMap);
}, i + "").start();
}
}
6、可重入锁 synchronized
和 Lock
synchronized
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 外层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 内层");
}
}
}
},"线程A").start();
}
//输出
线程A 外层
线程A 中层
线程A 内层
Lock
正确演示
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
System.out.println("外层");
try {
lock.lock();
System.out.println("内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
错误演示,假如某一个锁只加锁,不释放锁
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"内层");
} finally {
// lock.unlock();
}
} finally {
lock.unlock();
}
},"线程一").start();
new Thread(()->{
try {
lock.lock();
System.out.println("第二个线程所输出的内容");
}finally {
lock.unlock();
}
},"线程二").start();
}
//输出结果:会发现第二个线程的内容无法输出,而且程序并没停止。原因,第一个没有把锁释放,第线程二阻塞
线程一外层
线程一内层
7、制作一个死锁
public static void main(String[] args) {
Object resource1 = new Object();
Object resource2 = new Object();
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread().getName() + "获取resource1,尝试获取resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println(Thread.currentThread().getName()+"获取到resource2");
}
}
},"线程一").start();
new Thread(()->{
synchronized (resource2){
System.out.println(Thread.currentThread().getName() + "获取resource2,尝试获取resource1");
synchronized (resource1){
System.out.println(Thread.currentThread().getName()+"获取到resource1");
}
}
},"线程二").start();
}
8、Callable接口
Callable接口,是一种让线程执行完成后,能够返回结果的
在说到Callable接口的时候,我们不得不提到Runnable接口
/**
* 实现Runnable接口
*/
class MyThread implements Runnable {
@Override
public void run() {
}
}
我们知道,实现Runnable接口的时候,需要重写run方法,也就是线程在启动的时候,会自动调用的方法
同理,我们实现Callable接口,也需要实现call方法,但是这个时候我们还需要有返回值,这个Callable接口的应用场景一般就在于批处理业务,比如转账的时候,需要给一会返回结果的状态码回来,代表本次操作成功还是失败
/**
* Callable有返回值
* 批量处理的时候,需要带返回值的接口(例如支付失败的时候,需要返回错误状态)
*
*/
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("come in Callable");
return 1024;
}
}
最后我们需要做的就是通过Thread线程, 将MyThread2实现Callable接口的类包装起来
这里需要用到的是FutureTask类,他实现了Runnable接口,并且还需要传递一个实现Callable接口的类作为构造函数
// FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
// 这里通过了FutureTask接触了Callable接口
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
然后在用Thread进行实例化,传入实现Runnabnle接口的FutureTask的类
Thread t1 = new Thread(futureTask, "aaa");
t1.start();
最后通过 futureTask.get() 获取到返回值
// 输出FutureTask的返回值
System.out.println("result FutureTask " + futureTask.get());
这就相当于原来我们的方式是main方法一条龙之心,后面在引入Callable后,对于执行比较久的线程,可以单独新开一个线程进行执行,最后在进行汇总输出
最后需要注意的是 要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致阻塞,直到计算完成
也就是说 futureTask.get() 需要放在最后执行,这样不会导致主线程阻塞
也可以使用下面算法,使用类似于自旋锁的方式来进行判断是否运行完毕
// 判断futureTask是否计算完成
while(!futureTask.isDone()) {
}
注意
多个线程执行 一个FutureTask的时候,只会计算一次
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask, "BBB").start();
如果我们要两个线程同时计算任务的话,那么需要这样写,需要定义两个futureTask
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask2, "BBB").start();
// 实现 Callable接口
static class Call implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 使用lambda表达式创建一个futureTask
FutureTask<Integer> futureTask2 = new FutureTask<>(() -> {
return 250;
});
FutureTask<Integer> futureTask1 = new FutureTask<>(new Call());
// 开启一个线程一
new Thread(futureTask1,"线程一").start();
// 开启一个线程二
new Thread(futureTask2,"线程二").start();
// 判断线程是否执行完毕
while (!futureTask2.isDone()) {
System.out.println("线程还没结束,wait....");
}
// 获取线程二的返回值
Integer integer = futureTask2.get();
// 获取线程一的返回值
Integer integer1 = futureTask1.get();
System.out.println("线程一"+integer1);
System.out.println("线程二"+integer);
}
9、辅助类
**CountDownLatch
**一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程等待
public static void main(String[] args) throws InterruptedException {
// 声明一组有六个线程
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
一个同步辅助类,它允许一组线程互相等待,直到达到某一个公公屏障点,在设计一组固定大小的线程的程序中,这些线程必须不时的互相等待,此时 CyclicBarrier
很有用,因为该 Barrier
在释放等待线程后可以重用,所以称他为循环的 Barrier
public static void main(String[] args) {
final int NUMBER = 7;
CyclicBarrier barrier = new CyclicBarrier(NUMBER, () -> {
System.out.println("七颗龙珠,召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "颗龙珠被集齐");
// 等待
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
Semaphore
一个计数信号量,从概念上讲,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个 acquire()
,然后在获取该许可,每个 release()
添加一个许可,从而可能释放一个正在阻塞的获取者,但是,不使用实际许可对象, Semaphore
只对可用许可的号码进行计数,并采取相应的行动。
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() + "抢占了车位");
// 设置随机抢占时间0~5
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();
}
}
10、读写锁案例
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享。
缺点:
- 造成锁饥饿,一直读,没有写操作。
- 读的时候,不能写,只有读完之后才可以写,写操作可以读
未加锁前
class MyCache{
public volatile HashMap<String, Object> map = new HashMap<>();
// 写数据
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "正在写数据" + key);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"--写完了");
}
// 读数据
public Object get(String key) {
System.out.println(Thread.currentThread().getName() + "正在读数据" + key);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读数据完成");
return o;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", finalI);
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI + "");
}, String.valueOf(i)).start();
}
}
}
// 输出结果如下
1正在写数据1
4正在写数据4
2正在写数据2
3正在写数据3
5正在写数据5
2正在读数据2
3正在读数据3
1正在读数据1
4正在读数据4
5正在读数据5
5--写完了
5读数据完成
4读数据完成
3读数据完成
4--写完了
1--写完了
2读数据完成
1读数据完成
2--写完了
3--写完了
从输出结果我们可以看到,有些数据还没有写入完成,就已经被读取了,显然读到的数据是空,为了防止此类事件的发生,加入读写锁如下
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache{
public volatile HashMap<String, Object> map = new HashMap<>();
// 创建读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 写数据
public void put(String key, Object value) {
// 添加写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写数据" + key);
TimeUnit.SECONDS.sleep(1);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "--写完了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放写锁
readWriteLock.writeLock().unlock();
}
}
// 读数据
public Object get(String key) {
// 添加读锁
readWriteLock.readLock().lock();
Object o = null;
try {
System.out.println(Thread.currentThread().getName() + "正在读数据" + key);
TimeUnit.SECONDS.sleep(1);
o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读数据完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放读锁
readWriteLock.readLock().unlock();
}
return o;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 创建线程写数据
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", finalI);
}, String.valueOf(i)).start();
}
// 创建线程读数据
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI + "");
}, String.valueOf(i)).start();
}
}
}
// 输出结果如下
2正在写数据2
2--写完了
1正在写数据1
1--写完了
3正在写数据3
3--写完了
4正在写数据4
4--写完了
5正在写数据5
5--写完了
1正在读数据1
2正在读数据2
3正在读数据3
4正在读数据4
5正在读数据5
5读数据完成
1读数据完成
4读数据完成
2读数据完成
3读数据完成
从结果我们不难看出,都是先写入完成后才能读数据,而且,从结果我们也可以看出,对于写锁,是排他锁,同一时刻,只能有一个线程对其进行写操作,而对于读取数据的话,是共享锁,同一时刻,可以有多个线程对其进行读操作。
11、阻塞队列
简介
队列就可以想成是一个数组,从一头进入,一头出去,排队买饭
BlockingQueue 阻塞队列,排队拥堵,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
- 当蛋糕店的柜子空的时候,无法从柜子里面获取蛋糕
当阻塞队列是满时,从队列中添加元素的操作将会被阻塞
- 当蛋糕店的柜子满的时候,无法继续向柜子里面添加蛋糕了
也就是说 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其它线程往空的队列插入新的元素
同理,试图往已经满的阻塞队列中添加新元素的线程,直到其它线程往满的队列中移除一个或多个元素,或者完全清空队列后,使队列重新变得空闲起来,并后续新增
为什么需要 BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都帮你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己取控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
BlockingQueue核心方法
抛出异常 | 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException |
---|---|
特殊性 | 插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空 |
一直阻塞 | 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出, 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 |
超时退出 | 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
ArrayBlockingQueue
演示
package com.tzf.queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
/* System.out.println(blockingQueue.add("a"));// true
System.out.println(blockingQueue.add("b"));// true
System.out.println(blockingQueue.add("c"));// true
// System.out.println(blockingQueue.add("a"));// 报异常:Queue full
System.out.println(blockingQueue.element());// 返回队列中的第一个元素,但元素并不出队列
System.out.println(blockingQueue.remove());// 删除队列中的头一个元素
*/
/* 以上是出错会报异常 */
// .offer()向队列中插入一个元素
/*
System.out.println(blockingQueue.offer("a"));// true
System.out.println(blockingQueue.offer("b"));// true
System.out.println(blockingQueue.offer("c"));// true
// System.out.println(blockingQueue.offer("d"));// false 队列满了的话会返回false
System.out.println(blockingQueue.poll());// 从队列中删除元素,当队列中没有元素是返回null
*/
// put()向队列中插入元素,若队列满了,则阻塞,直到队列有空位置,再插入
blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.offer("a", 3L, TimeUnit.SECONDS);// 此语句阻塞三秒后自动放弃插入数据
// blockingQueue.put("a");// 此次添加将会阻塞
// take() 从队列中取出一个元素。若队列中没有元素可取,则阻塞,直到有元素时再取出元素
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());// 此次阻塞,等待元素插入队列
}
}
SynchronousQueue演示
SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储的BlockingQueue,每一个put操作必须等待一个take操作,否者不能继续添加元素
public static void main(String[] args) {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println("put 1");
queue.put(1);
System.out.println("put 2");
queue.put(2);
System.out.println("put 3");
queue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AA").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("过了三秒"+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("过了三秒"+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("过了三秒"+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BB").start();
}
结果 我们从最后的运行结果可以看出,每次AA线程向队列中添加阻塞队列添加元素后,AA输入线程就会等待 BB消费线程,BB消费后,BB处于挂起状态,等待AA在存入,从而周而复始,形成 一存一取的状态
put 1
过了三秒1
put 2
过了三秒2
put 3
过了三秒3
小demo
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,则这会给我们的程序带来不小的时间复杂度
现在我们使用新版的阻塞队列版生产者和消费者,使用:volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用
class MyResource {
AtomicInteger atomicInteger = new AtomicInteger();
// 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
BlockingQueue<String> blockingQueue = null;
// 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
private volatile boolean FLAG = true;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd() {
String data = null;
boolean retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while (FLAG) {
try {
data = atomicInteger.incrementAndGet() + "";
// 2秒存入1个data
retValue= blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data + "失败");
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产介绍");
}
public void myConsumer() {
String retValue;
// 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
// 当FLAG为true的时候,开始生产
while (FLAG) {
// 2秒存入1个data
try {
retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (retValue != null && retValue != "") {
System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue + "成功");
} else {
FLAG = false;
System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出");
// 退出消费队列
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 停止生产的判断
*/
public void stop() {
this.FLAG = false;
}
}
public class ProdConsumerBlockingQueueDemo {
public static void main(String[] args) {
// 传入具体的实现类, ArrayBlockingQueue
ArrayBlockingQueue<String> strings = new ArrayBlockingQueue<>(3);
MyResource myResource = new MyResource(strings);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
try {
myResource.myProd();
System.out.println("");
} catch (Exception e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
try {
myResource.myConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}, "B").start();
// 5秒后,停止生产和消费
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("");
System.out.println("");
System.out.println("5秒中后,生产和消费线程停止,线程结束");
myResource.stop();
}
}
12、线程池
12.1、为什么使用线程池
线程池做的主要工作就是控制运行的线程的数量,处理过程中,将任务放入到队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用、控制最大并发数、管理线程
线程池中的任务是放入到阻塞队列中的
优点
- 减低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
12.2、 创建线程池
Executors.newFixedThreadPool(int i)
:创建一个拥有 i 个线程的线程池- 执行长期的任务,性能好很多
- 创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待
Executors.newSingleThreadExecutor
:创建一个只有1个线程的 单线程池- 一个任务一个任务执行的场景
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
Executors.newCacheThreadPool()
; 创建一个可扩容的线程池- 执行很多短期异步的小程序或者负载教轻的服务器
- 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
Executors.newScheduledThreadPool(int corePoolSize)
:线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
根据阿里巴巴开发手册规定,不建议用如下方法创建线程(因为队列长度最大值为 Integer.MAX_VALUE
,可能会堆积大量请求,从而导致OOM
),使用自定义线程池,
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(8);
// 创建单个线程
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
// 线程池大小不确定,根据实际情况改变大小
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
线程池底层ThreadPoolExecutor
的七个参数介绍
- corePoolSize:核心线程数,线程池中的常驻核心线程数
- 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1、
- 相当有扩容后的线程数,这个线程池能容纳的最多线程数
- keepAliveTime:多余的空闲线程存活时间
- 当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
- 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
- LinkedBlockingQueue:链表阻塞队列
- SynchronousBlockingQueue:同步阻塞队列
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
- handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的Runnable的策略
ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize,// 最大线程数
long keepAliveTime, // 线程存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 阻塞队列的大小
ThreadFactory threadFactory, // 线程工厂,用于创建线程
RejectedExecutionHandler handler) // 拒绝策略
当线程池和阻塞队列都满后,就需要拒绝策略,有四种拒绝策略:
AbortPolicy
(默认):直接抛出RejectedExecutionException
异常阻止系统正常运行CallerRunsPolicy
:”调用者运行“一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务返回给调用者,从而降低新任务流量DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入到队列中,尝试再次提交事务DiscardPolicy
:该策略默默丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略
12.3、线程池底层工作原理
文字说明
- 在创建了线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
以顾客去银行办理业务为例,谈谈线程池的底层工作原理
- 最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理
- 后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
- 后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
- 假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
- 临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数
12.4、为什么不用默认创建的线程池?
线程池创建的方法有:固定数的,单一的,可变的,那么在实际开发中,应该使用哪个?
我们一个都不用,在生产环境中是使用自己自定义的
为什么不用 Executors 中JDK提供的?
根据阿里巴巴手册:并发控制这章
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
- 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
- Executors返回的线程池对象弊端如下:
- FixedThreadPool和SingleThreadPool:
- 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
- CacheThreadPool和ScheduledThreadPool
- 运行的请求队列长度为:Integer.MAX_VALUE,线程数上限太大导致oom
- FixedThreadPool和SingleThreadPool:
- Executors返回的线程池对象弊端如下:
12.5、自定义线程池
从上面我们知道,因为默认的Execu
ors创建的线程池,底层都是使用LinkBlockingQueue作为阻塞队列的,而LinkBlockingQueue虽然是有界的,但是它的界限是 Integer.MAX_VALUE 大概有20多亿,可以相当是无界的了,因此我们要使用ThreadPoolExecutor自己手动创建线程池,然后指定阻塞队列的大小
下面我们创建了一个 核心线程数为3,最大线程数为5,并且阻塞队列数为3的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,// 核心线程数
5,// 最大线程数
3L,// 线程存活时间
TimeUnit.SECONDS,// 存活时间的单位
new ArrayBlockingQueue<>(3), // 阻塞队列的大小
Executors.defaultThreadFactory(),// 线程工厂,y于创建线程
new ThreadPoolExecutor.AbortPolicy()); // 拒绝策略
}
12.6、线程池的合理参数
生产环境中如何配置 corePoolSize 和 maximumPoolSize
这个是根据具体业务来配置的,分为CPU密集型和IO密集型
- CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数 + 1个线程数
- IO密集型
由于IO密集型任务线程并不是一直在执行任务,则可能多的线程,如 CPU核数 * 2
IO密集型,即该任务需要大量的IO操作,即大量的阻塞
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上
所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集时,大部分线程都被阻塞,故需要多配置线程数:
参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右
例如:8核CPU:8/ (1 - 0.9) = 80个线程数
13、Fork/Join 框架
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成 若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进 行 join 汇总。
Fork/Join 框架与线程池的区别
采用 “工作窃取”模式(work-stealing): 当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上,在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中, 如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理 该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了 线程的等待时间,提高了性能
package com.tzf.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
class MyTask extends RecursiveTask<Integer> {
// 拆分插值不能大于10,
private static final Integer VALUE = 10;
private int begin;
private int end;
private int result;
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
if ((end - begin) <= VALUE) {
for (int i = begin; i <= end; i++) {
result += i;
}
} else {
int middle = begin + (end - begin) / 2;
// 拆分左边
MyTask myTask = new MyTask(begin, middle);
// 拆分右边
MyTask myTask1 = new MyTask(middle+1, end);
// 调用方法拆分
myTask.fork();
myTask1.fork();
// 合并结果
result = myTask.join() + myTask1.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyTask myTask = new MyTask(0, 100);
// 创建分支合并对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> task = forkJoinPool.submit(myTask);
// 获取最终合并的结果
Integer integer = task.get();
System.out.println(integer);
// 关闭池对象
forkJoinPool.shutdown();
}
}