ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

带你理解JavaEE多线程基础(下)

带你理解JavaEE多线程基础(下) 1. volatile关键字1.1 volatile保证内存可见性1写⼊volatile修饰的变量时改变线程⼯作内存中volatile变量副本的值将改变后的副本的值从⼯作内存刷新到主内存2在读取volatile修饰的变量时从主内存中读取volatile变量的最新值到线程的⼯作内存中从⼯作内存中读取volatile变量的副本案例没有加volatilepackage practice.thread; import java.util.Scanner; public class Demo11 { static class Counter { public int flag 0; } public static void main(String[] args) { Counter counter new Counter(); Thread t1 new Thread(() - { while (counter.flag 0) { // 什么也不做 } System.out.println(循环结束); }); Thread t2 new Thread(() - { Scanner sc new Scanner(System.in); System.out.println(输入一个整数:); counter.flag sc.nextInt(); }); t1.start(); t2.start(); } }1.2 volatile不保证原子性package practice.thread; public class Demo9 { public volatile static int count 0; public static void main(String[] args) throws InterruptedException { Thread t1new Thread(()-{ for(int i0;i50000;i){ count; } }); Thread t2new Thread(()-{ for(int i0;i50000;i){ count; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }给count加上volatile发现结果是小于100000的2. wait()和notify()线程是抢占式执行执行顺序不可控wait / notify / notifyAll用来协调多个线程执行顺序做线程间通信。重点wait()、notify()、notifyAll()是 Object 的方法不是 Thread 方法。2.1 waitwait 做三件事当前线程进入该对象的等待队列暂停执行释放当前持有的 synchronized 锁被唤醒后重新竞争获取锁拿到锁之后才继续往下执行强制要求wait 必须写在 synchronized 同步块内否则直接抛IllegalMonitorStateExceptionwait 结束等待的 4 种条件其它线程调用该对象 notify()唤醒线程wait(long timeout)超时时间到自动唤醒线程被interrupt()中断抛出InterruptedException虚假唤醒因此业务代码 wait 必须套在while循环不能用 ifObject locker new Object(); synchronized (locker) { System.out.println(等待中); locker.wait(); System.out.println(等待结束); }代码现象执行wait()线程就卡住等待需要别的线程 notify 唤醒。2.2 notify也必须在synchronized同步块内调用。作用唤醒该对象等待队列里的某一个线程多个线程等待时随机选一个没有先来后到。重要调用notify()不会立刻释放锁要等执行完同步代码块退出synchronized锁才释放被唤醒的线程才可以去抢锁。关键点等待线程和唤醒线程必须使用同一个对象锁 locker。2.3 notify All方法行为notify()唤醒等待队列随机 1 个线程notifyAll()唤醒等待队列全部线程再去竞争锁注意优先 notify只有确定需要全部唤醒才用 notifyAll。2.4 wait与sleep对比wait()sleep()所属Object 实例方法Thread 静态方法锁要求必须在 synchronized 内部调用会释放锁不需要锁不释放锁用途线程之间通信协调单纯让线程休眠固定时长唤醒方式notify /notifyAll/ 超时 /interrupt时间到自动结束 /interrupt 打断抛异常2.5 简单案例package Thread; import java.util.Scanner; public class Demo17 { public static void main(String[] args) throws InterruptedException { Object object new Object(); Thread t1 new Thread(() - { System.out.println(t1 开始); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(t1 结束); }); Thread t2 new Thread(() - { System.out.println(t2 开始); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(t2 结束); }); Thread t3 new Thread(() - { System.out.println(t3 开始); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(t3 结束); }); Thread t4 new Thread(() - { System.out.println(t4 开始); System.out.println(输入任意内容唤醒线程); Scanner scanner new Scanner(System.in); scanner.nextLine(); synchronized (object) { object.notifyAll(); } System.out.println(t4 结束); }); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); System.out.println(所有线程执行完毕); } }结果3. 单例模式这是常见的设计模式单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽不会创建出多个实例3.1 饿汉模式类加载的同时创建实例package Thread; // 单例模式 // 饿汉式 // 线程安全 // 可以直接使用 class Singleton{ private static Singleton instancenew Singleton(); public static Singleton getInstance(){ return instance; } private Singleton(){ } }3.2 懒汉模式类加载的时候不创建实例第⼀次使⽤的时候才创建实例class Singleton { private static Singleton instance null; private Singleton() {} public static Singleton getInstance() { if (instance null) { instance new Singleton(); } return instance; } }改进解释看3.4package Thread; // 单例模式 // 懒汉式 // 线程不安全 // 可以使用synchronized关键字 // 可以使用Double Check LockingDCL模式 class Singleton { private static volatile Singleton instance null; public static Object locker new Object(); public static Singleton getInstance() { if (instance null) { synchronized (locker) { if (instance null) { instance new Singleton(); } } } return instance; } private Singleton() { } } public class Demo19 { public static void main(String[] args) { Singleton s1 Singleton.getInstance(); Singleton s2 Singleton.getInstance(); System.out.println(s1 s2); // Singleton s3new Singleton(); } }3.3 应用场景解析一、场景前提单例类私有构造方法执行磁盘 IO读取 100GB 超大文件载入内存缓存。 业务目标全局只加载 1 次所有线程共享同一份内存数据。 使用实现未加锁原始懒汉式单例无任何同步控制二、多线程并发调用产生 4 类核心致命问题内存资源耗尽触发 OOM 崩溃多个线程同时判断instancenull各自执行new创建独立对象每个实例构造方法都会完整加载一份 100G 数据进堆内存2 个线程占用 200G、3 个线程占用 300G超出物理内存上限JVM 内存溢出服务宕机单例 “全局唯一一份内存” 的目标完全失效磁盘 IO 重复读写耗时成倍叠加IO 性能雪崩磁盘机械硬盘速度慢单次读取 100G 耗时 TN 个线程并发全量读取总耗时≈N×T业务响应时间大幅变长多线程争抢磁盘通道频繁寻址、IO 队列拥堵实际读取速度比串行更差磁盘带宽被占满拖累数据库、日志写入等其他业务操作系统底层资源句柄泄露每次加载都会打开磁盘文件每个实例持有独立文件描述符大量并发创建实例耗尽系统最大文件句柄后续文件读写、Socket 网络全部报错系统瘫痪业务数据不一致风险多线程重复加载间隙如果磁盘源文件发生修改不同实例缓存不同版本的 100G 数据业务读取结果混乱产生隐蔽难排查 bug三、两种可靠解决方案按需选择方案 1饿汉式单例【追求绝对安全推荐】类加载阶段由 JVM 保证只初始化一次不需要synchronized、不需要volatile无锁开销项目启动时一次性加载 100G 数据。适用项目启动后一定会使用这份大文件能够接受启动阶段的内存占用。方案 2DCL 双重检查锁 volatile 懒汉单例【延迟加载】外层if判空实例已创建直接返回避免频繁锁竞争Singleton.class类锁 内层二次判空防止并发重复创建对象volatile禁止对象初始化指令重排序避免拿到半初始化残缺对象适用不确定是否一定会用到 100G 数据希望使用时才加载节省启动内存。四、一句话总结无同步的懒汉单例多线程加载超大磁盘资源会重复创建重量级对象造成 OOM、IO 耗时翻倍、句柄耗尽、数据版本错乱必须使用线程安全的单例保证实例只初始化一次。3.4 DCL 双重检查 vs 直接全包锁DCL 双重检查锁标准最优public static Singleton getInstance() { // 第一层外层判断 if(instancenull){ synchronized (Singleton.class){ // 第二层内层判断 if(instancenull){ instancenew Singleton(); } } } return instance; }直接把整个判断逻辑全包在锁里面低效加锁public static Singleton getInstance() { synchronized (Singleton.class){ if(instancenull){ instancenew Singleton(); } } return instance; }最大区别锁的竞争频率天差地别1写法 B 全包锁的问题不管 instance 有没有创建完成每一次调用 getInstance() 都必须先抢占类锁第一次初始化所有线程排队抢锁没问题后续 99.9% 的请求对象早就存在了依然要执行加锁、解锁操作。锁操作涉及内核态切换、CAS 竞争、阻塞队列维护高并发下大量调用会产生严重性能损耗。2写法 A DCL 外层判断的作用外层 if(instance null) 做过滤对象已经实例化 → 直接 return完全不进入同步代码块不加任何锁只有第一次还未创建的时候才会去争抢锁。绝大多数业务场景下getInstance() 都是重复获取已存在的实例DCL 几乎无锁开销。内层第二个 if 的作用容易忽略假设没有内层判断if(instancenull){ synchronized (Singleton.class){ instancenew Singleton(); } }场景线程 A、线程 B 同时通过外层 if 判断都为 nullA 先拿到锁创建完毕释放锁B 接着拿到锁又会执行一次 new再次创建第二个对象单例失效。内层二次判空就是为了拦截这种情况锁内再校验一次是否已经被别的线程初始化。4. 阻塞队列阻塞队列是⼀种特殊的队列也遵守先进先出的原则当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素。当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素。阻塞队列典型的应用场景是生产者消费者模型4.1 生产者消费者模型⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯⽽通过阻塞队列来进⾏通讯所以⽣产者⽣产完数据之后不⽤ 等待消费者处理直接扔给阻塞队列消费者不找⽣产者要数据⽽是直接从阻塞队列⾥取。优点减少资源竞争生产者、消费者职责分离互不抢占执行逻辑通过队列解耦同步冲突大幅降低多线程竞争锁的概率。补充总结1阻塞队列put、take依旧争抢同一把锁入队、出队是串行操作并没有消除锁竞争。2旧模式生产者消费者混杂争抢业务锁锁内部包含耗时业务锁持有时间长锁冲突激烈。3新模式竞争收拢到阻塞队列生产者竞争入队消费者竞争出队锁内只执行队列简短存取操作执行完立即释放锁业务逻辑放到锁外部消费者拿到任务后可以并发执行业务。4本质没有消灭锁只是缩短锁持有时间将业务层锁竞争转移到队列层降低冲突开销。背诵一句话阻塞队列没有消除锁只是把竞争收拢到队列锁内只做简短存取业务放到锁外执行缩短锁占用时间减少锁竞争。降低耦合生产者只负责生产消费者只负责消费双方互不依赖互不干涉修改其中一方不会影响另一方。削峰填谷瞬时高并发任务先缓冲存放在队列防止瞬间压垮下游流量低谷再逐步消费积压任务实现流量平滑。缺点系统复杂度上升多线程 队列阻塞 同步等待机制对比单线程逻辑调试难度变大更容易出现并发 Bug。多层队列带来额外开销队列缓冲链路层数过多会带来排队延迟、更多上下文切换如果是分布式队列还会引入网络 IO 开销。代码package Thread; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; public class Demo21 { public static void main(String[] args) throws InterruptedException { BlockingDequeString queuenew LinkedBlockingDeque(1000); //生产者线程 Thread producernew Thread(()-{ int count0; try { while(true){ queue.put( count); System.out.println(添加一个元素:count); count; // Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } }); //消费者线程 Thread consumernew Thread(()-{ try { while(true){ String elementqueue.take(); System.out.println(消费一个元素:element); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); } }4.2 自定义阻塞队列两个线程对一个变量修改修改操作不是原子的那么修改操作需要锁注意wait 返回之后一定要重新检查等待条件。if只会判断一次while被唤醒后循环重复判断规避虚假唤醒带来的逻辑错误。package Thread; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; public class Demo22 { static class MyBlockedQueueT{ private T item[]null; private int head0; private int tail0; private int size0; Object lockernew Object(); public MyBlockedQueue(int capacity){ item(T[])new Object[capacity]; } public void put(T elem) throws InterruptedException{ synchronized(locker){ while (size item.length) { locker.wait(); } item[tail] elem; tail (tail 1) % item.length; size; locker.notify(); } } public T take() throws InterruptedException{ synchronized(locker){ while (size 0) { locker.wait(); } T elem item[head]; head (head 1) % item.length; size--; locker.notify(); return elem; } } } public static void main(String[] args) throws InterruptedException { MyBlockedQueueString queuenew MyBlockedQueueString(1000); Thread profucernew Thread(()-{ int count0; while(true){ try{ queue.put( count); System.out.println(增加了一个元素:count); count; // Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } } }); Thread consumernew Thread(()-{ while(true){ try{ String elementqueue.take(); System.out.println(消费一个元素:element); Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } } }); profucer.start(); consumer.start(); profucer.join(); consumer.join(); } }结果5. 线程池与工厂设计模式5.1 线程池线程池线程的创建和销毁的频率提升且达到一定程度此时产生的开销无法忽视提前创建一批线程放在池子里统一管理任务来了直接复用池内已有线程不反复新建、销毁线程。线程池优点避免频繁创建销毁线程的开销线程创建、销毁是操作系统系统调用开销大线程池复用已存在的线程对象。便于控制并发数量限制最大线程数防止无限制创建线程耗尽 CPU、内存资源。任务排队缓冲多余任务存入队列实现削峰填谷配置拒绝策略保护系统。便于统一管理线程复用、监控、关闭不需要手动逐个管理 Thread 对象。为什么比手动 new Thread 快手动new Thread()向操作系统申请内核线程资源属于系统调用开销高。线程池线程只创建一次循环从任务队列取任务反复执行复用内核线程省去反复创建销毁的系统调用开销。线程池线程分类核心线程正式员工一般不会回收释放只有线程池关闭才销毁。临时线程临时工核心线程处理不过来时创建压力降低空闲一段时间后就会被释放销毁。5.2 工厂设计模式⬇️1. 工厂模式解决什么问题普通对象使用构造方法new创建构造方法名固定无法通过重载区分不同初始化语义。例Point 类支持笛卡尔坐标(x,y)、极坐标(r,a)参数都是两个 double无法依靠构造器重载区分。class Point { public final double x; public final double y; // 构造器全部私有外部禁止 new只能走工厂方法 private Point(double x, double y) { this.x x; this.y y; } /** 静态工厂方法直角坐标 */ public static Point cartesian(double x, double y) { return new Point(x, y); } /** 静态工厂方法极坐标内部做数学换算返回直角坐标Point对象 */ public static Point polar(double r, double theta) { double x r * Math.cos(theta); double y r * Math.sin(theta); return new Point(x, y); } } public class Demo { public static void main(String[] args) { // 语义清晰看方法名就知道用的什么坐标系 Point p1 Point.cartesian(3, 4); Point p2 Point.polar(5, Math.PI / 2); System.out.println(p1.x , p1.y); System.out.println(p2.x , p2.y); } }工厂模式思路不对外暴露构造器创建对象提供一组静态工厂方法不同方法名代表不同创建逻辑方法内部调用构造器 new 对象对外隐藏对象创建细节。2. ThreadFactory线程工厂接口public interface ThreadFactory { Thread newThread(Runnable r); }作用线程池不直接new Thread()交给工厂接口newThread()方法来生产线程对象。可统一做线程初始化设置线程名称、是否守护线程、线程优先级。线程池内部需要大批量创建线程工厂模式统一管控线程创建逻辑。3. 拒绝策略 RejectedExecutionHandler触发时机核心线程全部占满阻塞队列也满再提交新任务执行拒绝策略。策略行为AbortPolicy默认直接抛出RejectedExecutionException异常CallerRunsPolicy提交任务的调用者线程自己执行任务不抛异常、不丢弃DiscardOldestPolicy丢弃队列存放最久的旧任务尝试执行新任务DiscardPolicy静默丢弃当前新任务无异常无日志提示4. Executors 工具类本质工厂类提供静态工厂方法封装ThreadPoolExecutor快速生成各类线程池。//固定线程数任务进无界队列 Executors.newFixedThreadPool(3); //单一线程 Executors.newSingleThreadExecutor(); //缓存线程池按需创建线程空闲回收 Executors.newCachedThreadPool(); //支持延迟/周期性定时任务 Executors.newScheduledThreadPool(10);生产环境不推荐直接使用 ExecutorsFixed / Single使用无界队列任务大量堆积会 OOMCached最大线程数为Integer.MAX_VALUE会疯狂创建线程导致 OOM企业实践直接实例化ThreadPoolExecutor完整构造方法手动指定核心线程、最大线程、队列、线程工厂、拒绝策略可控性更强。5. 补充知识点1 Lambda 变量捕获for 循环直接捕获循环变量i会出现数值错乱需要拷贝局部变量int id i;捕获拷贝后的局部变量。2队列区分LinkedBlockingQueue普通阻塞队列一端入队、一端出队LinkedBlockingDeque双端阻塞队列两端都可以入队、出队3 线程池工作线程伪代码while(true){ //队列无任务 take阻塞等待任务到来才唤醒 Runnable task queue.take(); task.run(); }简要背诵ThreadFactory工厂接口统一创建线程设置线程名、守护属性、优先级。拒绝策略线程 队列全部打满触发抛异常、调用者执行、丢弃旧任务、丢弃新任务。Executors 是线程池工厂工具类底层封装 ThreadPoolExecutor生产不建议直接用推荐手动构造 ThreadPoolExecutor。工厂模式将 new 对象封装到工厂方法解决构造器重载限制隐藏对象创建细节。6. 自定义线程工厂package Thread; import java.util.concurrent.LinkedBlockingQueue; class MyFixedThreadPool { //注意队列存放的应该是任务Runnable示例这里存放Thread属于写demo简化写法 private LinkedBlockingQueueThread queuenew LinkedBlockingQueue(); public MyFixedThreadPool(int n){ for(int i0;in;i){ Thread threadnew Thread(()-{ while(true){ try { Thread r queue.take(); r.run(); } catch (Exception e) { e.printStackTrace(); } } }); thread.start(); } } public void submit(Thread r){ try { queue.put(r); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Demo25 { public static void main(String[] args) { MyFixedThreadPool poolnew MyFixedThreadPool(3); pool.submit(new Thread(()-{ System.out.println(任务1); })); pool.submit(new Thread(()-{ System.out.println(任务2); })); pool.submit(new Thread(()-{ System.out.println(任务3); })); pool.submit(new Thread(()-{ System.out.println(任务4); })); pool.submit(new Thread(()-{ System.out.println(任务5); })); pool.submit(new Thread(()-{ System.out.println(任务6); })); } }6. 定时器1. 定时器作用类似闹钟任务不立即执行延迟一段时间 / 指定时刻执行。Java 标准库提供Timer也可以手写定时器。适用场景延迟任务、周期性任务例如 GC 的周期性工作。2. 底层存储优先队列 PriorityQueue不使用ArrayList任务插入无序查找最近要执行的任务需要遍历全部开销大。使用PriorityQueue小根堆堆顶永远是执行时间最早的任务直接取堆顶即可。需要封装任务类保存 Runnable任务 绝对时间戳执行时刻。时间存储存绝对时间戳不存相对 delay。传入 delay内部计算当前时间 delay得到绝对执行时刻避免计时偏差。3. 手写定时器两大核心问题① 线程安全一个线程提交任务schedule一个线程执行任务并发操作优先队列必须加synchronized锁保护队列。② 忙等CPU 空转错误写法while(true) { synchronized(locker){ if(queue.isEmpty()) continue; MyTimerTask task queue.peek(); long now System.currentTimeMillis(); if(now task.getTime()){ continue; // 时间未到持续循环CPU空转频繁抢释放锁 } } }问题时间未到就不停循环判断消耗大量 CPU还会引发线程饥饿。解决使用带超时时间的wait(timeout)if (curTime task.getTime()) { // 等待到任务执行时刻时间差作为wait参数 locker.wait(task.getTime() - curTime); }wait(超时时间)两种唤醒场景定时等待没有新任务等待指定时长时间到自动唤醒执行任务。主动唤醒schedule提交新任务调用notify唤醒工作线程新任务可能执行时间更早需要重新判断堆顶。注意JDK 的PriorityBlockingQueue仅队列为空时阻塞不能按时间差阻塞手写定时器需要自己加锁 wait 实现时间等待。4. 核心流程梳理封装任务类MyTimerTaskRunnable task long executeTime绝对执行时间戳PriorityQueueMyTimerTask小根堆按执行时间排序schedule(Runnable runnable, long delay)计算绝对执行时间封装任务入队notify唤醒工作线程工作线程循环逻辑获取锁读取堆顶任务未到执行时间wait(时间差)时间到期取出任务执行run()队列为空无限 wait等待新任务提交简要背诵定时器底层小根堆 PriorityQueue堆顶保存最近任务存储绝对时间戳。杜绝忙等用带超时 wait (时间差)既可定时唤醒新增任务也可 notify 唤醒。PriorityBlockingQueue 仅空队列阻塞手写定时器要自己实现 wait 时间等待。schedule 传入 delay转为System.currentTimeMillis()delay绝对时刻防止计时偏移。5. 自定义定时器package Thread; import java.sql.Time; import java.util.PriorityQueue; //由于PriorityQueue默认是按优先级排序的所以需要自定义比较器根据时间排序 class MyTimerTask implements ComparableMyTimerTask { private Runnable task; private long time; public MyTimerTask(Runnable task, long delay) { this.task task; this.time System.currentTimeMillis() delay; } public Runnable getTask() { return task; } public long getTime() { return time; } Override public int compareTo(MyTimerTask o) { // 根据时间排序先到先执行(小堆) return (int) (this.time - o.time); } } class MyTimer { Object lock new Object(); PriorityQueueMyTimerTask queue new PriorityQueue(); public MyTimer() { Thread t new Thread(() - { while (true) { try { synchronized (lock) { if (queue.isEmpty()) { // 队列为空等待任务 lock.wait(); } MyTimerTask peek queue.peek(); long curTime System.currentTimeMillis(); if (peek.getTime() curTime) { // 时间没到继续等待 // 注意这里的wait兼顾两种情况 // 1.有新任务就唤醒 // 2.没有新任务等待任务到时间 lock.wait(peek.getTime() - curTime); } else { // 时间到了执行任务 MyTimerTask task queue.poll(); task.getTask().run(); } } } catch (Exception e) { e.printStackTrace(); } } }); t.start(); } public void schedule(Runnable task, long delay) { synchronized (lock) { queue.offer(new MyTimerTask(task, delay)); lock.notify(); } } } public class Demo27 { public static void main(String[] args) { MyTimer timer new MyTimer(); System.out.println(定时器启动,3秒后执行任务1); timer.schedule(new Runnable() { Override public void run() { System.out.println(执行任务1); } }, 3000); System.out.println(定时器启动,5秒后执行任务2); timer.schedule(new Runnable() { Override public void run() { System.out.println(执行任务2); } }, 5000); } }
返回列表