2024年java面试准备--多线程篇(2)(一)https://developer.aliyun.com/article/1393119
10、ThreadLocal原理
ThreadLocal简介:
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的 专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB
原理:
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的
如何使用:
1)存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();
2)解决线程安全的问题
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
ThreadLocal内存泄漏的场景
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。
11、HashMap线程安全
死循环造成 CPU 100%
HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。
所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。
12、CountDownLatch
CountDownLatch是一个同步工具类,它通过一个计数器来实现的,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程就可以恢复执行任务。
CountDownLatch初始化一个全局计数器;如果想让某个线程处于等待中,该线程调用countdownLatch.await(),通过其他线程调用countdownLatch.countDown()减少计数器,直到减少到0,被await()挂起的线程恢复执行。可以实现1个线程等待一组线程执行完、实现一个线程释放一组线程、多个线程释放多个线程的场景。
方法详解:
- CountDownLatch(int count):count为计数器的初始值(一般需要多少个线程执行,count就设为几)。
- countDown(): 每调用一次计数器值-1,直到count被减为0,代表所有线程全部执行完毕。
- getCount():获取当前计数器的值。
- await(): 等待计数器变为0,即等待所有异步线程执行完毕。
- boolean await(long timeout, TimeUnit unit): 此方法与await()区别: ①此方法至多会等待指定的时间,超时后会自动唤醒,若 timeout 小于等于零,则不会等待 ②boolean 类型返回值:若计数器变为零了,则返回 true;若指定的等待时间过去了,则返回 false
在实时系统中的使用场景:
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数器为1的CountDownLatch,并让其他所有线程都在这个锁上等待,只需要调用一次countDown()方法就可以让其他所有等待的线程同时恢复执行。
- 开始执行前等待N个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统都已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是你用N个线程去访问共享资源,在每个测试阶段线程数量不同,并尝试产生死锁。
13、CyclicBarrier
CyclicBarrier 可以理解为循环栅栏, Cyclic 意为循环,也就是说这个计数器可以反复使用。
CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批线程等待另一批线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定,如下所示:
public CyclicBarrier(int parties) { this(parties, null); }
每调用一次await()方法都将使阻塞的线程数+1,只有阻塞的线程数达到设定值时屏障才会打开,允许阻塞的所有线程继续执行。除此之外,CyclicBarrier还有几点需要注意的地方:
- CyclicBarrier的计数器可以重置而CountDownLatch不行,这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次。而这也是循环屏障循环二字的语义所在。
- CyclicBarrier允许用户自定义barrierAction操作,这是个可选操作,可以在创建CyclicBarrier对象时指定
指定本局要拦截的线程数parties 及 本局结束时要执行的任务 public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
一旦用户在创建CyclicBarrier对象时设置了barrierAction参数,则在阻塞线程数达到设定值屏障打开前,会调用barrierAction的run()方法完成用户自定义的操作。
常用方法:
//参数parties:表示要到达屏障 (栅栏)的线程数量 //参数Runnable: 最后一个线程到达屏障之后要做的任务 //构造方法1 public CyclicBarrier(int parties) //构造方法2 指定本局要拦截的线程数parties 及 本局结束时要执行的任务 public CyclicBarrier(int parties, Runnable barrierAction) //线程调用await()方法表示当前线程已经到达栅栏,然后会被阻塞 public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } //带时限的阻塞等待 public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException { return dowait(true, unit.toNanos(timeout)); }
14、Semaphore
Semaphore是JDK提供的一个同步工具,它通过维护若干个许可证来控制线程对共享资源的访问。 如果许可证剩余数量大于零时,线程则允许访问该共享资源;如果许可证剩余数量为零时,则拒绝线程访问该共享资源。 Semaphore所维护的许可证数量就是允许访问共享资源的最大线程数量。 所以,线程想要访问共享资源必须从Semaphore中获取到许可证。 常用方法:
//默认获取一个许可 public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //默认释放一个许可 public void release() {sync.releaseShared(1); } //获取指定的许可数 public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); } //尝试获取指定的许可数 public boolean tryAcquire(int permits) { if (permits < 0) throw new IllegalArgumentException(); return sync.nonfairTryAcquireShared(permits) >= 0; } //释放指定的许可数 public void release(int permits) { if (permits < 0) throw new IllegalArgumentException(); sync.releaseShared(permits); }
当调用acquire方法时线程就会被阻塞,直到Semaphore中可以获得到许可证为止,然后线程再获取这个许可证。 当调用release方法时将向Semaphore中添加一个许可证,如果有线程因为获取许可证被阻塞时,它将获取到许可证并被释放;如果没有获取许可证的线程, Semaphore只是记录许可证的可用数量。 张三、李四和王五和赵六4个人一起去饭店吃饭,不过在特殊时期洗手很重要,饭前洗手也是必须的,可是饭店只有2个洗手池,洗手池就是不能被同时使用的公共资源,这种场景就可以用到Semaphore。
package onemore.study.semaphore; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; public class Customer implements Runnable { private Semaphore washbasin; private String name; public Customer(Semaphore washbasin, String name) { this.washbasin = washbasin; this.name = name; } @Override public void run() { try { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); Random random = new Random(); washbasin.acquire(); System.out.println( sdf.format(new Date()) + " " + name + " 开始洗手..."); Thread.sleep((long) (random.nextDouble() * 5000) + 2000); System.out.println( sdf.format(new Date()) + " " + name + " 洗手完毕!"); washbasin.release(); } catch (Exception e) { e.printStackTrace(); } } } //测试类 package onemore.study.semaphore; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; public class SemaphoreTester { public static void main(String[] args) throws InterruptedException { //饭店里只用两个洗手池,所以初始化许可证的总数为2。 Semaphore washbasin = new Semaphore(2); List<Thread> threads = new ArrayList<>(3); threads.add(new Thread(new Customer(washbasin, "张三"))); threads.add(new Thread(new Customer(washbasin, "李四"))); threads.add(new Thread(new Customer(washbasin, "王五"))); threads.add(new Thread(new Customer(washbasin, "赵六"))); for (Thread thread : threads) { thread.start(); Thread.sleep(50); } for (Thread thread : threads) { thread.join(); } } }
运行结果:
06:51:54.416 李四 开始洗手... 06:51:54.416 张三 开始洗手... 06:51:57.251 张三 洗手完毕! 06:51:57.251 王五 开始洗手... 06:51:59.418 李四 洗手完毕! 06:51:59.418 赵六 开始洗手... 06:52:02.496 王五 洗手完毕! 06:52:06.162 赵六 洗手完毕!
内部原理:
Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程的管理。Semaphore在构造时,需要传入许可证的数量,它最后传递给了AQS的state值。线程在调用acquire方法获取许可证时,如果Semaphore中许可证的数量大于0,许可证的数量就减1,线程继续运行,当线程运行结束调用release方法时释放许可证时,许可证的数量就加1。如果获取许可证时,Semaphore中许可证的数量为0,则获取失败,线程进入AQS的等待队列中,等待被其它释放许可证的线程唤醒。 深入原理:
代码中,这4个人会按照线程启动的顺序洗手嘛?
不会按照线程启动的顺序洗手,有可能赵六比王五先洗手。
原因:使用Semaphore的构造函数是这个:
public Semaphore(int permits) { sync = new NonfairSync(permits); }
在这个构造函数中,使用的是NonfairSync(非公平锁),这个类不保证线程获得许可证的顺序,调用acquire
方法的线程可以在一直等待的线程之前获得一个许可证。
有没有什么方法可保证他们的顺序?
可以使用Semaphore的另一个构造函数:
public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
在调用构造方法时,fair
参数传入true
,比如:
Semaphore washbasin = new Semaphore(2, true);
这样使用的是FairSync(公平锁),可以确保按照各个线程调用acquire
方法的顺序获得许可证。
内存模型
Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
原子性:
在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。
可见性:
Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同
有序性
在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。
1、volatile底层实现
作用:
1.可见性: 当一个线程修改了volatile修饰的变量的值,其他线程可以立即看到这个修改,保证了共享变量的可见性。
2.禁止指令重排序: 编译器和处理器在编译和执行代码时,可能会对指令进行重排序,但是volatile关键字可以禁止这种重排序,保证了程序的正确性。
3.保证原子性: volatile关键字可以保证一些简单的操作的原子性,例如++操作,但是对于复合操作,volatile关键字无法保证原子性。
底层实现:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
单例模式中volatile的作用:(DCL(Double Check Lock)单例为什么要加Volatile?)
防止代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
volatile防止指令重排,在DCL中,防止高并发情况下,指令重排造成的线程安全问题。
class Singleton{ private volatile static Singleton instance = null; //禁止指令重排 private Singleton() { } public static Singleton getInstance() { if(instance==null) { //减少加锁的损耗 synchronized (Singleton.class) { if(instance==null) //确认是否初始化完成 instance = new Singleton(); } } return instance; } }
volatile关键字和synchronized关键字的区别
- volatile关键字保证了共享变量的可见性和禁止指令重排序,但是无法保证原子性,而synchronized关键字可以保证原子性、有序性和可见性。
- volatile关键字适用于一些简单的操作,例如++操作,而synchronized关键字适用于复合操作。
- volatile关键字不会造成线程阻塞,而synchronized关键字可能会造成线程阻塞。
volatile能不能保证线程安全?
volatile不能保证线程安全,只能保证线程可见性,不能保证原子性
flag常量未使用volatile关键字的时候,程序一直不停止,因为无法感知flag的变化
使用volatile关键字后,因为volatile保证了可见性,感知到了flag的变化,程序停止并打印
2、AQS思想
AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式的同步器,是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,如:基于AQS实现的lock, CountDownLatch、CyclicBarrier、Semaphore需解决的问题:
- 状态的原子性管理
- 线程的阻塞与解除阻塞
- 队列的管理
AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH(虚拟的双向队列) 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。
lock:
是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。默认为非公平锁,但可以初始化为公平锁; 通过方法 lock()与 unlock()来进行加锁与解锁操作;
常见AQS锁:
CountDownLatch(倒计数器):
通过计数法(倒计时器),让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒;该⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。具体可以使用countDownLatch.await()来等待结果。多用于多线程信息汇总。
CompletableFuture(独占锁):
通过设置参数,可以完成CountDownLatch同样的多平台响应问题,但是可以针对其中部分返回结果做更加灵活的展示。
CyclicBarrier(循环栅栏):
字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。可以用于批量发送消息队列信息、异步限流。
Semaphore(信号量):
信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制。SpringHystrix限流的思想
为什么AQS使用的双向链表
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
3、happens-before
用来描述和可见性相关问题:如果第一个操作 happens-before 第二个操作,那么我们就说第一个操作对于第二个操作是可见的
常见的happens-before:volatile 、锁、线程生命周期。