并发编程三大特性

原子性

原子性的定义:原子性是指一个操作(多条指令)是不可分割的。 在一个线程在执行某一段指令时,其他的线程如果也想执行,需要等待前一个线程执行完毕后才能执行。

例如 i++,在代码中只有一行代码,看着是只有一个操作,但是对有CPU来说这是三个操作,1. 将i取出放入cpu,2.对i进行加1操作 3.将结果返回


原子性可以解决线程安全问题。在多个线程在同时对一个共享资源(共享变量)进行操作时,出现的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CompanyTest {

private static int count;

// 如果方法不追加synchronized,会导致200次++操作结束后,结果不是200
// 如果方法追加上了synchronized,200次++的操作结束后,结果就是预期的200了。
@SneakyThrows
public static synchronized void increment() {
TimeUnit.MILLISECONDS.sleep(100);
count++;
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
//等待线程结束
t1.join();
t2.join();
System.out.println(count);
}
}

可见性

在多线程环境下,每个线程可能会将变量缓存到自己的工作内存(即 CPU 缓存)中,而不是直接从主内存读取和写入。

线程 A 修改了变量 x,但线程 B 仍然看到旧值,因为它从自己的工作内存中读到了旧的副本,而不是主内存中的最新值。

实现可见性的三种方式:
1.volatile关键字
volatile关键字是专门用来实现可见性的,不加volatile则永远不会输出 t1线程结束!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(flag){

}
System.out.println("t1线程结束!");
});
t1.start();
Thread.sleep(100);
flag = false;
System.out.println("main线程将flag改为false");
}

  1. synchronized 重量级锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    while(flag){
    // 这里的println操作中,涉及到了synchronized操作,间接实现了可见性
    System.out.println(1);
    }
    System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
    }
  2. lock (lock的底层是volatile)

有序性

有序性是为了防止多线程中的指令重排序造成的不一样的结果

什么是指令重排序

  1. 指令重排序是指编译器或处理器为了提升性能,在不改变单线程语义的前提下,重新调整指令的执行顺序。
1
2
3
int a = 1;
int b = 2;
x = a + b;

即使你写的顺序是 a→b→x,编译器或 CPU 可能会先执行 b = 2,然后再执行 a = 1,只要单线程运行的结果是正确的,这种调整就是允许的。

  1. 指令重排序的三种类型:
类型描述
编译器重排序编译器在生成字节码时进行的重排序
JVM 重排序JVM 将字节码翻译为机器码时进行的重排序
CPU 重排序CPU 在执行时进行的乱序执行和写缓冲等优化
  1. 指令重排序的危害
    在单线程中,重排序是无害的。但在多线程环境下,如果没有适当的同步机制,指令重排序会导致难以察觉的并发错误。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //双重检查锁(DCL)中的重排序问题
    class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    //第二次判断是为了防止以下情况:A线程拿到锁了,并且new了对象,而B线程此时正好在 synchronized 获取锁这一行代码,A释放锁后,B会获取到锁,如果不加第二次判断,B线程也会new一个对象
    if (instance == null) {
    instance = new Singleton(); // 这行会被重排序!
    }
    }
    }
    return instance;
    }
    }

instance = new Singleton() 在JVM和CPU中分为以下三步

  1. 分配内存
  2. 将引用赋值给 instance
  3. 调用构造函数初始化对象

这三个操作是可能出现指令重排序的情况,可能就会造成,test != null,但是还没用执行第二步的初始化属性,导致其他线程拿着一个还未初始化完成的,或者说一个半成品对象去操作,这会带来一些线程安全的问题。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 线程 A
if (instance == null) {
instance = new Singleton(); // 被重排序
}

// 线程 B
if (instance != null) {
use(instance); // ⚠️可能访问未初始化的对象!
}
// 发生指令重排序后
线程 A:
1. 分配内存
2. 调用构造函数初始化对象 // instance ≠ null,但对象没构造好
3. 将引用赋值给 instance; // 构造函数稍后才执行

线程 B:
看到 instance ≠ null,立刻使用它

new一个新线程的方法

1. 继承 Thread 类

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程
}
}

2.实现 Runnable 接口

需要重写run方法

