文章目录 1、并发与并行 2、线程安全 3、线程、进程、协程 4、线程间通信 5、线程创建方式 6、8G内存创建的线程数 7、普通Java程序含有的线程 8、start()、run() 9、线程调度、6种状态、强制停止线程、上下文切换 10、守护线程、用户线程 11、 volatile 、synchronized 12、sleep() 、 wait() 13、线程安全、场景 14、ThreadLocal 15、内存模型、指令重排 16、Happens-Before、As-If-Serial 17、volatile 18、synchronized锁升级、Monitor 19、synchronized 、 ReentrantLock、Lock、AQS 20、非公平锁、公平锁 21、CAS 22、原子操作类 23、线程死锁 24、线程同步、互斥、锁要解决的问题、自旋锁 25、悲观锁、乐观锁 26、并发工具类:CountDownLatch 27、并发工具类:CyclicBarrier 27、并发工具类:Semaphore 28、并发工具类:Exchanger 29、ConcurrentHashMap 30、CopyOnWriteArrayList 30、BlockingQueue 31、线程池
1、并发与并行
①并行:多核CPU上的多任务处理,多个任务在同一时间真正地同时执行。②并发:单核CPU上的多任务处理,多个任务在同一时间段内交替执行,通过时间片轮转实现交替执行,解决IO密集型任务的瓶颈。
2、线程安全
①原子性:一个操作要么完全执行,要么完全不执行,不会出现中间状态。措施:atomic包和synchronized。
②可见性:当一个线程修改了共享变量,其他线程能够立即看到变化。措施:volatile关键字。
③有序性:确保线程不会因为死锁、饥饿导致无法继续执行。措施:读写屏障和禁止指令重排。
3、线程、进程、协程
①进程:操作系统资源分配的基本单位,有独立的资源。
②线程:独立执行的单元,共享进程资源,有独立的栈和寄存器。
③协程:比线程更轻量的并发单元、在单线程中并发执行,在用户态显式调度,避免线程切换时的内核态开销。
4、线程间通信
①共享内存:Java采用,JMM(抽象概念),共享变量存储在主内存中,每个线程私有本地内存存储共享变量的副本。②具体实现:i:volatile和synchronized关键字共享对象。ii:wait()和notify():wait()进入该对象等待池,释放锁; notify()唤醒对象等待池中一个线程进入锁池,等待获取锁。iii:Exchanger数据交换:一个线程调用exchange()方法,将数据传递给另一个线程,同时接收另一个线程数据。iv:Condition实现线程间的协调:await()负责阻塞、signal()和signalAll()负责通知。v:CompletableFuture:支持异步编程,允许线程在完成计算后将结果传递给其他线程。例子如下:
线程A与线程B之间通信2个步骤:①线程A把本地内存A中的共享变量副本刷新到主内存中。
②线程B到主内存中读取线程A刷新过的共享变量,再同步到自己的共享变量副本中。
5、线程创建方式
①继承Thread类:重写父类Thread的run()方法,并且调用start()方法启动线程。
class ThreadTask extends Thread {public void run() {System.out.println("上岸!");}public static void main(String[] args) {ThreadTask task = new ThreadTask();task.start();}
}
缺点:如果ThreadTask已经继承另外一个类,不能再继承Thread类,因为Java不支持多重继承。
②实现Runnable接口:重写Runnable接口run方法,并将实现类的对象作为参数传递给Thread对象构造方法,调用start方法启动线程。
class RunnableTask implements Runnable {public void run() {System.out.println("上岸!");}public static void main(String[] args) {RunnableTask task = new RunnableTask();Thread thread = new Thread(task);//实现类的对象作为参数传递给 Thread 对象构造方法thread.start();}
}
优点:避免Java的单继承限制
③实现Callable接口:重写Callable接口的call()方法,创建FutureTask对象,参数为Callable实现类的对象;
再创建Thread对象,参数为FutureTask对象,最后调用start()方法启动线程。
class CallableTask implements Callable<String> {public String call() {return "上岸!";}public static void main(String[] args) throws ExecutionException, InterruptedException {CallableTask task = new CallableTask();FutureTask<String> futureTask = new FutureTask<>(task);Thread thread = new Thread(futureTask);thread.start();System.out.println(futureTask.get());}
}
优点:获取线程的执行结果。
6、8G内存创建的线程数
①理论上:8000个,在64位OS中,创建线程会分配虚拟机栈,JVM默认是1024KB=1M。②实际上:JVM、操作系统本身运行就要占一定内存空间,实际上创建线程数远比8000少。
ThreadStackSize的单位是KB,默认的JVM栈大小。
7、普通Java程序含有的线程
①main 线程:程序执行入口。
②垃圾回收线程:一个后台线程,负责回收不再使用的对象。
③编译器线程:如JIT,负责把一部分热点代码编译后放到codeCache中。例子:
class ThreadLister {public static void main(String[] args) {// 获取所有线程的堆栈跟踪Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();for (Thread thread : threads.keySet()) {System.out.println("Thread: " + thread.getName() + " (ID=" + thread.getId() + ")");}}
}
输出结果分析如下:
Thread: main (ID=1) - :主线程,Java 程序启动时由 JVM 创建。
Thread: Reference Handler (ID=2) - :处理引用对象的,如软引用、弱引用和虚引用。负责清理被 JVM 回收对象。
Thread: Finalizer (ID=3) - :终结器线程,调用对象finalize 方法,执行特定的资源释放操作。
Thread: Signal Dispatcher (ID=4) - :信号调度线程,处理来自操作系统信号给 JVM 处理,如响应中断、停止等信号。
Thread: Monitor Ctrl-Break (ID=5) - :监视器线程,由一些特定的 IDE 创建,监控和管理程序执行或者处理中断。
8、start()、run()
①start() :创建一个新线程,并异步执行run()中代码,通知JVM调用底层线程调度机制来启动新线程。
②run() :一个普通同步方法调用,所有代码都在当前线程中执行,不会创建新线程。
class MyThread extends Thread {public void run() {System.out.println(Thread.currentThread().getName());}public static void main(String[] args) {MyThread t1 = new MyThread();t1.start(); // 创建一个新线程,并在新线程中执行 run()。输出maint1.run(); // 仅在主线程中执行 run(),没有创建新线程。输出Thread-0}
}原理如下:
调用start()后,线程进入就绪状态,等待操作系统调度;一旦调度执行,线程会执行其run()方法中的代码。
9、线程调度、6种状态、强制停止线程、上下文切换
线程调度方法:
①start :启动线程并让操作系统调度执行。
②sleep :让当前线程休眠一段时间,暂时让出指定时间的执行权。时间到接着参与CPU调度,获取CPU资源后继续执行。
③yield:让当前线程让出CPU使用权,回到就绪状态。
④wait :让当前线程等待。
⑤notify :唤醒一个等待的线程。
⑥stop :强制停止线程,目前已经处于废弃状态。
⑦interrupt:通知线程停止,但不会直接终止线程,线程自行处理中断标志。配合isInterrupted或 Thread.interrupted使用。
线程6种状态:
①new:线程被创建但未启动,已分配必要的资源。
②runnable :线程处于正在运行状态,由操作系统调度。
③blocked :获取一个锁以进入同步块/方法时,线程被阻塞,等待获取锁。
④waiting :线程等待其他线程的通知或中断。
⑤timed_waiting :线程会等待一段时间,超时后自动恢复。
⑥terminated :线程执行完毕,生命周期结束。线程生命周期五个主要阶段:新建、就绪、运行、阻塞和终止。
强制终止线程:
①调用线程的 interrupt() 方法,请求终止线程。
②在线程的 run() 方法中检查中断状态,如果线程被中断,就退出线程。
线程上下文切换:
定义:CPU 从一个线程切换到另一个线程执行时的过程。过程:
①在线程切换的过程中,CPU 需要保存当前线程的执行状态,并加载下一个线程的上下文。
②CPU 在同一时刻只能执行一个线程,为了实现多线程并发执行,采用时间片轮转在多个线程之间切换。
10、守护线程、用户线程
①作用:一种特殊线程,为其他线程提供服务。Java线程分为用户线程和守护线程。②区别:当最后一个非守护线程束时,JVM会正常退出,不管当前是否存在守护线程,守护线程是否结束并不影响 JVM 退出。
11、 volatile 、synchronized
①volatile :修饰成员变量,对变量访问均从共享内存中获取,并同步刷新回共享内存,保证所有线程对变量访问的可见性;禁止指令重排。②synchronized:修饰方法或同步代码块,确保多个线程在同一个时刻只有一个线程在执行方法或代码块。
12、sleep() 、 wait()
①sleep() :让当前线程休眠,属于Thread 类方法,不释放锁;在任何地方被调用;可指定的时间内暂停执行。②wait():让获得对象锁的线程等待,属于Object类方法,释放锁;只使用在同步代码块或同步方法中;配合notify或notifyAll使用。
13、线程安全、场景
①线程安全:在并发环境下,多个线程访问共享资源时,程序能够正确地执行,而不会出现数据不一致的问题。②措施:i:synchronized 关键字:对方法、代码块加锁。线程在执行同步方法、同步代码块时,获取锁,其他线程阻塞并等待锁。ii:ReentrantLock 并发重入锁,更细粒度的锁。iii:变量内存可见性,使用 volatile 关键字。iv:简单原子变量操作,使用 Atomic 原子类。v:对于线程独立的数据,使用 ThreadLocal 来为每个线程提供专属的变量副本。vi:对于需要并发容器的地方,使用 ConcurrentHashMap、CopyOnWriteArrayList 等。
例子:
Q:int变量初始为0,十个线程轮流对其进行++操作(循环10000次),结果大于10 万还是小于等于10万,为什么?
A:小于 100000,原因是多线程环境下,++ 操作并不是一个原子操作,而是分为读取、加 1、写回三个步骤。i:读取变量的值。ii:将读取到的值加 1。iii:将结果写回变量。多个线程读取到相同的值,然后对这个值进行加 1 操作,最终导致结果小于 100000。
线程安全的使用场景:单例模式。在多线程环境下,如果多个线程同时尝试创建实例,
单例类必须确保只创建一个实例,并提供一个全局访问点。
class Singleton {private static final Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}
饿汉式单例:一种比较直接的实现方式,通过在类加载时就立即初始化单例对象来保证线程安全。
class LazySingleton {private static volatile LazySingleton instance;private LazySingleton() {}public static LazySingleton getInstance() {if (instance == null) { // 第一次检查synchronized (LazySingleton.class) {if (instance == null) { // 第二次检查instance = new LazySingleton();}}}return instance;}
}懒汉式单例:第一次使用时初始化单例对象,使用双重检查锁定来确保线程安全,
volatile 关键字用来保证可见性,syncronized 关键字用来保证同步。
14、ThreadLocal
①特点:线程局部变量的工具类,每个线程访问变量副本独立,避免共享变量引起的线程安全问题,使得变量不需要同步处理,避免资源竞争。②使用ThreadLocal 分为四步:
//1、创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
//2、设置ThreadLocal变量的值
localVariable.set("毕业");
//3、获取ThreadLocal变量的值
String value = localVariable.get();
//4、删除ThreadLocal变量的值
localVariable.remove();
③底层原理:i:ThreadLocal对象并调用set方法,每个线程初始化维护一个ThreadLocalMap,通过set方法将对象存入Map。
ThreadLocalMap内部维护一个 Entry 数组,key是ThreadLocal对象,value是线程局部变量。Entry继承WeakReference,限定key是弱引用,弱引用好处是当内存不足时,JVM会回收ThreadLocal对象,将其对应Entry.value设置为null,很大程度上避免内存泄漏。ii:通过ThreadLocal的get方法从Map中取出对象。iii:Map的大小由ThreadLocal对象的多少决定。
④内存泄漏:i原因:ThreadLocalMap的Key是弱引用,Value是强引用。线程运行且value指向某个强引用对象,该对象不会被回收,导致内存泄漏。ii方法:用完ThreadLocal后,及时调用remove()释放内存空间,全部清除所有key为null的Entry。
⑤ThreadLocalMap源码:i:没实现Map接口,是一个简单的线性探测哈希表。设计目的是存储线程私有数据,不会有大量Key,采用线性探测更节省空间。
底层是数组,数组元素是Entry对象,Entry对象继承WeakReference,key是ThreadLocal对象,value是局部变量。ii:继承实现子线程不会继承父线程的 ThreadLocalMap,可使用 InheritableThreadLocal实现父线程用ThreadLocal给子线程传值。原因:每个线程都有两个ThreadLocalMap:ThreadLocal变量存储在threadLocals中,不会被子线程继承。InheritableThreadLocal变量存储在inheritableThreadLocals中,当new Thread()创建一个子线程时,Thread的init()会检查父线程是否有inheritableThreadLocals,如果有,拷贝InheritableThreadLocal变量到子线程。iii:扩容机制采用“先清理再扩容”策略,threshold默认值是数组长度三分之二,扩容时数组长度变为原来的2倍,并重新计算索引,如果发生哈希冲突,采用线性探测法来解决。
}
15、内存模型、指令重排
①内存模型:i:Java 虚拟机规范中定义的一个抽象模型,用来描述多线程环境中共享变量的内存可见性。ii:线程从主内存拷贝变量到工作内存,减少CPU访问RAM开销。每个线程都有变量副本,避免多个线程同时修改共享变量导致的数据冲突。iii:原子性保证操作不可中断,可见性保证变量修改后线程能看到最新值,有序性保证代码执行顺序一致,通过volatile、synchronized实现。
②指令重排:i:CPU或编译器为提高程序的执行效率,改变代码执行顺序的一种优化技术。ii:从Java源代码到最终执行的指令序列,会经历3种重排序:编译器重排序、指令并行重排序、内存系统重排序。
iii:重排可能会导致双重检查锁失效。
class Singleton {private static volatile Singleton instance;//单例模式使用volatile禁止指令重排public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 由于 volatile,禁止指令重排}}}return instance;}}
16、Happens-Before、As-If-Serial
①Happens-Before:Java内存模型定义的一种保证线程间可见性和有序性的规则。
如果操作A Happens-Before操作B:操作A的结果对操作B可见;操作A在时间上先于操作B执行,并且B不能重排序到A之前。JMM的Happens-Before原则,如下保证可见性:i:程序顺序规则:单线程内,代码按顺序执行;比如 a = 1; b = 2;,a 先于 b 执行。ii:监视器锁定规则:unlock() Happens-Before lock();比如synchronized释放锁后,获取锁的线程能够看到最新的数据。iii:volatile变量规则:写volatile变量Happens-Before读volatile。iv:传递性规则:A Happens-Before B 且 B Happens-Before C,则A Happens-Before C。v:线程启动规则:线程A执行操作ThreadB.start(),A线程的ThreadB.start()操作happens-before于线程B中的任意操作。vi:线程终止规则:线程的所有操作Happens-Before Thread.join();例如t.join()之后,主线程一定能看到t的修改。
②As-If-Serial规则:i:允许CPU和编译器优化代码顺序,但不会改变单线程的执行结果。ii:只适用于单线程,多线程环境仍然可能发生指令重排,需要volatile和synchronized等机制来保证有序性。
17、volatile
①保证可见性,线程修改volatile修饰的变量后,其他线程能够立即看到最新值。
②防止指令重排,volatile修饰的变量写入不会被重排序到它之前的代码。③JVM会在volatile变量的读写前后插入“内存屏障”,以约束CPU和编译器的优化行为:i:StoreStore屏障禁止普通写操作与volatile写操作的重排。ii:StoreLoad屏障会禁止volatile写与volatile读重排。iii:LoadLoad屏障会禁止volatile读与后续普通读操作重排。iv:LoadStore屏障会禁止volatile读与后续普通写操作重。
18、synchronized锁升级、Monitor
①synchronized:i特点:依赖JVM内部的Monitor对象来实现线程同步,不用手动去lock和unlock,JVM会自动加锁和解锁。ii原理:synchronized 加锁代码块时,JVM 会通过monitorenter、monitorexit指令来实现同步,禁止指令重排。②synchronized支持可重入:i:底层通过Monitor对象owner和count字段实现的,owner记录持有锁线程,count记录线程获取锁次数。ii:Java对象头包含一个Mark Word存储对象的状态,包括锁信息。当一个线程获取对象锁时,JVM会将该线程的ID写入Mark Word,并将锁计数器设为1。如果一个线程尝试再次获取已经持有的锁,JVM 会检查Mark Word中的线程ID。iii:如果ID匹配表示同一个线程,锁计数器递增。iv:当线程退出同步块时,锁计数器递减。如果计数器值为零,JVM 将锁标记为未持有状态,并清除线程ID信息。可重入:同一个线程可以多次获得同一个锁,而不会被阻塞。
③Monitor:i:JVM内置的同步机制,每个对象在内存中都有一个对象头Mark Word,用于存储锁的状态,以及Monitor对象的指针。Monitor结构如下:ii:Synchronized依赖对象头的Mark Word进行状态管理,支持无锁、偏向锁、轻量级锁,以及重量级锁。Hotspot 虚拟机中,Monitor由ObjectMonitor实现:
ObjectMonitor() {_count = 0; // 记录线程获取锁的次数(可重入锁),每次成功加锁 _count + 1,释放锁 _count - 1。_owner = NULL; //指向持有ObjectMonitor对象的线程,null表示没有线程持有锁。线程成功获取锁后更新为线程ID,释放锁后为null。_WaitSet = NULL; // 等待队列,调用 wait()线程会释放锁,并加入 _WaitSet,进入WAITING状态,等待notify() 唤醒。_cxq = NULL ;//阻塞队列,用于存放刚进入 Monitor 的线程(还未进入 _EntryList)。_EntryList = NULL ; // 竞争队列,所有等待获取锁的线程(BLOCKED 状态)会进入 _EntryList,等待锁释放后竞争执行权。 }
④synchronized保证可见性两步操作:i:加锁时,线程必须从主内存读取最新数据。ii:释放锁时,线程必须将修改的数据刷回主内存,其他线程获取锁后看到最新数据。
⑤synchronized锁升级:
JDK1.6的时候,为提升synchronized的性能,引入锁升级机制,从低开销的锁逐步升级到高开销的锁,以最大程度减少锁的竞争。i:没有线程竞争时,就使用低开销的“偏向锁”,没有额外的CAS操作;ii:轻度竞争时,使用“轻量级锁”,采用CAS自旋,避免线程阻塞;iii:只有在重度竞争时才使用“重量级锁”,由Monitor机制实现,需要线程阻塞,依赖于操作系统互斥量mutex来实现,会牵扯到os层面;mutex用于保证任何给定时间内,只有一个线程能执行某一段特定的代码段。⑥synchronized锁状态:i:偏向锁:一个线程第一次获取锁时,JVM会在对象头Mark Word记录线程ID,下次进入,如果是同一个线程直接执行,无需额外加锁。ii:轻量级锁:当多个线程尝试获取锁但不是同一个时段,偏向锁会升级为轻量级锁,等待锁的线程通过CAS自旋避免进入阻塞状态。iii:重量级锁:如果自旋失败,锁会升级为重量级锁,等待锁的线程会进入阻塞状态,等待监视器Monitor进行调度。
19、synchronized 、 ReentrantLock、Lock、AQS
①synchronized:
由JVM内部的Monitor机制实现, 自动加锁和解锁,在方法和代码块上加锁。②ReentrantLock:i:基于AQS实现,需要手动lock()和unlock(),只能在代码块上加锁,能指定公平锁还是非公平锁,提供一种能够中断等待锁的线程机制,通过lock.lockInterruptibly()来实现。ii:ReentrantLock的lock()方法的实现由ReentrantLock内部的Sync类来实现,涉及到线程的自旋、阻塞队列、CAS、AQS。iii:创建ReentrantLock的时候,传递参数 true 实现公平锁,默认是非公平锁。Q:并发量大的情况下,使用synchronized还是ReentrantLock?
A:倾向于ReentrantLock,原因:ReentrantLock提供超时和公平锁等特性,应对更复杂的并发场景。ReentrantLock允许更细粒度的锁控制,能有效减少锁竞争。ReentrantLock支持条件变量Condition,实现比synchronized更友好的线程间通信机制。
③Lock:
是JUC中的一个接口,最常用实现类包括可重入锁ReentrantLock、读写锁ReentrantReadWriteLock等。
Lock方法会首先尝试通过CAS来获取锁。如果当前锁没有被持有,将锁状态设置为1,表示锁已被占用。否则将当前线程加入AQS等待队列。
④AQS:
一个抽象类,维护一个共享变量state和一个线程等待队列,为ReentrantLock等类提供底层支持。
维护CLH 队列来维护等待线程,CLH 是三个作者Craig、Landin和Hagersten的首字母缩写,是一种基于链表的自旋锁。i:基本思想:如果被请求的共享资源处于空闲状态,则当前线程成功获取锁;否则,将当前线程加入到等待队列中,当其他线程释放锁时,
从等待队列中挑选一个线程,把锁分配给它。ii:源码:a:状态state由volatile变量修饰,保证多线程之间的可见性。b:同步队列由内部定义的Node类实现,每个Node包含等待状态、前后节点、线程的引用等,是一个先进先出的双向链表。iii:AQS两种同步方式:a:独占模式下:每次只能有一个线程持有锁,例如ReentrantLock。b:共享模式下:多个线程同时获取锁,例如Semaphore和CountDownLatch。iv:AQS核心方法包括:a:acquire:获取锁,失败进入等待队列。b:release:释放锁,唤醒等待队列中的线程。c:acquireShared:共享模式获取锁。d:releaseShared:共享模式释放锁。
20、非公平锁、公平锁
①公平锁:在多个线程竞争锁时,获取锁的顺序与线程请求锁的顺序相同,即先来先服务。i:公平锁的核心逻辑在AQS的hasQueuedPredecessors()方法中,该方法用于判断当前线程前面是否有等待的线程。如果队列前面有等待线程,当前线程不能抢占锁,按照队列顺序排队。如果队列前面没有线程或当前线程是队列头部线程,获取锁。②非公平锁:不保证线程获取锁的顺序,当锁被释放时,任何请求锁的线程都有机会获取锁,而不是按照请求的顺序。
21、CAS
①定义:CAS是一种乐观锁,比较一个变量的当前值是否等于预期值,如果相等,则更新值,否则重试。
在CAS 中,有三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)②过程:i:先判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新V,当前线程就放弃更新。ii:比较和替换的操作需要是原子的,不可中断的。Java中的CAS是由Unsafe类实现。③原子性:
CPU发出一个LOCK指令进行总线锁定,阻止其他处理器对内存地址进行操作,直到当前指令执行完成。
④CAS存在三个经典问题:ABA问题、自旋开销大、只能操作一个变量。
i:ABA问题:一个值原来是A,后来被改为B,再后来又被改回A,这时CAS会误认为值没有发生变化。措施:版本号/时间戳。
class OptimisticLockExample {private int version;private int value;public synchronized boolean updateValue(int newValue, int currentVersion) {if (this.version == currentVersion) {this.value = newValue;this.version++;return true;}return false;}
}
ii:自旋开销大:失败时会不断自旋重试,如果一直不成功,会给CPU带来执行开销。措施:限制自旋次数。
int MAX_RETRIES = 10;
int retries = 0;
while (!atomicInt.compareAndSet(expect, update)) {retries++;if (retries > MAX_RETRIES) {synchronized (this) { // 超过次数,使用 synchronized 处理if (atomicInt.get() == expect) {atomicInt.set(update);}}break;}}iii:多个变量同时更:将多个变量封装为一个对象,使用AtomicReference进行CAS更新。
22、原子操作类
①定义:原子操作类基于CAS+volatile实现的,底层依赖于Unsafe类,最常用的有AtomicInteger、AtomicLong、AtomicReference。②AtomicInteger基于volatile+CAS实现,底层依赖于Unsafe类。核心方法包括 getAndIncrement、compareAndSet。
23、线程死锁
死锁发生在多个线程相互等待对方释放锁。
①发生死锁四个条件:i:互斥:资源不能被多个线程共享,一次只能由一个线程使用。ii:持有并等待:一个线程已经持有一个资源,并且在等待获取其他线程持有的资源。iii:不可抢占:资源不能被强制从线程中夺走,必须等线程自己释放。iv:循环等待:存在一种线程等待链,线程A等待线程B持有的资源,线程B等待线程C持有的资源,直到线程N又等待线程A持有的资源。②死锁排查:i:系统级别上排查,在Linux生产环境中,先使用top ps命令查看进程状态,是否有进程占用过多资源。ii:使用JDK自带性能监控工具排查,使用jps -l查看当前进程,然后使用jstack 进程号 ,查看进程的线程堆栈信息、线程是否等待锁资源。iii:使用一些可视化的性能监控工具,JConsole、VisualVM查看线程的运行状态、锁的竞争情况。
24、线程同步、互斥、锁要解决的问题、自旋锁
同步关注的是线程之间的协作,互斥关注的是线程之间的竞争。
①同步:线程之间要密切合作,按照一定的顺序来执行任务。线程A先执行,线程B再执行。②互斥:线程之间要抢占资源,同一时间只能有一个线程访问共享资源。线程A在访问共享资源时,线程B不能访问。i:使用synchronized关键字或者Lock接口的实现类,如 ReentrantLock来给资源加锁。ii:锁在操作系统层面使用Mutex,某个线程进入临界区后,其他线程不能再进入临界区,要阻塞等待持有锁的线程离开临界区。
③锁要解决哪些问题?i:谁可以拿到锁,可以是类对象,当前的this对象、任何其他新建的对象。ii:抢占锁的规则,能不能抢占多次,自己能不能反复抢。iii:抢不到怎么办,自旋?阻塞?或者超时放弃?iv:锁被释放了还在等待锁的线程怎么办?是通知所有线程一起抢或者只告诉一个线程抢
④自旋锁:i:当线程尝试获取锁时,如果锁已经被占用,线程不会立即阻塞,而是通过自旋,循环等待方式不断尝试获取锁。用于锁持有时间短的场景,ReentrantLock的tryLock方法就用到自旋锁。ii:优点:避免线程切换带来的开销,缺点是如果锁被占用时间过长,会导致线程空转,浪费 CPU 资源。iii:默认自旋锁会一直等待,直到获取到锁为止。实际开发设置自旋次数或者超时时间。如果超过阈值,线程放弃锁或者进入阻塞状态。
25、悲观锁、乐观锁
①悲观锁:认为每次访问共享资源时都会发生冲突,所在在操作前一定要先加锁,防止其他线程修改数据。②乐观锁:认为冲突不会总是发生,在操作前不加锁,而是在更新数据时检查是否有其他线程修改数据。如果发现数据被修改,就会重试。
26、并发工具类:CountDownLatch
①定义:是JUC中同步工具类,协调多个线程之间同步,确保主线程在多个子线程完成任务后继续执行。核心思想通过一个倒计时计数器来控制多个线程的执行顺序。②过程:i:初始化一个 CountDownLatch 对象,指定一个计数器的初始值表示需要等待的线程数量。ii:然后在每个子线程执行完任务后,调用countDown()方法,计数器减1。iii:主线程调用await()方法进入阻塞状态,直到计数器为0,所有子线程都执行完任务后,主线程才会继续执行。Q:要查10万多条数据,用线程池分成20个线程去执行,怎么做到等所有的线程都查找完之后,最后一条结果查找结束才输出结果?
A:使用CountDownLatch来实现。CountDownLatch非常适合这个场景。i:创建 CountDownLatch 对象,初始值设定为20表示20个线程需要完成任务。ii:创建线程池,每个线程执行查询操作,查询完毕后调用countDown()方法,计数器减 1。iii:主线程调用await()方法,等待所有线程执行完毕。
27、并发工具类:CyclicBarrier
①定义:可循环使用的屏障,用于多个线程相互等待,直到所有线程都到达屏障后再同时执行。
②过程:i:初始化一个CyclicBarrier对象,指定一个屏障值N,表示需要等待的线程数量。ii:每个线程执行await()方法,表示已经到达屏障,等待其他线程,此时屏障值会减1。iii:当所有线程都到达屏障后,屏障值为0时,所有线程会继续执行。③区别:
CyclicBarrier让所有线程相互等待,全部到达后再继续;CountDownLatch让主线程等待所有子线程执行完再继续。
27、并发工具类:Semaphore
①定义:信号量控制同时访问某个资源的线程数量,确保最多只有指定数量线程能够访问某个资源,超过的必须等待。
②过程:i:初始化一个Semaphore对象,指定许可证数量,表示最多允许多少个线程同时访问资源。ii:在每个线程访问资源前,调用acquire()方法获取许可证,如果没有可用许可证,则阻塞等待。iii:访问完资源后,要调用release()方法释放许可证。③用途:流量控制,如数据库连接池、网络连接池。
28、并发工具类:Exchanger
①定义:交换者,在两个线程之间进行数据交换,支持双向数据交换。
②过程:A调用exchange(dataA),线程B调用exchange(dataB),它们会在同步点交换数据,即A得到B的数据,B得到A的数据。如果一个线程先调用exchange()会阻塞等待,直到另一个线程也调用exchange()。使用Exchanger先创建一个Exchanger对象,然后在两个线程中调用exchange()方法进行数据交换。
29、ConcurrentHashMap
①JDK7特点: HashMap的线程安全版本。i:JDK7是分段锁,整个Map会被分为若干段,每个段都可以独立加锁。不同的线程同时操作不同的段,从而实现并发。ii:HashEntry是一个单项链表,段继承ReentrantLock,每个段都是一个可重入锁。②put过程:i:计算key的hash,定位到段,段如果是空就先初始化;ii:使用ReentrantLock进行加锁,如果加锁失败就自旋,自旋超过次数就阻塞,保证一定能获取到锁;iii:遍历段中的键值对HashEntry,key相同直接替换,key不存在就插入。iv:释放锁。③get过程:i:先计算key的hash找到段,再遍历段中的键值对,找到就直接返回value。ii:get不用加锁,因为是value是volatile修饰,线程读取value时保证可见性。
JDK8使用一种更加细粒度的桶锁,配合CAS+synchronized代码块控制并发写入,最大程度减少锁的竞争。i:读操作,ConcurrentHashMap使用volatile变量来保证内存可见性。ii:写操作,ConcurrentHashMap优先使用CAS尝试插入,如果成功就返回;否则使用synchronized对代码块加锁处理。①put过程:i:计算key的hash确定桶在数组中的位置。如果数组为空,采用CAS初始化,确保只有一个线程在初始化数组。ii:如果桶为空,直接CAS插入节点。如果CAS操作失败,会退化为synchronized代码块来插入节点。插入过程会判断桶哈希是否小于0,小于0是红黑树,大于等于0是链表。实际红黑树节点TreeBin的hash值固定为-2。iii:如果链表长度超过 8,转换为红黑树。iv:插入新节点后,会调用addCount()方法检查是否需要扩容。②get过程:i:通过key的hash进行定位,如果该位置节点的哈希匹配且键相等,则直接返回值。ii:如果节点的哈希为负数,说明是个特殊节点,比如说如树节点或者正在迁移的节点,调用find方法查找。
30、CopyOnWriteArrayList
①定义:ArrayList的线程安全版本,适用于读多写少的场景。
核心思想:写操作时创建一个新数组,修改后再替换原数组,确保读操作无锁,提高并发性能。②过程:i:内部使用volatile变量来修饰数组array,保证读操作的内存可见性。ii:写操作的时候使用ReentrantLock来保证线程安全。iii:缺点:写操作的时候会复制一个新数组,如果数组很大,写操作的性能会受到影响。
30、BlockingQueue
①定义:JUC包下的一个线程安全队列,支持阻塞式的“生产者-消费者”模型。
当队列容器已满,生产者线程被阻塞,直到消费者线程取走元素后为止;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。②实现:i:阻塞队列使用ReentrantLock + Condition来确保并发安全。ii:以ArrayBlockingQueue为例,内部维护一个数组,使用两个指针分别指向队头和队尾。put先用ReentrantLock加锁,然后判断队列是否已满,如果已满就阻塞等待,否则插入元素。
31、线程池
①定义:管理和复用线程的工具,减少线程的创建和销毁开销。
ThreadPoolExecutor是线程池的核心实现,通过核心线程数、最大线程数、任务队列和拒绝策略来控制线程的创建和执行。使用线程池关注重点:i:合适的线程池大小。过小线程池会导致任务一直在排队;过大线程池会导致大家都在竞争 CPU 资源,增加上下文切换开销。ii:合适的任务队列。有界队列避免资源耗尽风险,但会导致任务被拒绝;无界队列虽然避免任务被拒绝,但会导致内存耗尽。使用LinkedBlockingQueue,传入参数来限制队列中任务的数量,不会出现OOM。iii:使用自定义线程池,而不是使用Executors创建的线程池。因为newFixedThreadPool线程池由于使用LinkedBlockingQueue,队列容量默认无限大,任务过多时会导致内存溢出;newCachedThreadPool线程池由于核心线程数无限大,当任务过多会导致创建大量的线程,导致服务器负载过高宕机。
线程池调优:i:根据任务类型设置核心线程数参数,如IO密集型任务会设置为 CPU 核心数*2 的经验值。ii:结合线程池动态调整的能力,在流量波动时通过setCorePoolSize平滑扩容,或者使用DynamicTp实现线程池参数的自动化调整。iii:通过内置的监控指标建立容量预警机制。通过JMX 监控线程池的运行状态,设置阈值,当线程池的任务队列长度超过阈值时,触发告警。线程池参数动态修改:i:线程池提供的setter方法在运行时动态修改参数。setCorePoolSize修改核心线程数、setMaximumPoolSize修改最大线程数。ii:调用setCorePoolSize()时如果新的核心线程数比原来大,线程池会创建新线程;如果更小,线程池不会立即销毁多余的线程,除非有空闲线程超过keepAliveTime。
②线程池工作流程:任务提交 → 核心线程执行 → 任务队列缓存 → 非核心线程执行 → 拒绝策略处理。i:线程池通过submit()或execute()提交任务。a:execute方法没有返回值,适用于不关心结果和异常的简单任务。b:submit 有返回值,适用于需要获取结果或处理异常的场景.ii:线程池会先创建核心线程来执行任务。iii:如果核心线程都在忙,任务会被放入任务队列中。iv:如果任务队列已满,且当前线程数量小于最大线程数,线程池会创建新的线程来处理任务。v:如果线程池中的线程数量已经达到最大线程数,且任务队列已满,线程池会执行拒绝策略。
③线程池7参数,重点关注:核心线程数、最大线程数、等待队列、拒绝策略。i:corePoolSize:核心线程数,长期存活,执行任务的主力。ii:maximumPoolSize:线程池允许的最大线程数。iii:workQueue:任务队列,存储等待执行的任务。iv:handler:拒绝策略,任务超载时的处理方式。线程数达到maximumPoolSiz,任务队列也满的时候,触发拒绝策略。a:AbortPolicy:默认拒绝策略,会抛RejectedExecutionException异常。b:CallerRunsPolicy:让提交任务的线程自己来执行这个任务,调用execute方法的线程。c:DiscardOldestPolicy:等待队列会丢弃队列中最老的一个任务,然后尝试重新提交被拒绝的任务。d:DiscardPolicy:丢弃被拒绝的任务,不做任何处理也不抛出异常。v:threadFactory:线程工厂,用于创建线程,可自定义线程名。vi:keepAliveTime:非核心线程的存活时间,空闲时间超过该值就销毁。vii:unit:keepAliveTime参数的时间单位。
④关闭线程池
i:调用线程池的shutdown或shutdownNow方法来关闭线程池。shutdown不会立即停止线程池,而是等待所有任务执行完毕后再关闭线程池。shutdownNow通过一系列动作来停止线程池,包括停止接收外部提交任务、 忽略队列里等待任务、尝试将正在跑的任务interrupt中断。shutdownNow不会真正终止运行的任务,给任务线程发送interrupt信号,任务是否真正终止取决于线程是否响应InterruptedException。
⑤线程池的线程数配置
分析线程池中执行的任务类型是CPU密集型还是IO密集型i:对于CPU密集型任务,尽量减少线程上下文切换,优化CPU使用率。核心线程数设置为处理器的核心数或核心数加一是较理想选择。+1是以备不时之需,如果某线程因等待系统资源而阻塞时,有多余的线程顶上去,不至于影响整体性能。ii:对于IO密集型任务,由于线程经常处于等待状态,等待IO操作完成,设置更多线程来提高并发,如CPU核心数的两倍。
⑥设置的线程数多了还是少了,如何查看?
通过监控和调试来判断线程数是多还是少。i:通过top命令观察CPU的使用率,如果CPU使用率较低,线程数过少;如果CPU使用率接近100%,但吞吐量未提升,线程数过多。ii:通过VisualVM分析线程运行情况,查看线程的状态、等待时间、运行时间等信息。iii:jstack命令查看线程堆栈信息,查看线程是否处于阻塞状态。有大量的BLOCKED线程说明线程数过多,竞争比较激烈。
⑦四种常见线程池:
本质上都是ThreadPoolExecutor的不同配置。i:固定大小线程池Executors.newFixedThreadPool(int nThreads):用于任务数量确定,且对线程数有明确要求场景。a:线程池大小固定,corePoolSize=maximumPoolSize,默认使用LinkedBlockingQueue阻塞队列,适用于任务量稳定场景。b:新任务提交时,线程池有空闲线程直接执行;如果没有,任务进入LinkedBlockingQueue等待。c:缺点是任务队列默认无界,可能导致任务堆积,甚至 OOM。ii:缓存线程池Executors.newCachedThreadPool():短时间内任务量波动较大场景。短时间内有大量文件处理任务或网络请求。a:线程池大小不固定,corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE。b:空闲线程超过60秒会被销毁,使用SynchronousQueue阻塞队列,适用于短时间内有大量任务的场景。c:提交任务时,线程池没有空闲线程,直接新建线程执行任务;如果有,复用线程执行任务。线程空闲 60 秒后销毁,减少资源占用。d:缺点是线程数没有上限,在高并发情况下可能导致 OOM。iii:定时任务线程池Executors.newScheduledThreadPool(int corePoolSize):定时执行任务场景。a:任务线程池大小可配置,支持周期性任务执行,使用DelayedWorkQueue阻塞队列,适用于周期性执行任务的场景。b:定时任务时,schedule()方法将任务延迟一定时间后执行一次;scheduleAtFixedRate()方法将任务延迟一定时间后以固定频率执行;scheduleWithFixedDelay() 方法将任务延迟一定时间后以固定延迟执行。iv:单线程线程池Executors.newSingleThreadExecutor():按顺序执行任务的场景。a:线程池只有1个线程,保证任务按提交顺序执行,使用LinkedBlockingQueue阻塞队列,适用于需要按顺序执行任务的场景。b:缺点是无法并行处理任务。
⑧五种线程池阻塞队列i:有界队列ArrayBlockingQueue:一个有界的先进先出阻塞队列,底层是一个数组,适合固定大小线程池。ii:无界队列LinkedBlockingQueue:底层是链表,如果不指定大小,默认大小 Integer.MAX_VALUE,相当于一个无界队列。a:任务执行时间比较长,会导致队列任务越积越多,导致内存使用不断飙升,最终出现OOM。iii:优先级队列PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。任务按照其自然顺序或Comparator来排序。适用于需要按照给定优先级处理任务的场景,比如优先处理紧急任务。iv:延迟队列DelayQueue;类似PriorityBlockingQueue,由二叉堆实现的无界优先级阻塞队列。v:同步队列SynchronousQueue:每个插入操作必须等待另一个线程的移除操作,任何一个移除操作都必须等待另一个线程的插入操作。
⑨线程池异常:i:使用try-catch捕获。ii:使用Future获取异常:使用submit(),关心任务返回值。iii:自定义ThreadPoolExecutor重写afterExecute方法:全局捕获所有任务异常。iv:使用UncaughtExceptionHandler捕获异常,使用execute(),不关心任务返回值。
⑩线程池5种状态
RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED 依次流转。i:RUNNING 状态的线程池可以接收新任务,并处理阻塞队列中的任务.ii:SHUTDOWN 状态的线程池不会接收新任务,但会处理阻塞队列中的任务。iii:STOP 状态的线程池不会接收新任务,也不会处理阻塞队列中的任务,并且会尝试中断正在执行的任务。iv:TIDYING 状态表示所有任务已经终止。v:ERMINATED 状态表示线程池完全关闭,所有线程销毁。