当前位置: 首页 > news >正文

集宁建设局网站深圳信息公司做关键词

集宁建设局网站,深圳信息公司做关键词,网上怎么做营销,创建全国文明城市黑板报JUC 今天我们来进入到 Java并发编程 JUC 框架的学习 #xff0c;内容比较多#xff0c;但希望我们都能静下心来#xff0c;耐心的看完这篇文章 文章目录JUC进程概述对比线程创建线程ThreadRunnableCallable线程方法APIrun startsleep yieldjoininterrupt打断线程打断 park终…JUC 今天我们来进入到 Java并发编程 JUC 框架的学习 内容比较多但希望我们都能静下心来耐心的看完这篇文章 文章目录JUC进程概述对比线程创建线程ThreadRunnableCallable线程方法APIrun startsleep yieldjoininterrupt打断线程打断 park终止模式daemon不推荐线程原理运行机制线程调度未来优化线程状态查看线程同步临界区syn-ed使用锁同步块同步方法线程八锁锁原理Monitor字节码锁升级升级过程偏向锁轻量级锁锁膨胀锁优化自旋锁锁消除锁粗化多把锁活跃性死锁形成定位活锁饥饿wait-ify基本使用代码优化park-un安全分析同步模式保护性暂停单任务版多任务版顺序输出交替输出异步模式传统版改进版阻塞队列内存JMM内存模型内存交互三大特性可见性原子性有序性cache缓存机制缓存结构缓存使用伪共享缓存一致处理机制volatile同步机制指令重排底层原理缓存一致内存屏障交互规则双端检锁检锁机制DCL问题解决方法ha-be设计模式终止模式Balking无锁CAS原理乐观锁Atomic常用API原理分析原子引用原子数组原子更新器原子累加器Adder优化机制伪共享源码解析ABAUnsafefinal原理不可变StateLocal基本介绍基本使用常用方法应用场景实现原理底层结构成员变量成员方法LocalMap成员属性成员方法清理方法内存泄漏变量传递基本使用实现原理进程 概述 进程程序是静止的进程实体的运行过程就是进程是系统进行资源分配的基本单位 进程的特征并发性、异步性、动态性、独立性、结构性 线程线程是属于进程的是一个基本的 CPU 执行单元是程序执行流的最小单元。线程是进程中的一个实体是系统独立调度的基本单位线程本身不拥有系统资源只拥有一点在运行中必不可少的资源与同属一个进程的其他线程共享进程所拥有的全部资源 关系一个进程可以包含多个线程这就是多线程比如看视频是进程图画、声音、广告等就是多个线程 线程的作用使多道程序更好的并发执行提高资源利用率和系统吞吐量增强操作系统的并发性能 并发并行 并行在同一时刻有多个指令在多个 CPU 上同时执行并发在同一时刻有多个指令在单个 CPU 上交替执行 同步异步 需要等待结果返回才能继续运行就是同步不需要等待结果返回就能继续运行就是异步 参考视频https://www.bilibili.com/video/BV16J411h7Rd 笔记的整体结构依据视频编写并随着学习的深入补充了很多知识 对比 线程进程对比 进程基本上相互独立的而线程存在于进程内是进程的一个子集 进程拥有共享的资源如内存空间等供其内部的线程共享 进程间通信较为复杂 同一台计算机的进程通信称为 IPCInter-process communication 信号量信号量是一个计数器用于多进程对共享数据的访问解决同步相关的问题并避免竞争条件共享存储多个进程可以访问同一块内存空间需要使用信号量用来同步对共享存储的访问管道通信管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件该文件同一时间只允许一个进程访问所以只支持半双工通信 匿名管道Pipes用于具有亲缘关系的父子进程间或者兄弟进程之间的通信命名管道Names Pipes以磁盘文件的方式存在可以实现本机任意两个进程通信遵循 FIFO 消息队列内核中存储消息的链表由消息队列标识符标识能在不同进程之间提供全双工通信对比管道 匿名管道存在于内存中的文件命名管道存在于实际的磁盘介质或者文件系统消息队列存放在内核中只有在内核重启操作系统重启或者显示地删除一个消息队列时该消息队列才被真正删除读进程可以根据消息类型有选择地接收消息而不像 FIFO 那样只能默认地接收 不同计算机之间的进程通信需要通过网络并遵守共同的协议例如 HTTP 套接字与其它通信机制不同的是可用于不同机器间的互相通信 线程通信相对简单因为线程之间共享进程内的内存一个例子是多个线程可以访问同一个共享变量 Java 中的通信机制volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer 线程更轻量线程上下文切换成本一般上要比进程上下文切换低 线程 创建线程 Thread Thread 创建线程方式创建线程类匿名内部类方式 start() 方法底层其实是给 CPU 注册当前线程并且触发 run() 方法执行线程的启动必须调用 start() 方法如果线程直接调用 run() 方法相当于变成了普通类的执行此时主线程将只有执行该线程建议线程先创建子线程主线程的任务放在之后否则主线程main永远是先执行完 Thread 构造器 public Thread()public Thread(String name) public class ThreadDemo {public static void main(String[] args) {Thread t new MyThread();t.start();for(int i 0 ; i 100 ; i ){System.out.println(main线程 i)}// main线程输出放在上面 就变成有先后顺序了因为是 main 线程驱动的子线程运行} } class MyThread extends Thread {Overridepublic void run() {for(int i 0 ; i 100 ; i ) {System.out.println(子线程输出i)}} }继承 Thread 类的优缺点 优点编码简单缺点线程类已经继承了 Thread 类无法继承其他类了功能不能通过继承拓展单继承的局限性 Runnable Runnable 创建线程方式创建线程类匿名内部类方式 Thread 的构造器 public Thread(Runnable target)public Thread(Runnable target, String name) public class ThreadDemo {public static void main(String[] args) {Runnable target new MyRunnable();Thread t1 new Thread(target,1号线程);t1.start();Thread t2 new Thread(target);//Thread-0} }public class MyRunnable implements Runnable{Overridepublic void run() {for(int i 0 ; i 10 ; i ){System.out.println(Thread.currentThread().getName() - i);}} }Thread 类本身也是实现了 Runnable 接口Thread 类中持有 Runnable 的属性执行线程 run 方法底层是调用 Runnable#run public class Thread implements Runnable {private Runnable target;public void run() {if (target ! null) {// 底层调用的是 Runnable 的 run 方法target.run();}} }Runnable 方式的优缺点 缺点代码复杂一点。 优点 线程任务类只是实现了 Runnable 接口可以继续继承其他类避免了单继承的局限性 同一个线程任务对象可以被包装成多个线程对象 适合多个多个线程去共享同一个资源 实现解耦操作线程任务代码可以被多个线程共享线程任务代码和线程独立 线程池可以放入实现 Runnable 或 Callable 线程任务对象 ​ Callable 实现 Callable 接口 定义一个线程任务类实现 Callable 接口申明线程执行的结果类型重写线程任务类的 call 方法这个方法可以直接返回执行的结果创建一个 Callable 的线程任务对象把 Callable 的线程任务对象包装成一个未来任务对象把未来任务对象包装成线程对象调用线程的 start() 方法启动线程 public FutureTask(CallableV callable)未来任务对象在线程执行完后得到线程的执行结果 FutureTask 就是 Runnable 对象因为 Thread 类只能执行 Runnable 实例的任务对象所以把 Callable 包装成未来任务对象线程池部分详解了 FutureTask 的源码 public V get()同步等待 task 执行完毕的结果如果在线程中获取另一个线程执行结果会阻塞等待用于线程同步 get() 线程会阻塞等待任务执行完成run() 执行完后会把结果设置到 FutureTask 的一个成员变量get() 线程可以获取到该变量的值 优缺点 优点同 Runnable并且能得到线程执行的结果缺点编码复杂 public class ThreadDemo {public static void main(String[] args) {Callable call new MyCallable();FutureTaskString task new FutureTask(call);Thread t new Thread(task);t.start();try {String s task.get(); // 获取call方法返回的结果正常/异常结果System.out.println(s);} catch (Exception e) {e.printStackTrace();}}public class MyCallable implements CallableString {Override//重写线程任务类方法public String call() throws Exception {return Thread.currentThread().getName() - Hello World;} }线程方法 API Thread 类 API 方法说明public void start()启动一个新线程Java虚拟机调用此线程的 run 方法public void run()线程启动后调用该方法public void setName(String name)给当前线程取名字public void getName()获取当前线程的名字线程存在默认名称子线程是 Thread-索引主线程是 mainpublic static Thread currentThread()获取当前线程对象代码在哪个线程中执行public static void sleep(long time)让当前线程休眠多少毫秒再继续执行Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争public static native void yield()提示线程调度器让出当前线程对 CPU 的使用public final int getPriority()返回此线程的优先级public final void setPriority(int priority)更改此线程的优先级常用 1 5 10public void interrupt()中断这个线程异常处理机制public static boolean interrupted()判断当前线程是否被打断清除打断标记public boolean isInterrupted()判断当前线程是否被打断不清除打断标记public final void join()等待这个线程结束public final void join(long millis)等待这个线程死亡 millis 毫秒0 意味着永远等待public final native boolean isAlive()线程是否存活还没有运行完毕public final void setDaemon(boolean on)将此线程标记为守护线程或用户线程run start run称为线程体包含了要执行的这个线程的内容方法运行结束此线程随即终止。直接调用 run 是在主线程中执行了 run没有启动新的线程需要顺序执行 start使用 start 是启动新的线程此线程处于就绪可运行状态通过新的线程间接执行 run 中的代码 说明线程控制资源类 run() 方法中的异常不能抛出只能 try/catch 因为父类中没有抛出任何异常子类不能比父类抛出更多的异常异常不能跨线程传播回 main() 中因此必须在本地进行处理 sleep yield sleep 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态阻塞sleep() 方法的过程中线程不会释放对象锁其它线程可以使用 interrupt 方法打断正在睡眠的线程这时 sleep 方法会抛出 InterruptedException睡眠结束后的线程未必会立刻得到执行需要抢占 CPU建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 yield 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用具体的实现依赖于操作系统的任务调度器会放弃 CPU 资源锁资源不会释放 join public final void join()等待这个线程结束 原理调用者轮询检查线程 alive 状态t1.join() 等价于 public final synchronized void join(long millis) throws InterruptedException {// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束while (isAlive()) {wait(0);} }join 方法是被 synchronized 修饰的本质上是一个对象锁其内部的 wait 方法调用也是释放锁的但是释放的是当前的线程对象锁而不是外面的锁 当调用某个线程t1的 join 方法后该线程t1抢占到 CPU 资源就不再释放直到线程执行完毕 线程同步 join 实现线程同步因为会阻塞等待另一个线程的结束才能继续向下运行 需要外部共享变量不符合面向对象封装的思想必须等待线程结束不能配合线程池使用 Future 实现同步get() 方法阻塞等待执行结果 main 线程接收结果get 方法是让调用线程同步等待 public class Test {static int r 0;public static void main(String[] args) throws InterruptedException {test1();}private static void test1() throws InterruptedException {Thread t1 new Thread(() - {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}r 10;});t1.start();t1.join();//不等待线程执行结束输出的10System.out.println(r);} }interrupt 打断线程 public void interrupt()打断这个线程异常处理机制 public static boolean interrupted()判断当前线程是否被打断打断返回 true清除打断标记连续调用两次一定返回 false public boolean isInterrupted()判断当前线程是否被打断不清除打断标记 打断的线程会发生上下文切换操作系统会保存线程信息抢占到 CPU 后会从中断的地方接着运行打断不是停止 sleep、wait、join 方法都会让线程进入阻塞状态打断线程会清空打断状态false public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(()-{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}, t1);t1.start();Thread.sleep(500);t1.interrupt();System.out.println( 打断状态: {} t1.isInterrupted());// 打断状态: {}false }打断正常运行的线程不会清空打断状态true public static void main(String[] args) throws Exception {Thread t2 new Thread(()-{while(true) {Thread current Thread.currentThread();boolean interrupted current.isInterrupted();if(interrupted) {System.out.println( 打断状态: {} interrupted);//打断状态: {}truebreak;}}}, t2);t2.start();Thread.sleep(500);t2.interrupt(); }打断 park park 作用类似 sleep打断 park 线程不会清空打断状态true public static void main(String[] args) throws Exception {Thread t1 new Thread(() - {System.out.println(park...);LockSupport.park();System.out.println(unpark...);System.out.println(打断状态 Thread.currentThread().isInterrupted());//打断状态true}, t1);t1.start();Thread.sleep(2000);t1.interrupt(); }如果打断标记已经是 true, 则 park 会失效 LockSupport.park(); System.out.println(unpark...); LockSupport.park();//失效不会阻塞 System.out.println(unpark...);//和上一个unpark同时执行可以修改获取打断状态方法使用 Thread.interrupted()清除打断标记 LockSupport 类在 同步 → park-un 详解 终止模式 终止模式之两阶段终止模式Two Phase Termination 目标在一个线程 T1 中如何优雅终止线程 T2优雅指的是给 T2 一个后置处理器 错误思想 使用线程对象的 stop() 方法停止线程stop 方法会真正杀死线程如果这时线程锁住了共享资源当它被杀死后就再也没有机会释放锁其它线程将永远无法获取锁使用 System.exit(int) 方法停止线程目的仅是停止一个线程但这种做法会让整个程序都停止 两阶段终止模式图示 打断线程可能在任何时间所以需要考虑在任何时刻被打断的处理方法 public class Test {public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt new TwoPhaseTermination();tpt.start();Thread.sleep(3500);tpt.stop();} } class TwoPhaseTermination {private Thread monitor;// 启动监控线程public void start() {monitor new Thread(new Runnable() {Overridepublic void run() {while (true) {Thread thread Thread.currentThread();if (thread.isInterrupted()) {System.out.println(后置处理);break;}try {Thread.sleep(1000); // 睡眠System.out.println(执行监控记录); // 在此被打断不会异常} catch (InterruptedException e) { // 在睡眠期间被打断进入异常处理的逻辑e.printStackTrace();// 重新设置打断标记打断 sleep 会清除打断状态thread.interrupt();}}}});monitor.start();}// 停止监控线程public void stop() {monitor.interrupt();} }daemon public final void setDaemon(boolean on)如果是 true 将此线程标记为守护线程 线程启动前调用此方法 Thread t new Thread() {Overridepublic void run() {System.out.println(running);} }; // 设置该线程为守护线程 t.setDaemon(true); t.start();用户线程平常创建的普通线程 守护线程服务于用户线程只要其它非守护线程运行结束了即使守护线程代码没有执行完也会强制结束。守护进程是脱离于终端并且在后台运行的进程脱离终端是为了避免在执行的过程中的信息在终端上显示 说明当运行的线程都是守护线程Java 虚拟机将退出因为普通线程执行完后JVM 是守护线程不会继续运行下去 常见的守护线程 垃圾回收器线程就是一种守护线程Tomcat 中的 Acceptor 和 Poller 线程都是守护线程所以 Tomcat 接收到 shutdown 命令后不会等待它们处理完当前请求 不推荐 不推荐使用的方法这些方法已过时容易破坏同步代码块造成线程死锁 public final void stop()停止线程运行 废弃原因方法粗暴除非可能执行 finally 代码块以及释放 synchronized 外线程将直接被终止如果线程持有 JUC 的互斥锁可能导致锁来不及释放造成其他线程永远等待的局面 public final void suspend()挂起暂停线程运行 废弃原因如果目标线程在暂停时对系统资源持有锁则在目标线程恢复之前没有线程可以访问该资源如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源则会导致死锁 public final void resume()恢复线程运行 线程原理 运行机制 Java Virtual Machine StacksJava 虚拟机栈每个线程启动后虚拟机就会为其分配一块栈内存 每个栈由多个栈帧Frame组成对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧对应着当前正在执行的那个方法 线程上下文切换Thread Context Switch一些原因导致 CPU 不再执行当前线程转而执行另一个线程 线程的 CPU 时间片用完垃圾回收有更高优先级的线程需要运行线程自己调用了 sleep、yield、wait、join、park 等方法 程序计数器Program Counter Register记住下一条 JVM 指令的执行地址是线程私有的 当 Context Switch 发生时需要由操作系统保存当前线程的状态PCB 中并恢复另一个线程的状态包括程序计数器、虚拟机栈中每个栈帧的信息如局部变量、操作数栈、返回地址等 JVM 规范并没有限定线程模型以 HotSopot 为例 Java 的线程是内核级线程1:1 线程模型每个 Java 线程都映射到一个操作系统原生线程需要消耗一定的内核资源堆栈线程的调度是在内核态运行的而线程中的代码是在用户态运行所以线程切换状态改变会导致用户与内核态转换进行系统调用这是非常消耗性能 Java 中 main 方法启动的是一个进程也是一个主线程main 方法里面的其他线程均为子线程main 线程是这些线程的父线程 线程调度 线程调度指系统为线程分配处理器使用权的过程方式有两种协同式线程调度、抢占式线程调度Java 选择 协同式线程调度线程的执行时间由线程本身控制 优点线程做完任务才通知系统切换到其他线程相当于所有线程串行执行不会出现线程同步问题缺点线程执行时间不可控如果代码编写出现问题可能导致程序一直阻塞引起系统的奔溃 抢占式线程调度线程的执行时间由系统分配 优点线程执行时间可控不会因为一个线程的问题而导致整体系统不可用缺点无法主动为某个线程多分配时间 Java 提供了线程优先级的机制优先级会提示hint调度器优先调度该线程但这仅仅是一个提示调度器可以忽略它。在线程的就绪状态时如果 CPU 比较忙那么优先级高的线程会获得更多的时间片但 CPU 闲时优先级几乎没作用 说明并不能通过优先级来判断线程执行的先后顺序 未来优化 内核级线程调度的成本较大所以引入了更轻量级的协程。用户线程的调度由用户自己实现多对一的线程模型多个用户线程映射到一个内核级线程被设计为协同式调度所以叫协程 有栈协程协程会完整的做调用栈的保护、恢复工作所以叫有栈协程无栈协程本质上是一种有限状态机状态保存在闭包里比有栈协程更轻量但是功能有限 有栈协程中有一种特例叫纤程在新并发模型中一段纤程的代码被分为两部分执行过程和调度器 执行过程用于维护执行现场保护、恢复上下文状态调度器负责编排所有要执行的代码顺序 线程状态 进程的状态参考操作系统创建态、就绪态、运行态、阻塞态、终止态 线程由生到死的完整过程生命周期当线程被创建并启动以后既不是一启动就进入了执行状态也不是一直处于执行状态在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态 线程状态导致状态发生条件NEW新建线程刚被创建但是并未启动还没调用 start 方法只有线程对象没有线程特征Runnable可运行线程可以在 Java 虚拟机中运行的状态可能正在运行自己代码也可能没有这取决于操作系统处理器调用了 t.start() 方法就绪经典叫法Blocked阻塞当一个线程试图获取一个对象锁而该对象锁被其他的线程持有则该线程进入 Blocked 状态当该线程持有锁时该线程将变成 Runnable 状态Waiting无限等待一个线程在等待另一个线程执行一个唤醒动作时该线程进入 Waiting 状态进入这个状态后不能自动唤醒必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒Timed Waiting 限期等待有几个方法有超时参数调用将进入 Timed Waiting 状态这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.waitTeminated结束run 方法正常退出而死亡或者因为没有捕获的异常终止了 run 方法而死亡 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ISw9ZWBy-1678148961772)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程6种状态.png)] NEW → RUNNABLE当调用 t.start() 方法时由 NEW → RUNNABLE RUNNABLE – WAITING 调用 obj.wait() 方法时 调用 obj.notify()、obj.notifyAll()、t.interrupt() 竞争锁成功t 线程从 WAITING → RUNNABLE竞争锁失败t 线程从 WAITING → BLOCKED 当前线程调用 t.join() 方法注意是当前线程在 t 线程对象的监视器上等待 当前线程调用 LockSupport.park() 方法 RUNNABLE – TIMED_WAITING调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n) RUNNABLE – BLOCKEDt 线程用 synchronized(obj) 获取了对象锁时竞争失败 查看线程 Windows 任务管理器可以查看进程和线程数也可以用来杀死进程tasklist 查看进程taskkill 杀死进程 Linux ps -ef 查看所有进程ps -fT -p 查看某个进程PID的所有线程kill 杀死进程top 按大写 H 切换是否显示线程top -H -p 查看某个进程PID的所有线程 Java jps 命令查看所有 Java 进程jstack 查看某个 Java 进程PID的所有线程状态jconsole 来查看某个 Java 进程中线程的运行情况图形界面 同步 临界区 临界资源一次仅允许一个进程使用的资源成为临界资源 临界区访问临界资源的代码块 竞态条件多个线程在临界区内执行由于代码的执行序列不同而导致结果无法预测称之为发生了竞态条件 一个程序运行多个线程是没有问题多个线程读共享资源也没有问题在多个线程对共享资源读写操作时发生指令交错就会出现问题 为了避免临界区的竞态条件发生解决线程安全问题 阻塞式的解决方案synchronizedlock非阻塞式的解决方案原子变量 管程monitor由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块保证同一时刻只有一个进程在管程内活动即管程内定义的操作在同一时刻只被一个进程调用由编译器实现 synchronized对象锁保证了临界区内代码的原子性采用互斥的方式让同一时刻至多只有一个线程能持有对象锁其它线程获取这个对象锁时会阻塞保证拥有锁的线程可以安全的执行临界区内的代码不用担心线程上下文切换 互斥和同步都可以采用 synchronized 关键字来完成区别 互斥是保证临界区的竞态条件发生同一时刻只能有一个线程执行临界区代码同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 性能 线程安全性能差线程不安全性能好假如开发中不会存在多线程安全问题建议使用线程不安全的设计类 syn-ed 使用锁 同步块 锁对象理论上可以是任意的唯一对象 synchronized 是可重入、不公平的重量级锁 原则上 锁对象建议使用共享资源在实例方法中使用 this 作为锁对象锁住的 this 正好是共享资源在静态方法中使用类名 .class 字节码作为锁对象因为静态成员属于类被所有实例对象共享所以需要锁住类 同步代码块格式 synchronized(锁对象){// 访问共享资源的核心代码 }实例 public class demo {static int counter 0;//static修饰则元素是属于类本身的不属于对象 与类一起加载一次只有一个static final Object room new Object();public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int i 0; i 5000; i) {synchronized (room) {counter;}}}, t1);Thread t2 new Thread(() - {for (int i 0; i 5000; i) {synchronized (room) {counter--;}}}, t2);t1.start();t2.start();t1.join();t2.join();System.out.println(counter);} }同步方法 把出现线程安全问题的核心方法锁起来每次只能一个线程进入访问 synchronized 修饰的方法的不具备继承性所以子类是线程不安全的如果子类的方法也被 synchronized 修饰两个锁对象其实是一把锁而且是子类对象作为锁 用法直接给方法加上一个修饰符 synchronized //同步方法 修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体 } //同步静态方法 修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体 }同步方法底层也是有锁对象的 如果方法是实例方法同步方法默认用 this 作为的锁对象 public synchronized void test() {} //等价于 public void test() {synchronized(this) {} }如果方法是静态方法同步方法默认用类名 .class 作为的锁对象 class Test{public synchronized static void test() {} } //等价于 class Test{public void test() {synchronized(Test.class) {}} }线程八锁 线程八锁就是考察 synchronized 锁住的是哪个对象直接百度搜索相关的实例 说明主要关注锁住的对象是不是同一个 锁住类对象所有类的实例的方法都是安全的类的所有实例都相当于同一把锁锁住 this 对象只有在当前实例对象的线程内是安全的如果有多个实例就不安全 线程不安全因为锁住的不是同一个对象线程 1 调用 a 方法锁住的类对象线程 2 调用 b 方法锁住的 n2 对象不是同一个对象 class Number{public static synchronized void a(){Thread.sleep(1000);System.out.println(1);}public synchronized void b() {System.out.println(2);} } public static void main(String[] args) {Number n1 new Number();Number n2 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n2.b(); }).start(); }线程安全因为 n1 调用 a() 方法锁住的是类对象n2 调用 b() 方法锁住的也是类对象所以线程安全 class Number{public static synchronized void a(){Thread.sleep(1000);System.out.println(1);}public static synchronized void b() {System.out.println(2);} } public static void main(String[] args) {Number n1 new Number();Number n2 new Number();new Thread(()-{ n1.a(); }).start();new Thread(()-{ n2.b(); }).start(); }锁原理 Monitor Monitor 被翻译为监视器或管程 每个 Java 对象都可以关联一个 Monitor 对象Monitor 也是 class其实例存储在堆中如果使用 synchronized 给对象上锁重量级之后该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针这就是重量级锁 Mark Word 结构最后两位是锁标志位 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8efRwl1-1678148961773)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构32位.png)] 64 位虚拟机 Mark Word [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AHDYaW0y-1678148961773)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构64位.png)] 工作流程 开始时 Monitor 中 Owner 为 null当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor 中只能有一个 Ownerobj 对象的 Mark Word 指向 Monitor把对象原有的 MarkWord 存入线程栈中的锁记录中轻量级锁部分详解 在 Thread-2 上锁的过程Thread-3、Thread-4、Thread-5 也执行 synchronized(obj)就会进入 EntryList BLOCKED双向链表Thread-2 执行完同步代码块的内容根据 obj 对象头中 Monitor 地址寻找设置 Owner 为空把线程栈的锁记录中的对象头的值设置回 MarkWord唤醒 EntryList 中等待的线程来竞争锁竞争是非公平的如果这时有新的线程想要获取锁可能直接就抢占到了阻塞队列的线程就会继续阻塞WaitSet 中的 Thread-0是以前获得过锁但条件不满足进入 WAITING 状态的线程wait-notify 机制 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QlyNLJGc-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png)] 注意 synchronized 必须是进入同一个对象的 Monitor 才有上述的效果不加 synchronized 的对象不会关联监视器不遵从以上规则 字节码 代码 public static void main(String[] args) {Object lock new Object();synchronized (lock) {System.out.println(ok);} }0: new #2 // new Object 3: dup 4: invokespecial #1 // invokespecial init:()V非虚方法 7: astore_1 // lock引用 - lock 8: aload_1 // lock synchronized开始 9: dup // 一份用来初始化一份用来引用 10: astore_2 // lock引用 - slot 2 11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】 12: getstatic #3 // System.out 15: ldc #4 // ok 17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V 20: aload_2 // slot 2(lock引用) 21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 22: goto 30 25: astore_3 // any - slot 3 26: aload_2 // slot 2(lock引用) 27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 28: aload_3 29: athrow 30: return Exception table:from to target type12 22 25 any25 28 25 any LineNumberTable: ... LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 lock Ljava/lang/Object;说明 通过异常 try-catch 机制确保一定会被解锁方法级别的 synchronized 不会在字节码指令中有所体现 锁升级 升级过程 synchronized 是可重入、不公平的重量级锁所以可以对其进行优化 无锁 - 偏向锁 - 轻量级锁 - 重量级锁 // 随着竞争的增加只能锁升级不能降级[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wxuReiq4-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-锁升级过程.png)] 偏向锁 偏向锁的思想是偏向于让第一个获取锁对象的线程这个线程之后重新获取该锁不再需要同步操作 当锁对象第一次被线程获得的时候进入偏向状态标记为 101同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功这个线程以后进入这个锁相关的同步块查看这个线程 ID 是自己的就表示没有竞争就不需要再进行任何同步操作 当有另外一个线程去尝试获取这个锁对象时偏向状态就宣告结束此时撤销偏向Revoke Bias后恢复到未锁定或轻量级锁状态 一个对象创建时 如果开启了偏向锁默认开启那么对象创建后MarkWord 值为 0x05 即最后 3 位为 101thread、epoch、age 都为 0 偏向锁是默认是延迟的不会在程序启动时立即生效如果想避免延迟可以加 VM 参数 -XX:BiasedLockingStartupDelay0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因在刚开始执行代码时会有好多线程来抢锁如果开偏向锁效率反而降低 当一个对象已经计算过 hashCode就再也无法进入偏向状态了 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁 撤销偏向锁的状态 调用对象的 hashCode偏向锁的对象 MarkWord 中存储的是线程 id调用 hashCode 导致偏向锁被撤销当有其它线程使用偏向锁对象时会将偏向锁升级为轻量级锁调用 wait/notify需要申请 Monitor进入 WaitSet 批量撤销如果对象被多个线程访问但没有竞争这时偏向了线程 T1 的对象仍有机会重新偏向 T2重偏向会重置对象的 Thread ID 批量重偏向当撤销偏向锁阈值超过 20 次后JVM 会觉得是不是偏向错了于是在给这些对象加锁时重新偏向至加锁线程 批量撤销当撤销偏向锁阈值超过 40 次后JVM 会觉得自己确实偏向错了根本就不该偏向于是整个类的所有对象都会变为不可偏向的新建的对象也是不可偏向的 轻量级锁 一个对象有多个线程要加锁但加锁的时间是错开的没有竞争可以使用轻量级锁来优化轻量级锁对使用者是透明的不可见 可重入锁线程可以进入任何一个它已经拥有的锁所同步着的代码块可重入锁最大的作用是避免死锁 轻量级锁在没有竞争时锁重入时每次重入仍然需要执行 CAS 操作Java 6 才引入的偏向锁来优化 锁重入实例 static final Object obj new Object(); public static void method1() {synchronized( obj ) {// 同步块 Amethod2();} } public static void method2() {synchronized( obj ) {// 同步块 B} }创建锁记录Lock Record对象每个线程的栈帧都会包含一个锁记录的结构存储锁定对象的 Mark Word [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6jJtP7Tf-1678148961774)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理1.png)] 让锁记录中 Object reference 指向锁住的对象并尝试用 CAS 替换 Object 的 Mark Word将 Mark Word 的值存入锁记录 如果 CAS 替换成功对象头中存储了锁记录地址和状态 00轻量级锁 表示由该线程给对象加锁 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-la2KBNpp-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理2.png)] 如果 CAS 失败有两种情况 如果是其它线程已经持有了该 Object 的轻量级锁这时表明有竞争进入锁膨胀过程如果是线程自己执行了 synchronized 锁重入就添加一条 Lock Record 作为重入的计数 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DASt1Fz6-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理3.png)] 当退出 synchronized 代码块解锁时 如果有取值为 null 的锁记录表示有重入这时重置锁记录表示重入计数减 1如果锁记录的值不为 null这时使用 CAS 将 Mark Word 的值恢复给对象头 成功则解锁成功失败说明轻量级锁进行了锁膨胀或已经升级为重量级锁进入重量级锁解锁流程 锁膨胀 在尝试加轻量级锁的过程中CAS 操作无法成功可能是其它线程为此对象加上了轻量级锁有竞争这时需要进行锁膨胀将轻量级锁变为重量级锁 当 Thread-1 进行轻量级加锁时Thread-0 已经对该对象加了轻量级锁 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxq4Ugn6-1678148961775)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理1.png)] Thread-1 加轻量级锁失败进入锁膨胀流程为 Object 对象申请 Monitor 锁通过 Object 对象头获取到持锁线程将 Monitor 的 Owner 置为 Thread-0将 Object 的对象头指向重量级锁地址然后自己进入 Monitor 的 EntryList BLOCKED [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIcMjg4a-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理2.png)] 当 Thread-0 退出同步块解锁时使用 CAS 将 Mark Word 的值恢复给对象头失败这时进入重量级解锁流程即按照 Monitor 地址找到 Monitor 对象设置 Owner 为 null唤醒 EntryList 中 BLOCKED 线程 锁优化 自旋锁 重量级锁竞争时尝试获取锁的线程不会立即阻塞可以使用自旋默认 10 次来进行优化采用循环的方式去尝试获取锁 注意 自旋占用 CPU 时间单核 CPU 自旋就是浪费时间因为同一时刻只能运行一个线程多核 CPU 自旋才能发挥优势自旋失败的线程会进入阻塞状态 优点不会进入阻塞状态减少线程上下文切换的消耗 缺点当自旋的线程越来越多时会不断的消耗 CPU 资源 自旋锁情况 自旋成功的情况 自旋失败的情况 自旋锁说明 在 Java 6 之后自旋锁是自适应的比如对象刚刚的一次自旋操作成功过那么认为这次自旋成功的可能性会高就多自旋几次反之就少自旋甚至不自旋比较智能Java 7 之后不能控制是否开启自旋功能由 JVM 控制 //手写自旋锁 public class SpinLock {// 泛型装的是Thread原子引用线程AtomicReferenceThread atomicReference new AtomicReference();public void lock() {Thread thread Thread.currentThread();System.out.println(thread.getName() come in);//开始自旋期望值为null更新值是当前线程while (!atomicReference.compareAndSet(null, thread)) {Thread.sleep(1000);System.out.println(thread.getName() 正在自旋);}System.out.println(thread.getName() 自旋成功);}public void unlock() {Thread thread Thread.currentThread();//线程使用完锁把引用变为nullatomicReference.compareAndSet(thread, null);System.out.println(thread.getName() invoke unlock);}public static void main(String[] args) throws InterruptedException {SpinLock lock new SpinLock();new Thread(() - {//占有锁lock.lock();Thread.sleep(10000); //释放锁lock.unlock();},t1).start();// 让main线程暂停1秒使得t1线程先执行Thread.sleep(1000);new Thread(() - {lock.lock();lock.unlock();},t2).start();} }锁消除 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除这是 JVM 即时编译器的优化 锁消除主要是通过逃逸分析来支持如果堆上的共享数据不可能逃逸出去被其它线程访问到那么就可以把它们当成私有数据对待也就可以将它们的锁进行消除同步消除JVM 逃逸分析 锁粗化 对相同对象多次加锁导致线程发生多次重入频繁的加锁操作就会导致性能损耗可以使用锁粗化方式优化 如果虚拟机探测到一串的操作都对同一个对象加锁将会把加锁的范围扩展粗化到整个操作序列的外部 一些看起来没有加锁的代码其实隐式的加了很多锁 public static String concatString(String s1, String s2, String s3) {return s1 s2 s3; }String 是一个不可变的类编译器会对 String 的拼接自动优化。在 JDK 1.5 之前转化为 StringBuffer 对象的连续 append() 操作每个 append() 方法中都有一个同步块 public static String concatString(String s1, String s2, String s3) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString(); }扩展到第一个 append() 操作之前直至最后一个 append() 操作之后只需要加锁一次就可以 多把锁 多把不相干的锁一间大屋子有两个功能睡觉、学习互不相干。现在一人要学习一人要睡觉如果只用一间屋子一个对象锁的话那么并发度很低 将锁的粒度细分 好处是可以增强并发度坏处如果一个线程需要同时获得多把锁就容易发生死锁 解决方法准备多个对象锁 public static void main(String[] args) {BigRoom bigRoom new BigRoom();new Thread(() - { bigRoom.study(); }).start();new Thread(() - { bigRoom.sleep(); }).start(); } class BigRoom {private final Object studyRoom new Object();private final Object sleepRoom new Object();public void sleep() throws InterruptedException {synchronized (sleepRoom) {System.out.println(sleeping 2 小时);Thread.sleep(2000);}}public void study() throws InterruptedException {synchronized (studyRoom) {System.out.println(study 1 小时);Thread.sleep(1000);}} }活跃性 死锁 形成 死锁多个线程同时被阻塞它们中的一个或者全部都在等待某个资源被释放由于线程被无限期地阻塞因此程序不可能正常终止 Java 死锁产生的四个必要条件 互斥条件即当资源被一个线程使用占有时别的线程不能使用不可剥夺条件资源请求者不能强制从资源占有者手中夺取资源资源只能由资源占有者主动释放请求和保持条件即当资源请求者在请求其他的资源的同时保持对原有资源的占有循环等待条件即存在一个等待循环队列p1 要 p2 的资源p2 要 p1 的资源形成了一个等待环路 四个条件都成立的时候便形成死锁。死锁情况下打破上述任何一个条件便可让死锁消失 public class Dead {public static Object resources1 new Object();public static Object resources2 new Object();public static void main(String[] args) {new Thread(() - {// 线程1占用资源1 请求资源2synchronized(resources1){System.out.println(线程1已经占用了资源1开始请求资源2);Thread.sleep(2000);//休息两秒防止线程1直接运行完成。//2秒内线程2肯定可以锁住资源2synchronized (resources2){System.out.println(线程1已经占用了资源2);}}).start();new Thread(() - {// 线程2占用资源2 请求资源1synchronized(resources2){System.out.println(线程2已经占用了资源2开始请求资源1);Thread.sleep(2000);synchronized (resources1){System.out.println(线程2已经占用了资源1);}}}}).start();} }定位 定位死锁的方法 使用 jps 定位进程 id再用 jstack id 定位死锁找到死锁的线程去查看源码解决优化 Thread-1 #12 prio5 os_prio0 tid0x000000001eb69000 nid0xd40 waiting formonitor entry [0x000000001f54f000]java.lang.Thread.State: BLOCKED (on object monitor) #省略 Thread-1 #12 prio5 os_prio0 tid0x000000001eb69000 nid0xd40 waiting for monitor entry [0x000000001f54f000]java.lang.Thread.State: BLOCKED (on object monitor) #省略Found one Java-level deadlock:Thread-1:waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),which is held by Thread-0 Thread-0:waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),which is held by Thread-1Java stack information for the threads listed above:Thread-1:at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)- waiting to lock 0x000000076b5bf1c0 (a java.lang.Object)- locked 0x000000076b5bf1d0 (a java.lang.Object)at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)at java.lang.Thread.run(Thread.java:745) Thread-0:at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)- waiting to lock 0x000000076b5bf1d0 (a java.lang.Object)- locked 0x000000076b5bf1c0 (a java.lang.Object)at thread.TestDeadLock$$Lambda$1/495053715Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程再利用 top -Hp 进程id 来定位是哪个线程最后再用 jstack 的输出来看各个线程栈 避免死锁避免死锁要注意加锁顺序 可以使用 jconsole 工具在 jdk\bin 目录下 活锁 活锁指的是任务或者执行者没有被阻塞由于某些条件没有满足导致一直重复尝试—失败—尝试—失败的过程 两个线程互相改变对方的结束条件最后谁也无法结束 class TestLiveLock {static volatile int count 10;static final Object lock new Object();public static void main(String[] args) {new Thread(() - {// 期望减到 0 退出循环while (count 0) {Thread.sleep(200);count--;System.out.println(线程一count: count);}}, t1).start();new Thread(() - {// 期望超过 20 退出循环while (count 20) {Thread.sleep(200);count;System.out.println(线程二count: count);}}, t2).start();} }饥饿 饥饿一个线程由于优先级太低始终得不到 CPU 调度执行也不能够结束 wait-ify 基本使用 需要获取对象锁后才可以调用 锁对象.wait()notify 随机唤醒一个线程notifyAll 唤醒所有线程去竞争 CPU Object 类 API public final void notify():唤醒正在等待对象监视器的单个线程。 public final void notifyAll():唤醒正在等待对象监视器的所有线程。 public final void wait():导致当前线程等待直到另一个线程调用该对象的notify()方法或 notifyAll()方法。 public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待或是被唤醒说明wait 是挂起线程需要唤醒的都是挂起操作阻塞线程可以自己去争抢锁挂起的线程需要唤醒后去争抢锁 对比 sleep() 原理不同sleep() 方法是属于 Thread 类是线程用来控制自身流程的使此线程暂停执行一段时间而把执行机会让给其他线程wait() 方法属于 Object 类用于线程间通信对锁的处理机制不同调用 sleep() 方法的过程中线程不会释放对象锁当调用 wait() 方法的时候线程会放弃对象锁进入等待此对象的等待锁定池不释放锁其他线程怎么抢占到锁执行唤醒操作但是都会释放 CPU使用区域不同wait() 方法必须放在**同步控制方法和同步代码块先获取锁**中使用sleep() 方法则可以放在任何地方使用 底层原理 Owner 线程发现条件不满足调用 wait 方法即可进入 WaitSet 变为 WAITING 状态BLOCKED 和 WAITING 的线程都处于阻塞状态不占用 CPU 时间片BLOCKED 线程会在 Owner 线程释放锁时唤醒WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒唤醒后并不意味者立刻获得锁需要进入 EntryList 重新竞争 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hNvz9pH1-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png)] 代码优化 虚假唤醒notify 只能随机唤醒一个 WaitSet 中的线程这时如果有其它线程也在等待那么就可能唤醒不了正确的线程 解决方法采用 notifyAll notifyAll 仅解决某个线程的唤醒问题使用 if wait 判断仅有一次机会一旦条件不成立无法重新判断 解决方法用 while wait当条件不成立再次 wait Slf4j(topic c.demo) public class demo {static final Object room new Object();static boolean hasCigarette false; //有没有烟static boolean hasTakeout false;public static void main(String[] args) throws InterruptedException {new Thread(() - {synchronized (room) {log.debug(有烟没[{}], hasCigarette);while (!hasCigarette) {//while防止虚假唤醒log.debug(没烟先歇会);try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(有烟没[{}], hasCigarette);if (hasCigarette) {log.debug(可以开始干活了);} else {log.debug(没干成活...);}}}, 小南).start();new Thread(() - {synchronized (room) {Thread thread Thread.currentThread();log.debug(外卖送到没[{}], hasTakeout);if (!hasTakeout) {log.debug(没外卖先歇会);try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(外卖送到没[{}], hasTakeout);if (hasTakeout) {log.debug(可以开始干活了);} else {log.debug(没干成活...);}}}, 小女).start();Thread.sleep(1000);new Thread(() - {// 这里能不能加 synchronized (room)synchronized (room) {hasTakeout true;//log.debug(烟到了噢);log.debug(外卖到了噢);room.notifyAll();}}, 送外卖的).start();} }park-un LockSupport 是用来创建锁和其他同步类的线程原语 LockSupport 类方法 LockSupport.park()暂停当前线程挂起原语LockSupport.unpark(暂停的线程对象)恢复某个线程的运行 public static void main(String[] args) {Thread t1 new Thread(() - {System.out.println(start...); //1Thread.sleep(1000);// Thread.sleep(3000)// 先 park 再 unpark 和先 unpark 再 park 效果一样都会直接恢复线程的运行System.out.println(park...); //2LockSupport.park();System.out.println(resume...);//4},t1);t1.start();Thread.sleep(2000);System.out.println(unpark...); //3LockSupport.unpark(t1); }LockSupport 出现就是为了增强 wait notify 的功能 waitnotify 和 notifyAll 必须配合 Object Monitor 一起使用而 park、unpark 不需要park unpark 以线程为单位来阻塞和唤醒线程而 notify 只能随机唤醒一个等待线程notifyAll 是唤醒所有等待线程park unpark 可以先 unpark而 wait notify 不能先 notify。类比生产消费先消费发现有产品就消费没有就等待先生产就直接产生商品然后线程直接消费wait 会释放锁资源进入等待队列park 不会释放锁资源只负责阻塞当前线程会释放 CPU 原理类似生产者消费者 先 park 当前线程调用 Unsafe.park() 方法检查 _counter 本情况为 0这时获得 _mutex 互斥锁线程进入 _cond 条件变量挂起调用 Unsafe.unpark(Thread_0) 方法设置 _counter 为 1唤醒 _cond 条件变量中的 Thread_0Thread_0 恢复运行设置 _counter 为 0 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-seT6ZMdS-1678148961776)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理1.png)] 先 unpark 调用 Unsafe.unpark(Thread_0) 方法设置 _counter 为 1当前线程调用 Unsafe.park() 方法检查 _counter 本情况为 1这时线程无需挂起继续运行设置 _counter 为 0 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MiHGw9PT-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理2.png)] 安全分析 成员变量和静态变量 如果它们没有共享则线程安全如果它们被共享了根据它们的状态是否能够改变分两种情况 如果只有读操作则线程安全如果有读写操作则这段代码是临界区需要考虑线程安全问题 局部变量 局部变量是线程安全的局部变量引用的对象不一定线程安全逃逸分析 如果该对象没有逃离方法的作用访问它是线程安全的每一个方法有一个栈帧如果该对象逃离方法的作用范围需要考虑线程安全问题暴露引用 常见线程安全类String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包 线程安全的是指多个线程调用它们同一个实例的某个方法时是线程安全的 每个方法是原子的但多个方法的组合不是原子的只能保证调用的方法内部安全 Hashtable table new Hashtable(); // 线程1线程2 if(table.get(key) null) {table.put(key, value); }无状态类线程安全就是没有成员变量的类 不可变类线程安全String、Integer 等都是不可变类内部的状态不可以改变所以方法是线程安全 replace 等方法底层是新建一个对象复制过去 MapString,Object map new HashMap(); // 线程不安全 String S1 ...; // 线程安全 final String S2 ...; // 线程安全 Date D1 new Date(); // 线程不安全 final Date D2 new Date(); // 线程不安全final让D2引用的对象不能变但对象的内容可以变抽象方法如果有参数被重写后行为不确定可能造成线程不安全被称之为外星方法public abstract foo(Student s); 同步模式 保护性暂停 单任务版 Guarded Suspension用在一个线程等待另一个线程的执行结果 有一个结果需要从一个线程传递到另一个线程让它们关联同一个 GuardedObject如果有结果不断从一个线程到另一个线程那么可以使用消息队列见生产者/消费者JDK 中join 的实现、Future 的实现采用的就是此模式 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pGNXQ7J-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停.png)] public static void main(String[] args) {GuardedObject object new GuardedObjectV2();new Thread(() - {sleep(1);object.complete(Arrays.asList(a, b, c));}).start();Object response object.get(2500);if (response ! null) {log.debug(get response: [{}] lines, ((ListString) response).size());} else {log.debug(cant get response);} }class GuardedObject {private Object response;private final Object lock new Object();//获取结果//timeout :最大等待时间public Object get(long millis) {synchronized (lock) {// 1) 记录最初时间long begin System.currentTimeMillis();// 2) 已经经历的时间long timePassed 0;while (response null) {// 4) 假设 millis 是 1000结果在 400 时唤醒了那么还有 600 要等long waitTime millis - timePassed;log.debug(waitTime: {}, waitTime);//经历时间超过最大等待时间退出循环if (waitTime 0) {log.debug(break...);break;}try {lock.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 3) 如果提前被唤醒这时已经经历的时间假设为 400timePassed System.currentTimeMillis() - begin;log.debug(timePassed: {}, object is null {},timePassed, response null);}return response;}}//产生结果public void complete(Object response) {synchronized (lock) {// 条件满足通知等待线程this.response response;log.debug(notify...);lock.notifyAll();}} }多任务版 多任务版保护性暂停 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4AR92Rvf-1678148961777)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停多任务版.png)] public static void main(String[] args) throws InterruptedException {for (int i 0; i 3; i) {new People().start();}Thread.sleep(1000);for (Integer id : Mailboxes.getIds()) {new Postman(id, id 号快递到了).start();} }Slf4j(topic c.People) class People extends Thread{Overridepublic void run() {// 收信GuardedObject guardedObject Mailboxes.createGuardedObject();log.debug(开始收信i d:{}, guardedObject.getId());Object mail guardedObject.get(5000);log.debug(收到信id:{}内容:{}, guardedObject.getId(),mail);} }class Postman extends Thread{private int id;private String mail;//构造方法Overridepublic void run() {GuardedObject guardedObject Mailboxes.getGuardedObject(id);log.debug(开始送信i d:{}内容:{}, guardedObject.getId(),mail);guardedObject.complete(mail);} }class Mailboxes {private static MapInteger, GuardedObject boxes new Hashtable();private static int id 1;//产生唯一的idprivate static synchronized int generateId() {return id;}public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}public static GuardedObject createGuardedObject() {GuardedObject go new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static SetInteger getIds() {return boxes.keySet();} } class GuardedObject {//标识Guarded Objectprivate int id;//添加get set方法 }顺序输出 顺序输出 2 1 public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {while (true) {//try { Thread.sleep(1000); } catch (InterruptedException e) { }// 当没有许可时当前线程暂停运行有许可时用掉这个许可当前线程恢复运行LockSupport.park();System.out.println(1);}});Thread t2 new Thread(() - {while (true) {System.out.println(2);// 给线程 t1 发放『许可』多次连续调用 unpark 只会发放一个『许可』LockSupport.unpark(t1);try { Thread.sleep(500); } catch (InterruptedException e) { }}});t1.start();t2.start(); }交替输出 连续输出 5 次 abc public class day2_14 {public static void main(String[] args) throws InterruptedException {AwaitSignal awaitSignal new AwaitSignal(5);Condition a awaitSignal.newCondition();Condition b awaitSignal.newCondition();Condition c awaitSignal.newCondition();new Thread(() - {awaitSignal.print(a, a, b);}).start();new Thread(() - {awaitSignal.print(b, b, c);}).start();new Thread(() - {awaitSignal.print(c, c, a);}).start();Thread.sleep(1000);awaitSignal.lock();try {a.signal();} finally {awaitSignal.unlock();}} }class AwaitSignal extends ReentrantLock {private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber loopNumber;}//参数1打印内容 参数二条件变量 参数二唤醒下一个public void print(String str, Condition condition, Condition next) {for (int i 0; i loopNumber; i) {lock();try {condition.await();System.out.print(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {unlock();}}} }异步模式 传统版 异步模式之生产者/消费者 class ShareData {private int number 0;private Lock lock new ReentrantLock();private Condition condition lock.newCondition();public void increment() throws Exception{// 同步代码块加锁lock.lock();try {// 判断 防止虚假唤醒while(number ! 0) {// 等待不能生产condition.await();}// 干活number;System.out.println(Thread.currentThread().getName() \t number);// 通知 唤醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public void decrement() throws Exception{// 同步代码块加锁lock.lock();try {// 判断 防止虚假唤醒while(number 0) {// 等待不能消费condition.await();}// 干活number--;System.out.println(Thread.currentThread().getName() \t number);// 通知 唤醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}} }public class TraditionalProducerConsumer {public static void main(String[] args) {ShareData shareData new ShareData();// t1线程生产new Thread(() - {for (int i 0; i 5; i) {shareData.increment();}}, t1).start();// t2线程消费new Thread(() - {for (int i 0; i 5; i) {shareData.decrement();}}, t2).start(); } }改进版 异步模式之生产者/消费者 消费队列可以用来平衡生产和消费的线程资源不需要产生结果和消费结果的线程一一对应生产者仅负责产生结果数据不关心数据该如何处理而消费者专心处理结果数据消息队列是有容量限制的满时不会再加入数据空时不会再消耗数据JDK 中各种阻塞队列采用的就是这种模式 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-75fSCNTw-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-生产者消费者模式.png)] public class demo {public static void main(String[] args) {MessageQueue queue new MessageQueue(2);for (int i 0; i 3; i) {int id i;new Thread(() - {queue.put(new Message(id,值id));}, 生产者 i).start();}new Thread(() - {while (true) {try {Thread.sleep(1000);Message message queue.take();} catch (InterruptedException e) {e.printStackTrace();}}},消费者).start();} }//消息队列类Java间线程之间通信 class MessageQueue {private LinkedListMessage list new LinkedList();//消息的队列集合private int capacity;//队列容量public MessageQueue(int capacity) {this.capacity capacity;}//获取消息public Message take() {//检查队列是否为空synchronized (list) {while (list.isEmpty()) {try {sout(Thread.currentThread().getName() :队列为空消费者线程等待);list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//从队列的头部获取消息返回Message message list.removeFirst();sout(Thread.currentThread().getName() 已消费消息-- message);list.notifyAll();return message;}}//存入消息public void put(Message message) {synchronized (list) {//检查队列是否满while (list.size() capacity) {try {sout(Thread.currentThread().getName():队列为已满生产者线程等待);list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//将消息加入队列尾部list.addLast(message);sout(Thread.currentThread().getName() :已生产消息-- message);list.notifyAll();}} }final class Message {private int id;private Object value;//get set }阻塞队列 public static void main(String[] args) {ExecutorService consumer Executors.newFixedThreadPool(1);ExecutorService producer Executors.newFixedThreadPool(1);BlockingQueueInteger queue new SynchronousQueue();producer.submit(() - {try {System.out.println(生产...);Thread.sleep(1000);queue.put(10);} catch (InterruptedException e) {e.printStackTrace();}});consumer.submit(() - {try {System.out.println(等待消费...);Integer result queue.take();System.out.println(结果为: result);} catch (InterruptedException e) {e.printStackTrace();}}); }内存 JMM 内存模型 Java 内存模型是 Java Memory ModelJMM本身是一种抽象的概念实际上并不存在描述的是一组规则或规范通过这组规范定义了程序中各个变量包括实例字段静态字段和构成数组对象的元素的访问方式 JMM 作用 屏蔽各种硬件和操作系统的内存访问差异实现让 Java 程序在各种平台下都能达到一致的内存访问效果规定了线程和内存之间的一些关系 根据 JMM 的设计系统存在一个主内存Main MemoryJava 中所有变量都存储在主存中对于所有线程都是共享的每条线程都有自己的工作内存Working Memory工作内存中保存的是主存中某些变量的拷贝线程对所有变量的操作都是先对变量进行拷贝然后在工作内存中进行不能直接操作主内存中的变量线程之间无法相互直接访问线程间的通信传递必须通过主内存来完成 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w1eR8vM3-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM内存模型.png)] 主内存和工作内存 主内存计算机的内存也就是经常提到的 8G 内存16G 内存存储所有共享变量的值工作内存存储该线程使用到的共享变量在主内存的的值的副本拷贝 JVM 和 JMM 之间的关系JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分这两者基本上是没有关系的如果两者一定要勉强对应起来 主内存主要对应于 Java 堆中的对象实例数据部分而工作内存则对应于虚拟机栈中的部分区域从更低层次上说主内存直接对应于物理硬件的内存工作内存对应寄存器和高速缓存 内存交互 Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作每个操作都是原子的 非原子协定没有被 volatile 修饰的 long、double 外默认按照两次 32 位的操作 lock作用于主内存将一个变量标识为被一个线程独占状态对应 monitorenterunclock作用于主内存将一个变量从独占状态释放出来释放后的变量才可以被其他线程锁定对应 monitorexitread作用于主内存把一个变量的值从主内存传输到工作内存中load作用于工作内存在 read 之后执行把 read 得到的值放入工作内存的变量副本中use作用于工作内存把工作内存中一个变量的值传递给执行引擎每当遇到一个使用到变量的操作时都要使用该指令assign作用于工作内存把从执行引擎接收到的一个值赋给工作内存的变量store作用于工作内存把工作内存的一个变量的值传送到主内存中write作用于主内存在 store 之后执行把 store 得到的值放入主内存的变量中 参考文章https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md 三大特性 可见性 可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值 存在不可见问题的根本原因是由于缓存的存在线程持有的是共享变量的副本无法感知其他线程对于共享变量的更改导致读取的值不是最新的。但是 final 修饰的变量是不可变的就算有缓存也不会存在不可见的问题 main 线程对 run 变量的修改对于 t 线程不可见导致了 t 线程无法停止 static boolean run true; //添加volatile public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(run){// ....}});t.start();sleep(1);run false; // 线程t不会如预想的停下来 }原因 初始状态 t 线程刚开始从主内存读取了 run 的值到工作内存因为 t 线程要频繁从主内存中读取 run 的值JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中减少对主存中 run 的访问提高效率1 秒之后main 线程修改了 run 的值并同步至主存而 t 是从自己工作内存中的高速缓存中读取这个变量的值结果永远是旧值 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8f3nOpHW-1678148961778)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-可见性例子.png)] 原子性 原子性不可分割完整性也就是说某个线程正在做某个具体业务时中间不可以被分割需要具体完成要么同时成功要么同时失败保证指令不会受到线程上下文切换的影响 定义原子操作的使用规则 不允许 read 和 load、store 和 write 操作之一单独出现必须顺序执行但是不要求连续不允许一个线程丢弃 assign 操作必须同步回主存不允许一个线程无原因地没有发生过任何 assign 操作把数据从工作内存同步会主内存中一个新的变量只能在主内存中诞生不允许在工作内存中直接使用一个未被初始化assign 或者 load的变量即对一个变量实施 use 和 store 操作之前必须先自行 assign 和 load 操作一个变量在同一时刻只允许一条线程对其进行 lock 操作但 lock 操作可以被同一线程重复执行多次多次执行 lock 后只有执行相同次数的 unlock 操作变量才会被解锁lock 和 unlock 必须成对出现如果对一个变量执行 lock 操作将会清空工作内存中此变量的值在执行引擎使用这个变量之前需要重新从主存加载如果一个变量事先没有被 lock 操作锁定则不允许执行 unlock 操作也不允许去 unlock 一个被其他线程锁定的变量对一个变量执行 unlock 操作之前必须先把此变量同步到主内存中执行 store 和 write 操作 有序性 有序性在本线程内观察所有操作都是有序的在一个线程观察另一个线程所有操作都是无序的无序是因为发生了指令重排序 CPU 的基本工作是执行存储的指令序列即程序程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程为了提高性能编译器和处理器会对指令重排一般分为以下三种 源代码 - 编译器优化的重排 - 指令并行的重排 - 内存系统的重排 - 最终执行指令现代 CPU 支持多级指令流水线几乎所有的冯•诺伊曼型计算机的 CPU其工作都可以分为 5 个阶段取指令、指令译码、执行指令、访存取数和结果写回可以称之为五级指令流水线。CPU 可以在一个时钟周期内同时运行五条指令的不同阶段每个线程不同的阶段本质上流水线技术并不能缩短单条指令的执行时间但变相地提高了指令地吞吐率 处理器在进行重排序时必须要考虑指令之间的数据依赖性 单线程环境也存在指令重排由于存在依赖性最终执行结果和代码顺序的结果一致多线程环境中线程交替执行由于编译器优化重排会获取其他线程处在不同阶段的指令同时执行 补充知识 指令周期是取出一条指令并执行这条指令的时间一般由若干个机器周期组成机器周期也称为 CPU 周期一条指令的执行过程划分为若干个阶段如取指、译码、执行等每一阶段完成一个基本操作完成一个基本操作所需要的时间称为机器周期振荡周期指周期性信号作周期性重复变化的时间间隔 cache 缓存机制 缓存结构 在计算机系统中CPU 高速缓存CPU Cache简称缓存是用于减少处理器访问内存所需平均时间的部件在存储体系中位于自顶向下的第二层仅次于 CPU 寄存器其容量远小于内存但速度却可以接近处理器的频率 CPU 处理器速度远远大于在主内存中的为了解决速度差异在它们之间架设了多级缓存如 L1、L2、L3 级别的缓存这些缓存离 CPU 越近就越快将频繁操作的数据缓存到这里加快访问速度 从 CPU 到大约需要的时钟周期寄存器1 cycle (4GHz 的 CPU 约为 0.25ns)L13~4 cycleL210~20 cycleL340~45 cycle内存120~240 cycle 缓存使用 当处理器发出内存访问请求时会先查看缓存内是否有请求数据如果存在命中则不用访问内存直接返回该数据如果不存在失效则要先把内存中的相应数据载入缓存再将其返回处理器 缓存之所以有效主要因为程序运行时对内存的访问呈现局部性Locality特征。既包括空间局部性Spatial Locality也包括时间局部性Temporal Locality有效利用这种局部性缓存可以达到极高的命中率 伪共享 缓存以缓存行 cache line 为单位每个缓存行对应着一块内存一般是 64 byte8 个 long在 CPU 从主存获取数据时以 cache line 为单位加载于是相邻的数据会一并加载到缓存中 缓存会造成数据副本的产生即同一份数据会缓存在不同核心的缓存行中CPU 要保证数据的一致性需要做到某个 CPU 核心更改了数据其它 CPU 核心对应的整个缓存行必须失效这就是伪共享 解决方法 padding通过填充让数据落在不同的 cache line 中 Contended原理参考 无锁 → Adder → 优化机制 → 伪共享 Linux 查看 CPU 缓存行 命令cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64内存地址格式[高位组标记] [低位索引] [偏移量] 缓存一致 缓存一致性当多个处理器运算任务都涉及到同一块主内存区域的时候将可能导致各自的缓存数据不一样 MESIModified Exclusive Shared Or Invalid是一种广泛使用的支持写回策略的缓存一致性协议CPU 中每个缓存行caceh line使用 4 种状态进行标记使用额外的两位 bit 表示) M被修改Modified 该缓存行只被缓存在该 CPU 的缓存中并且是被修改过的与主存中的数据不一致 (dirty)该缓存行中的内存需要写回 (write back) 主存。该状态的数据再次被修改不会发送广播因为其他核心的数据已经在第一次修改时失效一次 当被写回主存之后该缓存行的状态会变成独享 (exclusive) 状态 E独享的Exclusive 该缓存行只被缓存在该 CPU 的缓存中是未被修改过的 (clear)与主存中数据一致修改数据不需要通知其他 CPU 核心该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) 当 CPU 修改该缓存行中内容时该状态可以变成 Modified 状态 S共享的Shared 该状态意味着该缓存行可能被多个 CPU 缓存并且各个缓存中的数据与主存数据一致当 CPU 修改该缓存行中会向其它 CPU 核心广播一个请求使该缓存行变成无效状态 (Invalid)然后再更新当前 Cache 里的数据 I无效的Invalid 该缓存是无效的可能有其它 CPU 修改了该缓存行 解决方法各个处理器访问缓存时都遵循一些协议在读写时要根据协议进行操作协议主要有 MSI、MESI 等 处理机制 单核 CPU 处理器会自动保证基本内存操作的原子性 多核 CPU 处理器每个 CPU 处理器内维护了一块内存每个内核内部维护着一块缓存当多线程并发读写时就会出现缓存数据不一致的情况。处理器提供 总线锁定当处理器要操作共享变量时在 BUS 总线上发出一个 LOCK 信号其他处理器就无法操作这个共享变量该操作会导致大量阻塞从而增加系统的性能开销平台级别的加锁缓存锁定当处理器对缓存中的共享变量进行了操作其他处理器有嗅探机制将各自缓存中的该共享变量的失效读取时会重新从主内存中读取最新的数据基于 MESI 缓存一致性协议来实现 有如下两种情况处理器不会使用缓存锁定 当操作的数据跨多个缓存行或没被缓存在处理器内部则处理器会使用总线锁定 有些处理器不支持缓存锁定比如Intel 486 和 Pentium 处理器也会调用总线锁定 总线机制 总线嗅探每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了当处理器发现自己的缓存对应的内存地址的数据被修改就将当前处理器的缓存行设置为无效状态当处理器对这个数据进行操作时会重新从内存中把数据读取到处理器缓存中 总线风暴当某个 CPU 核心更新了 Cache 中的数据要把该事件广播通知到其他核心写传播CPU 需要每时每刻监听总线上的一切活动但是不管别的核心的 Cache 是否缓存相同的数据都需要发出一个广播事件不断的从主内存嗅探和 CAS 循环无效的交互会导致总线带宽达到峰值因此不要大量使用 volatile 关键字使用 volatile、syschonized 都需要根据实际场景 volatile 同步机制 volatile 是 Java 虚拟机提供的轻量级的同步机制三大特性 保证可见性不保证原子性保证有序性禁止指令重排 性能volatile 修饰的变量进行读操作与普通变量几乎没什么差别但是写操作相对慢一些因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行但是开销比锁要小 synchronized 无法禁止指令重排和处理器优化为什么可以保证有序性可见性 加了锁之后只能有一个线程获得到了锁获得不到锁的线程就要阻塞所以同一时间只有一个线程执行相当于单线程由于数据依赖性的存在单线程的指令重排是没有问题的线程加锁前将清空工作内存中共享变量的值使用共享变量时需要从主内存中重新读取最新的值线程解锁前必须把共享变量的最新值刷新到主内存中JMM 内存交互章节有讲 指令重排 volatile 修饰的变量可以禁用指令重排 指令重排实例 example 1 public void mySort() {int x 11; //语句1int y 12; //语句2 谁先执行效果一样x x 5; //语句3y x * x; //语句4 }执行顺序是1 2 3 4、2 1 3 4、1 3 2 4 指令重排也有限制不会出现4321语句 4 需要依赖于 y 以及 x 的申明因为存在数据依赖无法首先执行 example 2 int num 0; boolean ready false; // 线程1 执行此方法 public void actor1(I_Result r) {if(ready) {r.r1 num num;} else {r.r1 1;} } // 线程2 执行此方法 public void actor2(I_Result r) {num 2;ready true; }情况一线程 1 先执行ready false结果为 r.r1 1 情况二线程 2 先执行 num 2但还没执行 ready true线程 1 执行结果为 r.r1 1 情况三线程 2 先执行 ready true线程 1 执行进入 if 分支结果为 r.r1 4 情况四线程 2 执行 ready true切换到线程 1进入 if 分支为 r.r1 0再切回线程 2 执行 num 2发生指令重排 底层原理 缓存一致 使用 volatile 修饰的共享变量底层通过汇编 lock 前缀指令进行缓存锁定在线程修改完共享变量后写回主存其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障Memory BarrierMemory Fence 对 volatile 变量的写指令后会加入写屏障对 volatile 变量的读指令前会加入读屏障 内存屏障有三个作用 确保对内存的读-改-写操作原子执行阻止屏障两侧的指令重排序强制把缓存中的脏数据写回主内存让缓存行中相应的数据失效 内存屏障 保证可见性 写屏障sfenceStore Barrier保证在该屏障之前的对共享变量的改动都同步到主存当中 public void actor2(I_Result r) {num 2;ready true; // ready 是 volatile 赋值带写屏障// 写屏障 }读屏障lfenceLoad Barrier保证在该屏障之后的对共享变量的读取从主存刷新变量值加载的是主存中最新数据 public void actor1(I_Result r) {// 读屏障// ready 是 volatile 读取值带读屏障if(ready) {r.r1 num num;} else {r.r1 1;} }全能屏障mfencemodify/mix Barrier兼具 sfence 和 lfence 的功能 保证有序性 写屏障会确保指令重排序时不会将写屏障之前的代码排在写屏障之后读屏障会确保指令重排序时不会将读屏障之后的代码排在读屏障之前 不能解决指令交错 写屏障仅仅是保证之后的读能够读到最新的结果但不能保证其他线程的读跑到写屏障之前 有序性的保证也只是保证了本线程内相关代码不被重排序 volatile i 0; new Thread(() - {i}); new Thread(() - {i--});i 反编译后的指令 0: iconst_1 // 当int取值 -1~5 时JVM采用iconst指令将常量压入栈中 1: istore_1 // 将操作数栈顶数据弹出存入局部变量表的 slot 1 2: iinc 1, 1 交互规则 对于 volatile 修饰的变量 线程对变量的 use 与 load、read 操作是相关联的所以变量使用前必须先从主存加载线程对变量的 assign 与 store、write 操作是相关联的所以变量使用后必须同步至主存线程 1 和线程 2 谁先对变量执行 read 操作就会先进行 write 操作防止指令重排 双端检锁 检锁机制 Double-Checked Locking双端检锁机制 DCL双端检锁机制不一定是线程安全的原因是有指令重排的存在加入 volatile 可以禁止指令重排 public final class Singleton {private Singleton() { }private static Singleton INSTANCE null;public static Singleton getInstance() {if(INSTANCE null) { // t2这里的判断不是线程安全的// 首次访问会同步而之后的使用没有 synchronizedsynchronized(Singleton.class) {// 这里是线程安全的判断防止其他线程在当前线程等待锁的期间完成了初始化if (INSTANCE null) { INSTANCE new Singleton();}}}return INSTANCE;} }不锁 INSTANCE 的原因 INSTANCE 要重新赋值INSTANCE 是 null线程加锁之前需要获取对象的引用设置对象头null 没有引用 实现特点 懒惰初始化首次使用 getInstance() 才使用 synchronized 加锁后续使用时无需加锁第一个 if 使用了 INSTANCE 变量是在同步块之外但在多线程环境下会产生问题 DCL问题 getInstance 方法对应的字节码为 0: getstatic #2 // Field INSTANCE:Ltest/Singleton; 3: ifnonnull 37 6: ldc #3 // class test/Singleton 8: dup 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Ltest/Singleton; 14: ifnonnull 27 17: new #3 // class test/Singleton 20: dup 21: invokespecial #4 // Method init:()V 24: putstatic #2 // Field INSTANCE:Ltest/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Ltest/Singleton; 40: areturn17 表示创建对象将对象引用入栈20 表示复制一份对象引用引用地址21 表示利用一个对象引用调用构造方法初始化对象24 表示利用一个对象引用赋值给 static INSTANCE 步骤 21 和 24 之间不存在数据依赖关系而且无论重排前后程序的执行结果在单线程中并没有改变因此这种重排优化是允许的 关键在于 0:getstatic 这行代码在 monitor 控制之外可以越过 monitor 读取 INSTANCE 变量的值当其他线程访问 INSTANCE 不为 null 时由于 INSTANCE 实例未必已初始化那么 t2 拿到的是将是一个未初始化完毕的单例返回这就造成了线程安全的问题 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HM8H5kL3-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL出现的问题.png)] 解决方法 指令重排只会保证串行语义的执行一致性单线程但并不会关系多线程间的语义一致性 引入 volatile来保证出现指令重排的问题从而保证单例模式的线程安全性 private static volatile SingletonDemo INSTANCE null;ha-be happens-before 先行发生 Java 内存模型具备一些先天的“有序性”即不需要通过任何同步手段volatile、synchronized 等就能够得到保证的安全这个通常也称为 happens-before 原则它是可见性与有序性的一套规则总结 不符合 happens-before 规则JMM 并不能保证一个线程的可见性和有序性 程序次序规则 (Program Order Rule)一个线程内逻辑上书写在前面的操作先行发生于书写在后面的操作 因为多个操作之间有先后依赖关系则不允许对这些操作进行重排序 锁定规则 (Monitor Lock Rule)一个 unlock 操作先行发生于后面时间的先后对同一个锁的 lock 操作所以线程解锁 m 之前对变量的写解锁前会刷新到主内存中对于接下来对 m 加锁的其它线程对该变量的读可见 volatile 变量规则 (Volatile Variable Rule)对 volatile 变量的写操作先行发生于后面对这个变量的读 传递规则 (Transitivity)具有传递性如果操作 A 先行发生于操作 B而操作 B 又先行发生于操作 C则可以得出操作 A 先行发生于操作 C 线程启动规则 (Thread Start Rule)Thread 对象的 start()方 法先行发生于此线程中的每一个操作 static int x 10;//线程 start 前对变量的写对该线程开始后对该变量的读可见 new Thread(()-{ System.out.println(x); },t1).start();线程中断规则 (Thread Interruption Rule)对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生 线程终止规则 (Thread Termination Rule)线程中所有的操作都先行发生于线程的终止检测可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行 对象终结规则Finaizer Rule一个对象的初始化完成构造函数执行结束先行发生于它的 finalize() 方法的开始 设计模式 终止模式 终止模式之两阶段终止模式停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 class TwoPhaseTermination {// 监控线程private Thread monitor;// 停止标记private volatile boolean stop false;;// 启动监控线程public void start() {monitor new Thread(() - {while (true) {Thread thread Thread.currentThread();if (stop) {System.out.println(后置处理);break;}try {Thread.sleep(1000);// 睡眠System.out.println(thread.getName() 执行监控记录);} catch (InterruptedException e) {System.out.println(被打断退出睡眠);}}});monitor.start();}// 停止监控线程public void stop() {stop true;monitor.interrupt();// 让线程尽快退出Timed Waiting} } // 测试 public static void main(String[] args) throws InterruptedException {TwoPhaseTermination tpt new TwoPhaseTermination();tpt.start();Thread.sleep(3500);System.out.println(停止监控);tpt.stop(); }Balking Balking 犹豫模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事那么本线程就无需再做了直接结束返回 public class MonitorService {// 用来表示是否已经有线程已经在执行启动了private volatile boolean starting false;public void start() {System.out.println(尝试启动监控线程...);synchronized (this) {if (starting) {return;}starting true;}// 真正启动监控线程...} }对比保护性暂停模式保护性暂停模式用在一个线程等待另一个线程的执行结果当条件不满足时线程等待 例子希望 doInit() 方法仅被调用一次下面的实现出现的问题 当 t1 线程进入 init() 准备 doInit()t2 线程进来initialized 还为f alse则 t2 就又初始化一次volatile 适合一个线程写其他线程读的情况这个代码需要加锁 public class TestVolatile {volatile boolean initialized false;void init() {if (initialized) {return;}doInit();initialized true;}private void doInit() {} }无锁 CAS 原理 无锁编程Lock Free CAS 的全称是 Compare-And-Swap是 CPU 并发原语 CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法调用 UnSafe 类中的 CAS 方法JVM 会实现出 CAS 汇编指令这是一种完全依赖于硬件的功能实现了原子操作CAS 是一种系统原语原语属于操作系统范畴是由若干条指令组成 用于完成某个功能的一个过程并且原语的执行必须是连续的执行过程中不允许被中断所以 CAS 是一条 CPU 的原子指令不会造成数据不一致的问题是线程安全的 底层原理CAS 的底层是 lock cmpxchg 指令X86 架构在单核和多核 CPU 下都能够保证比较交换的原子性 程序是在单核处理器上运行会省略 lock 前缀单处理器自身会维护处理器内的顺序一致性不需要 lock 前缀的内存屏障效果 程序是在多核处理器上运行会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时CPU 会执行总线锁定或缓存锁定将修改的变量写入到主存这个过程不会被线程的调度机制所打断保证了多个线程对内存操作的原子性 作用比较当前工作内存中的值和主物理内存中的值如果相同则执行规定操作否则继续比较直到主内存和工作内存的值一致为止 CAS 特点 CAS 体现的是无锁并发、无阻塞并发线程不会陷入阻塞线程不需要频繁切换状态上下文切换系统调用CAS 是基于乐观锁的思想 CAS 缺点 执行的是循环操作如果比较不成功一直在循环最差的情况某个线程一直取到的值和预期值都不一样就会无限循环导致饥饿使用 CAS 线程数不要超过 CPU 的核心数采用分段 CAS 和自动迁移机制只能保证一个共享变量的原子操作 对于一个共享变量执行操作时可以通过循环 CAS 的方式来保证原子操作对于多个共享变量操作时循环 CAS 就无法保证操作的原子性这个时候只能用锁来保证原子性 引出来 ABA 问题 乐观锁 CAS 与 synchronized 总结 synchronized 是从悲观的角度出发总是假设最坏的情况每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上锁这样别人想拿这个数据就会阻塞共享资源每次只给一个线程使用其它线程阻塞用完后再把资源转让给其它线程因此 synchronized 也称之为悲观锁ReentrantLock 也是一种悲观锁性能较差CAS 是从乐观的角度出发总是假设最好的情况每次去拿数据的时候都认为别人不会修改所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过则获取现在最新的值如果别人没修改过直接修改共享数据的值CAS 这种机制也称之为乐观锁综合性能较好 Atomic 常用API 常见原子类AtomicInteger、AtomicBoolean、AtomicLong 构造方法 public AtomicInteger()初始化一个默认值为 0 的原子型 Integerpublic AtomicInteger(int initialValue)初始化一个指定值的原子型 Integer 常用API 方法作用public final int get()获取 AtomicInteger 的值public final int getAndIncrement()以原子方式将当前值加 1返回的是自增前的值public final int incrementAndGet()以原子方式将当前值加 1返回的是自增后的值public final int getAndSet(int value)以原子方式设置为 newValue 的值返回旧值public final int addAndGet(int data)以原子方式将输入的数值与实例中的值相加并返回实例AtomicInteger 里的 value原理分析 AtomicInteger 原理自旋锁 CAS 算法 CAS 算法有 3 个操作数内存值 V 旧的预期值 A要修改的值 B 当旧的预期值 A 内存值 V 此时可以修改将 V 改为 B当旧的预期值 A ! 内存值 V 此时不能修改并重新获取现在的最新值重新获取的动作就是自旋 分析 getAndSet 方法 AtomicInteger public final int getAndSet(int newValue) {/*** this: 当前对象* valueOffset: 内存偏移量内存地址*/return unsafe.getAndSetInt(this, valueOffset, newValue); }valueOffset偏移量表示该变量值相对于当前对象地址的偏移Unsafe 就是根据内存偏移地址获取数据 valueOffset unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField(value)); //调用本地方法 -- public native long objectFieldOffset(Field var1);unsafe 类 // val1: AtomicInteger对象本身var2: 该对象值得引用地址var4: 需要变动的数 public final int getAndSetInt(Object var1, long var2, int var4) {int var5;do {// var5: 用 var1 和 var2 找到的内存中的真实值var5 this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var4));return var5; }var5从主内存中拷贝到工作内存中的值每次都要从主内存拿到最新的值到本地内存然后执行 compareAndSwapInt() 再和主内存的值进行比较假设方法返回 false那么就一直执行 while 方法直到期望的值和真实值一样修改数据 变量 value 用 volatile 修饰保证了多线程之间的内存可见性避免线程从工作缓存中获取失效的变量 private volatile int valueCAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果 分析 getAndUpdate 方法 getAndUpdate public final int getAndUpdate(IntUnaryOperator updateFunction) {int prev, next;do {prev get(); //当前值cas的期望值next updateFunction.applyAsInt(prev);//期望值更新到该值} while (!compareAndSet(prev, next));//自旋return prev; }函数式接口可以自定义操作逻辑 AtomicInteger a new AtomicInteger(); a.getAndUpdate(i - i 10);compareAndSet public final boolean compareAndSet(int expect, int update) {/*** this: 当前对象* valueOffset: 内存偏移量内存地址* expect: 期望的值* update: 更新的值*/return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }原子引用 原子引用对 Object 进行原子操作提供一种读和写都是原子性的对象引用变量 原子引用类AtomicReference、AtomicStampedReference、AtomicMarkableReference AtomicReference 类 构造方法AtomicReferenceT atomicReference new AtomicReferenceT() 常用 API public final boolean compareAndSet(V expectedValue, V newValue)CAS 操作public final void set(V newValue)将值设置为 newValuepublic final V get()返回当前值 public class AtomicReferenceDemo {public static void main(String[] args) {Student s1 new Student(33, z3);// 创建原子引用包装类AtomicReferenceStudent atomicReference new AtomicReference();// 设置主内存共享变量为s1atomicReference.set(s1);// 比较并交换如果现在主物理内存的值为 z3那么交换成 l4while (true) {Student s2 new Student(44, l4);if (atomicReference.compareAndSet(s1, s2)) {break;}}System.out.println(atomicReference.get());} }class Student {private int id;private String name;//。。。。 }原子数组 原子数组类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray AtomicIntegerArray 类方法 /** * i the index * expect the expected value * update the new value */ public final boolean compareAndSet(int i, int expect, int update) {return compareAndSetRaw(checkedByteOffset(i), expect, update); }原子更新器 原子更新器类AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 利用字段更新器可以针对对象的某个域Field进行原子操作只能配合 volatile 修饰的字段使用否则会出现异常 IllegalArgumentException: Must be volatile type 常用 API static U AtomicIntegerFieldUpdaterU newUpdater(ClassU c, String fieldName)构造方法abstract boolean compareAndSet(T obj, int expect, int update)CAS public class UpdateDemo {private volatile int field;public static void main(String[] args) {AtomicIntegerFieldUpdater fieldUpdater AtomicIntegerFieldUpdater.newUpdater(UpdateDemo.class, field);UpdateDemo updateDemo new UpdateDemo();fieldUpdater.compareAndSet(updateDemo, 0, 10);System.out.println(updateDemo.field);//10} }原子累加器 原子累加器类LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator LongAdder 和 LongAccumulator 区别 相同点 LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的LongAddr 类是 LongAccumulator 类的一个特例只是 LongAccumulator 提供了更强大的功能可以自定义累加规则当accumulatorFunction 为 null 时就等价于 LongAddr 不同点 调用 casBase 时LongAccumulator 使用 function.applyAsLong(b base, x) 来计算LongAddr 使用 casBase(b base, b x) LongAccumulator 类功能更加强大构造方法参数中 accumulatorFunction 是一个双目运算器接口可以指定累加规则比如累加或者相乘其根据输入的两个参数返回一个计算值LongAdder 内置累加规则identity 则是 LongAccumulator 累加器的初始值LongAccumulator 可以为累加器提供非0的初始值而 LongAdder 只能提供默认的 0 Adder 优化机制 LongAdder 是 Java8 提供的类跟 AtomicLong 有相同的效果但对 CAS 机制进行了优化尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能 CAS 底层实现是在一个循环中不断地尝试修改目标值直到修改成功。如果竞争不激烈修改成功率很高否则失败率很高失败后这些重复的原子性操作会耗费性能导致大量线程空循环自旋转 优化核心思想数据分离将 AtomicLong 的单点的更新压力分担到各个节点空间换时间在低并发的时候直接更新可以保障和 AtomicLong 的性能基本一致而在高并发的时候通过分散减少竞争提高了性能 分段 CAS 机制 在发生竞争时创建 Cell 数组用于将不同线程的操作离散通过 hash 等算法映射到不同的节点上设置多个累加单元会根据需要扩容最大为 CPU 核数Therad-0 累加 Cell[0]而 Thread-1 累加 Cell[1] 等最后将结果汇总在累加时操作的不同的 Cell 变量因此减少了 CAS 重试失败从而提高性能 自动分段迁移机制某个 Cell 的 value 执行 CAS 失败就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作 伪共享 Cell 为累加单元数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的这个域是 ThreadLocalRandom 更新的 // Striped64.Cell sun.misc.Contended static final class Cell {volatile long value;Cell(long x) { value x; }// 用 cas 方式进行累加, prev 表示旧值, next 表示新值final boolean cas(long prev, long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}// 省略不重要代码 }Cell 是数组形式在内存中是连续存储的64 位系统中一个 Cell 为 24 字节16 字节的对象头和 8 字节的 value每一个 cache line 为 64 字节因此缓存行可以存下 2 个的 Cell 对象当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1]无论谁修改成功都会导致当前缓存行失效从而导致对方的数据失效需要重新去主存获取影响效率 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQdE5bIW-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享1.png)] sun.misc.Contended防止缓存行伪共享在使用此注解的对象或字段的前后各增加 128 字节大小的 padding使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行这样就不会造成对方缓存行的失效 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1O1S8rxm-1678148961779)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享2.png)] 源码解析 Striped64 类成员属性 // 表示当前计算机CPU数量 static final int NCPU Runtime.getRuntime().availableProcessors() // 累加单元数组, 懒惰初始化 transient volatile Cell[] cells; // 基础值, 如果没有竞争, 则用 cas 累加这个域当 cells 扩容时也会将数据写到 base 中 transient volatile long base; // 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁 transient volatile int cellsBusy;工作流程 cells 占用内存是相对比较大的是惰性加载的在无竞争或者其他线程正在初始化 cells 数组的情况下直接更新 base 域 在第一次发生竞争时casBase 失败会创建一个大小为 2 的 cells 数组将当前累加的值包装为 Cell 对象放入映射的槽位上 分段累加的过程中如果当前线程对应的 cells 槽位为空就会新建 Cell 填充如果出现竞争就会重新计算线程对应的槽位继续自旋尝试修改 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍然后 rehash数组长度总是 2 的 n 次幂默认最大为 CPU 核数但是可以超过如果核数是 6 核数组最长是 8 方法分析 LongAdder#add累加方法 public void add(long x) {// as 为累加单元数组的引用b 为基础值v 表示期望值// m 表示 cells 数组的长度 - 1a 表示当前线程命中的 cell 单元格Cell[] as; long b, v; int m; Cell a;// cells 不为空说明 cells 已经被初始化线程发生了竞争去更新对应的 cell 槽位// 进入 || 后的逻辑去更新 base 域更新失败表示发生竞争进入条件if ((as cells) ! null || !casBase(b base, b x)) {// uncontended 为 true 表示 cell 没有竞争boolean uncontended true;// 条件一: true 说明 cells 未初始化多线程写 base 发生竞争需要进行初始化 cells 数组// fasle 说明 cells 已经初始化进行下一个条件寻找自己的 cell 去累加// 条件二: getProbe() 获取 hash 值 m 的逻辑和 HashMap 的逻辑相同保证散列的均匀性// true 说明当前线程对应下标的 cell 为空需要创建 cell// false 说明当前线程对应的 cell 不为空进行下一个条件【将 x 值累加到对应的 cell 中】// 条件三: 有取反符号false 说明 cas 成功直接返回true 说明失败当前线程对应的 cell 有竞争if (as null || (m as.length - 1) 0 ||(a as[getProbe() m]) null ||!(uncontended a.cas(v a.value, v x)))longAccumulate(x, null, uncontended);// 【uncontended 在对应的 cell 上累加失败的时候才为 false其余情况均为 true】} }Striped64#longAccumulatecell 数组创建 // x null false | true final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {int h;// 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cellif ((h getProbe()) 0) {// 初始化 probe获取 hash 值ThreadLocalRandom.current(); h getProbe(); // 默认情况下 当前线程肯定是写入到了 cells[0] 位置不把它当做一次真正的竞争wasUncontended true;}// 表示【扩容意向】false 一定不会扩容true 可能会扩容boolean collide false; //自旋for (;;) {// as 表示cells引用a 表示当前线程命中的 celln 表示 cells 数组长度v 表示 期望值Cell[] as; Cell a; int n; long v;// 【CASE1】: 表示 cells 已经初始化了当前线程应该将数据写入到对应的 cell 中if ((as cells) ! null (n as.length) 0) {// CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null需要创建 new Cellif ((a as[(n - 1) h]) null) {// 判断 cellsBusy 是否被锁if (cellsBusy 0) { // 创建 cell, 初始累加值为 xCell r new Cell(x); // 加锁if (cellsBusy 0 casCellsBusy()) {// 创建成功标记进入【创建 cell 逻辑】boolean created false; try {Cell[] rs; int m, j;// 把当前 cells 数组赋值给 rs并且不为 nullif ((rs cells) ! null (m rs.length) 0 // 再次判断防止其它线程初始化过该位置当前线程再次初始化该位置会造成数据丢失// 因为这里是线程安全的判断进行的逻辑不会被其他线程影响rs[j (m - 1) h] null) {// 把新创建的 cell 填充至当前位置rs[j] r;created true; // 表示创建完成}} finally {cellsBusy 0; // 解锁}if (created) // true 表示创建完成可以推出循环了break;continue;}}collide false;}// CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 caselse if (!wasUncontended)wasUncontended true;// CASE 1.3: 当前线程 rehash 过如果新命中的 cell 不为空就尝试累加false 说明新命中也有竞争else if (a.cas(v a.value, ((fn null) ? v x : fn.applyAsLong(v, x))))break;// CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容else if (n NCPU || cells ! as)collide false; // 扩容意向改为false【表示不能扩容了】// CASE 1.5: 更改扩容意向如果 n NCPU这里就永远不会执行到case1.4 永远先于 1.5 执行else if (!collide)collide true;// CASE 1.6: 【扩容逻辑】进行加锁else if (cellsBusy 0 casCellsBusy()) {try {// 线程安全的检查防止期间被其他线程扩容了if (cells as) { // 扩容为以前的 2 倍Cell[] rs new Cell[n 1];// 遍历移动值for (int i 0; i n; i)rs[i] as[i];// 把扩容后的引用给 cellscells rs;}} finally {cellsBusy 0; // 解锁}collide false; // 扩容意向改为 false表示不扩容了continue;}// 重置当前线程 Hash 值这就是【分段迁移机制】h advanceProbe(h);}// 【CASE2】: 运行到这说明 cells 还未初始化as 为null// 判断是否没有加锁没有加锁就用 CAS 加锁// 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells这里不是线程安全的判断else if (cellsBusy 0 cells as casCellsBusy()) {// 初始化标志开始 【初始化 cells 数组】boolean init false;try { // 再次判断 cells as 防止其它线程已经提前初始化了当前线程再次初始化导致丢失数据// 因为这里是【线程安全的重新检查经典 DCL】if (cells as) {Cell[] rs new Cell[2]; // 初始化数组大小为2rs[h 1] new Cell(x); // 填充线程对应的cellcells rs;init true; // 初始化成功标记置为 true}} finally {cellsBusy 0; // 解锁啊}if (init)break; // 初始化成功直接跳出自旋}// 【CASE3】: 运行到这说明其他线程在初始化 cells当前线程将值累加到 base累加成功直接结束自旋else if (casBase(v base, ((fn null) ? v x :fn.applyAsLong(v, x))))break; } }sum获取最终结果通过 sum 整合保证最终一致性不保证强一致性 public long sum() {Cell[] as cells; Cell a;long sum base;if (as ! null) {// 遍历 累加for (int i 0; i as.length; i) {if ((a as[i]) ! null)sum a.value;}}return sum; }ABA ABA 问题当进行获取主内存值时该内存值在写入主内存时已经被修改了 N 次但是最终又改成原来的值 其他线程先把 A 改成 B 又改回 A主线程仅能判断出共享变量的值与最初值 A 是否相同不能感知到这种从 A 改为 B 又 改回 A 的情况这时 CAS 虽然成功但是过程存在问题 构造方法 public AtomicStampedReference(V initialRef, int initialStamp)初始值和初始版本号 常用API public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)期望引用和期望版本号都一致才进行 CAS 修改数据public void set(V newReference, int newStamp)设置值和版本号public V getReference()返回引用的值public int getStamp()返回当前版本号 public static void main(String[] args) {AtomicStampedReferenceInteger atomicReference new AtomicStampedReference(100,1);int startStamp atomicReference.getStamp();new Thread(() -{int stamp atomicReference.getStamp();atomicReference.compareAndSet(100, 101, stamp, stamp 1);stamp atomicReference.getStamp();atomicReference.compareAndSet(101, 100, stamp, stamp 1);},t1).start();new Thread(() -{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp 1)) {System.out.println(atomicReference.getReference());//100System.out.println(Thread.currentThread().getName() 线程修改失败);}},t2).start(); }Unsafe Unsafe 是 CAS 的核心类由于 Java 无法直接访问底层系统需要通过本地Native方法来访问 Unsafe 类存在 sun.misc 包其中所有方法都是 native 修饰的都是直接调用操作系统底层资源执行相应的任务基于该类可以直接操作特定的内存数据其内部方法操作类似 C 的指针 模拟实现原子整数 public static void main(String[] args) {MyAtomicInteger atomicInteger new MyAtomicInteger(10);if (atomicInteger.compareAndSwap(20)) {System.out.println(atomicInteger.getValue());} }class MyAtomicInteger {private static final Unsafe UNSAFE;private static final long VALUE_OFFSET;private volatile int value;static {try {//Unsafe unsafe Unsafe.getUnsafe()这样会报错需要反射获取Field theUnsafe Unsafe.class.getDeclaredField(theUnsafe);theUnsafe.setAccessible(true);UNSAFE (Unsafe) theUnsafe.get(null);// 获取 value 属性的内存地址value 属性指向该地址直接设置该地址的值可以修改 value 的值VALUE_OFFSET UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField(value));} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();throw new RuntimeException();}}public MyAtomicInteger(int value) {this.value value;}public int getValue() {return value;}public boolean compareAndSwap(int update) {while (true) {int prev this.value;int next update;// 当前对象 内存偏移量 期望值 更新值if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {System.out.println(CAS成功);return true;}}} }final 原理 public class TestFinal {final int a 20; }字节码 0: aload_0 1: invokespecial #1 // Method java/lang/Object.init:()V 4: aload_0 5: bipush 20 // 将值直接放入栈中 7: putfield #2 // Field a:I -- 写屏障 10: returnfinal 变量的赋值通过 putfield 指令来完成在这条指令之后也会加入写屏障保证在其它线程读到它的值时不会出现为 0 的情况 其他线程访问 final 修饰的变量 复制一份放入栈中直接访问效率高大于 short 最大值会将其复制到类的常量池访问时从常量池获取 不可变 不可变如果一个对象不能够修改其内部状态属性那么就是不可变对象 不可变对象线程安全的不存在并发修改和可见性问题是另一种避免竞争的方式 String 类也是不可变的该类和类中所有属性都是 final 的 类用 final 修饰保证了该类中的方法不能被覆盖防止子类无意间破坏不可变性 无写入方法set确保外部不能对内部属性进行修改 属性用 final 修饰保证了该属性是只读的不能修改 public final class Stringimplements java.io.Serializable, ComparableString, CharSequence {/** The value is used for character storage. */private final char value[];//.... }更改 String 类数据时会构造新字符串对象生成新的 char[] value通过创建副本对象来避免共享的方式称之为保护性拷贝 State 无状态成员变量保存的数据也可以称为状态信息无状态就是没有成员变量 Servlet 为了保证其线程安全一般不为 Servlet 设置成员变量这种没有任何成员变量的类是线程安全的 Local 基本介绍 ThreadLocal 类用来提供线程内部的局部变量这种变量在多线程环境下访问通过 get 和 set 方法访问时能保证各个线程的变量相对独立于其他线程内的变量分配在堆内的 TLAB 中 ThreadLocal 实例通常来说都是 private static 类型的属于一个线程的本地变量用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据所以是线程安全的 ThreadLocal 作用 线程并发应用在多线程并发的场景下 传递数据通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量减少传递复杂度 线程隔离每个线程的变量都是独立的不会互相影响 对比 synchronized synchronizedThreadLocal原理同步机制采用以时间换空间的方式只提供了一份变量让不同的线程排队访问ThreadLocal 采用以空间换时间的方式为每个线程都提供了一份变量的副本从而实现同时访问而相不干扰侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离基本使用 常用方法 方法描述ThreadLocal()创建 ThreadLocal 对象protected T initialValue()返回当前线程局部变量的初始值public void set( T value)设置当前线程绑定的局部变量public T get()获取当前线程绑定的局部变量public void remove()移除当前线程绑定的局部变量 public class MyDemo {private static ThreadLocalString tl new ThreadLocal();private String content;private String getContent() {// 获取当前线程绑定的变量return tl.get();}private void setContent(String content) {// 变量content绑定到当前线程tl.set(content);}public static void main(String[] args) {MyDemo demo new MyDemo();for (int i 0; i 5; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {// 设置数据demo.setContent(Thread.currentThread().getName() 的数据);System.out.println(-----------------------);System.out.println(Thread.currentThread().getName() --- demo.getContent());}});thread.setName(线程 i);thread.start();}} }应用场景 ThreadLocal 适用于下面两种场景 每个线程需要有自己单独的实例实例需要在多个方法中共享但不希望被多线程共享 ThreadLocal 方案有两个突出的优势 传递数据保存每个线程绑定的数据在需要的地方可以直接获取避免参数直接传递带来的代码耦合问题线程隔离各线程之间的数据相互隔离却又具备并发性避免同步方式带来的性能损失 ThreadLocal 用于数据连接的事务管理 public class JdbcUtils {// ThreadLocal对象将connection绑定在当前线程中private static final ThreadLocalConnection tl new ThreadLocal();// c3p0 数据库连接池对象属性private static final ComboPooledDataSource ds new ComboPooledDataSource();// 获取连接public static Connection getConnection() throws SQLException {//取出当前线程绑定的connection对象Connection conn tl.get();if (conn null) {//如果没有则从连接池中取出conn ds.getConnection();//再将connection对象绑定到当前线程中非常重要的操作tl.set(conn);}return conn;}// ... }用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量 public class ThreadLocalDateUtil {private static ThreadLocalDateFormat threadLocal new ThreadLocalDateFormat() {Overrideprotected DateFormat initialValue() {return new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);}};public static Date parse(String dateStr) throws ParseException {return threadLocal.get().parse(dateStr);}public static String format(Date date) {return threadLocal.get().format(date);} }实现原理 底层结构 JDK8 以前每个 ThreadLocal 都创建一个 Map然后用线程作为 Map 的 key要存储的局部变量作为 Map 的 value达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露因为 Thread 停止后无法通过 key 删除对应的数据 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tbx1aoOA-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8前.png)] JDK8 以后每个 Thread 维护一个 ThreadLocalMap这个 Map 的 key 是 ThreadLocal 实例本身value 是真正要存储的值 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)Map 里面存储 ThreadLocal 对象key和线程的私有变量valueThread 内部的 Map 是由 ThreadLocal 维护的由 ThreadLocal 负责向 map 获取和设置线程的变量值对于不同的线程每次获取副本值时别的线程并不能获取到当前线程的副本值形成副本的隔离互不干扰 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzLTCp1t-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8后.png)] JDK8 前后对比 每个 Map 存储的 Entry 数量会变少因为之前的存储数量由 Thread 的数量决定现在由 ThreadLocal 的数量决定在实际编程当中往往 ThreadLocal 的数量要少于 Thread 的数量当 Thread 销毁之后对应的 ThreadLocalMap 也会随之销毁能减少内存的使用防止内存泄露 成员变量 Thread 类的相关属性每一个线程持有一个 ThreadLocalMap 对象存放由 ThreadLocal 和数据组成的 Entry 键值对 ThreadLocal.ThreadLocalMap threadLocals null计算 ThreadLocal 对象的哈希值 private final int threadLocalHashCode nextHashCode()使用 threadLocalHashCode (table.length - 1) 计算当前 entry 需要存放的位置 每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象 private static AtomicInteger nextHashCode new AtomicInteger()斐波那契数也叫黄金分割数hash 的增量就是这个数字带来的好处是 hash 分布非常均匀 private static final int HASH_INCREMENT 0x61c88647成员方法 方法都是线程安全的因为 ThreadLocal 属于一个线程的ThreadLocal 中的方法逻辑都是获取当前线程维护的 ThreadLocalMap 对象然后进行数据的增删改查没有指定初始值的 threadlcoal 对象默认赋值为 null initialValue()返回该线程局部变量的初始值 延迟调用的方法在执行 get 方法时才执行该方法缺省默认实现直接返回一个 null如果想要一个初始值可以重写此方法 该方法是一个 protected 的方法为了让子类覆盖而设计的 protected T initialValue() {return null; }nextHashCode()计算哈希值ThreadLocal 的散列方式称之为斐波那契散列每次获取哈希值都会加上 HASH_INCREMENT这样做可以尽量避免 hash 冲突让哈希值能均匀的分布在 2 的 n 次方的数组中 private static int nextHashCode() {// 哈希值自增一个 HASH_INCREMENT 数值return nextHashCode.getAndAdd(HASH_INCREMENT); }set()修改当前线程与当前 threadlocal 对象相关联的线程局部变量 public void set(T value) {// 获取当前线程对象Thread t Thread.currentThread();// 获取此线程对象中维护的 ThreadLocalMap 对象ThreadLocalMap map getMap(t);// 判断 map 是否存在if (map ! null)// 调用 threadLocalMap.set 方法进行重写或者添加map.set(this, value);else// map 为空调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程参数2是局部变量createMap(t, value); }// 获取当前线程 Thread 对应维护的 ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; } // 创建当前线程Thread对应维护的ThreadLocalMap void createMap(Thread t, T firstValue) {// 【这里的 this 是调用此方法的 threadLocal】创建一个新的 Map 并设置第一个数据t.threadLocals new ThreadLocalMap(this, firstValue); }get()获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量 public T get() {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);// 如果此map存在if (map ! null) {// 以当前的 ThreadLocal 为 key调用 getEntry 获取对应的存储实体 eThreadLocalMap.Entry e map.getEntry(this);// 对 e 进行判空 if (e ! null) {// 获取存储实体 e 对应的 value值T result (T)e.value;return result;}}/*有两种情况有执行当前代码第一种情况: map 不存在表示此线程没有维护的 ThreadLocalMap 对象第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】就会设置为默认值 */// 初始化当前线程与当前 threadLocal 对象相关联的 valuereturn setInitialValue(); }private T setInitialValue() {// 调用initialValue获取初始化的值此方法可以被子类重写, 如果不重写默认返回 nullT value initialValue();Thread t Thread.currentThread();ThreadLocalMap map getMap(t);// 判断 map 是否初始化过if (map ! null)// 存在则调用 map.set 设置此实体 entryvalue 是默认的值map.set(this, value);else// 调用 createMap 进行 ThreadLocalMap 对象的初始化中createMap(t, value);// 返回线程与当前 threadLocal 关联的局部变量return value; }remove()移除当前线程与当前 threadLocal 对象相关联的线程局部变量 public void remove() {// 获取当前线程对象中维护的 ThreadLocalMap 对象ThreadLocalMap m getMap(Thread.currentThread());if (m ! null)// map 存在则调用 map.removethis时当前ThreadLocal以this为key删除对应的实体m.remove(this); }LocalMap 成员属性 ThreadLocalMap 是 ThreadLocal 的内部类没有实现 Map 接口用独立的方式实现了 Map 的功能其内部 Entry 也是独立实现 // 初始化当前 map 内部散列表数组的初始长度 16 private static final int INITIAL_CAPACITY 16;// 存放数据的table数组长度必须是2的整次幂。 private Entry[] table;// 数组里面 entrys 的个数可以用于判断 table 当前使用量是否超过阈值 private int size 0;// 进行扩容的阈值表使用量大于它的时候进行扩容。 private int threshold;存储结构 Entry Entry 继承 WeakReferencekey 是弱引用目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑Entry 限制只能用 ThreadLocal 作为 keykey 为 null (entry.get() null) 意味着 key 不再被引用entry 也可以从 table 中清除 static class Entry extends WeakReferenceThreadLocal? {Object value;Entry(ThreadLocal? k, Object v) {// this.referent referent key;super(k);value v;} }构造方法延迟初始化的线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象 ThreadLocalMap(ThreadLocal? firstKey, Object firstValue) {// 初始化table创建一个长度为16的Entry数组table new Entry[INITIAL_CAPACITY];// 【寻址算法】计算索引int i firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1);// 创建 entry 对象存放到指定位置的 slot 中table[i] new Entry(firstKey, firstValue);// 数据总量是 1size 1;// 将阈值设置为 当前数组长度 * 2/ 3。setThreshold(INITIAL_CAPACITY); }成员方法 set()添加数据ThreadLocalMap 使用线性探测法来解决哈希冲突 该方法会一直探测下一个地址直到有空的地址后插入若插入后 Map 数量超过阈值数组会扩容为原来的 2 倍 假设当前 table 长度为16计算出来 key 的 hash 值为 14如果 table[14] 上已经有值并且其 key 与当前 key 不一致那么就发生了 hash 冲突这个时候将 14 加 1 得到 15取 table[15] 进行判断如果还是冲突会回到 0取 table[0]以此类推直到可以插入可以把 Entry[] table 看成一个环形数组 线性探测法会出现堆积问题可以采取平方探测法解决 在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象并进行垃圾清理防止出现内存泄漏 private void set(ThreadLocal? key, Object value) {// 获取散列表ThreadLocal.ThreadLocalMap.Entry[] tab table;int len tab.length;// 哈希寻址int i key.threadLocalHashCode (len-1);// 使用线性探测法向后查找元素碰到 entry 为空时停止探测for (ThreadLocal.ThreadLocalMap.Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) {// 获取当前元素 keyThreadLocal? k e.get();// ThreadLocal 对应的 key 存在【直接覆盖之前的值】if (k key) {e.value value;return;}// 【这两个条件谁先成立不一定所以 replaceStaleEntry 中还需要判断 k key 的情况】// key 为 null但是值不为 null说明之前的 ThreadLocal 对象已经被回收了当前是【过期数据】if (k null) {// 【碰到一个过期的 slot当前数据复用该槽位替换过期数据】// 这个方法还进行了垃圾清理动作防止内存泄漏replaceStaleEntry(key, value, i);return;}}// 逻辑到这说明碰到 slot null 的位置则在空元素的位置创建一个新的 Entrytab[i] new Entry(key, value);// 数量 1int sz size;// 【做一次启发式清理】如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义那么进行 rehashif (!cleanSomeSlots(i, sz) sz threshold)// 扩容rehash(); }// 获取【环形数组】的下一个索引 private static int nextIndex(int i, int len) {// 索引越界后从 0 开始继续获取return ((i 1 len) ? i 1 : 0); }// 在指定位置插入指定的数据 private void replaceStaleEntry(ThreadLocal? key, Object value, int staleSlot) {// 获取散列表Entry[] tab table;int len tab.length;Entry e;// 探测式清理的开始下标默认从当前 staleSlot 开始int slotToExpunge staleSlot;// 以当前 staleSlot 开始【向前迭代查找】找到索引靠前过期数据找到以后替换 slotToExpunge 值// 【保证在一个区间段内从最前面的过期数据开始清理】for (int i prevIndex(staleSlot, len); (e tab[i]) ! null; i prevIndex(i, len))if (e.get() null)slotToExpunge i;// 以 staleSlot 【向后去查找】直到碰到 null 为止还是线性探测for (int i nextIndex(staleSlot, len); (e tab[i]) ! null; i nextIndex(i, len)) {// 获取当前节点的 keyThreadLocal? k e.get();// 条件成立说明是【替换逻辑】if (k key) {e.value value;// 因为本来要在 staleSlot 索引处插入该数据现在找到了i索引处的key与数据一致// 但是 i 位置距离正确的位置更远因为是向后查找所以还是要在 staleSlot 位置插入当前 entry// 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置tab[i] tab[staleSlot];tab[staleSlot] e;// 条件成立说明向前查找过期数据并未找到过期的 entry但 staleSlot 位置已经不是过期数据了i 位置才是if (slotToExpunge staleSlot)slotToExpunge i;// 【清理过期数据expungeStaleEntry 探测式清理cleanSomeSlots 启发式清理】cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 条件成立说明当前遍历的 entry 是一个过期数据并且该位置前面也没有过期数据if (k null slotToExpunge staleSlot)// 探测式清理过期数据的开始下标修改为当前循环的 index因为 staleSlot 会放入要添加的数据slotToExpunge i;}// 向后查找过程中并未发现 k key 的 entry说明当前是一个【取代过期数据逻辑】// 删除原有的数据引用防止内存泄露tab[staleSlot].value null;// staleSlot 位置添加数据【上面的所有逻辑都不会更改 staleSlot 的值】tab[staleSlot] new Entry(key, value);// 条件成立说明除了 staleSlot 以外还发现其它的过期 slot所以要【开启清理数据的逻辑】if (slotToExpunge ! staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSnZ9xSE-1678148961780)(https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry流程.png)] private static int prevIndex(int i, int len) {// 形成一个环绕式的访问头索引越界后置为尾索引return ((i - 1 0) ? i - 1 : len - 1); }getEntry()ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key调用 getEntry 获取对应的存储实体 e private Entry getEntry(ThreadLocal? key) {// 哈希寻址int i key.threadLocalHashCode (table.length - 1);// 访问散列表中指定指定位置的 slot Entry e table[i];// 条件成立说明 slot 有值并且 key 就是要寻找的 key直接返回if (e ! null e.get() key)return e;else// 进行线性探测return getEntryAfterMiss(key, i, e); } // 线性探测寻址 private Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) {// 获取散列表Entry[] tab table;int len tab.length;// 开始遍历碰到 slot null 的情况搜索结束while (e ! null) {// 获取当前 slot 中 entry 对象的 keyThreadLocal? k e.get();// 条件成立说明找到了直接返回if (k key)return e;if (k null)// 过期数据【探测式过期数据回收】expungeStaleEntry(i);else// 更新 index 继续向后走i nextIndex(i, len);// 获取下一个槽位中的 entrye tab[i];}// 说明当前区段没有找到相应数据// 【因为存放数据是线性的向后寻找槽位都是紧挨着的不可能越过一个 空槽位 在后面放】可以减少遍历的次数return null; }rehash()触发一次全量清理如果数组长度大于等于长度的 2/3 * 3/4 1/2则进行 resize private void rehash() {// 清楚当前散列表内的【所有】过期的数据expungeStaleEntries();// threshold len * 2 / 3就是 2/3 * (1 - 1/4)if (size threshold - threshold / 4)resize(); }private void expungeStaleEntries() {Entry[] tab table;int len tab.length;// 【遍历所有的槽位清理过期数据】for (int j 0; j len; j) {Entry e tab[j];if (e ! null e.get() null)expungeStaleEntry(j);} }Entry 数组为扩容为原来的 2 倍 重新计算 key 的散列值如果遇到 key 为 null 的情况会将其 value 也置为 null帮助 GC private void resize() {Entry[] oldTab table;int oldLen oldTab.length;// 新数组的长度是老数组的二倍int newLen oldLen * 2;Entry[] newTab new Entry[newLen];// 统计新table中的entry数量int count 0;// 遍历老表进行【数据迁移】for (int j 0; j oldLen; j) {// 访问老表的指定位置的 entryEntry e oldTab[j];// 条件成立说明老表中该位置有数据可能是过期数据也可能不是if (e ! null) {ThreadLocal? k e.get();// 过期数据if (k null) {e.value null; // Help the GC} else {// 非过期数据在新表中进行哈希寻址int h k.threadLocalHashCode (newLen - 1);// 【线程探测】while (newTab[h] ! null)h nextIndex(h, newLen);// 将数据存放到新表合适的 slot 中newTab[h] e;count;}}}// 设置下一次触发扩容的指标threshold len * 2 / 3;setThreshold(newLen);size count;// 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用table newTab; }remove()删除 Entry private void remove(ThreadLocal? key) {Entry[] tab table;int len tab.length;// 哈希寻址int i key.threadLocalHashCode (len-1);for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) {// 找到了对应的 keyif (e.get() key) {// 设置 key 为 nulle.clear();// 探测式清理expungeStaleEntry(i);return;}} }清理方法 探测式清理沿着开始位置向后探测清理过期数据沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位重定位后的元素理论上更接近 i entry.key (table.length - 1)让数据的排列更紧凑会优化整个散列表查询性能 // table[staleSlot] 是一个过期数据以这个位置开始继续向后查找过期数据 private int expungeStaleEntry(int staleSlot) {// 获取散列表和数组长度Entry[] tab table;int len tab.length;// help gc先把当前过期的 entry 置空在取消对 entry 的引用tab[staleSlot].value null;tab[staleSlot] null;// 数量-1size--;Entry e;int i;// 从 staleSlot 开始向后遍历直到碰到 slot null 结束【区间内清理过期数据】for (i nextIndex(staleSlot, len); (e tab[i]) ! null; i nextIndex(i, len)) {ThreadLocal? k e.get();// 当前 entry 是过期数据if (k null) {// help gce.value null;tab[i] null;size--;} else {// 当前 entry 不是过期数据的逻辑【rehash】// 重新计算当前 entry 对应的 indexint h k.threadLocalHashCode (len - 1);// 条件成立说明当前 entry 存储时发生过 hash 冲突向后偏移过了if (h ! i) {// 当前位置置空tab[i] null;// 以正确位置 h 开始向后查找第一个可以存放 entry 的位置while (tab[h] ! null)h nextIndex(h, len);// 将当前元素放入到【距离正确位置更近的位置有可能就是正确位置】tab[h] e;}}}// 返回 slot null 的槽位索引图例是 7这个索引代表【索引前面的区间已经清理完成垃圾了】return i; }启发式清理向后循环扫描过期数据发现过期数据调用探测式清理方法如果连续几次的循环都没有发现过期数据就停止扫描 // i 表示启发式清理工作开始位置一般是空 slotn 一般传递的是 table.length private boolean cleanSomeSlots(int i, int n) {// 表示启发式清理工作是否清除了过期数据boolean removed false;// 获取当前 map 的散列表引用Entry[] tab table;int len tab.length;do {// 获取下一个索引因为探测式返回的 slot 为 nulli nextIndex(i, len);Entry e tab[i];// 条件成立说明是过期的数据key 被 gc 了if (e ! null e.get() null) {// 【发现过期数据重置 n 为数组的长度】n len;// 表示清理过过期数据removed true;// 以当前过期的 slot 为开始节点 做一次探测式清理工作i expungeStaleEntry(i);}// 假设 table 长度为 16// 16 1 88 1 44 1 22 1 11 1 0// 连续经过这么多次循环【没有扫描到过期数据】就停止循环扫描到空 slot 不算因为不是过期数据} while ((n 1) ! 0);// 返回清除标记return removed; }参考视频https://space.bilibili.com/457326371/ 内存泄漏 Memory leak内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果内存泄漏的堆积终将导致内存溢出 如果 key 使用强引用使用完 ThreadLocal threadLocal Ref 被回收但是 threadLocalMap 的 Entry 强引用了 threadLocal造成 threadLocal 无法被回收无法完全避免内存泄漏 如果 key 使用弱引用使用完 ThreadLocal threadLocal Ref 被回收ThreadLocalMap 只持有 ThreadLocal 的弱引用所以threadlocal 也可以被回收此时 Entry 中的 key null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行依然存在强引用链value 不会被回收而这块 value 永远不会被访问到也会导致 value 内存泄漏 两个主要原因 没有手动删除这个 EntryCurrentThread 依然运行 根本原因ThreadLocalMap 是 Thread的一个属性生命周期跟 Thread 一样长如果没有手动删除对应 Entry 就会导致内存泄漏 解决方法使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以 ThreadLocal 内部解决方法在 ThreadLocalMap 中的 set/getEntry 方法中通过线性探测法对 key 进行判断如果 key 为 nullThreadLocal 为 null会对 Entry 进行垃圾回收。所以使用弱引用比强引用多一层保障就算不调用 remove也有机会进行 GC 变量传递 基本使用 父子线程创建子线程的线程是父线程比如实例中的 main 线程就是父线程 ThreadLocal 中存储的是线程的局部变量如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 public static void main(String[] args) {ThreadLocalString threadLocal new InheritableThreadLocal();threadLocal.set(父线程设置的值);new Thread(() - System.out.println(子线程输出 threadLocal.get())).start(); } // 子线程输出父线程设置的值实现原理 InheritableThreadLocal 源码 public class InheritableThreadLocalT extends ThreadLocalT {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals new ThreadLocalMap(this, firstValue);} }实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,// 该参数默认是 trueboolean inheritThreadLocals) {// ...Thread parent currentThread();// 判断父线程创建子线程的线程的 inheritableThreadLocals 属性不为 nullif (inheritThreadLocals parent.inheritableThreadLocals ! null) {// 复制父线程的 inheritableThreadLocals 属性实现父子线程局部变量共享this.inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }// .. } // 【本质上还是创建 ThreadLocalMap只是把父类中的可继承数据设置进去了】 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap); }private ThreadLocalMap(ThreadLocalMap parentMap) {// 获取父线程的哈希表Entry[] parentTable parentMap.table;int len parentTable.length;setThreshold(len);table new Entry[len];// 【逐个复制父线程 ThreadLocalMap 中的数据】for (int j 0; j len; j) {Entry e parentTable[j];if (e ! null) {ThreadLocalObject key (ThreadLocalObject) e.get();if (key ! null) {// 调用的是 InheritableThreadLocal#childValue(T parentValue)Object value key.childValue(e.value);Entry c new Entry(key, value);int h key.threadLocalHashCode (len - 1);// 线性探测while (table[h] ! null)h nextIndex(h, len);table[h] c;size;}}} }参考文章https://blog.csdn.net/feichitianxia/article/details/110495764
http://www.hkea.cn/news/14313823/