1
2
3
4
5
6
7
8
9
10
11
12
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
MyRunnable task = new MyRunnable(); // 创建任务
Thread thread = new Thread(task); // 创建线程
thread.start(); // 启动线程
}
}

  • 用匿名内部类简化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class RunnableDemo {
    public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("线程运行中:" + Thread.currentThread().getName());
    }
    });
    thread.start();
    }
    }

    3. 使用 Callable 接口 + FutureTask

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    return "线程返回结果:" + Thread.currentThread().getName();
    }

    public static void main(String[] args) throws Exception {
    FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    Thread t1 = new Thread(futureTask);
    t1.start();

    // 获取线程执行结果
    String result = futureTask.get();
    System.out.println("线程执行结果:" + result);
    }
    }

    线程中断

    线程中断(Thread Interruption)是 Java 提供的一种优雅终止线程的机制,它并不会强制终止线程,而是向线程发送一个“中断请求”信号,由线程自己决定何时响应、如何响应。

核心方法:

方法作用
interrupt()向目标线程发出中断请求
isInterrupted()判断线程是否被中断(不清除中断状态)
interrupted()(静态方法)判断当前线程是否中断,同时清除中断状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ThreadInterruptExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个线程任务
Runnable task = () -> {
try {
System.out.println("线程启动:" + Thread.currentThread().getName());
for (int i = 1; i <= 10; i++) {
System.out.println("执行任务:" + i);

// 模拟耗时操作,支持响应中断
Thread.sleep(1000);

// 可选:额外判断中断状态(即使 sleep 不被中断)
if (Thread.currentThread().isInterrupted()) {
System.out.println("检测到中断状态,准备退出...");
break;
}
}
} catch (InterruptedException e) {
// 如果 sleep 被中断,会进入此处
System.out.println("线程在休眠中被中断!");
} finally {
System.out.println("线程资源清理完毕,退出!");
}
};

// 启动线程
Thread worker = new Thread(task, "工作线程");
worker.start();

// 主线程等待 3 秒后中断子线程
Thread.sleep(3000);
System.out.println("主线程发送中断信号...");
worker.interrupt(); // 向 worker 发出中断请求
}
}

这个例子展示了一个线程中断的情况,当主线程执行了 worker.interrupt(),就会将 Thread.currentThread().isInterrupted() 设置为true,这样可以进行判断手动停止线程,如果该线程没有sleep,wait等阻塞的话,进行线程中断的话,不会有任何影响,直到该线程执行到代码 Thread.currentThread().isInterrupted()的时候会为true,可手动停止代码,如果线程处于sleep,wait的阻塞状态会立刻抛出异常

锁的分类

悲观锁&乐观锁

Java中的悲观锁,比如synchronized,ReentrantLock,ReentrantReadWriteLock。

Java中的乐观锁,采用的CAS操作,CompareAndSwap(比较和交换),CAS是基于CPU原语实现的。

悲观锁: 悲观锁在获取不到锁资源后,会将当前线程挂起(BLOCKED,WAITING,TIMED_WAITING),线程挂起这个事情,不是JVM层面能解决的问题。需要操作系统来完成这个事情。那就需要涉及到用户态和内核态之间的切换,这种切换,会影响一定效率。

乐观锁: 乐观锁不涉及线程挂起,不涉及用户态和内核态之间的切换。如果长时间执行乐观锁的机制,但是一直无法成功,会浪费一些CPU性能,都在做尝试,但是一直没成功。

如果可以,最好是在尝试几次就能成功的场景,使用乐观锁实现。如果这个锁资源获取需要一定的时间,最好使用悲观锁。

公平锁&&非公平锁

公平锁:用于在多线程环境中控制资源的访问顺序。它的核心目的是保证线程获取锁的顺序是公平的,也就是说,先请求锁的线程应当先获得锁,避免线程“饿死”。
非公平锁:是一种允许线程在任何时刻尝试获取锁的机制,不保证先请求的线程一定先获得锁。这可能会导致某些线程“插队”,从而提高整体性能

在Java中,synchronized只支持非公平锁。

ReentrantLock和ReentrantReadWriteLock既支持公平锁也支持非公平锁,可以通过有参构造传递boolean的值类决定是否公平(默认非公平锁)。

1
2
3
4
5
// 默认不传递,fair是false,new的是NonfairSync,如果主动传递true作为参数,构建FairSync作为公平锁的实现
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

