juc是java.util.current的简写,意思是并发编程。
线程协作
这是一个问题,以我们上面学习的多线程知识做不到生产者和消费者的这种应用场景。
也可以通过一个标志位来判断,true则有消息,否则无。
管程法
package com.demo.threadLock;
// 测试:生产者消费者模型--》利用缓冲区解决:管程法
import lombok.AllArgsConstructor;
import lombok.Data;
// 生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
// 生产者
@Data
@AllArgsConstructor
class Productor extends Thread{
SynContainer synContainer;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产鸡的数量:" + i);
}
}
}
// 消费者
@Data
@AllArgsConstructor
class Consumer extends Thread{
SynContainer synContainer;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("消费鸡的数量:" + synContainer.pop().id);
}
}
}
// 产品
@Data
@AllArgsConstructor
class Chicken{
int id;
}
// 缓冲区
@Data
class SynContainer{
// 需要一个容器大小
Chicken[] chickens = new Chicken[5];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken){
// 如果容器满了,就需要等待消费者消费
if (count==chickens.length){
// 通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,我们就需要生产产品放进容器
chickens[count] = chicken;
count++;
// 可以通知消费者消费了
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop(){
// 判断能否消费
if (count==0){
// 等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 通知生产者生产,吃完了
this.notifyAll();
return chicken;
}
}
信号灯法
就是利用一个标志位
package com.demo.threadLock;
import lombok.AllArgsConstructor;
// 测试生产者消费者问题2:信号灯法,标志位解决
public class TestPc2 {
public static void main(String[] args) {
Product product = new Product();
new Productor2(product).start();
new Consumer2(product).start();
}
}
// 生产者
@AllArgsConstructor
class Productor2 extends Thread{
Product product;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.production(i+"");
}
}
}
// 消费者
@AllArgsConstructor
class Consumer2 extends Thread{
Product product;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.consume();
}
}
}
// 产品
class Product{
// 生产者生产,消费者等待 t
// 消费者消费,生产者等待 f
String huawei;
boolean flag = true;
// 生产
public synchronized void production(String huawei){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产了:" + huawei);
// 通知消费者消费
this.notifyAll();
this.huawei = huawei;
this.flag = !this.flag;
}
// 消费
public synchronized void consume(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费了:" + huawei);
// 通知消费者消费
this.notifyAll();
this.flag = !this.flag;
}
}
虚假唤醒
package com.example.javasestudy.thread.juc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class WaitAndNotify {
public static void main(String[] args) {
DataTow dataTow = new DataTow();
// 苦逼员工一号
new Thread(() -> {
dataTow.work();
},"苦逼员工一号").start();
// 苦逼员工二号
new Thread(() -> {
dataTow.work();
},"苦逼员工二号").start();
// 心地善良的老板
new Thread(() -> {
dataTow.issue();
},"可爱的老板").start();
}
}
class DataTow {
// 任务数量
public volatile int number = 0;
// 员工做任务动作
public synchronized void work() {
try {
if(number == 0) {
System.out.println(Thread.currentThread().getName() + "正在等待老板通知");
// 没有任务就阻塞等待
wait();
}
// 被唤醒后做的任务
number--;
System.out.println(Thread.currentThread().getName() + "完成了工作并下班。" + "剩余工作任务的条数:" + number);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 老板发布任务动作
public synchronized void issue() {
try {
// 等待3秒让员工都提前进入等待环节
System.out.println(Thread.currentThread().getName() + "正在让苦逼员工愉快的等待中...");
Thread.sleep(3000);
// 老板发布任务
number++;
System.out.println(Thread.currentThread().getName() + "发布了任务并且喝了一口茶后通知了员工。" + "剩余工作任务的条数:" + number);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 通知所有员工都来做任务,并且有事先走了
this.notifyAll();
}
}
// 苦逼员工一号正在等待老板通知
// 苦逼员工二号正在等待老板通知
// 可爱的老板正在让苦逼员工愉快的等待中...
// 可爱的老板发布了任务并且喝了一口茶后通知了员工。剩余工作任务的条数:1
// 苦逼员工二号完成了工作并下班。剩余工作任务的条数:0
// 苦逼员工一号完成了工作并下班。剩余工作任务的条数:-1
// 居然出现了负数,这就肯定是不对的了
// 解决办法就是把if换成while
// while (number == 0) {
// System.out.println(Thread.currentThread().getName() + "正在等待老板通知");
// 没有任务就阻塞等待
// wait();
//}
// 苦逼员工一号正在等待老板通知
// 苦逼员工二号正在等待老板通知
// 可爱的老板正在让苦逼员工愉快的等待中...
// 可爱的老板发布了任务并且喝了一口茶后通知了员工。剩余工作任务的条数:1
// 苦逼员工二号完成了工作并下班。剩余工作任务的条数:0
// 苦逼员工一号正在等待老板通知
// 此时程序仍未停止,因为有一个线程并没有收到任务,还在等待
虚假唤醒
就是唤醒了多余的线程。
- 是多线程环境(有多个线程拿到了同一个对象锁,并进行了wait()阻塞等待)。
- 是拥有该对象锁的线程调用了notifyAll()方法。
如果在多线程环境中满足了以上两个条件就有可能造成wait()虚假唤醒。
例如以上例子,老板只发布一个任务,而两个员工都执行了任务,并且num都-1,那肯定就不对了。
原因是代码中的if无论为真,num–都会执行,把if换成while循环之后即可解决,while如果不为真下面的代码就都不会执行。
使用while循环去循环判断一个条件,而不是使用if只判断一次条件;即wait()要在while循环中。
juc版本
通过Lock找到Condition
package com.atlinxi.gulimall.springdemo.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class B {
public static void main(String[] args) {
Data2 data2 = new Data2();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data2.increment();
}
},"a").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data2.decrement();
}
},"b").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data2.increment();
}
},"c").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data2.decrement();
}
},"d").start();
}
}
class Data2 { // 数字,资源类
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// condition.await(); // 等待
// condition.signalAll(); // 唤醒全部
// ++
public void increment() {
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 decrement() {
lock.lock();
try {
while (number == 0) {
// 等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 输出和上面的synchronized是一样的,线程是随机的,
// 而我们想做到a执行完了通知b执行,b执行完了通知c执行这样有序执行
// 专业术语叫作精准通知和唤醒线程
Condition实现精准通知唤醒
可以设置多个同步监视器,每个监视器监视一个资源。
package com.atlinxi.gulimall.springdemo.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* a执行完调用b,b执行完调用c,c执行完调用a
*/
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data3.printA();
}
},"a").start();
new Thread(()->{for (int i = 0; i < 5; i++) {
data3.printB();
}},"b").start();
new Thread(()->{for (int i = 0; i < 5; i++) {
data3.printC();
}},"c").start();
//a->aaaaaaaa
//b->bbbbbbb
//c->aaaaaaaa
//a->aaaaaaaa
//b->bbbbbbb
//c->aaaaaaaa
//a->aaaaaaaa
//b->bbbbbbb
//c->aaaaaaaa
//a->aaaaaaaa
//b->bbbbbbb
//c->aaaaaaaa
//a->aaaaaaaa
//b->bbbbbbb
//c->aaaaaaaa
//
//Process finished with exit code 0
}
}
class Data3{
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; // 1a 2b 3c
public void printA(){
lock.lock();
try {
// 业务代码
while (number!=1){
// 等待
condition.await();
}
System.out.println(Thread.currentThread().getName() + "->aaaaaaaa");
// 唤醒b
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
// 业务代码
while (number!=2){
// 等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "->bbbbbbb");
// 唤醒b
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
// 业务代码
while (number!=3){
// 等待
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "->aaaaaaaa");
// 唤醒b
number = 1;
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
锁是什么?如何判断锁的是谁?8锁现象
如何判断锁的是谁?永远知道什么锁,锁到底锁的是谁!
锁只有两个,对象和Class
。
8锁现象实际上就是说,在8种情况下,锁究竟是对象还是Class。
package com.atlinxi.gulimall.springdemo.juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁,就是关于锁的8个问题
*
* 1. TimeUnit.SECONDS.sleep(1); 标准情况下,先输出发短信,后输出打电话
* 2. sendSms延迟4s,先输出发短信,后输出打电话
* 我们使用的同步方式是synchronized,锁是被调用方法的对象,也就是phone,
* 两个线程用的是同一把锁,谁先拿到,谁先执行
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
// 不是先调用的先执行,其实是锁的问题
new Thread(()->{
phone.sendSms();
},"a").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"b").start();
}
}
class Phone{
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendSms");
}
public synchronized void call(){
System.out.println("call");
}
}
package com.atlinxi.gulimall.springdemo.juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3. sendSms()是同步方法,hello()不是同步方法,不存在抢锁,所以是hello先输出
* 4. 两个对象,一个sendSms(),一个call(),先执行call(),
* 两个对象,自然就是两把锁,所以按正常的执行就好。
*
*/
public class Test2 {
public static void main(String[] args) {
// 两个对象,
Phone2 phone = new Phone2();
Phone2 phone2 = new Phone2();
// 不是先调用的先执行,其实是锁的问题
new Thread(() -> {
phone.sendSms();
}, "a").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "b").start();
}
}
class Phone2 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendSms");
}
public synchronized void call() {
System.out.println("call");
}
public void hello(){
System.out.println("hello");
}
}
package com.atlinxi.gulimall.springdemo.juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5. 增加两个静态同步方法,只有一个对象,sendSms()先输出
* 静态方法,类一加载就有了!锁的是Class,一个类只能有一个class对象,静态方法锁的是Class,所以用的是同一个锁
* 6. 增加两个静态同步方法,两个对象,还是sendSms()先输出,因为一个类只能有一个Class对象,即使是两个对象依然是同一把锁
*/
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
Phone3 phone2 = new Phone3();
// 不是先调用的先执行,其实是锁的问题
new Thread(() -> {
phone.sendSms();
}, "a").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "b").start();
}
}
class Phone3 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendSms");
}
public static synchronized void call() {
System.out.println("call");
}
}
package com.atlinxi.gulimall.springdemo.juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 7. sendSms()是静态同步方法,call()是普通同步方法,同一个对象,先输出call(),
* 因为不是同一把锁,静态的锁是Class,普通的锁是对象
* 8. sendSms()是静态同步方法,call()是普通同步方法,同一个对象,先输出call(),
* 原因依然是两把锁
*/
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
Phone4 phone2 = new Phone4();
// 不是先调用的先执行,其实是锁的问题
new Thread(() -> {
phone.sendSms();
}, "a").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "b").start();
}
}
class Phone4 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendSms");
}
public synchronized void call() {
System.out.println("call");
}
}
集合类不安全
package com.atlinxi.gulimall.springdemo.juc.unsafe;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
// java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
public static void main(String[] args) {
// 并发下 ArrayList 不安全的
/**
* 解决方案:
* 1. Vector类线程安全
* 2. Collections.synchronizedList(new ArrayList<>())
* 3. juc解决方案 new CopyOnWriteArrayList<>()
*
*
* Vector的add()是通过synchronized修饰的,只要synchronized修饰的方法效率相对较低
*/
// CopyOnWrite 写入时复制
// 简称 COW 计算机设计领域的一种优化策略;是提高效率的一种方式
// 多个线程调用的时候,list(资源)读取的时候是固定的,
// 写入的时候可能存在一种覆盖操作(后来的线程把之前的线程覆盖掉),
// 写入的时候避免覆盖,造成数据问题!(写入的时候复制一份)
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}).start();
}
}
}
package com.atlinxi.gulimall.springdemo.juc.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
// new HashSet<>() java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
/**
* 解决方案:
* 1. Collections.synchronizedSet(new HashSet<>());
* 2. juc解决方案,new CopyOnWriteArraySet<>()
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
package com.atlinxi.gulimall.springdemo.juc.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapTest {
public static void main(String[] args) {
// map是这样用的么? 不是,工作中不用HashMap
// 默认等价于什么? new HashMap<>(16,0.75) 初始化容量,加载因子
Map<String, String> hashMap = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
hashMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(hashMap);
},String.valueOf(i)).start();
}
}
}
CopyOnWriterArrayList原理
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
- 线程安全的,多线程环境下可以直接使用,无需加锁;
- 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
- 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。
从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁;
- 从原数组中拷贝出新数组;
- 在新数组上进行操作,并把新数组赋值给数组容器;
- 解锁
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到,代码如下:
private transient volatile Object[] array;
整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。
优点
读操作(不加锁)性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。
缺点
-
一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份。数据量大时,对内存压力较大,可能会引起频繁GC;
-
二是无法保证实时性(只保证最终一致性,不保证实时一致性),因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
CopyOnWriteArrayList 不适用于写操作过多、实时性要求高的场景,需要根据具体的应用场景进行权衡和选择。
如果出现数据不一致的情况,可以考虑使用其他线程安全的集合类,如Collections.synchronizedList。
Collections.synchronizedList在执行add()等方法的时候是加了synchronized关键字的,但是iterator()却没有加。所以在使用的时候需要加上synchronized。
在并发的场景下,如果并发强度较小,性能要求不苛刻,且锁可控的场景下,可使用Collections.synchronizedList,既保证了数据一致又保证了线程安全,性能够用;
Callable
与Runnable相比
- 可以有返回值
- 可以抛出异常
- 方法不同,run()/call()
package com.atlinxi.gulimall.springdemo.juc.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<V>()).start();
// new Thread(new FutureTask<V>(Callable)).start();
MyThread myThread = new MyThread();
FutureTask<String> futureTask = new FutureTask<>(myThread);
new Thread(futureTask,"a").start();
new Thread(futureTask,"b").start(); // 结果会被缓存,两个线程执行call()的话,只会打印一个call()
String s = futureTask.get(); // 获取Callable的返回结果
// get()可能会产生阻塞,或者使用异步通信来处理
System.out.println(s);
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("call()");
return "123";
}
}
辅助工具类
package com.atlinxi.gulimall.springdemo.juc.add;
import java.util.concurrent.CountDownLatch;
// 减法计数器 等待计数器归零再向下执行
// 场景是 必须要执行任务的时候,再使用
// 原理,每次有线程调用countDown()数量-1,计数器变为0,await()就会被唤醒,继续执行
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"go out");
countDownLatch.countDown(); // -1
},String.valueOf(i)).start();
}
countDownLatch.await(); // 等待计数器归零,然后再向下执行
System.out.println("close door");
// 不加 countDownLatch.await(); 的结果
// close door的位置是发生变化的
// 0go out
// 3go out
// 2go out
// 1go out
// close door
// 5go out
// 4go out
// 加 countDownLatch.await(); 的结果
// close door 永远是最后一个(等待计数器归零)
// 0go out
// 5go out
// 4go out
// 2go out
// 3go out
// 1go out
// close door
}
}
package com.atlinxi.gulimall.springdemo.juc.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
// 加法计数器
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐七颗龙珠召唤神龙
*
* 如果这儿是8,而下面只有7个线程,程序就会停在那儿,
* 因为永远都不能集齐八颗龙珠
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功");
});
for (int i = 0; i < 7; i++) {
// lambda不能操作到 i
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + temp);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
// Thread-00
// Thread-55
// Thread-44
// Thread-33
// Thread-22
// Thread-11
// Thread-66
// 召唤神龙成功
}
}
package com.atlinxi.gulimall.springdemo.juc.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
//
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量(停车位),
// 场景是并发限流,控制最大的线程数
// 多个共享资源互斥的使用
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// acquire() 得到
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); // release() 释放
}
},String.valueOf(i)).start();
}
}
// 0抢到车位
// 2抢到车位
// 1抢到车位
// 0离开车位
// 1离开车位
// 3抢到车位
// 4抢到车位
// 2离开车位
// 5抢到车位
// 4离开车位
// 3离开车位
// 5离开车位
}
ReadWriteLock
ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有作者。 write lock是独家的。
读可以被多线程同时读,写的时候只能有一个线程去写。
package com.demo.juc.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* ReadWriteLock
*
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
//
// for (int i = 0; i < 5; i++) {
// final int temp = i;
// new Thread(() -> {
// myCache.put(temp + "", temp);
//
// }, String.valueOf(i)).start();
// }
//
//
// for (int i = 0; i < 5; i++) {
// final int temp = i;
// new Thread(() -> {
// myCache.get(temp + "");
//
// }, String.valueOf(i)).start();
// }
//
// }
/**
* 不使用读写锁的部分结果
* 0写入0
* 4写入4
* 4写入ok4
*
* 应该是0写入,0写入ok这样,上面的话就是线程不安全了
*/
MyCacheLock myCache = new MyCacheLock();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp);
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
/**
*
* 可以看到,加入读写锁后,写线程是每一个线程单独执行,是线程安全的
* 读线程是线程不安全的
*
*
* 0写入0
* 0写入ok0
* 3写入3
* 3写入ok3
* 1写入1
* 1写入ok1
* 2写入2
* 2写入ok2
* 4写入4
* 4写入ok4
* 0读取0
* 0读取ok0
* 4读取4
* 1读取1
* 3读取3
* 3读取ok3
* 2读取2
* 4读取ok4
* 2读取ok2
* 1读取ok1
*
* Process finished with exit code 0
*/
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 存,写
public void put(String key, Object val) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, val);
System.out.println(Thread.currentThread().getName() + "写入ok" + key);
}
// 取,读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok" + key);
}
}
class MyCacheLock {
private volatile Map<String, Object> map = new HashMap<>();
// 读写锁,更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 存,写,写入的时候,只希望同时只有一个线程写
public void put(String key, Object val) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, val);
System.out.println(Thread.currentThread().getName() + "写入ok" + key);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
// 取,读,所有人都可以读
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok" + key);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
ReadWriteLock 第二种解释
读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
总结来说,读写锁的特点是:读读不互斥、读写互斥、写写互斥。
读写锁使用
在 Java 语言中,读写锁是使用 ReentrantReadWriteLock 类来实现的,其中:
-
ReentrantReadWriteLock.ReadLock 表示读锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。
-
ReentrantReadWriteLock.WriteLock 表示写锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。
package com.atlinxi.gulimall.springdemo.juc.rw;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo2 {
public static void main(String[] args) {
// 1. 读读不互斥
// 多个线程可以同时获取到读锁,称之为读读不互斥
// 创建写锁
final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
// 不能这么创建读写锁,明显不是一个对象了,怎么可能互斥呢,被自己蠢哭了
// final ReentrantReadWriteLock.ReadLock readLock = new ReentrantReadWriteLock().readLock();
// final ReentrantReadWriteLock.WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
// for (int i = 0; i < 5; i++) {
// new Thread(() -> {
// try {
// readLock.lock();
//
// System.out.println(Thread.currentThread().getName() + "=》得到锁");
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } finally {
// System.out.println(Thread.currentThread().getName() + "=》释放锁");
// readLock.unlock();
// }
//
// }).start();
// }
/**
* Thread-3=》得到锁
* Thread-2=》得到锁
* Thread-0=》得到锁
* Thread-4=》得到锁
* Thread-1=》得到锁
* Thread-2=》释放锁
* Thread-3=》释放锁
* Thread-1=》释放锁
* Thread-4=》释放锁
* Thread-0=》释放锁
*
*
*/
// 读锁和写锁同时使用是互斥的(也就是不能同时获得),这称之为读写互斥
// for (int i = 0; i < 5; i++) {
// new Thread(() -> {
// try {
// readLock.lock();
//
// System.out.println(Thread.currentThread().getName() + "=》读锁得到锁");
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } finally {
// System.out.println(Thread.currentThread().getName() + "=》读锁释放锁");
// readLock.unlock();
// }
//
// }).start();
// }
//
//
//
// for (int i = 0; i < 5; i++) {
// new Thread(() -> {
// try {
// writeLock.lock();
//
// System.out.println(Thread.currentThread().getName() + "=》写锁得到锁");
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } finally {
// System.out.println(Thread.currentThread().getName() + "=》写锁释放锁");
// writeLock.unlock();
// }
//
// }).start();
// }
/**
* Thread-0=》读锁得到锁
* Thread-1=》读锁得到锁
* Thread-4=》读锁得到锁
* Thread-0=》读锁释放锁
* Thread-1=》读锁释放锁
* Thread-4=》读锁释放锁
* Thread-5=》写锁得到锁
* Thread-5=》写锁释放锁
* Thread-8=》写锁得到锁
* Thread-8=》写锁释放锁
* Thread-9=》写锁得到锁
* Thread-9=》写锁释放锁
* Thread-2=》读锁得到锁
* Thread-3=》读锁得到锁
* Thread-3=》读锁释放锁
* Thread-2=》读锁释放锁
* Thread-6=》写锁得到锁
* Thread-6=》写锁释放锁
* Thread-7=》写锁得到锁
* Thread-7=》写锁释放锁
*
* Process finished with exit code 0
*
*
*
*
*
*
*/
// 多个线程同时使用写锁也是互斥的,这称之为写写互斥
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "=》写1锁得到锁");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "=》写1锁释放锁");
writeLock.unlock();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "=》写2锁得到锁");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "=》写2锁释放锁");
writeLock.unlock();
}
}).start();
}
/**
*
* Thread-0=》写1锁得到锁
* Thread-0=》写1锁释放锁
* Thread-1=》写1锁得到锁
* Thread-1=》写1锁释放锁
* Thread-4=》写1锁得到锁
* Thread-4=》写1锁释放锁
* Thread-2=》写1锁得到锁
* Thread-2=》写1锁释放锁
* Thread-3=》写1锁得到锁
* Thread-3=》写1锁释放锁
* Thread-5=》写2锁得到锁
* Thread-5=》写2锁释放锁
* Thread-8=》写2锁得到锁
* Thread-8=》写2锁释放锁
* Thread-9=》写2锁得到锁
* Thread-9=》写2锁释放锁
* Thread-6=》写2锁得到锁
* Thread-6=》写2锁释放锁
* Thread-7=》写2锁得到锁
* Thread-7=》写2锁释放锁
*
* Process finished with exit code 0
*
* Process finished with exit code 0
*
*
*
*/
}
}
优点分析
-
提高了程序执行性能:多个读锁可以同时执行,相比于普通锁在任何情况下都要排队执行来说,读写锁提高了程序的执行性能。
-
避免读到临时数据:读锁和写锁是互斥排队执行的,这样可以保证了读取操作不会读到写了一半的临时数据。
读写锁适合多读少写的业务场景,此时读写锁的优势最大。
阻塞队列
队列写入元素的时候:如果队列满了,就必须阻塞等待
队列获取元素的时候:如果队列是空的,必须阻塞等待生产
由以上类结构图可以看出,BlockingQueue不是新的东西
使用阻塞队列的场景:多线程并发处理,线程池!
-
SynchronousQueue:是一个特殊的阻塞队列,它并不保存任何元素,每次插入操作必须等待另一个线程的移除操作,每次移除操作必须等待另一个线程的插入操作,因此它可以用于两个线程之间进行数据交换。
-
LinkedBlockingQueue:是一个无界(元素无上限,Integer.Max_Value)的阻塞队列,底层是由链表实现的,可以存储任意数量的元素。
-
ArrayBlockingQueue:是一个有界的阻塞队列,底层是由数组实现的,
-
DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
-
PriorityBlockingQueue:是一个支持优先级的阻塞队列,底层是由堆实现的,可以根据元素的优先级顺序进行排序。当添加元素时,会根据元素的优先级自动排序,获取元素时会返回当前队列中优先级最高的元素。当队列为空时,获取元素的操作将会阻塞,直到队列中有元素可用。
ArrayBlockingQueue 阻塞队列
package com.demo.juc.bq;
import java.util.Collection;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class TestBq {
public static void main(String[] args) throws InterruptedException {
// List Set Collection
// BlockingQueue 不是新的东西
// test1();
// test2();
// test3();
test4();
}
/**
* 队列满了还添加/队列空了还取出
*
* 抛出异常
*/
public static void test1(){
// 队列的大小
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
// 都是t
System.out.println(arrayBlockingQueue.add("a"));
System.out.println(arrayBlockingQueue.add("b"));
System.out.println(arrayBlockingQueue.add("c"));
// java.lang.IllegalStateException: Queue full 抛出异常!
// System.out.println(arrayBlockingQueue.add("d"));
// 先进先出 abc
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
// java.util.NoSuchElementException
// System.out.println(arrayBlockingQueue.remove());
}
/**
* 队列满了还添加,会返回false
*
* 队列空了还取出,会返回null
*
* 两种都不抛出异常,有返回值
*/
public static void test2(){
ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
// t t t f
System.out.println(arrayBlockingQueue.offer("a"));
System.out.println(arrayBlockingQueue.offer("b"));
System.out.println(arrayBlockingQueue.offer("c"));
System.out.println(arrayBlockingQueue.offer("d"));
// 查看队首元素是谁,a
// 两个方法都是同样的作用
System.out.println(arrayBlockingQueue.element());
System.out.println(arrayBlockingQueue.peek());
// a b c null
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
}
/**
* 等待,阻塞(一直)
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
// 队列没有位置了,就会一直阻塞
// blockingQueue.put("d");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
// 没有这个元素,一直阻塞
System.out.println(blockingQueue.take());
}
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
// 等待2s,如果还没有位置就超时退出
// blockingQueue.offer("d",2, TimeUnit.SECONDS);
blockingQueue.poll();
blockingQueue.poll();
blockingQueue.poll();
// 等待2s,如果还没有元素可以取出就退出
blockingQueue.poll(2,TimeUnit.SECONDS);
}
}
SynchronousQueue 同步队列
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素。
相当于该队列容量只有1。
package com.demo.juc.bq;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* 同步队列
*
* 和其他的BlockingQueue不一样,SynchronousQueue不存储元素
* put了一个元素,必须先从里面take出来,否则不能再put进去值
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>(); // 同步队列
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + "put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + "put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName() + "put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
/**
* t1put 1
* t21
* t1put 2
* t22
* t1put 3
* t23
*
*
* 如果是阻塞队列就是全放进去,然后再全取出来
*/
}
}
线程池
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
线程池常问问题:三大方法、7大参数、4种拒绝策略
程序的运行,本质:占用系统的资源!优化资源的使用!
线程池、连接池、内存池、对象池。。。。。。创建和销毁十分浪费资源。
池化技术:开启和关闭线程是非常消耗资源的,我们事先准备好一些资源,有人要用,就来拿,用完之后再还回去。
线程池的好处:
-
降低资源的消耗
通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗。 -
提高响应的速度
重复利用线程池中线程,不需要每次都创建(使用线程池就能很好地避免频繁创建和销毁。) -
方便管理
线程对服务器来说是稀缺资源,如果无限制的去创建,肯定会导致系统的访问速度下降。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
总结
:线程复用、可以控制最大并发、管理线程。
这就类似于公司只要有一个新任务就招一个员工
,最终会将公司的资源耗尽,这和我们的业务一样,最终分配给我们的堆空间或者栈空间,内存都是有限的,
如果在我们高并发系统里,比如这个业务非常大,要进行100W的异步任务查询,现在有100W个请求进来,假设一个请求就要开启10个异步任务,直接就会new 1000W个Thread,肯定会导致我们资源耗尽而最终的系统崩溃
我们以后在业务代码中,继承Thread类,实现Runnable接口,实现Callable接口,以上三种启动线程的方式都不用
,将所有的多线程异步任务都交给线程池执行
这就好比我们公司中50个员工,有任务的话就交给其中一个,如果都有活儿,就等处理完了再处理这个任务,这样做的好处就是我们达到了资源控制
我们只有这50个人,消耗的资源也就是这50个人的资源
线程池api
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。
真正的线程池接口是 java.util.concurrent.ExecutorService
。 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂
,生成一些常用的线程池。官方建议使用Executors(但阿里不推荐,听阿里的)
工程类来创建线程池对象。 Executors
类中有个创建线程池的方法如下:
// ExecurtorService:真正的线程池接口。常见子类ThreadPoolExecutors
// 执行线程任务,没返回值
void execute(Runnable command);
// 执行线程任务,有返回值
<T> Future<T> submit(Callable<T> task);
// 关闭连接池
void shutdown();
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
三大方法
package com.demo.juc.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 用Executors或new ThreadPoolExecutor()创建线程池实际上都是实现ExecutorService
// newFixedThreadPool 一个固定数量的线程池
// 这个线程池不应该是每一个异步任务都创建一个线程池
// 而是应该整个系统一个线程池,大家都把自己的任务交给这个池里执行
// 当前系统中池只有一两个,可能有核心业务的或者非核心业务的
// 每个异步任务,直接提交给线程池,让它自己去执行
// Executors 工具类、3大方法
// 使用了线程池之后,使用线程池来创建线程
public class Demo01 {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单个线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定的线程池大小
ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的线程池
try {
for (int i = 0; i < 10; i++) {
// submit可以传入Runnable接口和Callable接口,可以获取到任务的返回值
// execute 可以只能传入Runnable接口,不能获取到任务的返回值
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
/**
*
* 以上三个不同的线程池操作线程的结果分别是
*
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
* pool-1-thread-1ok
*
* Process finished with exit code 0
* 同一个线程
*
*
*
*
*
*
*pool-1-thread-1ok
* pool-1-thread-3ok
* pool-1-thread-2ok
* pool-1-thread-5ok
* pool-1-thread-4ok
* pool-1-thread-3ok
* pool-1-thread-1ok
* pool-1-thread-4ok
* pool-1-thread-5ok
* pool-1-thread-2ok
*
* Process finished with exit code 0
*
*
*
*
*
*
* newCachedThreadPool()
*
* 如果同时执行10个任务,它创建的就有10个线程
*
* 测试如果是100个任务,它创建的是三十多个线程(这个数量没有逻辑)
*
*
*/
}
}
源码分析
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 发现三大方法的本质是ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小【一直存在,除非设置allowCoreThreadTimeOut】
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
/** 工作顺序:
*
* 1. 线程池创建,准备好 core 数量的核心线程,准备接受任务
* 2. 新的任务进来,用 core 准备好的空闲线程执行。
* 3. core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
* 4. 阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
* max 都执行好了。Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁。
* 最终保持到 core 大小
*
* new LinkedBlockingQueue<>();默认是Integer的最大值。内存不够
* 5. 如果线程数开到了 max 的数量,还有新任务进来,就会使用 reject 指定的拒绝策略进行处理
* 6. 所有的线程创建都是由指定的 factory 创建的
*
*
* 面试题
*
* 一个线程池,core 7,max 20,queue 50,100并发进来怎么分配的?
*
* 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
*/
七大参数和四种拒绝策略
package com.atlinxi.gulimall.springdemo.juc.pool;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
拒绝策略,我觉得目的可能是避免发生拒绝的,如果出现了拒绝,那就是服务器承载不了那么多的线程并发,需要减少并发或者提升服务器的性能了,
所以它主要目的应该是临时处理和记录,以便后续的处理,一般来说肯定是不想抛弃该任务而做的补偿措施。
线程池工作中,如果任务量很大,超过系统实际承载能力时,如果不予理睬,接着可能系统就崩溃了,
所以jdk内置提供了线程池的4种拒绝策略,合理的解决这种问题。
*/
/**
* ThreadPoolExecutor.CallerRunsPolicy() 哪来的去哪里
* 不启动线程池线程,直接调用run方法,这样的话实际上就是同步调用了。
* 它会让调用线程池的线程自己来运行任务。
* 当任务被拒绝时,由提交任务的线程来执行该任务。
* 第一,新提交的任务不会被抛弃,不会造成业务损失。
第二,由于谁提交任务谁负责任务,提交任务的路线必须负责任务,
执行任务需要时间,在此期间,提交任务的路线被占有,不提交新任务,任务提交速度变慢,相当于负面反馈。
在此期间,线程池的线程也可以充分利用这个时间执行一部分任务,腾出一定的空间,相当于给线程池一定的缓冲期。
*
* ThreadPoolExecutor.AbortPolicy() 超过最大线程直接报错;
* 丢弃任务并抛出RejectedExecutionException异常,默认策略
*
* ThreadPoolExecutor.DiscardPolicy() 队列满了,丢掉任务,不会抛出异常
*
* ThreadPoolExecutor.DiscardOldestPolicy() 当任务被拒绝时,丢弃队列中最老(最早)的一个任务,并尝试再次提交被拒绝的任务。
*
* 同时jdk也为我们提供了RejectedExecutionHandler接口,可以根据实际应用场景去自定义策略
实际开发中我们会使用自定义拒绝策略,因为在自定义拒绝策略灵活好控制,
可以在自定义拒绝策略中发送一条通知给消息中心,让消息中心发送告警信息给开发者,
这样可以实时的监控线程池的运行状况,并能及时发现和排查问题。
*/
public class PoolTest {
public static void main(String[] args) {
// 自定义线程池,工作中创建线程池只会用这种方式
// 最大线程到底该如何定义
// 1. cpu密集型 如果服务器是12核的,就能支持12条线程同时执行,
// 几核就定义为几,可以保证cpu效率最高
// 获取cpu的核数
// 这就是cpu密集型的逻辑
sout(Runtime.getRuntime().availableProcessors());
// 2. io密集型。判断程序中十分耗io的线程,大于这个数即可,通常会设置两倍,
// 除了这15个,还会剩下15个执行别的线程,就不会造成系统的阻塞
// 程序中有15个大型任务,io十分占用资源
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());// 最大线程数和队列都满了,但是还有人进来,不处理这个线程,并抛出异常
try {
// 最大承载:队列 + 最大线程数
for (int i = 0; i < 9; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
/**
* 线程池的线程使用顺序
*
* 核心线程数 -》 队列 -》 最大线程数 -》 拒绝策略
*
* 6个任务以内2个线程执行
* 6个任务3个线程执行
* 7个任务4个线程执行
* 8个任务5个线程执行
* 9个任务由于超过了最大线程数,我们的拒绝策略又是AbortPolicy(),所以报java.util.concurrent.RejectedExecutionException
*
*/
}
}
自定义拒绝策略
dubbo
直接继承的 AbortPolicy
- 输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在
- 输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草,这个可以参考
- 继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性
// 通过实现RejectedExecutionHandler接口,自定义一个拒绝策略类,重写它的rejectedExecution()方法:
public class CustomRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + "被拒绝了,执行入库操作,之后手动补偿");
}
}
如何判断线程池中的任务是否执行完成?
判断线程池任务执行是否完成,有以下几种方法:
// isTerminated()
// 线程池提供了一个原生函数isTerminated()来判断线程池中的任务是否全部完成。
// 当线程池中所有任务都执行完成之后,线程池就会进入终止状态,此时调用isTerminated()方法返回的结果就是true
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
// 提交任务
executor.execute(() -> {
// 任务代码
});
// 关闭线程池
executor.shutdown();
// 判断任务是否完成
if (executor.isTerminated()) {
System.out.println("所有任务已完成");
// getCompletedTaskCount():我们可以通过判断线程池中计划执行任务和已完成任务,
// 来判断线程池是否已经执行完成。如果相等就代表已经执行完成了
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
// 提交任务
executor.execute(() -> {
// 任务代码
});
// 判断任务是否完成
if (executor.getTaskCount() == executor.getCompletedTaskCount()) {
System.out.println("所有任务已完成");
}
// FutureTask():当你提交一个Callable或者RRunnable任务到达线程池的时候,
// 你可以将它包装到一个FutureTask对象中。然后调用FutureTask的get()方法来获取任务的结果,
// 如果任务没有完成,get()方法将会阻塞,直到任务完成并且结果可用。所以,只要get()方法有返回结果,就直到任务是否完成
// 创建一个 Callable 任务
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 执行任务,返回结果
return doSomeWork();
}
};
// 创建一个 FutureTask 对象来包装任务
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 提交任务到线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.execute(futureTask);
// 获取任务结果
try {
Integer result = futureTask.get(); // 这里会阻塞,直到任务完成
System.out.println("任务已完成,结果是:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
线程池的执行原理
Java线程池的执行原理主要包括以下几个步骤:
-
线程池创建:通过ThreadPoolExecutor类创建线程池,并设置核心线程数、最大线程数、队列容量、保持存活时间等参数。
-
任务提交:调用execute或submit方法提交任务。
-
任务执行:如果当前运行的线程数小于核心线程数,则创建一个新线程来执行任务;如果大于或等于核心线程数且任务队列未满,则将任务加入到任务队列中等待执行;如果队列也满了,并且当前运行的线程数小于最大线程数,则创建新的非核心线程来执行任务;如果达到最大线程数,则拒绝策略处理无法执行的任务。
-
任务执行结束:执行完毕的线程会回到线程池中等待下一个任务,而非核心线程在保持存活时间内会保持存活。
ForkJoin
ForkJoin在jdk1.7,并执行任务!提高效率,大数据量(上亿)!
大数据:Map Reduce(把大任务拆分为小任务)
ForkJoin特点:工作窃取
a线程执行到一半儿,b线程已经执行完毕,此时,b线程会把a线程的任务偷过来执行。
这个里面维护的是一个双端队列,所以从上面可以执行,从下面也可以执行。
package com.atlinxi.gulimall.springdemo.juc.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* 求和计算的任务
*
* 如何使用forkjoin
*
* 1. forkjoinpool 通过它来执行
* 2. 计算任务 forkjoinpool.execute(ForkJoinTask task)
* 3. 计算类要继承RecursiveTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
// 临界值,可以通过调整temp的值来进行调优
private Long temp = 10000L;
public static void main(String[] args) {
int sum = 0;
// 10_0000_0000 代表 10亿
for (int i = 0; i < 10_0000_0000; i++) {
sum += i;
}
System.out.println(sum);
}
// 计算方法
@Override
protected Long compute() {
if ((end-start)<temp){
Long sum = 0L;
for (Long i = start; i < end; i++) {
sum += i;
}
return sum;
}else { // forkjoin
long middle = (start + end) / 2; // 中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
task2.fork();
return task1.join() + task2.join();
}
}
}
package com.atlinxi.gulimall.springdemo.juc.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
/**
* 同一个任务,别人效率高你几十倍
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// sum=499999999500000000时间:9775
// test1();
// sum=499934463999828390时间:4985
// test2();
// sum=500000000500000000时间:355
// test3();
}
// 普通程序员
public static void test1(){
long start = System.currentTimeMillis();
Long sum = 0L;
for (Long i = 0L; i < 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
// 会使用ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
// execute(task) 执行任务,没有返回值
// submit(task) 提交任务,有返回值
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
// 使用stream并行流
public static void test3(){
long start = System.currentTimeMillis();
// Stream并行流
// range() 和 rangeClosed() 的区别,就是是否闭合
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
}
异步回调 CompletableFuture
Future设计的初衷:对将来的某个事件的结果进行建模
package com.atlinxi.gulimall.springdemo.juc.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 异步调用:Ajax
*
* 异步执行
* 成功回调
* 失败回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发起一个请求
// 没有返回值的 runAsync 异步回调
// CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName()+"runAsync=>Void");
// });
//
// System.out.println("2222");
//
// future.get(); // 获取阻塞执行结果
// 2222
//ForkJoinPool.commonPool-worker-1runAsync=>Void
// 有返回值的异步回调
// ajax,成功和失败的回调
// 失败返回的是错误信息
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer");
System.out.println(10/0);
return 1024;
});
//whenComplete() 正常执行的回调函数,t 正常的返回结果,u 没错误是null,有错误是错误信息
// exceptionally() 发生异常的回调函数,可以获取异常信息并返回指定的值
Integer integer = future.whenComplete((t, u) -> {
System.out.println(t);
System.out.println(u);
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 233;
}).get();
System.out.println(integer);
}
}
部分内容转载自:
https://blog.csdn.net/Saintmm/article/details/121092830
https://www.bilibili.com/video/BV1B7411L7tE/?p=8&spm_id_from=pageDriver&vd_source=64c73c596c59837e620fed47fa27ada7
https://blog.csdn.net/sufu1065/article/details/124722777
https://baijiahao.baidu.com/s?id=1763862719437069832&wfr=spider&for=pc
https://blog.csdn.net/qq_61777435/article/details/133754333
https://blog.51cto.com/u_16099186/8790430
https://www.cnblogs.com/pony1223/p/9509205.html
无论你在背后喊刘怡婷或房思琪,我都会回头的。
房思琪的初恋乐园
林奕含