JUC并发编程
JUC简介
java.util.concurrent在并发编程中使用的工具类
线程和进程
- 进程:一个程序,QQ.exe、Music.exe一个进程可以包含多个线程,至少包含一个!
JAVA程序默认有几个线程?两个 main 和GC线程
- 线程:一个线程只属于一个进程,线程是程序最小的执行单位
并发和并行
- 并发:指的是CPU执行多个任务,但不会同一时间去完成
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
- 并行: 指的是CPU可以支持同一时间处理多个任务
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发编程的本质:充分利用CPU的资源
线程状态
public enum State {
//创建 new Tread
NEW,
//运行 Tread.start()
RUNNABLE,
//遇见synchronized 线程的状态
BLOCKED,
//当前线程调用wait(),join()以及LockSupport.park() 会是当前线程进入等待状态,需要其它线程使用notify()或notifyAll() 唤醒。
WAITING,
//在 Thread.sleep(long)这种情况下会发生这个状态
TIMED_WAITING,
//线程完全结束
TERMINATED;
}
wait和sleep的区别
- wait 来自Object类,sleep来自Thread类
- wait会释放锁,sleep不会释放锁
- wait作用在同步代码块中,sleep可以在任何地方使用
线程不安全代码
package Safe;
public class Test1 {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "小红").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "小蓝").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}, "小紫").start();
}
}
class Ticket {
private Integer ticket = 50;
public void sale() {
if (ticket != 0) {
System.out.println("当前的售票员" + Thread.currentThread().getName() + "卖出了一张票" + (ticket--) + "剩余" + ticket + "张票");
}
}
}
线程并发发现票的顺序有问题
Synchronized
在操作库存的操作上加上锁
public synchronized void sale() {
if (ticket != 0) {
System.out.println("当前的售票员" + Thread.currentThread().getName() + "卖出了一张票" + (ticket--) + "剩余" + ticket + "张票");
}
}
发现线程并发顺序也是一样的
ReentrantLock
class Ticket {
private Integer ticket = 50;
Lock lock = new ReentrantLock();
public void sale() {
//上锁
lock.lock();
try {
if (ticket != 0) {
System.out.println("当前的售票员" + Thread.currentThread().getName() + "卖出了一张票" + (ticket--) + "剩余" + ticket + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
这里我们发现默认创建的ReentrantLock使用的是非公平锁
- 非公平锁:指的是多线程争抢一个资源的时候并不会根据线程的先来后到原则获取锁,可以插队。
- 公平锁: 有序的获取锁。
Synchronized和ReentrantLock的区别
- Synchronized是Java内置的关键字,ReentrantLock是一个类
- Synchronized无法判断获取锁的状态,ReentrantLock可以判断是否获取到锁
- Synchronized会自动释放锁,ReentrantLock必须手都释放锁,不释放锁会死锁。
- Synchronized为非公平锁,ReentrantLock默认为非公平锁,也可以修改为公平锁。
- Synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;eentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时进行终端。
传统线程通信wait和notify线程通信
package Safe;
public class PC {
public static void main(String[] args) {
Data data = new Data();
//男生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i <20 ; i++) {
data.manWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//女生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i <20 ; i++) {
data.woManWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
//男生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i <20 ; i++) {
data.manWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"C").start();
//女生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i <20 ; i++) {
data.woManWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"D").start();
}
}
class Data {
//默认从男生开始工作
private String name = "男生";
//男生开始工作
public synchronized void manWokr() throws InterruptedException {
while (name.equals("女生")) {
this.wait();
}
System.out.println(Thread.currentThread().getName()+"上完班了,我要下班了。给女生打电话让她上班");
this.name = "女生";
//通知所有女生开始上班
this.notifyAll();
}
//女生开始工作
public synchronized void woManWokr() throws InterruptedException {
while (name.equals("男生")) {
this.wait();
}
System.out.println(Thread.currentThread().getName()+"上完班了,我要下班了。给男生打电话让他上班");
this.name = "男生";
//通知所有男生开始上班
this.notifyAll();
}
}
发现线程相互通信交替执行
Lock实现线程通信
package Safe;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test2 {
public static void main(String[] args) {
Data2 data = new Data2();
//男生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
data.manWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
//女生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
data.woManWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
//男生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
data.manWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
//女生开始准备上班
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
data.woManWokr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "D").start();
}
}
class Data2 {
//默认从男生开始工作
private String name = "男生";
private Lock locks = new ReentrantLock();
private Condition condition = locks.newCondition();
//男生开始工作
public void manWokr() throws InterruptedException {
locks.lock();
try {
while (name.equals("女生")) {
//释放锁
condition.await();
}
System.out.println(Thread.currentThread().getName() + "上完班了,我要下班了。给女生打电话让她上班");
this.name = "女生";
//唤醒其它线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
locks.unlock();
}
}
//女生开始工作
public void woManWokr() throws InterruptedException {
locks.lock();
try {
while (name.equals("男生")) {
//释放锁
condition.await();
}
System.out.println(Thread.currentThread().getName() + "上完班了,我要下班了。给男生打电话让他上班");
this.name = "男生";
//通知所有男生开始上班
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
locks.unlock();
}
}
}
相比于传统的Object实现线程通信,Condition 优势在于可以精确唤醒某个线程
package Safe;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class C {
public static void main(String[] args) {
DataDemo dataDemo = new DataDemo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
dataDemo.a();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
dataDemo.b();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
dataDemo.c();
}
}).start();
}
}
class DataDemo {
private Lock lock = new ReentrantLock();
//创建三个线程相互通信的Condition (线程相互通信的关键)
Condition A = lock.newCondition();
Condition B = lock.newCondition();
Condition C = lock.newCondition();
//默认从1开始
private Integer integer = new Integer(1);
public void a() {
lock.lock();
try {
while (integer != 1) {
A.await();
}
System.out.println("线程A开始执行");
integer++;
B.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void b() {
lock.lock();
try {
while (integer != 2) {
B.await();
}
System.out.println("线程b开始执行");
integer++;
C.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void c() {
lock.lock();
try {
while (integer != 3) {
C.await();
}
System.out.println("线程c开始执行");
integer = 1;
A.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
这里我们发现线程之间正在有序的执行
8锁问题(百度)
核心:synchronized 只会锁两样东西,一个是对象,一个是Class,修饰类的是锁Class,修饰对象的是锁当前对象。
集合类不安全
CopyOnWriteArrayList
CopyOnWriteArrayList替换Vector,CopyOnWriteArrayList (写入时复制)底层采用lock解决并发问题,性能比Vector要高
public static void main(String[] args) {
//List<String> list = new Vector<>();
List<String> list = new ArrayList<>();
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
for (int i = 0; i < 10; i++) {
new Thread(() -> copyOnWriteArrayList.add(UUID.randomUUID().toString().substring(0, 5))).start();
}
}
CopyOnWriteArraySet
同上
Set<String> data = new CopyOnWriteArraySet();
Callable使用
相比于Runable的不同
- Callablle有返回值
- Callablle可以抛出异常
- Callablle的方法名是Call,Runable的方法名是run
package Safe;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test3 {
public static void main(String[] args) throws Exception {
Brother brother = new Brother();
FutureTask futureTask = new FutureTask(brother);
new Thread(futureTask).start();
//get可能会阻塞,因为需要等待线程处理完结果
Integer o = (Integer) futureTask.get();
System.out.println(o);
}
}
class Brother implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1024;
}
}
常用辅助类
CountDownLatch
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
public static void main(String[] args) throws InterruptedException {
//班长管理六个学生
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"执行玩业务了");
//告诉班长完成了
countDownLatch.countDown();
},"线程"+i).start();
}
//等待所有学生都执行完
countDownLatch.await();
System.out.println("班长开始干活了");
}
CyclicBarrier
CyclicBarrier 类似CountDownLatch 只不过CountDownLatch是满足到指定数量才会工作。
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("我已经集齐七个龙珠,开始召唤神龙");
});
for (int i = 0; i <7 ; i++) {
//lambda表达式内部访问外部变量需要将属性定义为常量
int finalI = i;
new Thread(()->{
System.out.println("集齐了第"+ finalI +"个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
Semaphore 信号量
常用于限制流量
public static void main(String[] args) throws Exception {
//限流:默认六个停车位
Semaphore semaphore = new Semaphore(6);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
//获取车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "获得了车位,去买菜去。。。。。");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放车位
System.out.println("买完菜了开车走了");
semaphore.release();
}
}).start();
}
}
ReadWriteLock
ReadWriteLock是包含共享锁和排他锁的数据同步解决方案,主要是为了提高并发情况的效率。
那么什么时候情况下会加锁?
- 读 读 (没锁)
- 读 写 (有锁)
- 写 写 (有锁)
代码测试
class ReadWriteLockDemo {
//互斥锁
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//共享锁
static Lock readLock = readWriteLock.readLock();
//独占锁
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock) {
try {
lock.lock();
TimeUnit.SECONDS.sleep(2);
System.out.println("我在读");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void write(Lock lock, int data) {
try {
lock.lock();
TimeUnit.SECONDS.sleep(2);
value = data;
System.out.println("我在写");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
我们这里采用互斥锁实现读写分离
public static void main(String[] args) {
//采用ReentrantLock实现数据同步
//读
for (int i = 0; i < 5; i++) {
new Thread(() -> ReadWriteLockDemo.read(ReadWriteLockDemo.lock)).start();
}
//写
for (int i = 0; i < 3; i++) {
new Thread(() -> ReadWriteLockDemo.write(ReadWriteLockDemo.lock, new Random().nextInt())).start();
}
}
结论:发现采用互斥锁发现总共耗时(读的到时间 乘以 线程)+(写的时间*线程数)
我们这里采用读写锁
public static void main(String[] args) {
//采用ReentrantReadWriteLock实现数据同步
//读
for (int i = 0; i < 5; i++) {
new Thread(() -> ReadWriteLockDemo.read(ReadWriteLockDemo.readLock)).start();
}
//写
for (int i = 0; i < 3; i++) {
new Thread(() -> ReadWriteLockDemo.write(ReadWriteLockDemo.writeLock, new Random().nextInt())).start();
}
}
结论:发现采用读写锁发现总共耗时(读的到时间)+(写的时间*线程数)效率大幅度提升
阻塞队列
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
常用的队列主要有以下两种:
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件
写入:如果队列满了就必须阻塞等待
读取:如果队列为空就必须阻塞等待生产
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
ArrayBlockingQueue
代码示例
public static void main(String[] args) throws Exception {
BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<String>(3);
//添加的元素多余3会报错: ava.lang.IllegalStateException: Queue full
arrayBlockingQueue.add("3");
//移除空队列会报错:java.util.NoSuchElementException
arrayBlockingQueue.remove();
//添加的元素多余3会返回false
arrayBlockingQueue.offer("3");
//移除空队列会返回null
arrayBlockingQueue.poll();
//添加元素超过3会进入等待。等待队列少于3进行插入操作
arrayBlockingQueue.put("a");
//移除元素 等待队列有值再进行移除操作
arrayBlockingQueue.take();
//添加一个元素,如果超过一秒还没有多余的位置就结束等待
arrayBlockingQueue.offer("3", 1, TimeUnit.SECONDS);
//移除一个元素,如果超过疫苗还没有多余得到位置就结束等待
arrayBlockingQueue.poll(1, TimeUnit.SECONDS);
//检查队列首元素,peek不会报异常,element 会报异常
arrayBlockingQueue.peek();
arrayBlockingQueue.element();
}
SynchronousQueue
相比于ArrayBlockingQueue一次可以添加多个元素,SynchronousQueue一次只能往队列里面添加一个元素,并且取出之后才能继续添加元素。
代码示例
public static void main(String[] args) {
BlockingQueue<String> data = new SynchronousQueue<>();
//producer
new Thread(() -> {
try {
System.out.println("添加了k1");
data.put("k1");
System.out.println("添加了k2");
data.put("k2");
System.out.println("添加了k3");
data.put("k3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//consumer
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("我取出了" + data.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("我取出了" + data.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("我取出了" + data.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
线程池
线程池出现的原因是因为频繁的创建线程和销毁线程都很耗cpu资源,所以创立一个池来缓存线程,将任务结束的线程不杀死,而是缓存到线程池,需要线程直接去池里面拿,减少了创建线程和销毁线程的步骤,从而更方便的来管理线程。
线程池的优势:
- 降低资源的消耗(减少平凡的创建线程和销毁线程)
- 提高响应速度
- 方便管理
使用注意事项
Executors的四种线程池
public static void main(String[] args) {
//余强则强,与弱则弱 不用给定线程池大小根据业务自动扩展线程池大小
ExecutorService executorService = Executors.newCachedThreadPool();
//固定线程池大小
ExecutorService executorService1 = Executors.newFixedThreadPool(5);
//定时任务线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//创建单一线程池
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) {
//线程池每次创建的容量都不一样
//executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
//发现固定5个线程名都一样
//executorService1.execute(() -> System.out.println(Thread.currentThread().getName()));
//定时任务延迟执行
//延迟三秒执行
//scheduledExecutorService.schedule(() -> System.out.println(Thread.currentThread().getName()), 3, TimeUnit.SECONDS);
//发现线程名只有一个
//executorService2.execute(() -> System.out.println(Thread.currentThread().getName()));
}
//关闭线程池,避免阻塞无法关闭
executorService.shutdown();
executorService1.shutdown();
scheduledExecutorService.shutdown();
executorService2.shutdown();
}
线程池的7大参数
-
corePoolSize(Core)
线程正常由Core业务员处理 -
maximumPoolSize(Max)
当候客区坐满了人的时候就开放Max业务员进行业务处理 -
keepAliveTime(Max)
Max业务员多少时间内没有处理业务就自动关闭了 -
unit(Max)
时间单位和keepAliveTime关联(时,分,秒) -
workQueue(候客区)
采用哪种候客区模式进行排队有界队列:
ArrayBlockingQueue(先进先出)
SynchronousQueue (一次只能进入一个人,处理完之后再进另外一个人)无界队列:
LinkedBlockingQueue:(基于单向链表的无界的阻塞队列,尾部插入元素,头部取出元素)
PriorityBlockingQueue (一个具有 优先级的无限阻塞队列 )
LinkedBlockingDeque(可以从尾部插入/取出元素,还可以从头部插入元素/取出元素) -
threadFactory
使用 defaultThreadFactory 创建的线程同属于相同的线程组,具有同为 Thread.NORM_PRIORITY 的优先级,以及名为 “pool-XXX-thread-” 的线程名(XXX为创建线程时顺序序号),且创建的线程都是非守护进程。 -
handler
决绝策略 当Core满了 ,Max满了,候客区也满了,就要把进来的人拒之门外
四大拒绝策略(handler)
-
AbortPolicy
默认策略,直接跑出异常阻止系统正常运行 -
CallerRunsPolicy
“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回馈至发起方比如main线程 -
DiscardOldestPolicy
抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 -
DiscardPolicy
直接丢弃任务,不给予任何处理也不跑出异常,如果允许任务丢失,这是最好的一种方案
扩展:cpu密集和io密集
- cpu密集
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高
- io密集
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!