java里面的线程和操作系统的线程一样吗?
Java底层采用pthread—create创建线程,本质上Java程序创建的线程,就是和操作系统一样的
使用多线程注意问题:
Java线程安全在三个方面体现:
原子性:提供互斥访问,操作不可中断,要么全部执行成功,要么完全不执行,不会被其他线程干扰,Java中使用了atomic包(这个包提供了一些支持原子操作的累,这些类可以在多线程环境下保证操作的原子性)和synchronized关键字来确保原子性
可见性:一个线程对主内存的修改可以及时被其它线程看到,Java使用synchronized和volatile这两个关键字确保可见性
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察一般杂乱无序,在Java中使用了happens-before原则确保有序性
保证数据的一致性有哪些方案呢?
事务管理:使用数据库事务确保一组数据库操作要么全部成功提交要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)属性,数据库事务可以保证数据的一致性
‘锁机制:使用锁是西安共享资源的互斥访问。可以使用synchronized关键字、ReentrantLock或其它锁来控制并发访问
版本控制:通过乐观锁的方式,在更新数据时记录数据的版本信息,壁板同时对同一数据修改
线程创建四种方式
1.继承Thread类:继承后重写run方法,主线程创建其实例后调用start方法启动
优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接用this
缺点:无法继承其它父类
static class Mythread extends Thread{@Overridepublic void run() {System.out.println(this.currentThread());}}public static void main(String[] args) throws IOException {Mythread mythread = new Mythread();mythread.start();}
2.实现Runnable接口:同样重写run方法,然后将此对象作为参数传递给Thread类构造器,创建Thread对象后调用其start()方法启动(可以使用lambda+匿名内部类实现)
优点:可继承其它类,可以多个线程共享同一个目标对象,适合多个线程处理同一份资源,可以将CPU和代码分开形成清晰模型,体现面向对象思想
缺点:编程稍显复杂,要访问当前线程,使用Thread.currentThrad()方法
static class Mythread implements Runnable{@Overridepublic void run() {//线程执行代码System.out.println(Thread.currentThread());}}public static void main(String[] args) throws IOException {Thread thread = new Thread(new Mythread());thread.start();}
3.实现Callable接口与FutureTask:java.util.concurrent.Callable接口类似于Runnable,但Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需要将他包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口
缺点:编程稍显复杂,要访问当前线程,使用Thread.currentThrad()方法
优点:可以继承其它类,多个线程共享一个目标对象,还可以获取返回值和异常
static class MyCallback implements Callable<Integer> {public Integer call() throws Exception {return 1;}}public static void main(String[] args) throws IOException {MyCallback task = new MyCallback();FutureTask<Integer> future = new FutureTask<>(task);Thread thread = new Thread(future);thread.start();try {System.out.println(future.get()); //阻塞获取} catch (Exception e) {e.printStackTrace();}}
4.使用线程池(Executor框架):
缺点:增加程序复杂度,涉及到线程池参数调整和故障排查时,错误的配置可能导致死锁、资源耗尽等问题
优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销, 显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程处理。线程池能有效控制运行的线程数量,防止因无限制创建过多线程导致系统资源耗尽(如内存溢出)。合理配置线程池可最大化CPU利用率和系统吞吐量
static class Task implements Runnable {public void run() {System.out.println("task");}}public static void main(String[] args) throws IOException {ExecutorService executor = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) {executor.submit(new Task());}executor.shutdown();}
怎么启动线程
通过Thrad类的stat()方法
如何停止一个线程的运行:
1.异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态,会抛出异常,达到中断线程的效果
2.在沉睡中停止:先将线程sleep,然后调用interrup标记中断状态,interrupt会将阻塞状态的线程中断,会抛出中断一场,达到停止线程效果
3.stop()暴力停止:直接调用stop()方法暴力停止(已弃用),强制停止可能是的清理工作未完成
4.使用return停止:调用interrupt标记为中断状态后,在run方法中判断房前相乘状态,如果为中断状态则return,能停止线程
5.通过Future取消任务,使用线程池提交任务,并通过Future.cancel()停止线程,依赖中断机制
调用 interrupt 是如何让线程抛出异常的?
每个线程都一个与之关联的布尔属性来表示中断状态,中断状态初始值为false,当一个线程被其它线程调用Thread.interrupt()方法中断时,会根据实际情况做出响应:
如果该线程正在执行低级别可中断方法(如Thread.sleep()\Thread.join()或Object.wait()),则会接触阻塞并派出InterruptException异常
否则Thread.interrupt()仅设置线程的中断状态,在该被中断线程中稍后稍后可通过轮询中断状态来决定是否要停止当前正在执行的任务
public class InterruptDemo {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {// 情况1:响应阻塞方法的中断try {System.out.println("Thread is sleeping...");Thread.sleep(5000); // 可中断的阻塞} catch (InterruptedException e) {System.out.println("Thread was interrupted during sleep!");// 中断状态已被清除,需恢复Thread.currentThread().interrupt();}// 情况2:手动检查中断状态while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread is running...");}System.out.println("Thread exited by interrupt flag.");});thread.start();Thread.sleep(1000); // 主线程等待1秒thread.interrupt(); // 中断子线程}
}
总结:
interrupt()
的本质:协作式通知线程“有人希望你停止”,而非强制终止。- 阻塞方法:直接抛出
InterruptedException
,中断状态被清除。 - 非阻塞代码:需通过
isInterrupted()
或interrupted()
轮询中断状态。 - 最佳实践:捕获
InterruptedException
后,要么恢复中断状态,要么妥善清理资源后退出。
Java线程的状态
可以调用线程Thread中的getState()方法获取当前线程的状态。
创建:还未调用start方法,尚未启动的线程
就绪:调用了start,等待调度
运行:正在运行
阻塞:等待监视器锁时,陷入阻塞
等待:当前线程正在等待另一线程执行特定操作(如notify)
超时等待:具有指定等待时间的等待状态
终止:线程完成执行
sleep和wait区别:
所属分类不同:sleep是Thread类的静态方法,可以在任何地方直接通过Thread.sleep()调用。wait是Object实例方法,通过对象调用
锁释放情况:Thread.sleep()调用时,线程暂停执行指定时间,但不会释放持有锁的对象。object.wait()会释放对象锁进入等待状态,知道其他线程调用相同对象的notify()或notifyAll()方法唤醒
使用条件:sleep任意位置使用,wait必须在同步代码块调用,因为它需要释放锁
唤醒机制:sleep休眠结束后,线程回复到就绪状态,等待cpu调度。wait需要其它线程调用方法唤醒。notify()随机唤醒一个在该对象上等待的线程,notifyAll()唤醒所有
sleep会释放cpu吗:
是的,会释放CPU,但不会释放锁
当线程调用sleep方法后,主动让出CPU时间片,进入超时等待状态。此时操作系统会将CPU分配给其它处于就绪状态的线程
sleep()不会释放锁,所以其它线程尝试获取同一把锁,它们会被阻塞
阻塞和等待的区别:
触发条件:线程进入阻塞状态因为拿不到锁。进入等待状态是因为它在等待另一个线程执行某些操作,例如调用Object.wait()方法、Thread.join()方法或LockSupport.park()方法。此时不消耗CPU资源,不参与锁竞争
唤醒机制:当一个线程阻塞等待锁时,一旦锁被释放,线程有机会重新尝试获取锁。如果此时锁未被其它线程获取,那么线程可以从阻塞状态进入就绪状态。而线程在等待状态中需要被显示唤醒
总结:
阻塞是锁竞争失败后被动触发的状态,等待是人为的主动触发
阻塞的唤醒自动触发,等待的唤醒被其它线程掌控
等待状态的线程如何恢复到运行状态
核心机制是通过外部事件触发或资源可用性变化。比如被其它线程唤醒
notify 和 notifyAll 的区别?
notify随机唤醒一个
notifyAll唤醒所有,所有线程开始竞争锁,拿到锁的变就绪,没拿到就阻塞
notify 选择哪个线程?
notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。
JVM有很多实现,比较流行的就是hotspot,hotspot对notofy()的实现并不是我们以为的随机唤醒,,而是“先进先出”的顺序唤醒。
不同线程间如何通信:
1.共享变量:多个线程可以访问和修改同一个共享变量。通常使用synchronized
关键字或 volatile
关键字:
volatile关键字确保flag变量在多个线程间的可见性,生产者在睡眠两秒后将flag设置为true,消费者线程遭flag为false时等待,直到flag变为true
2.Object
类中的 wait()
、notify()
和 notifyAll()
方法可以用于线程间的协作。wait()
方法使当前线程进入等待状态,notify()
方法唤醒在此对象监视器上等待的单个线程,notifyAll()
方法唤醒在此对象监视器上等待的所有线程。
lock是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行,消费者线程调用lock.wait进入等待,释放锁,生产者执行完让你无后调用lock.notify唤醒等待的消费者线程 3.java.util.concurrent.locks
包中的 Lock
和 Condition
接口提供了比 synchronized
更灵活的线程间通信方式。Condition接口的await()方法类似于wait()方法,signal()方法类似于notify方法,signalAll()类似于notifyAll()方法
ReentrantLock时Lock接口的一个实现类,condition是通过lock.newCondition()方法创建的,消费者线程调用condition.await进入等待状态,生产者线程执行完生产任务后调用 condition.signal()
方法唤醒等待的消费者线程。 4.java.util.concurrent
包中的 BlockingQueue
接口提供了线程安全的队列操作,当队列满时,插入元素的线程会被阻塞,当队列为空时,获取元素的线程会被阻塞
LinkedBlockingQueue
是 BlockingQueue
接口的一个实现类,自定义容量为 1。 生产者线程调用 queue.put(1)
方法将元素插入队列,如果队列已满,线程会被阻塞;消费者线程调用 queue.take()
方法从队列中取出元素,如果队列为空,线程会被阻塞 5.CountDownLatch。是一个同步辅助类,允许一个或多个线程等待其他线程完成操作 • CountDownLatch(int count)
:构造函数,指定需要等待的线程数量。 • countDown()
:减少计数器的值。 • await()
:使当前线程等待,直到计数器的值为 0。
6.CyclicBarrier。CyclicBarrier
是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达某个公共屏障点。 CyclicBarrier(int parties, Runnable barrierAction)
:构造函数,指定参与的线程数量和所有线程到达屏障点后要执行的操作。
await()
:使当前线程等待,直到所有线程都到达屏障点。 7.Semaphore,是一个计数信号量,控制同时访问特定资源的线程数量
Semaphore(int permits):构造函数,指定信号量的初始许可数量
aquire():获取一个许可,如果没有许可则阻塞
release():释放一个许可
并发安全
juc你常用的类?
线程池相关:
ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。可灵活配置线程池参数:核心线程数、最大线程数、任务队列等
Executors:线程池工厂类,静态方法创建线程池,newFixedThreadPool(固定大小线程池)、newCachedThreadPool(可缓存线程池)、newSingleThreadExecutor(单线程池)
并发集合类:
concurrentHashMap:volatile+CAS+synchronized保证线程安全的hashmap
copyOnWriteArrayList:线程安全列表,读写分离,高并发性能豪
同步工具类:
CountDownLatch:计数器,初始化为线程数量,每个线程完成任务时,计数器减一,当计数器为0时,等待的线程继续执行
CycliBarrier:让一组线程互相等待,知道达到某个屏障点再一起执行,与CountDownLatch不同的时,CycliBarrier可以重复使用,当所有线程都通过屏障后,它会重置,再次用于下一轮等待
Semaphore:信号量:阻塞控制访问某个资源的线程数量,常用于数据库连接池,线程池中的线程数量等
原子类:
AtomicInterger:原子证书类,提供原子性的自增自减比较交换,硬件级别的原子指令
AtomicReference:原子引用类,用于对对象应用进行原子操作,保证多线程环境下,对对象的更新操作是原子性的,要么全部成功,要么全部失败。常用于实现无锁的数据结构或者需要对对象进行原子更新的场景
怎么保证多线程安全:
synchronized关键字:对象锁是通过synchronized关键字锁定对象的监视器(monitor)实现的
volatile关键字:用于确保变量看到是该变量的最新之,而不是本地寄存器的副本
Lock接口和ReentrantLock类:提供更灵活的锁管理和更高的性能
原子类:提供原子操作,如AtomicInteger
、AtomicLong
用于更新基本类型的变量无需额外同步
线程局部变量:ThreadLocal类为每个线程提供独立的变量副本,这样的每个线程都拥有自己的变量,消除竞争
并发集合:ConcurrentHashMap、ConcurrentLinkedQueue等
JUC工具类:Semaphore,CyclicBarrier 用于控制线程间协作
Java中常用的锁,什么场景使用?
内置锁(synchronized):一个线程进入它修饰的代码块中会获取关联对象的锁。它加锁时有无锁、偏向锁、轻量级锁、重量级锁几个级别。偏向锁用于当一个线程进入同步块时,没有其他任何线程竞争,使用偏向锁,减少锁开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁设计操作系统的互斥锁
**ReentrantLock:**提供了比synchronized
更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等,ReentrantLock
使用lock()
和unlock()
方法来获取和释放锁。
读写锁(ReadWriteLock):java.util.concurrent.locks.ReadWriteLock
接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者
乐观锁悲观锁:synchronized
和ReentrantLock
都是悲观锁的例子,乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳实现 自旋锁:是一种机制,在等待时持续循环检查锁是否可用,但过度自旋浪费CPU
synchronized和reentrantlock区别
synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也叫监视器锁
使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁实现。作用是实现原子性操作和解决共享变量的内存可见性问题
执行monitorenter指令时会尝试获取对象锁,如果对象没有锁定或者已经获得了锁,锁的计数器+1.此时其他竞争锁的线程会进入等待队列中,执行monitorexit时会把计数器-1,当计数器值为0时,锁释放,处于等待队列中的线程继续竞争锁。
synchronized是排他锁,由于Java线程和操作系统原生线程是一一对应,线程被阻塞或者唤醒时会从用户态切换到内核态,这种转换非常消耗性能
从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,释放锁的过程时将工作内存中的共享变量写回主内存.
如果再深入到源码,synchronized实际上有两个队列waitSet和entryList
synchronized
背后的 Monitor (锁监视器)维护了两个队列:EntryList 和 WaitSet。当多个线程竞争锁时,它们会排队进入 EntryList,等待获取锁。线程成功获取锁后成为锁的所有者,并进入临界区,可重入锁机制会将计数器加一。
如果线程在同步代码块中调用 wait()
,它将释放锁(计数器归零,当前线程设为 null),并加入到 WaitSet
。被 notify()
或 notifyAll()
唤醒后,这些线程会进入 EntryList,再次参与锁竞争。当线程执行完毕或主动退出 synchronized
块时,锁计数器减一,直至为零后释放锁,允许其他线程进入临界区。
ReentrantLock工作原理:
ReentrantLock的底层实现住哟啊依赖AbstractQueudSynchronizer(AQS)抽象类,AQS是一个提供基本同步机制的框架,包括队列、状态值等
ReentrantLock再AQS基础上通过内部类Sync来实现具体锁操作。不同Sync子类实现了公平锁和非公平锁的不同逻辑:
可中断性:意味着线程在等待锁的过程中,可以被其他线程中断而提前结束,在底层,它使用了与LockSupport.park()和LockSupport.unpark()相关的机制实现
设置超时时间:支持在尝试获取锁时设置超时时间,内部通过tryAcquireNanos方法实现
公平锁和非公平锁:直接创建ReentrantLock对象时,默认非公平锁。公平锁时按照线程等待的顺序获取锁。公平锁可以在创建ReentrantLock时传入true设置
ReentrantLock fairLock = new ReentrantLock(true);
多个条件变量: ReentrantLock支持多个条件变量,每个条件变量与一个ReentrantLock关联,使得线程灵活进行等待和唤醒,而不仅仅时基于对象监视器的wait和notify。多个条件变量的实现依赖于Condition接口:
ReentrantLock lock = new ReentrantLock();
Condition confition = lock.newCondition();
condition.await();
condition.signal();
可重入性:同一个线程可重复获取,不会造成死锁,通过内部的holdCount计数实现。
synchronized和reentrantlock应用场景
synchronized:简单同步需求(使用简单,不需要额外资源管理);代码块同步(精细控制同步范围,减少锁持有时间,提高并发性能);内置锁使用(synchronized使用内置锁也叫监视器锁,在需要使用对象作为锁对象情况下使用)
reentrantlock:高级使用需求(公平、响应中断、定时尝试、多个条件变量、复杂同步结构)
除了用synchronized,还有什么方法可以实现线程同步?
ReentrantLock
、volatile
、Atomic
synchronized锁静态方法和普通方法区别?
普通方法锁当前实例,静态方法锁类的clas对象
synchronized和reentrantlock区别?
synchronized:可修饰方法和代码块,自动获取释放锁,非公平,不能响应中断,JVM通过监视器实现
reentrantlock:只能在代码块,手动获取释放锁,可公平可非公平,可响应中断解决死锁,基于AQS实现
synchronized 支持重入吗?如何实现的?
底层利用计算机系统mutex Lock实现,每一个可重入锁会关联一个线程id和一个锁状态status。
一个线程请求方法会检查锁状态:
如果为0,代表锁没被占用,使用CAS获取锁,将线程ID替换成自己的
如果不是0,代表有线程占用,此时如果线程ID是自己的id,如果是可重入,status+1,然后获取该锁
释放是-1,直到status变0,释放锁
syncronized锁升级的过程讲一下
过程是:无锁→偏向锁→轻量级锁→重量级锁,只能升级不能降级
无锁:这是没有开启偏向锁的状态,JDK1.6之后偏向锁默认开启,但是有一个偏向延迟,需要在JVM启动后多少秒之后才开启,这个可以通过JVM参数设置,同时是否开启偏向锁也可以通过JVM参数设置
偏向锁:这是偏向锁开启之后的锁状态,如果没有任何线程拿到此锁,此时叫匿名偏向,当一个线程拿到偏向锁,下次想要竞争锁只需要拿到线程ID跟MarkWord当中存储的线程ID比较,如果ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和线程挂起的操作
轻量级锁:这个状态下线程主要是同CAS实现,将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord内容设置为只想DisplacedMarkWord指针,如果设置成功则获取锁。在线程出临界区时,也需要CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁后将被挂起的线程唤醒
重量级锁:当有两个以上的线程获取锁的时候轻量级锁会被升级为重量级锁,因为CAS如果没有成功的话始终在自旋,很消耗CPU,但是升级为重量级锁之后,线程会被操作系统调度后挂起,节约CPU
全过程:线程A进入synchronized开始抢锁,JVM会判断当前是否偏向锁状态,如果是,就会根据MarkWord中存储的线程ID判断当前线程A是都是持有偏向锁的线程,如果是忽略check,线程A直接执行临界区的代码
但是如果MarkWord线程不是线程A,就会自旋尝试获取,获取到了就把MarkWord线程ID改为自己的。如果竞争失败,立马撤销偏向锁,膨胀为轻量级锁
后续的竞争线程都会通过自旋来尝试获取,如果自旋成功,那么锁的状态任然是轻量级,如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞(涉及操作系统用户态和内核态的转换)
JVM对Synchornized的优化?
锁膨胀:就是锁省级,多了无锁、偏向锁及轻量级锁,大部分场景不需要用户态到内核态的装欢了,提升性能
锁消除:某些情况下,JVM检测不到某段代码被共享和竞争的可能,会把它所属的同步锁消除
锁粗化:多个连续的加锁、解锁连接在一起,扩展一个更大范围锁
自适应自旋锁:通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的
介绍AQS:
是一个Java抽象类,用于构建锁、同步器、协作工具类的工具类(框架)
核心思想:如果被请求的共享资源空闲,,就将当前请求资源的线程设置为有效工作线程,将共享资源设置为锁定,如果被占用,就需要一定的阻塞等待唤醒机制保证锁分配,这个机制主要是CLH队列的变体实现,将暂时获取不到锁的线程加入队列
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的ui列是CLH变体的虚拟双向队列
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。
AQS广泛用于控制并发流程的类,如下图:
其中Sync
是这些类中都有的内部类,其结构如下:
可以看到:Sync
是AQS
的实现。 AQS
主要完成的任务:
同步状态(比如说计数器)的原子性管理;线程的阻塞和解除阻塞;队列的管理。
AQS原理
AQS核心:状态;FIFO队列;抽象或默认定义的方法
状态state:
这里state的具体含义,会根据具体实现类的不同而不同:比如在Semapore里,他表示剩余许可证的数量;在CountDownLatch里,它表示还需要倒数的数量;在ReentrantLock中,state用来表示“锁”的占有情况,包括可重入计数,当state的值为0的时候,标识该Lock不被任何线程所占有。
state是volatile修饰的,并被并发修改,所以修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖于unsafe类。
FIFO队列:
这个队列用来存放“等待的线程,AQS就是“排队管理器”,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来有这个刚刚释放的锁。 AQS会维护一个等待的线程队列,把线程都放到这个队列里,这个队列是双向链表形式。
实现获取/释放等方法:
这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同;
获取方法:获取操作会以来state变量,经常会阻塞(比如获取不到锁的时候)。在Semaphore中,获取就是acquire方法,作用是获取一个许可证;而在CountDownLatch里面,获取就是await方法,作用是等待,直到倒数结束;
释放方法:在Semaphore中,释放就是release方法,作用是释放一个许可证;在CountDownLatch里面,获取就是countDown方法,作用是将倒数的数减一; 需要每个实现类重写tryAcquire和tryRelease等方法。
CAS和AQS关系:
区别:
CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)、新值(B)。,如果内存位置V的值等于预期值A,则更新为B,否则不做操作(Java中添加自旋机制重复尝试)
AQS:一个用于构建锁和同步器的框架:许多同步器都是基于 AQS 构建的。AQS 使一个 volatile
的整数变量 state
来表示同步状态,通过内置的 FIFO
队列来管理等待线程。AQS 的 acquire
操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列中,并阻塞等待。release
操作会释放资源,并唤醒等待队列中的线程。
CAS 和 AQS 两者的联系:
AQS 内部使用 CAS 操作来更新 state
变量,以实现线程安全的状态修改
Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?
Thread类中,有个Thread.ThreadLocalMap成员变量
ThreadLocalMap内部维护了Entry数组,每个entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值
ThreadLocal作用:
线程隔离(线程之间互不影响)、降低耦合度(同一个线程内的多个函数或组件之间减少参数传递)、性能优势(避免线程间同步开销)
ThreadLocal原理: 实现依赖与Thread类中的一个ThreadLocalMap字段(Entry数组),每个线程有自己的ThreadLocalMap,当你创建一个ThreadLocal
变量时,它实际上就是一个ThreadLocal
对象的实例。 每个ThreadLocal
对象都可以存储任意类型的值,这个值对每个线程来说是独立的。
可能存在问题:
当一个线程结束时,ThreadLocalMap也会随之销毁,但是ThreadLocal对象本身不会立即被垃圾回收,知道没有其他引用指向它位置。可能造成内存泄漏
因此,使用时需要显示调用remove()方法,或者正确清理ThreadLocal变量。因为ThreadLocalMap会直须持有ThreadLocal变量的引用
CAS缺点:
ABA问题:比较版本号或时间戳解决
循环时间长开销大:
只能保证一个共享变量的原子操作
为什么不能所有的锁都用CAS?
自旋太久太多吃CPU
voliatle关键字有什么作用?
无法保证原子性
保证可见性:保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取
禁止指令重排:内存屏障保证
读volatile前:不能让后面的操作提前执行;写volatile后:不能让前面的操作延迟执行。
volatile可以保证线程安全吗?
不完全能保证。不能解决多线程并发下的复合操作问题如i++。
非公平锁吞吐量为什么比公平锁大:
公平:线程频繁进行休眠运行状态切换
非公平:先通过CAS
避免死锁:使用资源有序分配法,破坏环路等待条件
线程池
介绍线程池工作原理
为了减少频繁创建线程和销毁线程带来的性能损耗
线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个,如果满了,就加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,按照一些策略处理
**corePoolSize:**线程池核心线程数量,默认线程池中线程数量如果小于等于corePoolSize,即使线程空闲,也不会被销毁
**maximumPoolSize:**线程池最多可容纳的线程数量
**keepAliveTime:**当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
unit: keepAliveTime的时间单位
workQueue:工作队列
threadFactory:线程工厂
handler:拒绝策略
有哪些拒绝策略:
CallerRunPolicy,使用线程池的调用者所在的线程执行被拒绝的任务
AbortPolicy,直接抛出一个任务被线程池拒绝的异常
DiscardPolicy,不做任何处理,静默拒绝
DiscardOldestPolicy,抛弃最老的任务,执行该任务
实现RejectedExecutionHandler接口自定义拒绝策略
线程池参数设置经验
CPU密集型:corePoolSize = CPU核数+1
IO密集型:corePoolSize = CPU核数*2
使用SynchronousQueue
确保任务直达线程,避免队列延迟 拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。
固定线程数避免资源波动,队列缓冲任务,拒绝策略兜底。
配合监控告警(如队列使用率>80%触发扩容)
根据下游RT(响应时间)调整线程数,队列防止瞬时峰值。
自定义拒绝策略将任务暂存Redis,异步重试。
核心线程数设置为0可不可以?
可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。
当核心线程数为 0 时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。
线程池种类:
ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务
FixedThreadPool:固定大小线程池
CachedThreadPool:缓存线程池,它的特点在于线程数是几乎可以无限增加的
SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的
SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程
线程池中shutdown (),shutdownNow()这两个方法有什么作用?
提交给线程池的任务可以被取消
向线程池提交任务时,会得到一个Future
对象。这个Future
对象提供了几种方法来管理任务的执行,包括取消任务。