可重入锁&&不可重入锁

  1. 可重入锁:同一个线程在外层方法获取锁之后,能在内层方法中自动获得锁的代码执行权。**不会因为再次获取锁而被阻塞
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class ReentrantExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
    lock.lock();
    try {
    System.out.println("outer");
    inner(); // 同一个线程再次加锁
    } finally {
    lock.unlock();
    }
    }

    public void inner() {
    lock.lock(); // 如果是不可重入锁,这里会死锁
    try {
    System.out.println("inner");
    } finally {
    lock.unlock();
    }
    }

    public static void main(String[] args) {
    new ReentrantExample().outer();
    }
    }

    可重入锁中会有一个计数器,记录了获取锁的次数,获取几次锁就需要释放几次锁 Java 中的 synchronized 和 ReentrantLock 都是可重入锁

  2. 不可重入锁:同一个线程如果尝试再次获取自己已经持有的锁,会被阻塞或死锁。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Lock lock = new NonReentrantLock();

    void outer() {
    lock.lock();
    inner(); // 会死锁
    lock.unlock();
    }

    void inner() {
    lock.lock(); // 死锁,因为当前线程已经持有锁
    lock.unlock();
    }
特性可重入锁 (synchronized, ReentrantLock)不可重入锁(需要自己实现)
重入能力支持不支持
死锁风险低(递归调用安全)高(递归调用可能死锁)
实现复杂度较高,需维护线程状态和计数器简单
典型应用synchronizedReentrantLock自定义锁、嵌入式系统

互斥锁&共享锁

  1. 互斥锁:同一时间只允许一个线程/事务持有该锁并访问资源,其他线程必须等待锁释放才能继续。也称写锁
    特点:

    • 也叫 写锁(Write Lock)

    • 独占访问:一个线程获取后,其它线程必须阻塞

    • 适用于修改资源的场景,确保数据一致性

      1
      2
      3
      4
      5
      6
      7
      8
      ReentrantLock lock = new ReentrantLock();

      lock.lock();
      try {
      // 修改资源(独占访问)
      } finally {
      lock.unlock();
      }
      1
      2
      SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
      //FOR UPDATE 是一种 互斥锁(排它锁),阻止其他事务修改或读取该记录(视隔离级别而定)。
  2. 共享锁:允许多个线程/事务同时持有锁来读取资源,但不允许写入。
    特点:

    • 也叫 读锁(Read Lock)

    • 多个线程可并发读取,但不能写入

    • 旦某线程申请了写锁,读写都需等待写锁释放

1
2
3
4
5
6
7
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
// 并发读取资源
} finally {
lock.readLock().unlock();
}
1
2
3
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;

//LOCK IN SHARE MODE 是共享锁,多个事务可读取,但不能修改该行。
特性互斥锁(写锁)共享锁(读锁)
是否独占✅ 是❌ 否(可共享)
读操作❌ 阻塞其他线程读取✅ 允许多个线程读取
写操作✅ 允许写❌ 禁止写
典型用途数据写入、修改数据读取
性能较低(锁粒度粗)较高(读多写少场景)

CAS

CAS 是一种无锁(lock-free)并发编程技术,用于实现线程安全的操作。它是 CPU 层面支持的原子操作指令,Java 在底层通过调用它来实现原子变量更新(如 AtomicInteger、AtomicReference 等)。

  1. 为什么称CAS是一种无锁的并发编程

    CAS 更像是一种编程思想,核心思想是:
    当且仅当预期值与内存中的当前值一致时,才更新为新值。否则什么都不做。
    这是一种乐观锁策略:认为并发很少发生,所以先尝试修改,失败了再重试。

    假设我们有一个变量 V,预期值为 E,新值为 N,那么:
    1
    2
    3
    4
    5
    6
    if (V == E) {
    V = N;
    return true;
    } else {
    return false;
    }
    如果变量当前值和期望值相等,就将其更新为新值;

否则,表示有其他线程已经修改了它,更新失败。

  1. CAS的优点
优点描述
✅ 无锁机制避免传统锁带来的上下文切换开销,提高并发性能
✅ 原子性强硬件保证,天然线程安全
✅ 效率高没有加锁阻塞,线程不会被挂起
  1. CAS的缺点

❌ 1. ABA 问题
A 线程看到变量从 A → B → A,CAS 会认为没有变化,但实际上已经变化过;

解决办法:使用 AtomicStampedReference 或 AtomicMarkableReference,引入版本号或标记位。

❌ 2. 自旋消耗 CPU(会进行while操作)
在高并发冲突严重时,线程可能会不断地自旋重试,浪费 CPU。

❌ 3. 只能操作一个变量
不能实现多个变量的原子更新;

