Synchronized、ReentrantLock、ReentrantReadWriteLock
一、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁
1. 公平锁和非公平锁:
1.1 是什么?
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁;在高并发的情况下,有可能会造成优先级反转或者饥饿现象;
1.2 两者区别?
公平锁/非公平锁:
并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁;
关于两者的区别:
公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从从队列中取到自己;
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就在采用类似公平锁的那种方式;
1.3 题外话?
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量大比公平锁大;对于Sysnchronized而言,也是一种非公平锁;
2. 可重入锁(又名递归锁):【ReentrantLock】
2.1 是什么?
可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,同一个线程在外层方法获取锁的时候,在进入内层方法的时候会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁所同步着的代码块。
2.2 ReentrantLock/Synchronized就是一个典型的可重入锁
2.3 可重入锁最大的作用是避免死锁
参考代码1:Synchronized
package com.example.code.lock; class Phone { public synchronized void sendSMS() { System.out.println(Thread.currentThread().getName() + " invoked sendSMS()"); this.sendEmail(); } public synchronized void sendEmail() { System.out.println(Thread.currentThread().getName() + " invoked sendEmail()"); } } /** * 同步方法可以调用另一个同步方法 * 0 invoked sendSMS() //0线程在外层方法获取锁的时候 * 0 invoked sendEmail() //0在进入内层方法的时候会自动获取锁 * 1 invoked sendSMS() * 1 invoked sendEmail() */ public class LockDemo { public static void main(String[] args) { Phone phone = new Phone(); for (int i = 0; i < 2; i++) { new Thread(() -> {phone.sendSMS();},String.valueOf(i)).start(); } } }
参考代码2:ReentrantLock
package com.example.code.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Phone { public void sendSMS() { System.out.println(Thread.currentThread().getName() + " invoked sendSMS()"); this.sendEmail(); } public void sendEmail() { System.out.println(Thread.currentThread().getName() + " invoked sendEmail()"); } } /** * ReentrantLock是典型的可重入锁 * 注意:锁几次释放几次 */ public class LockDemo { public static void main(String[] args) { Phone phone = new Phone(); Lock lock = new ReentrantLock(); for (int i = 0; i < 2; i++) { new Thread(() -> { lock.lock(); lock.lock(); try { phone.sendSMS(); }finally { lock.unlock(); lock.unlock(); } }, String.valueOf(i)).start(); } } }
3. 自旋锁:【循环,不断尝试】
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试去获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU;
案例:手写自旋锁
package com.example.code.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * 题目:实现一个自旋锁 * 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。 * * 通过CAS完成自旋锁,A线程先进来调用myLock方法自己持有5秒钟,B随后进来后发现当前线程拥有锁,表示null,所以只能通过自选等待,直到A释放锁后B随后抢到 */ public class SpinLockDemo { AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void myLock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + " come in o(n_n)o"); while (!atomicReference.compareAndSet(null, thread)) {} } public void myUnlock() { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(thread.getName() + " invoked myUnlock()"); } public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); for (int i = 0; i < 2; i++) { new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); },String.valueOf(i)).start(); } } }
4. 独占锁(写锁) / 共享锁(读锁) / 互斥锁:【ReentrantReadWriteLock】
独占锁:指该锁一次只能被一个线程持有。对ReentrantLock和Synchronized而言都是独占锁;
共享锁:该锁可以被多个线程持有;
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁;该锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
代码验证:
package com.example.code.lock; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; class MyCache { private volatile Map<String, Object> map = new HashMap<>(16); ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key, Object value) { readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " 线程正在写入:" + key); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName() + " 线程正在写入完成。"); }finally { readWriteLock.writeLock().unlock(); } } public void get(String key) { readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " 线程正在读取:"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 线程读取完成。" + map.get(key)); }finally { readWriteLock.readLock().unlock(); } } } /** * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行 * 但是,如果有一个线程想去写共享资源,就不应该有其他线程可以对该资源进行读或写 * 小总结: * 读 - 读能共存 * 写 - 读不能共存 * 写 - 写不能共存 * 写操作:原子 + 独占,整个过程必须是完整的统一体,中间不许被分割,被打断 */ public class ReadWriterLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 0; i < 2; i++) { final int temp = i ; new Thread(() -> { myCache.put(String.valueOf(temp),temp); },String.valueOf(i)).start(); } for (int i = 0; i < 2; i++) { final int temp = i ; new Thread(() -> { myCache.get(String.valueOf(temp)); },String.valueOf(i + i)).start(); } } }
二、CountDownLatch/CyclicBarrier/Semaphore使用过吗?
1. CountDownLatch:倒计时锁(人走完,才能关门)
让一些线程阻塞直到另一个线程完成操作后才被唤醒;CountDownLatch主要有两个方法,当一个过多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器变为0时,因调用await方法被阻塞的线程会被唤醒,继续执行。
案例一:教室关门
/** * @Author luliang * @Date 2020-01-02 16:00 * countDownLatch:倒计时锁 */ public class CountDownLatchDemo { 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(); },String.valueOf(i)).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName() + " ****班长最后关门****"); } }
案例二:枚举在企业中的正常用法
package com.example.code2019.Lock; import java.util.concurrent.CountDownLatch; enum CountryEnum { ONE(1,"齐"),TOW(2,"楚"),THREE(3,"燕"),FOUR(4,"赵"),FIVE(5,"魏"),SIX(6,"韩"); @Getter private Integer retCode; @Getter private String retMessage; CountryEnum(Integer retCode, String retMessage) { this.retCode = retCode; this.retMessage = retMessage; } public static CountryEnum forEach_CountryEnum(int index) { CountryEnum[] values = CountryEnum.values(); for (CountryEnum element : values) { return element; } return null; } } /** * @Author luliang * @Date 2020-01-02 16:00 * countDownLatch:倒计时锁 */ public class CountDownLatchDemo { public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 国,被灭"); countDownLatch.countDown(); },CountryEnum.forEach_CountryEnum(i).getRetMessage()).start(); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " ***秦统一***"); } }
2. CyclicBarrier:循环屏障(人到齐,才能开会)
CylicBarrier的字面意思是可循环(Cylic)使用的屏障(Barrier)。它要做的事情是,让一组线程达到一个屏障(也叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CylicBarrier的await()方法。
案例:开会
public class CylicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("***开始开会***"); }); for (int i = 0; i < 7; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 号签到"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
3. Semaphore:信号量/信号灯
信号量主要有两个目的,一个用于多个共享资源互斥使用,另一个用于并发线程数的控制;
案例:抢车位
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(5);//模拟5个车位 for (int i = 0; i < 10; i++) { final int temp = i; new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 号抢到车位"); TimeUnit.SECONDS.sleep(temp); System.out.println(Thread.currentThread().getName() + "号,停车" + temp + "秒后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } },String.valueOf(i)).start(); } } }
三、阻塞队列知道吗?
队列:先进先出、栈:先进后出
1. 队列 + 阻塞队列:
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞;当阻塞队列是满时,往队列里添加元素的操作将会被阻塞;
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从队列中移除一个或多个元素或者清空队列后使队列重新变得空闲起来并后续新增;
2. 为什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue:
好处是我们不需要关系什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
3. BlockingQueue的核心方法:
4. 架构梳理 + 种类分析
ArrayBlockingQueue:由数组组成的有界阻塞队列;
LinkedBlockingQueue:由链表结构组成的有界阻塞队列;(默认大小Integer.Max_VALUE)
PriorityBlockingQueue:支持优先级排序的无界阻塞队列;
DelayQueue:使用优先级队列实现的延迟无界阻塞队列;
SychronousQueue:不存储元素的队列,也即单个元素的队列;
LinkedTransferQueue:由链表结构组成的无界阻塞队列;
LinkedBlockingDeque:由链表结构组成的双向阻塞队列;
5. 用在哪里
5.1 生产者消费者模式:
sync --> wait --> notify
lock --> await --> single
案例一:传统版:ReentrantLock、单个Condition
class ShareDate { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws Exception { lock.lock(); try { //1.判断 while (number != 0) { //等待,不能生产 condition.await(); } //2.干活 number++; System.out.println(Thread.currentThread().getName() + " " + number); //3.通知唤醒 condition.signalAll(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void decrement() throws Exception { lock.lock(); try { //1.判断 while (number == 0) { //等待,不能生产 condition.await(); } //2.干活 number--; System.out.println(Thread.currentThread().getName() + " " + number); //3.通知唤醒 condition.signalAll(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } /** * 题目:一个初始值为0的变量,两个线程对其交替操作,一个人加1一个人减1,来5轮 * * 高内聚、低耦合的前提下,线程操作资源类 * 判断、干活、唤醒、通知 * 严防多线程环境下的虚假唤醒 * * 1. 线程 操作 资源类 * 2. 判断 干活 通知 * 3. 防止多线程虚假唤醒机制 */ public class ProdConsumer_TraditionDemo { public static void main(String[] args) { ShareDate shareDate = new ShareDate(); for (int i = 0; i < 5; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { try { shareDate.increment(); } catch (Exception e) { e.printStackTrace(); } } },"A" + i).start(); new Thread(() -> { for (int j = 0; j < 5; j++) { try { shareDate.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"B" + i).start(); } } }
案例二:传统版:ReentrantLock、多个Condition
package com.example.code2019.Lock; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 题目:synchronized和lock有什么区别?用新的lock有什么好处?你具体说说 * 1. 原始构成: * synchronized是关键字属于JVM层面锁,底层通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调用wait/notify等方法 * Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁; * 2. 使用方法: * synchronized不需要用户去手动释放锁,当synchronized代码执行完毕后系统会自动让线程释放对锁的占用 * ReentrantLock则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成。 * 3. 等待是否可中断: * synchronized不可中断,除非抛出异常或者正常运行完成 * ReentrantLock可中断,1.设置超时时间 tryLock(long timeout, TimeUnit unit) * 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断 * 4. 加锁是否公平: * synchronized非公平锁 * ReentrantLock两者都可以,默认公平锁,构造方法可传入Boolean值,true为公平锁,false为非公平锁 * * 5. 绑定多个条件Condition * synchronized没有 * ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么唤醒全部线程。 * *================================================================================================================== * 题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下: * AA打印5次,BB打印10次,cc打印15次,...... 来10轮 */ class ShareResource { private int number = 1; //A:1 B:2 C:3 private Lock lock = new ReentrantLock(); private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); private Condition c3 = lock.newCondition(); public void print5() { lock.lock(); try { //1. 判断 while (number != 1) { c1.await(); } //2. 干活 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } //3. 通知 number = 2 ; c2.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void print10() { lock.lock(); try { //1. 判断 while (number != 2) { c2.await(); } //2. 干活 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } //3. 通知 number = 3 ; c3.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void print15() { lock.lock(); try { //1. 判断 while (number != 3) { c3.await(); } //2. 干活 for (int i = 0; i < 15; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } //3. 通知 number = 1 ; c1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } public class SyncAndReentrantLockDemo { public static void main(String[] args) { ShareResource shareResource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print5(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print10(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print15(); } },"C").start(); } }
案例三:阻塞队列版(不用手动控制阻塞、唤醒)
package com.example.code2019.Lock; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyResource { private volatile boolean FLAG = true; //开启/关闭,生产者与消费者 private AtomicInteger atomicInteger = new AtomicInteger(); BlockingQueue<String> blockingQueue = null; public MyResource(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; System.out.println(blockingQueue.getClass().getName()); } public void myProd() throws Exception{ String data = null; boolean retValue ; while (FLAG) { data = atomicInteger.incrementAndGet() +""; retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); if (retValue) { System.out.println(Thread.currentThread().getName() + " 插入队列" + data + "成功。"); }else { System.out.println(Thread.currentThread().getName() + " 插入队列" + data + "失败。"); } TimeUnit.MILLISECONDS.sleep(100); } System.out.println(Thread.currentThread().getName() + " 生产者停止,FLAG = false"); } public void myConsumer() throws Exception{ String result = null ; while (FLAG) { result = blockingQueue.poll(2, TimeUnit.SECONDS); if (result == null || result.equalsIgnoreCase("")) { FLAG = false; System.out.println(Thread.currentThread().getName() + " 超过2秒没有取得数据,消费者退出。"); return; } System.out.println(Thread.currentThread().getName() + " 消费队列" + result + "成功。"); } } public void stop() { this.FLAG = false; } } /** * @Author luliang * @Date 2020-01-03 16:32 * volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用 */ public class ProdConsumer_BlockQueueDemo { public static void main(String[] args) { MyResource myResource = new MyResource(new ArrayBlockingQueue<>(5)); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "生产者启动"); try { myResource.myProd(); } catch (Exception e) { e.printStackTrace(); } },"Prod").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "消费者启动"); try { myResource.myConsumer(); } catch (Exception e) { e.printStackTrace(); } },"Consumer").start(); //线程暂停一会 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } myResource.stop(); System.out.println(" 2秒钟时间到,活动结束"); } }
5.2 线程池:构造注入,传接口
获得线程的4种方法:Thread、Runnable、Callable(FutureTask中间人)、线程池
案例一:Callable获取线程
package com.example.code2019.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; class MyThread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("***come in call***"); TimeUnit.SECONDS.sleep(5); return 1024; } } /** * @Author luliang * @Date 2020-01-03 19:08 * 1. FutureTask类实现了Runnable接口,构造可以传入Callable,相当于中间人,Thead可以接收Runnable,所以也能接收FutureTask * 2. 建议将futureTask.get(),放在最后,如果放在前面,线程没有执行完毕,还没有返回值的话,会造成线程阻塞; * 3. 如果非常需要线程执行结果,可以使用while (!futureTask.isDone()){},来组合使用,相当于“自旋锁” * 4. 如果多个个线程传入同一个FutureTask,Callable接口的call()方法只会执行一次;如果要想call()多次执行,传入不同的FutureTask */ public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new MyThread()); new Thread(futureTask, "AA").start(); new Thread(futureTask, "BB").start(); System.out.println(Thread.currentThread().getName() + "正在执行"); int result01 = 10 ; while (!futureTask.isDone()){} Integer integer = futureTask.get(); System.out.println(result01 + integer); } }
5.3 消息中间件:
四、线程池用过吗?ThreadPoolExecutor谈谈你的理解?
1. 为什么用线程池,优势:
线程池做的主要工作是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,在从队列中取出任务来执行。
它的主要特点:线程复用、控制最大并发数、管理线程。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控;
2. 线程池如何使用:【ThreadPoolExcutor + Queue】
架构说明:
java中的线程池是通过Excutor框架实现的,该框架中用到了Excutor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
编码实现:
了解:
Executors.newScheduledThreadPool():待时间调度的
Executors.newWorkStealingPool():java8新增,使用目前机器上可用的处理器作为它的并行级别
重点:
Executors.newFixedThreadPool(1):一池固定式线程 //执行长期任务,性能好很多
Executors.newSingleThreadExecutor():一池单个线程 //一个任务一个任务执行的场景
Executors.newCachedThreadPool():一池多线程 //执行很多短期异步的小程序或负载较轻的服务器
ThreadPoolExcutor:
3. 线程池的几个重要参数介绍:
七大参数:
public ThreadPoolExecutor(int corePoolSize, 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. corePoolSize:线程池中的常驻核心线程数;
在创建了线程池后,当有请求任务,就会安排池中的线程去执行请求任务,近似理解为今日当值线程;
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
2. maixmumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于1;
3. keepAliveTime:多余空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁,直到只剩下corePoolSize个线程为止;
4. unit:keepAliveTime的单位;
5. workQueue:任务队列,被提交但尚未执行的任务;
6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程
7. handle:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数
/** * 一个线程池,可以理解为银行网点 * @param corePoolSize 核心数,(今日值日窗口) * @param maximumPoolSize 最大窗口,(最大窗口) * @param keepAliveTime 空闲线程存活时间(没有新请求时,空闲线程的存活时间(maximumPoolSize-corePoolSize)) * @param unit keepAliveTime的单位,毫秒/秒/分钟 * @param workQueue 阻塞队列(候客区) * @param threadFactory 线程工厂(银行网点统一的东西,logo/胸卡/制服) * @param handler 拒绝策略(拒绝后面的请求) */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }
4. 说说线程池的底层工作原理:
五、线程池用过吗?生产上你如何设置合理参数?
1. 线程池的拒绝策略你谈谈:
是什么?
等待队列也排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置拒绝策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新的业务流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失这是一种最好的方案。
以上拒绝策略均实现了RejectedExecutionHandle接口;
2. 你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用哪个多?超级大坑
答案是一个都不用,我们生产上只使用自定义的。
Executors中JDK已经给你提供了,为什么不用?
3. 你在工作中如何使用线程池的,是否自定义过线程池的使用
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
package com.example.code.threadpool; import java.util.concurrent.*; /** * 最大线程(5)+ 队列(3) * new ThreadPoolExecutor.AbortPolicy():超过,直接报异常 * pool-2-thread-4 办理业务 * pool-2-thread-2 办理业务 * pool-2-thread-3 办理业务 * java.util.concurrent.RejectedExecutionException: * Task com.example.code.threadpool.ThreadPoolExcutorDemo$$Lambda$1/1854731462@27bc2616 rejected from * java.util.concurrent.ThreadPoolExecutor@3941a79c[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] * pool-2-thread-1 办理业务 * pool-2-thread-5 办理业务 * pool-2-thread-4 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-2 办理业务 * new ThreadPoolExecutor.CallerRunsPolicy():超过,既不抛弃也不丢弃任务,返回调用线程处理 * main 办理业务 * pool-2-thread-4 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-2 办理业务 * pool-2-thread-1 办理业务 * pool-2-thread-5 办理业务 * main 办理业务 * pool-2-thread-4 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-2 办理业务 * new ThreadPoolExecutor.DiscardOldestPolicy():超过,丢弃最久的,执行最新的 * pool-2-thread-1 办理业务 * pool-2-thread-5 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-2 办理业务 * pool-2-thread-4 办理业务 * pool-2-thread-2 办理业务 * pool-2-thread-1 办理业务 * pool-2-thread-5 办理业务 * new ThreadPoolExecutor.DiscardPolicy():超过,直接丢弃 * pool-2-thread-2 办理业务 * pool-2-thread-5 办理业务 * pool-2-thread-4 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-1 办理业务 * pool-2-thread-3 办理业务 * pool-2-thread-2 办理业务 * pool-2-thread-5 办理业务 */ public class ThreadPoolExcutorDemo { public static void main(String[] args) { Executors.newSingleThreadExecutor(); ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 2, 5, 1L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); try { for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " 办理业务"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); } }catch (Exception e) { e.printStackTrace(); }finally { threadPool.shutdown(); } } }
4. 合理配置线程池你是如何考虑的?
System.out.println("CPU核心数:" + Runtime.getRuntime().availableProcessors());
CPU密集型:
CPU密集型的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力有限。
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核心数 + 1个线程 的线程池。
IO密集型:
第一种:
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如:CPU核心数*2
第二种:
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会造成大量的CPU运算能力浪费在等待。所以,在IO密集型任务中使用多线程可以大大加速程序运行,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型,大部分线程都被阻塞,顾需要多配置线程数:
参考公式:CPU核心数 / (1 - 阻尼系数) 阻塞系数在0.8~0.9之间 例如:8 / (1-0.9) = 80个线程数
六、死锁编码及定位分析?
1. 是什么?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源造成的一种互相等待的现象,若无外力干涉它们都将无法推进下去,如果系统资源充足,进程的请求资源都能够得到满足,死锁出现的可能性就很低,否则就会因为争夺有限的资源而陷入死锁。
产生死锁的原因:系统资源不足、进程运行推进的顺序不合适、资源分配不当
2. 代码:
package com.example.code.lock; /** * 死锁是指两个或两个以上的进程在执行过程中,因争夺资源造成的一种互相等待的现象,若无外力干涉它们都将无法推进下去 */ class HoldLockThread implements Runnable { private String lockA; private String lockB; public HoldLockThread(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockA + " 尝试获取:" + lockB); synchronized (lockB) { System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockB + " 尝试获取:" + lockA); } } } } public class DeadLockDemo { public static void main(String[] args) { new Thread(new HoldLockThread("lockA", "lockB")).start(); new Thread(new HoldLockThread("lockB", "lockA")).start(); } }
3. 解决:jps、jstack
jps命令定位进程号:jps -l
jstack找到死锁查看:jstack 4820
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)