1. JUC概述
JUC是java.util.concurrent工具包的简称,这是一个处理线程的工具包,jdk1.5开始出现的。
2. Synchronized 与 Lock
2.1 Synchronized
Synchronized是java中的关键字,是一种同步锁。可以加到代码块、方法和类上。加锁和解锁都是自动完成的,不需要额外的代码。
2.2 Lock
Lock是JUC包下的一个接口,使用的时候需要手动加锁和解锁,不能够自动解锁。不手动释放锁的话可能会引起死锁,所以释放锁一般会写在finally中。
2.3 Synchronized与Lock区别
Synchronized与Lock的主要区别有以下几点:
- lock是一个接口,而synchronized是java的一个关键字
- synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。
- lock可以让等待的线程中断。而synchronized不能,synchronized锁住的线程会一直等待下去。
- lock可以知道有没有成功获取到锁,而synchronized不行。
3. 线程间的通信
3.1 实现线程间通信
synchronized通过wait()方法让线程等待,通过notify()和notifyAll()唤醒线程。
lock通过await()方法让线程等待,通过signal()和signalAll()唤醒线程。
实现的流程一般都是:判断(不能用if,必须使用while)->操作->通知。
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
while(number != 1){
this.wait();
}
//干活
number--;
//通知其他线程
System.out.println(Thread.currentThread( ).getName()+" :: "+number );
this.notifyAll();
}
class Test{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(number != 0) {
condition.await();
}
//干活
number++;
system.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他线程
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
}
3.2 线程间定制化通信
让线程按照约定的顺序进行输出,就是某个线程运行完后唤醒指定的其他线程。
比如:A线程指定唤醒B,B指定唤醒C,C指定唤醒A。
实现代码如下:
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1; // 1 AA 2 BB 3 CC
//创建Lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag != 1) {
//等待
c1.await();
}
//干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//通知
flag = 2; //修改标志位 2
c2.signal(); //通知BB线程
}finally {
//释放锁
lock.unlock();
}
}
//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 2) {
c2.await();
}
for (int i = 1; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 3;
//通知CC线程
c3.signal();
}finally {
lock.unlock();
}
}
//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 3) {
c3.await();
}
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 1;
//通知AA线程
c1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
}
}
4. ArrayList线程不安全
ArrayList是线程不安全的,在多线程的情景下可能会报并发异常的错误,因为它的add方法没有加锁。
要想保证集合的安全,有以下几种方案:
- 使用Vector集合
Vector集合的add方法有synchronized修饰,虽然安全性提升了,但是性能却是下降了,一般不用该集合。 - 使用Collections工具包下的synchronizedList(List list)
这种方案用的也不多,比较常用的是JUC里的方案,也就是第三种解决方案。 - juc工具包下的 new CopyOnWriteArrayList(),写时复制。
其底层原理就是add的时候,首先复制一份新的集合并且长度是原集合长度+1,把传来的数据放到集合最后一位,然后把原集合的引用指向新集合。由于是在复制的集合中写数据,所以不影响集合的读操作。整个过程是用lock加锁的,所以不会有并发问题。
5.HashSet线程不安全
要想保证HashSet的线程安全,有以下几种方案:
- 使用Collections工具包下的synchronizedSet(Set s)
该方法不常用。
-juc工具包下的 new CopyOnWriteArraySet(),写时复制。
其底层使用的是ArrayList的写时复制,CopyOnWriteArrayList()。
常见面试题:
- HashSet底层数据结构是什么?
HashMap - HashMap是键值对,为什么HashSet的add方法只添加了一个元素,添加的是key还是value?
HashSet的add添加的元素是HashMap的key,其value是一个常量。HashSet只关心key不关心value。
6.HashMap线程不安全
要想保证HashMap的线程安全,有以下几种方案:
- 使用Collections工具包下的synchronizedMap(Map<K,V> m)
该方法不常用。 - juc工具包下的 new ConcurrentHashMap<>()
7.多线程锁
7.1 公平锁和非公平锁
公平锁:
先到的线程先获取锁,保证了线程获取锁的顺序。
非公平锁:
多个线程的情况下,先申请锁的线程未必就会先获得锁。无法保证线程获取锁的顺序。
JUC包下的ReentrantLock默认是非公平锁,可以设置为公平锁。Synchronized也是非公平锁。
7.2 可重入锁
可重入锁也就递归锁,指的是同一线程的外层函数获得锁之后,内层递归函数仍然能获取该锁。
A方法调用B方法,A方法有锁的话,B方法会自动获取到A方法的锁。
ReentrantLock和Synchronized都是非公平可重入锁。
7.3 自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
8. 线程池
8.1 常用线程池
- newFixedThreadPool(int num) 固定长度的线程池,超出长度的线程池会在队列等待。
- newSingleThreadExecutor() 单个线程的线程池,保证任务按照指定顺序进行。
- newCachedThreadPool()不固定长度的线程池,线程不够用会自动创建,空闲时间超过指定时间会自动回收。
8.2 线程池7大参数
- corePoolSize
线程池常驻核心线程数 - maximumPoolSize
线程池能够容纳同时执行的最大线程数 - keepAliveTime
多余空闲线程的存活时间 - unit
空闲线程存活时间的单位 - workQueue
任务队列,被提交但尚未被执行的任务。 - threadFactory
线程工厂,用于创建线程。一般用默认的即可。 - handler
拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时如何拒绝新的线程。
线程池运行原理:
- 在创建了线程池后,等待提交过来的任务请求。
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
8.3 拒绝策略
- AbortPolicy
默认拒绝策略,直接抛出异常阻止系统正常运行。 - CallerRunsPolicy
"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。 - DiscardOldestPolicy
抛弃队列中等待最久的任务,然后把当前任务假如队列中尝试再次提交当前任务。 - DiscardPolicy
直接丢弃任务,不做任何处理,也不抛出异常。
8.4 开发中使用哪个线程池比较好?为什么?
一个都没有使用,用的是自定义的线程池。
- FixedThreadPool 和 SingleThreadPool
允许请求的队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM - CachedThreadPool和ScheduledThreadPool
允许创建的最大线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM
8.5 线程池合理配置线程数
首页要明白,业务时CPU密集型还是IO密集型。
//查看CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
CPU密集型
CPU密集型是指,一般需要大量的计算工作,没有阻塞,CPU全速允许。
这种情况下尽量减少线程的切换,因为线程上下文的切换也会占用CPU。
参考公式:CPU核数+1个线程的线程池
IO密集型
IO密集型任务并不是一直执行任务,应配置尽可能多的线程数,如CPU核数*2。
还有一种公式:CPU核数 / (1-阻塞系数)。 阻塞系数在0.8-0.9之间。