解决办法:使用加锁或 AtomicReferenceFieldUpdater 等更复杂的机制。

  1. Java中提供了一个类,Unsafe类,Unsafe中提供了CAS的操作方法

synchronized

synchronized的锁是基于对象实现的。在Java中,Object类中提供了wait和notify之类的,做一个锁的操作。而Java中所有的类都是继承Object的,所以所有对象都可以作为一把锁。

synchronized的使用

  1. 对象锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class LockTest {

    public static int count = 0;

    /**
    * 这个synchronized方法是基于LockTest的对象,作为一把锁
    */
    public synchronized void increment(){
    LockTest.count++;
    }


    /**
    * 此时调用test.increment()方法时,底层使用的就是test对象作为锁。
    * @param args
    */
    public static void main(String[] args) {
    LockTest test = new LockTest();
    test.increment();
    }
    }
  2. 类锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class LockTest {

    public static int count = 0;

    /**
    * 这个synchronized的static方法是基于LockTest.class,作为一把锁
    */
    public static synchronized void increment(){
    LockTest.count++;
    }


    /**
    * 此时这种方式,就是基于LockTest.class作为锁,全局就一把
    * @param args
    */
    public static void main(String[] args) {
    LockTest.increment();
    LockTest.increment();
    }
    }

    static 强调的是“附属于类”,适合设计那些无需实例就能使用、且由所有对象共享的成员;例如例子中的 public static int count = 0,这个count在类加载的时候就会存在了,所有的实例化对象(例如 LockTest test = new LockTest())都能访问这个count,而不是每个对象都有一个属于自己的count,static用来修饰方法也是一样的

  3. 同步代码块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class LockTest {

    public static int count = 0;

    /**
    * synchronized (对象),此时这个对象就是当前锁资源
    */
    public static void increment(){
    synchronized (LockTest.class){
    count++;
    }
    }

    /**
    * synchronized (对象),此时这个对象就是当前锁资源
    */
    public static void decrement(){
    synchronized (LockTest.class){
    count--;
    }
    }
    }

    synchronized的优化

  4. 锁消除:锁消除就是编译器的一个优化的技术(JIT),用于消除不必要的同步锁操作,提升程序的执行效率。
  5. 锁膨胀/粗化:锁膨胀也是编译器JIT做的一个优化手段,用于扩大锁的范围,从而避免频繁的加锁释放锁操作,从而提升程序的性能。
  6. 锁升级:锁升级是指 随着锁竞争的加剧,JVM 自动将锁从低级别升级到高级别
    1
    无锁 → 偏向锁 → 轻量级锁 → 重量级锁

    锁升级是 不可逆的,一旦升级,不能降级。例如,一旦进入重量级锁,就不会退回轻量级锁。

1.无锁(No Lock)

对象没有被任何线程访问。

普通的非同步对象处于此状态。

  1. 偏向锁

当只有一个线程访问对象同步块时,为了减少无竞争情况下的开销,会将锁偏向某个线程。

适用于线程偏向性强的场景(即,大多数时间都是一个线程访问该对象)。

  1. 轻量级锁

有多个线程交替访问(无实际竞争),通过使用 CAS 操作避免重量级的线程阻塞和唤醒。

在同步块进入前尝试使用 CAS 抢占锁。

  1. 重量级锁

多个线程同时访问某个对象时发生了实际竞争,系统不得不使用操作系统的互斥量机制。

会造成线程阻塞,性能开销较大。

升级路径触发条件说明
无锁 → 偏向锁对象第一次被线程访问,JVM 开启偏向锁,且没有竞争
偏向锁 → 轻量级锁另一个线程尝试访问被偏向的对象(产生潜在竞争)
轻量级锁 → 重量级锁多线程并发争用同一锁对象,CAS 自旋失败

ReentrantLock

ReentrantLock 也是互斥锁

ReentrantLock在加锁方面就提供了4中方式:

