Skip to content

线程

线程是并发编程的基础

线程池的状态

线程池一共有6种状态

状态名称含义典型场景
NEW (新建)线程已被创建,但尚未启动(未调用start()方法)Thread thread = new Thread();
RUNNABLE (可运行)线程正在JVM中执行或正准备执行,但可能正在等待操作系统分配CPU时间片调用start()后,等待CPU调度或正在运行
BLOCKED (阻塞)线程正在等待获取一个监视器锁(Monitor Lock),以便进入同步代码块或方法竞争synchronized锁时未能立即获取成功
WAITING (等待)线程进入无限期等待,直到被其他线程显式唤醒。此状态不占用CPU时间片调用Object.wait(), Thread.join(), LockSupport.park()
TIMED_WAITING (超时等待)线程进入有时间限制的等待。时间到期后会自动唤醒,或在等待期间被其他线程唤醒调用Thread.sleep(long millis), Object.wait(long timeout), Thread.join(long millis)
TERMINATED (终止)线程已经执行完毕(run()方法正常退出或因异常而终止)run()方法执行结束

img

wait() 和 sleep() ⽅法的区别

  • 来源不同

    sleep() 来⾃ Thread 类,wait() 来⾃ Object 类。

  • 对于同步锁的影响不同

    sleep() 不会该表同步锁的⾏为,如果当前线程持有同步锁,那么 sleep 是不会让线程释放同步锁的。wait() 会释放同步锁,让其他线程进⼊ synchronized 代码块执⾏。

  • 使⽤范围不同

    sleep() 可以在任何地⽅使⽤。wait() 只能在同步控制⽅法或者同步控制块⾥⾯使⽤,否则会抛 IllegalMonitorStateException。

  • 恢复⽅式不同

    两者会暂停当前线程,但是在恢复上不太⼀样。sleep() 在时间到了之后会重新恢复;wait() 则需要其他线程调⽤同⼀对象的 notify()/nofityAll() 才能重新恢复。

线程的 sleep() ⽅法和 yield() ⽅法有什么区别?

线程执⾏ sleep() ⽅法后进⼊超时等待(TIMED_WAITING)状态,⽽执⾏ yield() ⽅法后进⼊就绪(READY)状态。

sleep() ⽅法给其他线程运⾏机会时不考虑线程的优先级,因此会给低优先级的线程运⾏的机会;

yield() ⽅法只会给相同优先级或更⾼优先级的线程以运⾏的机会。

编写多线程程序有⼏种实现⽅式?

  1. 实现thread
  2. 实现runnable
  3. 实现callback
java
var t = new Thread() {
    @Override
    public void run() {
        //....
    }
};
var t2 = new Thread(() -> {
    //....
});
ExecutorService tpe=Executors.newFixedThreadPool(1);
Callable<Integer> c3 = () -> 1;
tpe.submit(c3);

synchronized 和 Lock 的区别

1)Lock 是⼀个接⼝;synchronized 是 Java 中的关键字,synchronized 是内置的语⾔实现;

2)Lock 在发⽣异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使⽤Lock 时需要在 finally 块中释放锁;synchronized 不需要⼿动获取锁和释放锁,在发⽣异常时,会动释放锁,因此不会导致死锁现象发⽣;

3)Lock 的使⽤更加灵活,可以有响应中断、有超时时间等;⽽ synchronized 却不⾏,使⽤synchronized 时,等待的线程会⼀直等待下去,直到获取到锁;

4)在性能上,随着近些年 synchronized 的不断优化,Lock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官⽅推荐尽量使⽤ synchronized,除⾮ synchronized ⽆法满⾜需求时,则可以使⽤ Lock。

为什么说 synchronized 是⼀种悲观锁?乐观锁的实现原理⼜是什么?什么是CAS,它有什么特性?

synchronized 显然是⼀个悲观锁,因为它的并发策略是悲观的:不管是否会产⽣竞争,任何的数据操作都必须加锁,⽤⼾态核⼼态转换,维护锁计数器和检查是否被阻塞的线程需要被唤醒等操作

