目录
1. AQS(AbstractQueenSynchronizer)
2. Synchronize和ReentrantLock的区别?
一. 线程 |
1、线程的生命周期(5种状态)
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态。这是因为时间片非常短,线程切换极快,区分这两个无意义。
线程上下文切换指的是:比如CPU时间片耗尽导致线程切换时,需要保存当前线程的信息(比如程序计数器,栈信息等),这也需要消耗CPU和内存资源,频繁切换就会导致效率低下。
2、创建线程的方式
- 继承Thread类,重写run()方法
- 实现runnable接口,重写run()方法
- 实现callable接口,重写call方法
- 用线程池Executors工具类创建
runnable接口和callable接口有什么不同?
callable有返回值,runnable没有。
start()和run()方法方法有什么区别?
start()用于启动线程,只能执行一次;run()用于执行线程中的方法,可以执行很多次。
3. 用户线程和守护线程
- 用户线程:运行在前台,执行具体任务。(如:主线程,连接网络的子线程)
- 守护线程:运行在后台,为其他线程服务。一旦所有用户线程运行结束,守护线程会随JVM一起结束工作。(如:垃圾回收线程)
用户线程结束,JVM退出。守护线程不会影响JVM退出。
- 进程:操作系统最小执行单位
- 线程:CPU最小执行单位
- 协程:轻量级线程,由用户控制,在用户态执行
一个线程中可以有多个协程。线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行。
4. 如何查看线程的CPU利用率
- top命令查看所有的进程
- top -H -p [进程号] 查看进程的所有线程(这里就可以查看CPU利用率)
- jstack [进程号] | grep "[线程号的16进制表示]" 查看线程堆栈
二. 多线程 |
1、创建线程池
(1)使用Executors创建(不建议使用)
- newFixThreadPool(最常用,效率高):创建固定大小的线程池。来新任务,就创建新线程,直到达到最大线程数。
- newCacheThreadPool:创建缓存线程池,不规定大小,使用的少,就回收一部分,使用的多,就多创建。
- newScheduleThreadPool:创建无限大小的线程池,可用于执行周期性任务。
- newSingleThreadExcutor:创建单线程线程池。只有一个线程在运行,该线程异常时才有新的线程来代替他。
阿里的规范:
(2)使用ThreadPoolExecutor方式创建
【ThreadPoolExecutor 线程池参数】
三个非常重要的参数:
- corePoolSize:核心线程数,线程池中长期存活的线程数。
- maxinumPool:最大线程数,线程池允许工作的最大线程。
- workQueue:当有新任务来临,但是核心线程数已满,就会放入队列中。
其他参数:
- allowCoreThreadTimeOut:允许核心线程超时。
- keepAliveTime:空闲线程存活时间,线程空闲时间超过keepAliveTime,就会退出,直到线程数量=核心线程数;当allowCoreThreadTimeOut=true,则会直到线程数量=0。
- unit:等待时间单位。
- handler:拒绝策略。当线程池中线程已达到最大线程数,就会按拒绝策略丢弃线程。
代码示例:
ThreadPoolExecutor executor= new ThreadPoolExecutor(
5, //核心线程池大小,CPU密集型一般设置为n+1,IO密集型设置为2n
5, //最大线程数一般设置和核心线程数一致,避免了队列已满时创建和销毁线程的开销
60, //超时了没有人调用就会释放,默认值是60秒
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,丢弃最早未执行的任务
线城池执行流程:
2、工作队列有哪些?
- ArrayBlockingQueue:基于数组的有界阻塞队列,适合读写性能高、容量固定的场景
- LinkedBlockingQueue:基于链表的阻塞队列,适合增删性能高、长度可变的场景。最大长度默认是Integer.max_value,即2 的31 次方- 1
- PriorityBlokingQueue:有优先级的阻塞队列
3、拒绝策略有哪些?
- abortPolicy:丢弃任务并抛异常。(默认)
- discardPolicy:直接丢弃任务。
- discardOldestPolicy:丢弃最早未执行的任务。
- callerRunPolicy:调用者执行该任务。(会导致效率非常低)。
4、提交任务到线城池
- execute:提交不需要返回值的任务。
- submit:提交需要返回值的任务。
submit提交任务:
会返回Future对象,并且可以通过Future的get()获取返回值,get()方法会阻塞当前线程直到任务完成。
而使用get(long timeout, TimeUnit unit)则会阻塞当前线程一段时间后立即返回,这时候线程可能没有执行结束。
//execute()用于提交Runnable任务
executor.execute(new RunnableTask());
//submit()用于提交Callable任务
Future<Result> future = executor.submit(new CallableTask());
5、关闭线程池
可以调用用shutdown()或shutdownNow()来关闭线程池。
- shutdown():把线程池状态设置为SHUTDOWN,正在执行的任务会继续执行,没执行的不再执行。
- shutdownNow():把线程池状态设置为STOP,正在执行的任务被停止,没执行的不再执行。
三. 线程安全 |
1、并发编程三大特性
特性 | 含义 | 产生原因 | 解决办法 |
---|---|---|---|
原子性 | 一个或多个操作,要么同时成功,要么同时失败 | CPU执行任务时切换线程导致的 | Atomic开头的原子类可以、synchronize、Lock可以解决原子性问题 |
可见性 | 访问共享变量时,一个线程修改对另一个线程可见 | 各个线程访问的是当前CPU的缓存信息 | synchronize、Lock、volatile可以解决可见性问题 |
有序性 | 程序按照代码先后顺序执行 | CPU指令重排序导致的 | Happen-Before原则可以解决有序性问题 |
什么是Happen-Before原则?
存在依赖关系的不允许重排序。
2、如何解决线程安全问题?
- 使用JUC包下的Atomic原子类,如AtomicInteger
- synchronize
- Lock
Q:什么时候会出现线程安全问题?
A:有共享变量时才会出现线程安全问题。
Q:servlet线程安全吗?
A:不安全。servlet是单例的,多线程访问共享资源时,必然导致线程安全问题。但是一般servlet是无状态的,即没有共享数据,所以某种意义上可以认为是线程安全的。
3、死锁?
死锁是指两个或两个以上线程因为竞争资源而造成的阻塞现象。
3.1 死锁的产生条件?
- 互斥:一个锁只能被一个线程持有
- 请求并保持:线程一直保持持有锁
- 不剥夺:其他线程不能强行剥夺锁,除非持有锁的线程主动释放
- 循环等待:等待锁的线程形成了环路,造成永久阻塞
3.2 如何避免死锁?
只需破坏四个条件中的一个。
- 破坏互斥:不能破坏,我们本身就希望锁是互斥的
- 破坏请求与保持:一次申请所有资源
- 破坏不剥夺:占有一部分资源的线程进一步申请其他资源时,如果申请不到,就主动释放它占有的资源
- 破坏循环等待:按照顺序申请资源
// thread1先获取锁1再获取锁2,thread2先获取锁2再获取锁1
// 如果thread1获取锁1等待锁2,thread2获取锁2等待锁1,就会造成死锁
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and resource 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 2 and resource 1...");
}
}
});
thread1.start();
thread2.start();
}
}
死锁检测:可以用jconsole或jVisualVM进行分析检测,有死锁的话有响应的提示,点到对应的线程里就能看到线程在wait lock monitor
数据库死锁:
-- 事务1 begin; -- SQL1更新id为1的 update user set age = 1 where id = 1; -- SQL2更新id为2的 update user set age = 2 where id = 2; commit;
-- 事务2 begin; -- SQL1更新id为2的 update user set age = 3 where id = 2; -- SQL2更新id为1的 update user set age = 4 where id = 1; commit;
4. synchronize
synchronize取得的都是对象锁或类锁,而不是把某段代码或某个方法作为锁,并且必须是同一把锁才能锁住。
synchronize修饰非静态方法的时候是对象锁,修饰静态方法是类锁
对象锁的范围是同一个对象,类锁它的范围是类的所有对象
为什么叫对象锁,因为锁的范围是同一个对象
4.1 说一下synchronize的原理?
synchronize底层是通过对象监视器monitor实现的,monitor内部维护了一个锁计数器,如果为0,说明处于空闲状态,可以获取锁,否则获取不到。
任何java对象都有一个monitor与之关联,当monitor被持有后,对象就处于锁定状态。
- 如果是synchronize代码块,底层具体是monitorenter和monitorexit两个指令实现的
- 如果是synchronize修饰方法,就用ACC_SYNCHRONIZED标识这是一个同步方法
4.2 可重入锁、自旋锁、偏向锁、轻量级锁、重量级锁、共享锁、排他锁、可中断锁的概念?
- 可重入锁:已经获取锁的线程可以再次获取锁。原理是底层维护了一个计数器,当线程获取锁则加一,再次进入时,判断是同一线程则继续加一,释放锁时减一。
- 自旋锁:让一个线程在获取锁时忙循环一段时间,如果短时间内能获取锁,就避免进入阻塞状态。
- 偏向锁:顾名思义,偏向锁会偏向第一个获取锁的线程。如果运行过程中,同步锁只有一个线程访问,则给线程加偏向锁,在对象头中记录线程ID,当线程下次再想获取锁,不需要重新申请锁可直接进入同步代码;如果多个线程抢占,则升级为轻量级锁。
- 轻量级锁:轻量级锁是一种乐观锁,它认为锁的竞争较小,使用CAS来获取锁。
- 重量级锁:可以认为是java中的监视器锁(monitor),使用互斥量来获取锁。
- 共享锁:也叫读锁。共享锁就是多个事务可以对同一数据共享同一把锁,都能访问到数据,但是只能读不能写。
- 排它锁:也叫写锁。排它锁不能和其他锁共存,但是可读可写。
- 可中断锁:获取锁的过程可以中断,当一个线程在等待锁时,如果另一个线程对其调用interrupt()方法就会中断获取锁。ReentranLock是可中断锁,Synchronize是不可中断锁
- 公平锁:先来后到,每个线程获取锁的顺序按照线程请求锁的先后顺序
- 非公平锁:随机,每个线程获取锁的顺序是随机的。
4.3 锁升级的原理?
锁状态:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。
- 当有一个线程访问到同步代码块时,升级为偏向锁,并在对象头中存储threadId为当前线程id;
- 再有线程访问时,比较当前线程id和对象头中threadId是否一致,如果一致可再次进入,如果不一致则升级为轻量级锁;
- 再有锁竞争时,通过CAS修改对象头中的锁标志位。如果锁标志位是“释放”,则修改为“锁定”,此时该线程获取到锁。没有获取到锁的线程进行自旋,十次自旋之后还没获取到锁则升级为重量级锁。
4.4 说一下Java中的对象头
对象头主要包括了mark word(标记字段) 和 类型指针(用于确定这个对象是哪个类的实例)。
Mark Word存储的信息根据锁状态不同而有区别,主要是:对象hashcode、GC标记、锁标记位等
5. volatile
volatile用于解决可见性问题。有两个作用:
- 保证可见性(对该变量的写操作会立即同步到主内存,对该变量的读操作会从主内存获取最新的值)
- 禁止指令重排序(编译器不会对volatile修饰的关键字进行指令进行指令重排序,保证有序性)
volatile规定:如果多个线程操作volatile修饰的变量,那么这个变量的写操作必须在读操作之前完成,禁止指令重排序。
volatile是如何解决可见性问题的?
volatile通过内存屏障来解决可见性问题。内存屏障是一条CPU指令,用于保证指令的执行顺序。
写内存屏障:在写指令后插入store barrier指令,强制写入本地内存的数据立即更新到主内存中,使其他线程可见。并通过cpu总线通知其他线程的本地缓存失效。
读内存屏障:缓存中的数据失效,从主内存中读取最新数据。
四. 线程间的通信 |
1. 多线程如何通信?
- synchronize加锁的线程用:Object的 wait/nofity/notifyAll 方法
- reentrantLock类加锁的线程用:Condition的 await/signal/signalAll 方法
多线程如何进行交换数据?
通过管道进行线程间通信。(1)字节流 (2)字符流
2. wait和sleep有什么区别?
- wait用于线程间的通信,sleep用于停止执行;
- wait释放锁,sleep不释放锁;
- wait必须被唤醒(notify,notifyAll),sleep自然醒。
3. 为什么wait、notify定义在Object类中?
因为任意对象都可以作为锁,任意对象都能使用的方法一定在Object类中。
为什么不把wait、notify定义在Thread类中?
因为一个线程可以持有多个锁,如果定义在Thread类中,释放锁时就不知道释放的是哪个锁
为什么sleep()定义在Thread类中?
因为sleep()是让当前线程停止执行,并不涉及到对象锁
4. yield的作用?join的作用?
t1.join():作用是当前线程等待被调用的线程(t1)执行完毕后再执行
Thread.yield:暂时释放cpu执行权,线程状态由运行状态转为就绪状态。
LockSupport.park()作用是当前线程进入等待状态,直到其他线程调用unpark
//主线程要等t1线程执行完毕才能执行
public static void main(String[] args){
Thread t1 = new Thread(new Worker("thread-1"));
t1.start();
t1.join();
System.out.println("main end");
}
5. java内存模型
java内存模型定义了本地内存和共享内存之间的交互规则和行为,以及保证多线程之间的内存原子性、可见性、有序性
共享变量存储在共享内存中,工作内存中存储线程私有变量和共享变量的副本。线程操作共享变量时,先复制一份到本地内存中,待操作完毕,再把最新值放入共享内存。
CPU高级缓存
CPU缓存是位于CPU内部的高速存储器,用于存储CPU从内存读取的数据以及频繁使用的指令。CPU通过缓存减少对内存的访问次数,以提高CPU访问数据的速度和效率。
CPU三级缓存:CPU的高速缓存有三层,L1、L2、L3。
- 一级缓存访问速度最快和访问延迟最低,主要用于暂存 CPU 频繁访问的数据和指令;
- 二级缓存容量较大,速度较快,主要用于提高一级缓存的命中率,一级缓存找不到的就到二级缓存找;
- 三级缓存容量最大,速度相对较慢,主要用于多个 CPU 核心之间的数据共享和协作,三级缓存通常是所有 CPU 核心共享的,能够提高系统的并发性能。
线程本地内存用于存储线程私有数据以及线程栈空间,CPU高速缓存用于加速对数据的访问
CPU高速缓存造成的问题
高速缓存和主存之间容易数据不一致,操作系统(windows和Linux)都通过内存模型解决了这个问题。Java也有自己的内存模型,java当然也可以复用操作系统提供的内存模型,但是这会导致换一套系统代码就无法运行了,所以java也提供了自己的内存模型
五. JUC(java.util.concurrent) |
(一)atomic(核心CAS)
1. concurrent包中有哪些原子类?
原子类:atomicInteger,atomicLong,atomicBoolean,atomicReference
原子数据:atomicIntegerArray,atomicLongArray
解决ABA问题的原子类
atomicMarkableReference:通过引入Boolean变量来反应中间有没有变过
atomicStampedReference:通过引入int来累加反应中间有没有变过
2. atomic原子类的原理?
主要利用CAS(compare and swap) 、volatile 和 native方法 来保证原子操作。
CAS的原理:
CAS包含三个参数,valueOffset、expect、update。
- valueOffset是要修改的变量的内存地址;
- expect是期望值;
- update是新值。
拿valueOffset内存地址中的值和expect比较,如果相等,则把valueOffset中的值更新为update
(二)Lock(ReentrantLock、AQS)
1. ReentrantLock
ReentrantLock是java中的独占锁,它实现了Lock接口,和Synchronize的功能类似,但是比Synchronize功能更强大和灵活。ReentrantLock内部实现了AQS,用于实现锁的获取和释放机制。
ReentrantLock使用
ReentrantLock lock = new ReentrantLock(); 以下都用 lock.
- 获取锁 lock() 该方法阻塞,如果锁被其他线程持有,则当前线程会被阻塞,直到获取锁
- 尝试获取锁 tryLock() 返回值为true或false,该方法非阻塞,获取不到锁就返回false
- 释放锁 unlock()
- 可中断的获取锁 lockInterruptibly() 将来可以调用 interrupt()中断锁
2. AQS
AQS(AbstractQueenSynchronizer)是一个同步器。ReentrantLock的同步器就继承了AQS。AQS的核心思想主要是两个方面:同步状态和等待队列
- 同步状态:AQS使用一个整型来表示锁的状态。比如0代表锁空闲,1代表被某个线程持有。
- 等待队列(CLH队列):AQS使用一个双向链表来管理等待获取同步状态的线程。当一个线程获取同步状态失败,就加入等待队列,并进入阻塞状态,直到获取同步状态为止。
AQS主要包含两个核心方法:
- acquire(int arg):尝试获取同步状态,获取失败则加入等待队列,并阻塞当前线程,直到获取同步状态为止
- release(int arg):释放同步状态,并唤醒等待队列中某个线程让其有机会获取同步状态
AQS的模板方法主要有:
- 独占式 获取和释放同步状态,如tryAcquire() tryRelease()
- 共享式 获取和释放同步状态,如tryAcquireShared() 和 tryReleaseShared()
- 获取 等待在同步队列中的线程集合,如getQueueThreads()
ReentranLock是公平锁还是非公平锁?
ReentranLock默认就是非公平锁,除非在构造函数中传true。
ReentranLock实现非公平锁的原理是:在调用lock()方法时先试用CAS获取锁,获取不到才调用acquire(),再获取不到才加入等待队列。
3. Synchronize和ReentrantLock的区别?
- Synchronize是关键字,ReentrantLock是类;
- Synchronize自动加锁释放锁,ReentrantLock需要手动;
- Synchronize可以修饰类、方法、代码块,ReentrantLock只适用于代码块;
- Synchronize底层是用对象监视器monitor实现锁,ReentrantLock底层是用AQS加锁和释放锁
- Synchronize只能是非共平锁,ReentranLock可以指定是公平锁还是非共平锁
- Synchronize是不可中断锁,ReentrantLock可以选择中断锁或不可中断锁
项目中用的是Synchronize还是Lock?
Lock多一点,首先Lock可以手动加锁和释放锁,更加灵活,但是一定要注意在finally中释放锁。其次,Lock提供了tryLock方法,一定时间内获取不到锁就放弃,不会一直阻塞。
(三)阻塞队列(BlockingQueue)
1. 什么是阻塞队列?常见阻塞队列有哪些?
阻塞队列是支持两个附加操作的队列。当队列为空时,获取操作被阻塞,等待队列变为非空;当队列已满时,存储操作被阻塞,等待队列可用。
常见阻塞队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列,适合读写性能高、容量固定的场景
- LinkedBlockingQueue:基于链表的阻塞队列,适合增删性能高、长度可变的场景。最大长度默认是Integer.max_value,即2 的31 次方- 1
- PriorityBlokingQueue:有优先级的阻塞队列
2. 如何使用Condition创建阻塞队列?
定义一个普通队列Queue、Lock、以及两个Condition(一个代表生产者,一个代表消费者)
- put()方法中,加锁,判断队列是否已满,如果是则producer调用await()让线程等待。否则给队列中添加元素,并调用consumer的signal(),通知消费者队列中有数据可用,释放锁
- get()方法中,加锁,判断队列是否为空,如果是则consumer调用await()让线程等待。否则从队列中获取元素,并调用producer的signal(),通知生产者队列中有空间可用,释放锁
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BlockingQueueExample<E> {
private final Queue<E> queue;
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition producer = lock.newCondition();
private final Condition consumer = lock.newCondition();
public BlockingQueueExample(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<>();
}
public void put(E element) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
System.out.println("Queue is full, producer is waiting...");
producer.await();
}
queue.offer(element);
System.out.println("Produced: " + element);
consumer.signal(); // 通知消费者队列中有数据可用
} finally {
lock.unlock();
}
}
public E get() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("Queue is empty, consumer is waiting...");
consumer.await();
}
E element = queue.poll();
System.out.println("Consumed: " + element);
producer.signal(); // 通知生产者队列中有空间可用
return element;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
BlockingQueueExample<Integer> blockingQueue = new BlockingQueueExample<>(5);
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
blockingQueue.put(i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
blockingQueue.get();
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
Condition的作用?
允许线程等待某个条件成立,或者通知其他线程某个条件已经满足
为什么Condition通常和Lock一起使用?
因为条件等待操作通常涉及到共享资源的访问和修改,Lock来保证互斥,Condition解决线程的等待和唤醒。
Condition和Object的wait/notify()有什么区别?
- Object内部只有一个线程等待队列,wait()把线程加入等待队列,notify()随机唤醒等待队列中的一个线程
- 而一个Lock可以创造多个Condition,这意味着可以根据不同等待条件创造不同的Condition,从而独立控制多个等待队列
(四)并发容器
1. ConcurrentHashMap
详见java基础篇
2. CopyOnWriteArrayList
CopyOnWriteArrayList是一个并发容器,多个线程并发遍历时不会抛并发修改异常。
在CopyOnWriteArrayList中,如果是写操作,会复制一份底层数据副本,写操作修改数据副本,读操作还读取原list,写操作完成之后把原list指针指向数据副本。
主要实现思想:读写分离;最终一致性;使用另外开辟空间的思路,来解决并发冲突。
3 ThreadLocal
ThreadLocal是什么?
ThreadLocal是线程内部的局部变量,属于线程私有,不被其他线程共享。
ThreadLocal如何使用?有哪些应用场景?
使用:
ThreadLocal<String> localName = new ThreadLocal(); localName.set("张三"); String name = localName.get(); localName.remove();
应用场景:
(1)Spring用ThreadLocal保证一个线程用的是同一个数据库连接池。(把connection放入ThreadLocal中)
(2)用ThreadLocal来解决日志串联的问题。比如把traceID放到ThreadLocal中,然后每次日志打印都加上traceID,这样微服务内部整个调用链路就能串联起来,快速定位问题。微服务之间传递traceID可以在请求头中传递。
ThreadLocal的原理?
ThreadLocal是线程内部的一个变量,这个变量的类型是Map(ThreadLocalMap),ThreadLocal存储值的时候,是把自己本身(即ThreadLocal,此为弱引用)当做key,要set的值当做value,放入一个ThreadLocalMap中,ThreadLocalMap被线程持有。
源码:ThreadLocal主要是set、get、remove方法。
【set源码】
【get源码】
【remove源码】
为什么不调用remove方法会造成内存泄漏?
因为ThreadLocalMap持有ThreadLocal弱引用和value强引用,JVM下一次GC就会回收ThreadLocal弱引用,map中就会出现,key为null,但value不为null的entry,导致value这个强引用无法被回收。而ThreadLocalMap和当前线程的生命周期一样长,如果当前线程是线程池中的线程,该线程后续又没有再调用get set remove方法,就会造成内存泄漏。
注:调用get set方法的时候,会清除map中key为null的值。
为什么Entry中的key要设为弱引用?
因为设为弱引用能防止大多数情况的内存泄漏。如果设为强引用,不调用remove方法,key和value都无法被回收。但是设为弱引用,当key被回收,key为null时,下一次该线程调用get set方法,就能清除key为null的值了。
(五)并发工具
1. CountDownLatch(闭锁)
CountDownLunch底层维护了一个计数器,作用是一个线程会等待若干线程执行完毕后他才执行。
CountDownLatch的重要方法
//构造函数,参数count为计数值 public CountDownLatch(int count) { };
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await() throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() { };
//任务
public class CountDownLatchThread implements Runnable {
private String threadName;
private CountDownLatch latch;
public CountDownLatchThread(String threadName, CountDownLatch latch) {
this.threadName = threadName;
this.latch = latch;
}
@Override
public void run() {
System.out.println("threadName = " + threadName + " running");
latch.countDown();
}
}
//使用
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread threadA = new Thread(new CountDownLatchThread("A", latch));
Thread threadB = new Thread(new CountDownLatchThread("B", latch));
threadA.start();
threadB.start();
//当前线程被挂起,直到count=0
latch.await();
System.out.println("主线程执行完毕,主线程继续执行");
}
}
2. CyclicBarrier(栅栏)
CyclicBarrier作用是等待其他线程全都到达某一个屏障点(Barrier),再一起执行。
//任务
public class CyclicBarrierThread extends Thread{
CyclicBarrier cyclicBarrier;
public CyclicBarrierThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "到达栅栏 A");
cyclicBarrier.await(); // 用await()设置屏障点
System.out.println(Thread.currentThread().getName() + "冲破栅栏 A");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "到达栅栏 B");
cyclicBarrier.await(); // 用await()设置屏障点
System.out.println(Thread.currentThread().getName() + "冲破栅栏 B");
} catch (Exception e) {
e.printStackTrace();
}
}
}
//使用
public class CyclicBarrierTest {
public static void main(String[] args) {
int threadNum = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "完成最后任务");
}
});
for (int i = 0; i < threadNum; i++) {
new CyclicBarrierThread(cyclicBarrier).start();
}
}
}
CountDownLatch和CyclicBarrier都维护了一个计数器,它们的区别是:
- CountDownLatch是等其他线程执行完毕当前线程才回继续执行,CycliBarriar是等待其他线程全都变成某个状态,再一起执行。
- 调用CountDownLatch的countDown()方法线程不会阻塞,CyclicBarriar调用await()方法线程会阻塞。
- CountDownLatch不能复用,CyclicBarrier可以循环使用
3. Semaphore(信号量)
Semaphore是信号量,作用是限制某个代码块的并发数。Semaphore的构造函数里传int值,表示最多可用 i 个线程可同时访问Semaphore。
重要方法:
//构造函数,permits表示许可线程的数量 public Semaphore(int permits) //构造函数,fair表示是否是公平锁 public Semaphore(int permits, boolean fair)
//获取许可并阻塞 public void acquire() throws InterruptedException //释放许可 public void release()
//任务线程
public class SemaphoreThread extends Thread {
private Semaphore semaphore;
public SemaphoreThread(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "acquire");
Thread.sleep(1000);
semaphore.release();
System.out.println(Thread.currentThread().getName() + "release");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//使用
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i=0; i<5; i++){
new SemaphoreThread(semaphore).start();
}
}
}
4. Future和FutureTask有什么用?
Future和FutureTask都用于执行异步任务,Future是接口,FutureTask实现了future和runable接口
Future:线程池submit方法的返回值就是Future接口,submit方法里要传一个callable实现类
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "hello";
}
});
String result = future.get();
executor.shutdown();
}
FutureTask:执行异步任务时,可以往里边传入callable的实现类,再通过线程或线程池执行,就能对这个异步任务的结果进行等待获取、判断是否完成、取消等。
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "hello futureTask";
}
});
// 方式一:直接用线程启动 futureTask
// new Thread(futureTask).start();
// 方式二:线程池启动
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(futureTask);
futureTask.get(); //获取异步结果
executor.shutdown();
}
CompletableFuture和Future的区别?
CompletableFuture实现了Future接口。
- Future的作用是获取异步计算的结果;
- CompletableFuture还可以组合异步任务、在任务执行完毕设置回调函数等
public class FutureDemo { public static void main(String[] args) { // supplyAsync:用于创建异步任务 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 异步执行的任务,返回结果为字符串 return "Hello, CompletableFuture!"; }); // thenApply:对结果进行转换处理。入参为function,返回值为CompletableFuture对象 CompletableFuture<String> thenApplyFuture = future.thenApply(result -> { // 将字符串转换为长度 return result.toUpperCase(); }); // thenAccept:对结果进行消费处理。入参为consumer,没有任何返回值 future.thenAccept(result -> { // 打印结果 System.out.println("thenAccept: " + result); }); // thenRun:在异步任务完成后执行指定操作。入参为Runnable,Runnable不接收任何参数,也不返回任何结果 future.thenRun(() -> { // 打印完成信息 System.out.println("thenRun: Task completed."); }); // 等待异步任务完成 try { future.get(); // 阻塞等待异步任务完成 thenApplyFuture.get(); // 阻塞等待thenApplyFuture完成 } catch (Exception e) { e.printStackTrace(); } } }