lock()死等的方式
lockInterruptibly()死等的方式,但是在死等时,允许中断
tryLock()蜻蜓点水,尝试一下,拿到返回true,没拿到返回false。
tryLock(time,unit)尝试time.unit时间,拿到返回true,没拿到返回false,同时在time.unit时间内,允许中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static void xx() {
// lock.lock() -> 排队拿锁,拿不到死等。等到拿到锁。
lock.lock();
try {
System.out.println("我是业务逻辑");
int i = 1 / 0;
} finally {
lock.unlock();
}
// ==================================================================
try {
// lock.lockInterruptibly() -> 允许被中断的拿锁方式 interrupt
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
try {
System.out.println("我是业务逻辑");
int i = 1 / 0;
} finally {
lock.unlock();
}
// ==================================================================
// lock.tryLock() -> 尝试一下拿锁,拿到执行业务,拿不到,返回false
boolean b1 = lock.tryLock();
if (b1) {
try {
System.out.println("我是业务逻辑");
int i = 1 / 0;
} finally {
lock.unlock();
}
}
// ==================================================================
boolean b2 = false;
try {
// lock.tryLock(2, TimeUnit.SECONDS); -> 尝试2s,拿锁,拿到返回true。没拿到返回false,在2s内,如果被中断了,就抛出异常
b2 = lock.tryLock(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
if (b2) {
try {
System.out.println("我是业务逻辑");
int i = 1 / 0;
} finally {
lock.unlock();
}
}
}

公平锁&非公平锁

非公平锁(默认):多个线程抢锁,没有顺序,性能高;

公平锁(可选):按照请求顺序排队,防止“饥饿”,但性能稍差。

1
2
Lock fairLock = new ReentrantLock(true);  // 公平锁
Lock unfairLock = new ReentrantLock(); // 非公平锁

  • 非公平锁的加锁流程:
  1. 调用 lock() 方法
  2. 先尝试直接 CAS 修改 state = 0 → 1
    • 成功:抢锁成功,设置当前线程为持有者,直接进入临界区
    • 失败:进入 AQS.acquire()
  3. tryAcquire() 再尝试获取锁:
    • state == 0:再直接 CAS 抢锁(允许插队)
    • 否则如果当前线程是持有锁线程 → 支持重入 → state++
    • 否则 → 抢锁失败,准备排队
  4. addWaiter(Node.EXCLUSIVE)
    • 把当前线程封装成 Node 加入 AQS 队列尾部
  5. acquireQueued()
    • 若自己是 head.next → 继续尝试抢锁
    • 否则挂起(LockSupport.park)
  6. 等待持有锁的线程 unlock(),调用 unparkSuccessor() 唤醒下一个
  • 公平锁的加锁流程:
  1. 调用 lock() 方法
  2. 直接调用 AQS.acquire()
  3. tryAcquire()
    • state == 0 && !hasQueuedPredecessors():
      → 若前面没有排队线程 → 尝试 CAS 抢锁
    • 否则:
      • 当前线程是否是持有者? → 是:重入
      • 否:抢锁失败,准备排队
  4. addWaiter(Node.EXCLUSIVE)
    • 把当前线程封装成 Node 加入 AQS 队列尾部
  5. acquireQueued()
    • 若自己是 head.next → 再次尝试 tryAcquire()
    • 否则挂起(LockSupport.park)
  6. 等待前驱线程释放锁,unpark 唤醒当前线程
  • 流程对比:
步骤非公平锁公平锁
1lock() → 先直接 CAS 抢锁lock() → 直接进入 acquire()
2tryAcquire() 中直接 CAS 抢锁tryAcquire() 中先判断是否排队再抢锁
3插队?✔️ 支持❌ 不支持,必须排队
4加入 AQS 队列:Node(线程)加入 AQS 队列:Node(线程)
5是 head.next?尝试获取锁是 head.next?判断排队情况再尝试获取锁
6否则 LockSupport.park 阻塞线程否则 LockSupport.park 阻塞线程
7唤醒下一个等待节点唤醒下一个等待节点
项目非公平锁公平锁
lock() 方法先尝试直接 CAS 抢锁,失败才排队直接调用 acquire(),不抢锁
tryAcquire() 方法中逻辑state == 0 → 直接 CAS 抢锁state == 0 → 先检查队列是否有前驱再决定是否抢锁
抢锁行为不管队列有没有线程,优先尝试抢锁若前面有等待线程,自己不能插队
性能更快,但可能导致线程饥饿更公平,线程获取锁的顺序更可控,但吞吐量略低

ReentrantLock 与 synchronized 的区别

对比项synchronizedReentrantLock
锁机制类型隐式锁,基于 JVM显式锁,由程序员控制
解锁方式自动释放(方法/代码块执行完)需手动 unlock(),否则可能死锁
是否可中断不可中断可通过 lockInterruptibly() 实现中断响应
是否可限时尝试加锁不支持支持 tryLock(timeout) 设定等待时间
是否公平锁不支持,默认非公平支持公平锁和非公平锁构造方式
性能表现较轻量,适合简单同步场景功能强大,适合复杂并发控制

线程池

线程池通过重复利用固定数量的线程来执行大量任务,可以大幅降低线程创建和销毁的开销,提升系统性能和响应速度。

线程池的核心参数

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程空闲存活时间
TimeUnit unit, // 上面时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
  1. corePoolSize(核心线程数)
    指线程池中保留的最小线程数。

即使这些线程处于空闲状态,也不会被回收,始终保留。

当有任务来时,先用核心线程执行。

  1. maximumPoolSize(最大线程数)
    线程池中允许存在的最大线程数量。

当任务太多,队列满了,就会创建非核心线程来执行任务,直到达到最大线程数。

超过该值,就会触发拒绝策略。

  1. ③ keepAliveTime(非核心线程空闲存活时间)
    非核心线程在空闲状态下最多保持多长时间不被销毁。

超过这个时间未被使用,线程会被自动回收。

⚠️ 注意:

默认情况下只对非核心线程有效;
但可以使用 allowCoreThreadTimeOut(true) 让核心线程也超时回收。

  1. ④ unit(时间单位)
    keepAliveTime 的时间单位,使用 TimeUnit 枚举,如:
  • TimeUnit.SECONDS
  • TimeUnit.MILLISECONDS 等
  1. ⑤ workQueue(任务队列)
    存放提交但尚未执行的任务。
    队列种类影响线程池的行为模型。
队列类型特点说明
ArrayBlockingQueue有界阻塞队列,FIFO,常用于限制任务数量
LinkedBlockingQueue无界队列,任务会无限堆积,容易 OOM(⚠️默认Executors使用)
SynchronousQueue直接交接,不存储任务(用于 CachedThreadPool
PriorityBlockingQueue支持任务优先级排序
  1. ⑥ threadFactory(线程工厂)
    用于创建线程,可以自定义线程名称、是否守护线程、线程组等。

便于线程监控和调试。

默认实现是 Executors.defaultThreadFactory(),你也可以自定义:

1
2
3
4
5
new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r, "my-thread-" + count.incrementAndGet());
}
}

⑦ RejectedExecutionHandler(拒绝策略)
当线程池 线程数达到最大,任务队列满时,执行失败任务的处理策略。

四种内置策略:

策略类行为说明
AbortPolicy(默认)抛出 RejectedExecutionException 异常
CallerRunsPolicy谁提交任务,谁就自己去执行(同步)
DiscardPolicy直接丢弃任务,不抛异常
DiscardOldestPolicy丢弃队列中最老的任务,尝试提交当前任务

线程池的简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.*;

public class ThreadPoolExample {

public static void main(String[] args) {

// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 非核心线程空闲时间
TimeUnit.SECONDS, // 空闲时间单位
new LinkedBlockingQueue<>(2), // 任务队列(最多排队2个任务)
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

// 提交5个任务
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() +
" 执行任务:" + taskId);
try {
Thread.sleep(2000); // 模拟任务执行耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 关闭线程池(执行完任务再关闭)
executor.shutdown();
}
}

🧾 解释说明:
核心线程:2 个 → 最多同时有 2 个线程在工作

队列容量:2 → 除了正在执行的任务外,还能再缓冲两个任务

最大线程:4 → 如果队列满了,最多还能额外启动 2 个线程

提交 5 个任务:

  • 任务 1 和 2 → 被核心线程直接执行

  • 任务 3 和 4 → 被放入等待队列

  • 任务 5 → 队列已满,创建新线程执行

超过最大线程 + 队列容量时(第 7 个以上任务)→ 触发拒绝策略

submit() 和 execute() 的区别

submit()可以获取到现成的返回值,execute() 不能获取到线程的返回值

方法名返回值是否能获取任务执行结果是否能捕获异常
execute()void❌ 不能获取❌ 无法捕获异常(除非 try-catch)
submit()Future<?>✅ 可以获取✅ 能捕获异常通过 Future.get() 抛出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.*;

public class SubmitExample2 {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();

Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});

// 获取任务返回值
Integer result = future.get(); // 会阻塞直到任务完成
System.out.println("任务返回结果:" + result);

executor.shutdown();
}
}

结束语

多线程的知识先到此结束,其实很早之前学过一遍了,但是忘的一干二净了,所以来复习一遍,其实还有很多多线程的知识没有学完,等后面有时间了或者遇到没见过的多线程问题会继续补充