乐观锁的实现原理:我们可以使⽤基于冲突检测的乐观并发策略。先进⾏操作,如果没有其他线程征⽤数据,那么就操作成功了;

乐观锁的核⼼算法是 CAS(Compareand Swap,⽐较并交换),它涉及到三个操作数:内存值、预期值、新值。并且仅当预期值和内存值相同时才将内存值修改为新值。

CAS 具有原⼦性,它的原⼦性由 CPU 硬件指令实现保证,即使⽤ JNI 调⽤ Native ⽅法调⽤由 C++ 编写的硬件级指令,JDK中提供了 Unsafe 类执⾏这些操作。

synchronized 各种加锁场景的作⽤范围

java
public synchronized void method() {} //当前实例作为对象锁
public static synchronized void method() {} //当前类作为对象锁
synchronized (Lock.class) {} //制定的类作为对象锁
synchronized (this) {} //当前实例作为对象锁,特别注意,this在静态方法中不存在
public static Object monitor = new Object(); //指定实例作为对象锁
synchronized (monitor) {}

死锁

死锁产生

互斥条件:进程对所分配到的资源进⾏排他性控制,即在⼀段时间内某资源仅为⼀个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

占有等待:进程已经获得了⾄少⼀个资源,但⼜对其他资源发出请求,⽽该资源已被其他进程占有,此时该进程的请求被阻塞,但⼜对⾃⼰获得的资源保持不放。

不可被剥夺:进程已获得的资源在未使⽤完毕之前,不可被其他进程强⾏剥夺,只能由⾃⼰释放。

循环等待:存在⼀种进程资源的循环等待链,链中每⼀个进程已获得的资源同时被 链中下⼀个进程所请求。

预防死锁

互斥条件:在系统⾥取消互斥。若资源不被⼀个进程独占使⽤,那么死锁是肯定不会发⽣的。例如:共享数据变成线程独有。StringBuilder代替StringBuffer

占有等待:1)采⽤资源预先分配策略,即进程运⾏前申请全部资源,满⾜则运⾏,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。

不可被剥夺:当进程占有某些资源后⼜进⼀步申请其他资源⽽⽆法满⾜,则该进程必须释放它原来占有的资源。

循环等待:实现资源有序分配策略,将系统的所有资源统⼀编号,所有进程只能采⽤按序号递增的形式申请资源。

Synchronized锁的膨胀升级过程

Synchronized(同步锁)可以根据情况升级锁,以提高性能

image-20250920182059043

同步锁数据主要存储在对象头中image-20250920182221632

偏向锁:在⼤多数情况下,锁不仅不存在多线程竞争,⽽且总是由同⼀线程多次获得,因此为了减少同⼀线程获取锁(会涉及到⼀些CAS操作,耗时)的代价⽽引⼊偏向锁。

轻量级锁:JVM会利⽤CAS尝试把对象原本的Mark Word更新回Lock Record的指针,成功就说明加锁成功,于是改变锁标志位,执⾏相关同步操作。如果失败了,判断当前对象的Mark Word是否指向当前线程的栈帧,如果是就表⽰当前线程已经持有该对象锁。如果不是,说明当前对象锁被其他线程持有,于是进⾏⾃旋。

重量级锁:若cas自选失败,会将锁升级成重量级锁,获取锁必须强制等待才能获取锁。

JUC

AQS

AQS(AbstractQueuedSynchronizer)定义了⼀套多线程访问共享资源的同步器框架,是⼀个依赖状态的同步器。AQS定义了很多并发中的⾏为,⽐如:

• 阻塞等待队列

• 共享/独占

• 公平/⾮公平

• 可重⼊

• 允许中断

ReentrantLock介绍

ReentrantLock是基于AQS框架实现的锁,它类似于Synchronized互斥锁,可以保证线程安全。基于

AQS强⼤的并发特性和处理多线程的能⼒,ReentrantLock相⽐Synchronized,拥有更多的特性,⽐

如⽀持⼿动加锁、解锁,⽀持公平锁等。