)
1. JUC常见类1.1 Callable接口Callable是⼀个interface相当于把线程封装了⼀个返回值方便程序猿借助多线程的方式计算结果。案例创建线程计算123...1000package practice.thread; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Demo23 { public static void main(String[] args) throws Exception{ CallableInteger callablenew CallableInteger() { Override public Integer call() throws Exception{ int result0; for(int i1;i1000;i){ resulti; } return result; } }; FutureTaskInteger futureTasknew FutureTask(callable); Thread tnew Thread(futureTask); t.start(); System.out.println(futureTask.get()); } }结果和Runnable的区别对比项RunnableCallable方法void run()V call() throws Exception返回值无返回值 void有返回值泛型 V异常run()不能抛出受检异常内部只能 try‑catch 捕获call()可以直接抛出 Exception不用内部捕获配合 Threadnew Thread(runnable)直接传入不能直接给 Thread必须包装FutureTask包位置java.lang.Runnablejava.util.concurrent.Callable属于 JUC源码// Runnable public interface Runnable { void run(); } // Callable public interface CallableV { V call() throws Exception; }1.2 ReentrantLock类这个也是可重入锁和synchronized类似实现互斥效果保证线程安全。基本用法lock()上锁等不到就死等。trylock(超时时间)上锁等不到就等一段时间后放弃。unlock()解锁和synchronized的区别对比维度synchronizedReentrantLock实现层面JVM 底层实现 (C)隐式锁JDK 代码实现 (AQS)显式锁java.util.concurrent.locks加解锁方式自动进入块加锁出块自动释放异常也会自动解锁手动lock()上锁unlock()解锁unlock 必须写在 finally忘记释放会死锁锁模式只能非公平锁支持公平锁 / 非公平锁构造参数true开启公平获取锁行为获取不到锁就死等阻塞1.lock()死等2.tryLock()非阻塞拿不到直接返回 false3.tryLock(时间,单位)超时放弃4.lockInterruptibly()可被中断等待唤醒wait() / notify() / notifyAll()唤醒随机一个或者全部等待线程搭配Condition可以创建多个条件队列精准分组唤醒部分线程锁降级 / 升级不支持支持锁降级写锁→读锁读写锁 ReentrantReadWriteLock不支持锁升级性能JDK1.6 之后做锁优化 (偏向 / 轻量 / 重量级)性能和 ReentrantLock 接近竞争激烈场景灵活性更好1.3 原子类原⼦类内部用的是CAS实现所以性能要比加锁实现i高很多。有以下原子类AtomicBooleanAtomicIntegerAtomicIntegerArrayAtomicLongAtomicReferenceAtomicStampedReference自增/减Java 代码作用对应原子类方法返回值i先获取旧值再自增getAndIncrement()返回自增之前的值i先自增再获取新值incrementAndGet()返回自增之后的值i--先获取旧值再自减getAndDecrement()返回自减之前的值--i先自减再获取新值decrementAndGet()返回自减之后的值代码示例AtomicInteger ai new AtomicInteger(0); // i int a ai.getAndIncrement(); // a0ai变成1 // i int b ai.incrementAndGet(); // b2ai变成2 // i-- int c ai.getAndDecrement(); // c2ai变成1 // --i int d ai.decrementAndGet(); // d0ai变成01.4 线程池频繁的创建和销毁线程会比较低效所以需要线程池如果有线程不用了就把它放入线程池等再使用的时候拿出来就不用重复的销毁和创建了。1. ExecutorService和Executors1ExecutorServiceExecutorService表示⼀个线程池实例ExecutorService的submit方法能够向线程池中提交若干个任务import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { // 创建固定大小线程池核心最大线程数10 ExecutorService pool Executors.newFixedThreadPool(10); // 提交Runnable任务 pool.submit(new Runnable() { Override public void run() { System.out.println(hello); } }); // 关闭线程池 pool.shutdown(); } }2ExecutorsExecutors是⼀个工厂类能够创建出几种不同风格的线程池Executors 创建线程池四种方式newFixedThreadPool创建固定线程数的线程池newCachedThreadPool创建线程数目动态增长的线程池newSingleThreadExecutor创建只包含单个线程的线程池newScheduledThreadPool支持延迟执行、周期性重复执行任务相当于进阶版 TimerExecutors 是工具类提供静态方法快速创建线程池本质是对 ThreadPoolExecutor 的封装。2. ThreadPoolExecutorThreadPoolExecutor提供更多的参数可以进行细节的参数搭配1构造方法的参数将线程池比作公司corePoolSize核心线程数正式员工默认不回收maximumPoolSize最大线程数 核心线程 非核心线程临时工keepAliveTime非核心线程允许的空闲时长空闲超时回收unitkeepAliveTime 的时间单位workQueue存储任务的阻塞队列threadFactory线程工厂负责创建线程对象RejectedExecutionHandler拒绝策略线程池满负荷时对新任务的处理策略AbortPolicy()抛出异常默认CallerRunsPolicy()交给提交任务的调用者线程执行DiscardOldestPolicy()丢弃队列中最旧任务DiscardPolicy()丢弃新来的任务代码案例package practice.thread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Demo27 { public static void main(String[] args) { ThreadPoolExecutor poolnew ThreadPoolExecutor(5,10,0,TimeUnit.SECONDS,new ArrayBlockingQueue(100),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy()); for(int i1;i200;i){ int taskIdi; Runnable Runnablenew Runnable() { Override public void run(){ System.out.println(执行任务taskId); } }; pool.submit(Runnable); } } }结果结果解析由于没有写sleep线程就是瞬间运行的这里设置的核心线程有5个最大线程10个包括核心线程拒绝策略是丢弃最新任务再加上是并发执行没有先后顺序一部分任务会被丢弃如果加上sleep(1000)就会按照先后的顺序执行队列满的时候会丢弃新的任务即任务 1‑110 应当执行111‑200 应当全部丢弃。3. 线程池工作流程1.5 信号量semaphore通常表示资源个数本质是计数器比如在一个停车场车辆出去相当于资源-1进来就是资源1如果发现没有空位了要么就正常阻塞要么另外找地方停车。锁相当于值为1的特殊信号量。代码package practice.thread; import java.util.concurrent.Semaphore; /** * Semaphore信号量示例最多允许3个线程同时访问资源 */ public class Demo28 { public static void main(String[] args) { // 许可3同一时刻最多3个线程执行临界区 Semaphore semaphore new Semaphore(3); for (int i 1; i 5; i) { int taskId i; new Thread(() - { try { System.out.println(线程 taskId 尝试获取许可); semaphore.acquire(); System.out.println(线程 taskId 获取到许可执行业务); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { semaphore.release(); System.out.println(线程 taskId 释放许可); } }).start(); } } }结果1.6 CountDownLatch同时等待N个任务执行结束。好比比赛需要所有人到终点才会公布成绩。代码package practice.thread; import java.util.concurrent.CountDownLatch; public class Demo26 { public static void main(String[] args) throws Exception{ CountDownLatch latchnew CountDownLatch(10); Runnable runnablenew Runnable(){ Override public void run(){ try { System.out.println(开始执行当前进程); Thread.sleep((long)Math.random()*10000); System.out.println(当前进程运行结束); latch.countDown(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }; for(int i0;i10;i){ new Thread(runnable).start(); } latch.await(); System.out.println(程序运行结束); } }结果:1.7 可能的面试题1. 线程同步的方式有哪些synchronized、ReentrantLock、Semaphore 都可以实现线程同步。2. 为什么有了 synchronized还需要 JUC 的 Lock以 ReentrantLock 举例1锁释放synchronized 由 JVM 自动获取、自动释放锁ReentrantLock 需要手动lock()加锁、手动unlock()释放锁编码更灵活。2锁申请行为synchronized 获取锁失败会一直阻塞死等ReentrantLock 提供tryLock()可以设置等待时间超时直接放弃获取锁。3公平 / 非公平锁synchronized 只能是非公平锁ReentrantLock 默认非公平锁构造方法传入true可以开启公平锁。4等待唤醒机制synchronized 使用wait() / notify()唤醒随机一个等待线程无法指定ReentrantLock 搭配Condition可以精准唤醒指定条件下的线程。3. AtomicInteger 的实现原理底层基于 CASCompare And Swap乐观锁机制。伪代码class AtomicInteger { private int value; public int getAndIncrement() { int oldValue value; // CAS比较旧值如果没变则更新为 oldValue1失败就循环重试 while ( CAS(value, oldValue, oldValue1) ! true) { oldValue value; } return oldValue; } }1.8 线程安全的集合类1. 多线程环境使用ArrayList1自己使用同步机制(synchronized或者ReentrantLock)前面博客和本文1.2内容有介绍。2Collections.synchronizedList(new ArrayList)synchronizedList是标准库提供的⼀个基于synchronized进⾏线程同步的List.synchronizedList的关键操作上都带有synchronized即返回的synchronizedList 的关键方法(add,get,set .... 都是带有synchronized 关键字的)。3使用CopyOnWriteArrayList当我们往⼀个容器添加元素的时候不直接往当前容器添加而是先将当前容器进⾏Copy复制出⼀个新的容器然后新的容器里添加元素。添加完元素之后再将原容器的引用指向新的容器。读操作直接读取原数组不加锁多个线程可以并发读。写操作add/set/remove不直接修改原有数组。先把旧数组复制一份得到新数组。在新数组上完成新增 / 删除修改。修改完成后把对象内部引用指向新数组。✅ 优点读多写少场景性能很高读不需要加锁读线程完全不阻塞。❌ 缺点内存开销大写操作要完整复制数组数组越大复制占用内存、CPU 越高。数据一致性问题弱一致性写完成之前读线程读到的依旧是旧数组数据不能立刻看到最新写入的数据。2. 多线程环境使用队列ArrayBlockingQueue基于数组实现的阻塞队列LinkedBlockingQueue基于链表实现的阻塞队列PriorityBlockingQueue基于堆实现带优先级的阻塞队列TransferQueue最多存放一个元素的阻塞队列3. 多线程环境使用哈希表HashMap 是线程不安全的并发场景下直接使用会出现数据错乱、死循环等问题。多线程环境下可选择 Hashtable 或者 ConcurrentHashMap。1HashtableHashtable 的实现方式很简单在 put、get 等核心方法上加 synchronized。public synchronized V put(K key, V value) {} public synchronized V get(Object key) {}锁对象为 this也就是整个 Hashtable 对象全局只有一把锁。缺点锁冲突严重只要多线程操作该哈希表无论访问的是哪个桶的数据都会触发锁竞争并发性能差。size 属性依靠 synchronized 同步维护每次修改都要竞争全局锁效率低下。扩容效率低触发扩容时由当前这一个线程一次性完成全部元素拷贝迁移元素数量大时持有锁时间很长阻塞其他所有线程操作。一把锁锁住整个哈希表并发性能差JDK 已经不推荐使用。2ConcurrentHashMap以 JDK1.8 版本为例针对多线程场景专门优化的线程安全哈希表对比 Hashtable 做了大量优化。① 锁粒度优化锁桶锁链表头节点写操作使用 synchronized不再锁整个 Map 对象而是锁哈希桶以每个链表的头结点作为锁对象。不同哈希桶的数据线程之间互不阻塞可以并发修改只有多个线程恰好操作同一个哈希桶的数据时才会发生锁竞争极大降低锁冲突概率。读操作不加锁依靠 volatile 保证内存可见性读取到最新的数据。② size 计数基于 CAS 更新不再使用synchronized维护 size借助 CAS 原子操作完成 size 的增减更新规避重量级锁带来的开销。③ 扩容化整为零多线程协助迁移Hashtable 是单线程一次性搬运全部数据ConcurrentHashMap 采用渐进式扩容触发扩容的线程先创建容量更大的新数组不会一次性搬运全部元素每次只搬运一小部分桶的数据。扩容过程中新数组、老数组会同时存在。后续执行get/put/remove的其他线程都会协助参与数据搬家每个线程负责搬运一部分。全部桶迁移完成之后才删除旧数组。扩容阶段新的插入操作只会写入新数组。查询操作需要同时查询老数组和新数组。优点每次只搬少量数据线程持有锁时间很短不会长时间阻塞其他线程充分利用多线程提升扩容效率。Hashtable 与 ConcurrentHashMap对比对比项HashtableConcurrentHashMap(JDK1.8)锁粒度锁整个对象 this全局一把锁锁哈希桶链表头节点桶级别锁锁冲突任意线程操作任意元素都会竞争锁仅操作同一个桶才发生锁竞争读操作加 synchronized 锁不加锁volatile 保证可见性size 维护synchronized 全局锁CAS 原子更新扩容方式单线程一次性全部拷贝迁移渐进式扩容多线程协助分批迁移新老数组共存4. 并发哈希表可能的面试题1ConcurrentHashMap的读是否要加锁为什么读操作不需要加锁。目的降低锁冲突概率借助volatile关键字保证可以读取到修改后的最新数据。2介绍下ConcurrentHashMap的锁分段技术锁分段是JDK1.7的实现JDK1.8已经废弃。 将若干哈希桶划分为一个Segment段对每个段单独加锁只有线程访问的数据落在同一个Segment时才会发生锁竞争降低锁冲突。3ConcurrentHashMap在JDK1.8做了哪些优化取消Segment分段锁改为锁哈希桶以链表头结点作为锁对象。底层结构数组链表/红黑树链表长度≥8转为红黑树。size计数使用CAS更新。扩容采用渐进式多线程协助迁移。读操作不加锁依靠volatile保证可见性。4Hashtable、HashMap、ConcurrentHashMap三者区别HashMap线程不安全key允许为null。Hashtable线程安全使用synchronized锁整个对象并发效率低key不允许为null。ConcurrentHashMap线程安全JDK1.8锁链表头节点锁粒度细使用CAS扩容优化key不允许为null。2. 死锁2.1 死锁是什么多个线程被阻塞其中一个或多个线程都等待某个资源的释放线程被无期限地阻塞程序无法正常结束资源lock1已经分配给线程 T1T1 现在想要申请lock2资源lock2已经分配给线程 T2T2 现在想要申请lock1形成循环等待闭环两个线程都无法获取需要的资源进入死锁。2.2 如何避免死锁死锁产生的四个必要条件互斥资源同一时间只能给到一个线程占有且等待T1 拿着 lock1同时申请 lock2T2 拿着 lock2同时申请 lock1不可剥夺资源不能强行抢夺循环等待T1→R2→T2→R1→T1形成环路就是上图闭环箭头。当上述四个条件都成⽴的时候便形成死锁。死锁的情况下如果打破上述任何⼀个条件便可让死锁消失。其中破坏死锁简单的措施就是破解循环等待这里模拟死锁代码package practice.thread; public class Demo29 { public static void main(String[] args) throws Exception { Object lock1 new Object(); Object lock2 new Object(); Thread t1 new Thread(() - { synchronized (lock1) { try { Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } synchronized (lock2) { } } }); t1.start(); Thread t2 new Thread(() - { synchronized (lock2) { try { Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } synchronized (lock1) { } } }); t2.start(); System.out.println(程序结束); } }结果这里程序并没有真正结束如果想正常运行则把t2的lock1和lock2调换位置即可