相关文章:

  • 昆明做网站要多少钱大冶市规划建设局网站
  • 在线开发培训网站建设网站建设公司如何找客户
  • 河北网站建设开发微信小程序双人游戏情侣
  • 网站建设哪些模板号开平建设局网站
  • 在哪找做调查赚钱的网站wordpress主题导出
  • 手机网站开发方案工信部网站手机备案查询
  • 学习aspmvc网站开发 书网络推广方式主要有
  • 东莞快速做网站什么信息发布型网站
  • 昆明营销型网站建设平面ui设计是学什么
  • 普升高端品牌网站建设圣诞树html网页代码
  • 免费网站建设公司设计网站公司咨询亿企邦
  • 做网站优化选阿里巴巴还是百度全国企业公示信息系统查询
  • 网站平台建设总结企业网站做的公司
  • 深圳外贸建站网络推广联客易门户网站开发过程
  • 哪个网站专门做游戏脚本有赞分销
  • 在哪一个网站上做劳务合同备案济南网站建设内容
  • 网站建设开发客户开场白百度一下百度首页
  • 公司seo推广营销网站商会网站制作
  • 如何制作动漫网站模板下载创造网站需要什么条件
  • 360免费建站系统域名过期做的网站怎么办
  • wordpress信用卡支付网站制作公司都找乐云seo
  • 网站首页图片效果盘锦网站开发公司
  • 学校网站群建设uc浏览器关键词排名优化
  • 东莞网站推广教程莱芜金点子最新招聘信息港
  • gta5买房网站正在建设门户网站开发技术
  • 网站里可以增加网址吗wordpress默认文本编辑器
  • 泰州网站建设费用韩国导航地图app
  • 网页模板哪个网站可以下载网页界面设计的特点是什么
  • thinkphp租房网站开发集团公司网站设计
  • 网站建设需要注意哪些问题网站建设皿金手指谷哥壹柒