目录
七.JUC(java.util.concurrent)常见的类
一.线程池
线程池通过预先创建并维护一组线程,能够实现纯用户态操作,可以减少线程创建和销毁时的用户态与内核态切换开销。
1.线程池如何减少模式切换
1.预先创建线程:线程池在初始化时就创建了一定数量的线程(核心线程),避免了每次任务到来时都需要新建线程。
2.线程复用:线程池中的线程在执行完一个任务后不会立即销毁,而是保持存活状态等待下一个任务,避免了频繁的线程创建和销毁。
3.减少系统调用:每次创建新线程都需要进行系统调用(从用户态切换到内核态),线程池通过复用线程减少了这种切换。
2.创建线程池
(1).使用ThreadPoolExecutor构造方法
ThreadPoolExecutor executor = new ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
1. corePoolSize(核心线程数)
-
线程池中保持的最小线程数量
-
即使线程空闲也不会被回收(除非设置allowCoreThreadTimeOut)
-
新任务提交时优先创建核心线程
2. maximumPoolSize(最大线程数)
-
线程池允许的最大线程数量
-
当工作队列满时,线程池会创建新线程直到达到此限制
-
必须 ≥ corePoolSize
最大线程数=核心线程数(最先创建的/不会被销毁)+非核心线程数(后来创建的/可能被销毁)
通过最大线程数和核心线程数能够完成自动缩容/扩容的作用
3. keepAliveTime(线程空闲时间)
-
非核心线程允许空闲的最大时间
-
超过此时间且当前线程数>corePoolSize时,线程会被终止
-
可设置unit参数指定时间单位
4. unit(时间单位)
-
keepAliveTime的时间单位
-
常用值:TimeUnit.SECONDS、TimeUnit.MILLISECONDS等
5. BlockingQueue<Runnable> workQueue(工作队列)
-
用于保存等待执行的任务的阻塞队列
-
常用实现类:
-
ArrayBlockingQueue
:有界队列 -
LinkedBlockingQueue
:无界队列(默认) -
SynchronousQueue
:不存储元素的队列 -
PriorityBlockingQueue
:优先级队列
-
6. threadFactory(线程工厂)=>Thread类的工厂类,会提供多个工厂方法,创建出Thread对象
-
用于创建新线程的工厂
-
可以自定义线程名称、优先级、守护状态等
-
默认使用
Executors.defaultThreadFactory()
可以统一设置线程的属性
7. handler(拒绝策略)
-
当线程池和工作队列都饱和时的处理策略
-
内置策略:
-
AbortPolicy
:默认,任务队列满,抛出RejectedExecutionException -
CallerRunsPolicy
:由提交任务的线程执行该任务 -
DiscardPolicy
:把任务队列中最新的任务丢弃 -
DiscardOldestPolicy
:把任务队列中最老的任务(最早被加入,还没执行)丢弃了,空余位置留给新任务
-
(2).使用Executors工厂方法
// 固定大小线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 单线程线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 可缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
Executors 是一个工具类/工厂类,提供了一系列静态工厂方法来创建不同类型的 ExecutorService 实例。
ExecutorService 是 Java 并发包 (java.util.concurrent
) 中的一个接口,它扩展了更基础的 Executor 接口,提供了更丰富的线程池管理功能。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//接口实现线程池
public class Demo18 {
public static void main(String[] args) {
ExecutorService executorService= Executors.newCachedThreadPool();
for (int i = 0; i <100 ; i++) {
final int id=i;
executorService.submit(new Runnable() {
@Override
public void run() {
String name =Thread.currentThread().getName();
System.out.println("hello"+name+id);
}
});
}
}
}
自主实现线程池
class MyThreadPool{
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
//n表示线程池的线程数目
public MyThreadPool(int n){
for (int i = 0; i < n; i++) {
Thread t=new Thread(()->{
try {
while(true){
Runnable task=queue.take();
task.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.setDaemon(true);
t.start();//调用start才会执行lambda
}
}
//在线程池中添加新任务
public void submit(Runnable task) throws InterruptedException {
queue.put(task);
}
}
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool=new MyThreadPool(4);//创建线程池,可以存储4个线程
for (int i = 0; i <10 ; i++) {
int id=i;
pool.submit(()->{
Thread cur=Thread.currentThread();
System.out.println("hello"+cur.getName()+","+id);
});
}
//确保线程池的线程有足够时间执行完任务,因为线程池中的线程被设置为后台线程,主线程结束进程结束后台进程就会直接结束
Thread.sleep(1000);
}
}
3.线程池工作流程
-
- 当提交任务时,线程池首先检查核心线程数(
corePoolSize
)是否已满。未满则创建新线程执行任务。 - 若核心线程已满,任务会被放入阻塞队列等待。
- 若阻塞队列也满了,才会判断当前线程数是否小于最大线程数(
maximumPoolSize
)。若小于,则创建新线程(非核心线程)执行任务。 - 若队列满且线程数已达最大值,则触发拒绝策略。
- 当提交任务时,线程池首先检查核心线程数(
二.定时器
实现方法
//定时器Timer类
import java.util.Timer;
import java.util.TimerTask;
public class Demo20 {
public static void main(String[] args) {
Timer timer=new Timer();//这个类的内部有专门的前台线程执行任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello task1");
}
},1000);//TimerTask继承自Runnable接口,并且还是抽象方法,需要实现
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello task2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello task3");
}
},3000);
timer.cancel();//关闭定时器
}
}
三.锁的策略
1.乐观锁vs悲观锁
加锁时预测出现竞争的概率大=>悲观锁
加锁时预测出现竞争的概率小=>乐观锁
synchronized既是乐观锁又是悲观锁
2.重量级锁vs轻量级锁
加锁操作开销很大=>重量级
加锁操作开销很小=>轻量级
synchronized既是重量级锁又是轻量级锁
3.挂起等待锁vs自旋锁
遇到锁冲突,把线程阻塞,等待唤醒=>涉及到系统内部调度,开销很大=>挂起等待锁(重量级锁的典型实现)
遇到锁冲突,不着急阻塞,而是进行重试,直到锁释放=>用户态的操作,不涉及内核和线程调度=>自旋锁(轻量级锁的典型实现)(基于cas完成)
synchronized既是挂起等待锁又是自旋锁
4.公平锁vs不公平锁
操作系统内部线程调度随机,不做任何限制=>非公平锁
依赖额外的数据结构记录线程的先后顺=>公平锁
synchronized是非公平锁
5.可重入锁vs不可重入锁
一个线程,一把锁,连续加两次
不死锁=>可重入锁
死锁=>不可重入锁
synchronized是可重入锁
6.读写锁vs普通互斥锁
加锁 ,解锁=>普通互斥锁
加读锁,加写锁,解锁=>读写锁(如果代码逻辑中只进行读操作/写操作,就可以只用读锁/写锁)
读锁和读锁不存在锁竞争
读锁和写锁以及写锁和写锁存在锁竞争
synchronized是普通互斥锁
四.锁的升级
升级过程
Java中的锁升级是指从无锁状态逐步升级到重量级锁的过程,这是Java Synchronized关键字实现锁优化的核心机制。锁升级过程主要分为以下几个阶段:
1. 无锁状态
-
对象刚创建时,没有任何线程竞争
-
此时对象的Mark Word中存储的是对象的哈希码等信息
2. 偏向锁(Biased Locking)
-
进入synchronized代码块,锁会进入偏向模式
-
没有真正加锁,只是标记
-
在下面的过程中,如果没有别的线程来争取,就始终保持标记
3. 轻量级锁(Lightweight Locking)(自旋锁)
-
当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁
-
JVM会在当前线程的栈帧中创建锁记录(Lock Record)
4. 重量级锁(Heavyweight Locking)
-
当自旋超过一定次数(默认10次)或等待线程超过一定数量,锁会升级为重量级锁
-
此时Mark Word中存储的是指向操作系统互斥量(mutex)的指针
-
未获取到锁的线程会被阻塞,进入内核态的等待队列
-
重量级锁会导致线程在用户态和内核态之间切换,开销较大
锁升级的特点
-
不可逆性:锁只能升级不能降级(从JDK 15开始,HotSpot实现了锁降级)
-
自适应自旋:JVM会根据历史情况动态调整自旋次数
-
锁消除:针对synchronized进行的编译器优化,会对不需要的锁进行消除(比如某个变量只在一个线程中使用)
-
锁的粒度:取决于加锁和解锁之间有多少逻辑
-
锁粗化:对连续多次加锁解锁操作合并为一次范围更大的锁操作
锁升级机制是为了在减少锁开销和提高并发性能之间找到平衡:
-
无竞争时使用偏向锁,几乎无开销
-
轻度竞争时使用轻量级锁,避免线程阻塞
-
重度竞争时使用重量级锁,避免CPU空转
这种机制使得Synchronized在低竞争和高竞争环境下都能有较好的性能表现。
五.CAS(compare and swap)
与加锁不同的解决线程安全的另一种思路,是通过"原子性"来防止插队
CAS不是函数而是一条"cpu指令"
主要进行比较一个内存和寄存器的值,如果内容相同,把内存器和另一个寄存器的值交换
CAS 操作包含三个操作数:
- 内存位置(V)
-
预期原值(A)
-
新值(B)
操作逻辑:
-
当且仅当 V 的值等于 A 时,才会将 V 的值更新为 B
-
无论是否更新成功,都会返回 V 的旧值
AtomicInteger atomicInt = new AtomicInteger(0);
// CAS 操作
boolean success = atomicInt.compareAndSet(0, 1); // 如果当前值是0,则更新为1
ABA问题
CAS循环检测,判定是否出现其他线程穿插执行了的判定依据=>检测值没有改变 (×)
用下面的代码进行讲解为什么上面的结论不正确
AtomicInteger atomicInt = new AtomicInteger(100);
// 线程1:100 -> 50
atomicInt.compareAndSet(100, 50);
// 线程2:50 -> 100
atomicInt.compareAndSet(50, 100);
// 线程3:认为值还是100(实际上已经发生过穿插)
atomicInt.compareAndSet(100, 200); // 会成功,但可能不符合业务预期
解决方法
引入"版本号"概念,约定版本号只能加不能减,每次修改值都会原子的把版本号+1
再次使用aba判定时,不是拿数值判定是否有插队,而是拿版本号判定
value=1000;
version=1
oldVersion=version
if(CAS(version,oldVersion,通过原子的方式进行version+1和value的修改))
//版本相同则进行修改,不同则认为失败.原子方式修改需要AtomicStampedReference的引用
六.CAS应用场景
原子类/自旋锁=>内部方法基于cas实现
使用原子类解决线程安全问题(与synchronized作用类似)
import java.util.concurrent.atomic.AtomicInteger;
//使用原子类解决线程安全问题
public class java22 {
//private static int count=0;
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i <5000 ; i++) {
//count++;java中不支持运算符重载=>不支持让各种运算符对某个类的对象使用
count.getAndIncrement();//先获取旧值在自增
//++count
//count.incrementAndGet();
//count--
//count.getAndDecrement();
//--count
//count.decrementAndGet();
//count+=n
//count.addAndGet(n);
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <5000 ; i++) {
//count++;
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count"+count);
}
}
如何理解内部方法基于CAS实现,起到维护线程安全的作用,用下面代码解释
class AtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue=value;
//value 是内存的值,将其放到寄存器当中为oldValue
//在这个时候可能会有其他线程穿插进来执行,就可能出现线程不安全问题,但是下面的CAS能够识别是否存在穿插
while (CAS(value,oldValue,oldValue)!=true){
//此处while循环进入的条件是当value与oldValue值不同时进入
oldValue=value;//此时更新oldValue的值
}
return oldValue;
}
}
图解
自旋锁基于CAS实现伪代码
//自旋锁基于cas实现伪代码
public class Demo23 {
private Thread owner=null;//owner是一个标记,用于记录当前哪个线程持有锁
//通过CAS看是否锁被某个线程持有
//CAS中如果owner!=null,说明锁被其他线程持有,返回false =>!false=>true 继续循环等待
//owner==null 说明锁被释放,将当前线程赋给owner,返回true=>!true=>false 退出循环
while (!CAS(this.owner,null,Thread.currentThread())){
}
public void unlock(){
this.owner=null;
}
}
七.JUC(java.util.concurrent)常见的类
1.callable接口
类似于Runnable void run表示任务的过程没有返回值
callable中有一个T call方法,同样描述一个任务,要求返回一个值
通过代码区分
//run方法
public class Demo24 {
private static int result=0;//用来将run方法无法返回的值取出来
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
int sum=0;
@Override
public void run() {
for (int i = 0; i < 4; i++) {
sum+=i;
}
result=sum;//通过成员变量解决线程间数据交互的问题,该变量能够被各种线程各种逻辑获取
}
});
t.start();
t.join();
System.out.println(result);
}
}
使用callcable中的call方法就可以解决上面的情况
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i < 1000; i++) {
sum+=i;
}
return sum;
}
};
//Thread无法直接接受callable对象作为参数
//所以需要搭配FutureTask(类似于取餐号码牌)
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
//get方法能够拿到call的返回结果
//如果call没有执行完,get会阻塞等待
System.out.println(futureTask.get());
}
}
2.ReentrantLock(可重入锁)
是一种锁的经典用法,在synchronized出现之前一直使用这种方式加锁
import java.util.concurrent.locks.ReentrantLock;
public class Demo25 {
private static int sum=0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker=new ReentrantLock();
Thread t1=new Thread(()->{
for (int i = 0; i <5000 ; i++) {
try{
locker.lock();
sum++;
}finally{
locker.unlock();
}
//. 防止锁泄漏(避免死锁)
//如果 lock()之后代码抛出异常,而 unlock()没有被执行,锁将永远不会被释放,导致其他线程无法获取锁(死锁)。
//finally块确保无论是否发生异常,锁都会被释放。
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
try{
locker.lock();
sum++;
}finally{
locker.unlock();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum);
}
}
相对于synchronized优势
1.ReentrantLock支持trylock行为,加锁不成可以直接返回,不像lock会阻塞等待
2.ReentrantLock支持公平锁(内部维护队列记录加锁顺序,会按照顺序从队列中取)
ReentrantLock locker=new ReentrantLock(true);
3.等待通知机制
synchronized 搭配wait notify,notify只能随机唤醒一个线程
ReentrantLock搭配Condition类,唤醒功能更丰富,能够指定唤醒
八.信号量(Semaphore)
就是一个计数器,描述了"可用资源"的个数,最少为0=>可当作锁来使用
分为两个操作一个是申请资源(p操作),一个是释放资源(v操作)
Semaphore semaphore=new Semaphore(许可证数量)
p操作=>semaphore.acquire()
v操作=>semaphore.release()
p v操作本质上是操作计数器的值=>原子的过程
主要用于固定资源个数的场景,也能充当锁,如下所示
package thread;
import java.util.concurrent.Semaphore;
public class Demo26 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
//充当锁
Semaphore semaphore=new Semaphore(1);
Thread t1=new Thread(()->{
for (int i = 0; i <10000 ; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <10000 ; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
CountDownLatch
CountDownLatch是Java并发包(java.util.concurrent
)中的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作后再继续执行。其核心思想基于计数器机制,通过递减计数来实现线程间的协调。内部维护一个计数器,初始值为设定的正整数。当线程调用countDown()
时,计数器减1;调用await()
的线程会被阻塞,直到计数器变为0
用下面的例子解释,在运动会赛跑中,有3个选手比赛,在程序中我们设置为3个并发执行的线程,只有全部的线程执行结束也就是所有运动员到达终点比赛才会结束,那我们如何才能知道这几个线程什么时候才能全部结束,这时就会使用CountDownLatch
package thread;
import java.util.concurrent.CountDownLatch;
public class Demo {
public static int id;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
final int id=i;
Thread t=new Thread(()->{
System.out.println("运动员"+id+"开始比赛");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("运动员"+id+"到达终点");
//撞线操作
latch.countDown();
});
t.start();
}
latch.await();//等待全部线程结束后
System.out.println("比赛结束");
}
}
ConCurentHashMap
每个链表的头节点作为锁对象,多线程使用哈希表尽量使用ConCurentHashMap,原因如下