tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

    • Java中创建线程的几种方式
    • 并发相关概念与体系图
    • 线程状态与操作系统的用户态、内核态
    • 线程中的声明与守护线程_基础
    • 程序中的幽灵错误_基础
    • JDK并发包
    • 线程池相关
    • 并发中的安全集合
    • 生产者和消费者
    • 玩转单例模式
    • 一些工具类的原理
    • 并发包中的AQS
    • ThreadLocal与JMM
    • 锁的探究
    • 线程顺序执行与等待和通知
    • 线程间通信与等待、通知机制
      • 交替打印-练习
        • 思考与提示
        • Synchronized的版本
        • 一些问题
        • 加final修饰
        • 其他参考-答案
      • 死锁例子-编写
        • 一个例子-常量池
        • 解析与细节
        • 修改后的例子
        • Lock版本
        • 可重入锁
        • 注意事项
        • 适合使用 ReentrantLock 的场景:
        • 锁加final的原因
        • 防止重新赋值
        • 避免空指针异常
        • 内存可见性
    • 线程池与任务调度
  • JVM合集

  • 实战细节与其他

  • 代码之丑与提升

  • 《Java》学习笔记
  • 并发合集
EffectTang
2025-08-23
目录

线程间通信与等待、通知机制

# 线程间通信与等待、通知机制

# 交替打印-练习

编写一个程序,使用两个线程交替打印数字1~100。线程A打印奇数,线程B打印偶数。要求输出顺序为1,2,3,4,...,99,100。

# 思考与提示

2个线程要按照一定顺序执行,就表明彼此之间有一个类似信号的东西,就像红绿灯,红灯停,绿灯行。程序员就是通过锁来实现各个线程的彼此之间的——协做执行,而不是各自执行各自的。一定不要有感觉这个很难的念头,再难的问题也可以分解成多个简单的问题。

  • 使用wait()和notify()方法实现线程间协作。

  • lock和Condition

# Synchronized的版本

public class Bemo {

    int a = 1;
    public static void main(String[] args) {
        Bemo dema = new Bemo();

        Runnable t1 = () -> {
            synchronized (dema){
                while (dema.a<101){
                    if(dema.a % 2 == 1){
                        System.out.println("t1---"+dema.a);
                        dema.a++;
                        dema.notify();
                    }else {
                        try {
                            dema.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }


                }
            }
        };

        Runnable t2 = () -> {

            synchronized (dema){
                while (dema.a<101){
                    if(dema.a % 2 == 0){
                        System.out.println("t2:"+dema.a);
                        dema.a++;
                        dema.notify();
                    }else {
                        try {
                            dema.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        };

        Thread tt1 = new Thread(t1);
        Thread tt2 = new Thread(t2);
        tt1.start();
        tt2.start();

    }
}
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
53

# 一些问题

以上是一个简单的参考,但这里还有一些问题:

  • 【问题】tt1一定比tt2先执行吗?

答案是:tt1 不一定比 tt2 先执行。

原因在于:

  1. tt1.start(); tt2.start(); 只是告诉 JVM “让这两个线程准备运行”,并不保证谁先获得 CPU 执行权。
  2. 线程调度是由操作系统和 JVM 的调度器决定的,无法人为保证顺序,即使你写在 start() 语句的先后顺序里,执行顺序仍然是不可预测的。
  3. 所以,有可能 tt2 先拿到锁,开始打印偶数;也可能 tt1 先拿到锁,开始打印奇数;甚至可能 main 线程在 tt1、tt2 启动后还插队执行一会儿。
  • 【问题】:其中的变量a 是否需要加关键字 final进行修饰?

不需要给 a 加 final!

原因在于 final 在 Java 中表示“引用或值不可重新赋值”,而你这个程序中 a 是一个不断递增的计数器,需要修改,所以不能 final。

你可能是担心在多线程下“可见性”问题,是否需要 volatile 或 final 来保证线程安全?

在你这个程序中:

  • a 的所有读写都在 synchronized(dema) 块中进行;
  • synchronized 已经保证了可见性和互斥:
    • 进入同步块时,会从主内存中读取 a;
    • 离开同步块时,会把修改后的 a 刷回主内存;
    • 所以不用额外加 volatile 或 final。

# 加final修饰

  • 【问题】:为什么有时候编译器会提示,给变量加上final?【值得关注】

比如:局部变量在 Lambda 或匿名内部类中使用

int num = 10;
Runnable r = () -> {
    num++; // ❌ 编译错误
};

1
2
3
4
5

为什么要使用final修饰呢。

核心原因是:匿名内部类或 Lambda 捕获的局部变量,本质上是把它复制一份到内部类对象里,而不是直接引用原来的栈变量。为了避免歧义,Java 要求这个变量是 final 或“effectively final”。

以下是它的执行过程:

int x = 10;
new Thread(new Runnable() {
    public void run() {
        System.out.println(x);
    }
}).start();

1
2
3
4
5
6
7

x 是在 main 方法里的一个 局部变量,存储在 栈帧 中。

new Runnable() {...} 会生成一个匿名内部类对象,这个对象在 堆 中。

当你在匿名类中访问 x 时,Java 编译器其实做了 “值捕获”:

  • 它会把 x 的值拷贝到内部类对象中,作为一个 隐藏字段。
  • 执行 System.out.println(x); 时,其实访问的是对象里的副本。

假设允许你在匿名类外部修改 x:

int x = 10;
Runnable r = new Runnable() {
    public void run() {
        System.out.println(x);
    }
};
x = 20; // 修改了 x
r.run(); // 输出啥?

1
2
3
4
5
6
7
8
9

这里会产生歧义:

  • 线程 run() 时访问的 x,是修改前的副本还是修改后的新值?
  • 因为局部变量 x 是栈上的,生命周期跟方法调用结束绑定,但 Runnable 对象可能在方法退出后还存活并运行,这样就可能访问到一个已经销毁的栈变量。

为了避免这种混乱,Java 的设计选择了:

  • 不允许捕获可变的局部变量。
  • 要么明确 final,要么“effectively final”(即从未修改)。

对多线程共享变量来说,final 只保证引用不变,不保证线程安全。

final 更常见用途:

  • 修饰变量:值或引用不变;
  • 修饰方法:方法不能被子类重写;
  • 修饰类:类不能被继承。

# 其他参考-答案

import java.util.concurrent.atomic.AtomicInteger;

public class CounterExample {
    // 方法1:使用synchronized
    private static int counter1 = 0;
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 方法1:synchronized
        Thread[] threads1 = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads1[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    synchronized (lock) {
                        counter1++;
                    }
                }
            });
            threads1[i].start();
        }
        for (Thread t : threads1) t.join();
        System.out.println("counter1: " + counter1); // 应输出100000

        // 方法2:使用AtomicInteger
        AtomicInteger counter2 = new AtomicInteger(0);
        Thread[] threads2 = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads2[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter2.incrementAndGet(); // 原子操作
                }
            });
            threads2[i].start();
        }
        for (Thread t : threads2) t.join();
        System.out.println("counter2: " + counter2.get()); // 应输出100000
    }
}
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

# 死锁例子-编写

什么叫死锁,死锁是指两个或多个线程在执行过程中,因争夺共享资源而相互等待,导致这些线程都无法继续执行下去的现象。

简单说,线程 A 等线程 B 释放资源,线程 B 又等线程 A 释放资源,结果谁也不让谁,程序卡住了。

请用代码模拟一个死锁例子:

# 一个例子-常量池

public class Cemo {

    Integer a = 0;
    Integer b = 0;

    public static void main(String[] args) {

        Cemo cemo = new Cemo();

        Thread t1 = new Thread(() -> {
            synchronized (cemo.a){
                System.out.println("t1-----获取 aaaa");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (cemo.b){
                    System.out.println("t1-----获取 bbb");

                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (cemo.b){
                System.out.println("t2-----获取 bbb");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (cemo.a){
                    System.out.println("t2-----获取 aaa");

                }
            }
        });

        t1.start();
        t2.start();
    }
}

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

# 解析与细节

上述例子,考虑执行顺序:

  1. 如果t1先运行,它会获取a的锁,然后睡眠。
  2. 在t1睡眠期间,t2运行,获取b的锁,然后睡眠。
  3. t1醒来后尝试获取b的锁,但b的锁已经被t2持有,所以t1等待。
  4. t2醒来后尝试获取a的锁,但a的锁已经被t1持有,所以t2等待。

此时,t1持有a等待b,t2持有b等待a,形成循环等待,因此会发生死锁。

但是注意:这里使用的锁是Integer对象,而Integer是 immutable(不可变)的,并且有缓存机制(-128到127之间的整数会被缓存)。

由于a和b都是0,所以它们实际上是同一个缓存对象(即cemo.a和cemo.b是同一个对象,因为0在缓存范围内)。

所以,实际上只有一个锁对象(Integer.valueOf(0)),因此不会死锁。

如果a和b的值不同(比如一个0,一个1),那么就是两个不同的锁对象,就会发生死锁。

# 修改后的例子

Integer a = 0;
Integer b = 1;
// 需要的改动
1
2
3

另外,即使a和b都是0,如果我们使用new Integer(0)来创建,那么也会是两个不同的对象,也会死锁。

此外注意,自动装箱使用的是Integer.valueOf(),对于0会返回缓存对象。

注意:字符串,String也是有常量池的,如果

String a = "123"; String b = "123";

也是不会发生死锁的,因为是同一个对象。

# Lock版本

下面使用的锁是可重入锁,可重入锁(ReentrantLock)是 Java 并发包 (java.util.concurrent.locks) 中提供的一种高级线程同步机制,它比传统的 synchronized 关键字更灵活、功能更强大。

# 可重入锁

可重入锁是一种允许同一个线程多次获取同一把锁的同步机制。当一个线程持有锁时,它可以再次获取该锁而不会被阻塞,这就是"可重入"的含义。

public static void main(String[] args) {

        Lock lock1 = new ReentrantLock();
        Lock lock2 = new ReentrantLock();

        Runnable t1 = () -> {
            lock1.lock();
            System.out.println("t1 ------ lock111");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lock2.lock();
            System.out.println("t1 ------ lock222");
            lock2.unlock();
            lock1.unlock();

        };

        Runnable t2 = () -> {
            lock2.lock();
            System.out.println("t2 ------ lock222");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            lock1.lock();
            System.out.println("t2 ------ lock111");
            lock1.unlock();
            lock2.unlock();

        };

        Thread t3 = new Thread(t1);
        Thread t4 = new Thread(t2);
        t3.start();
        t4.start();

    }
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

# 注意事项

在获取锁之后,一定要注意释放,否则会导致死锁,推荐使用finally。

public class ReentrancyDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void outer() {
        lock.lock();
        try {
            System.out.println("外层方法获取锁");
            inner(); // 调用内层方法
        } finally {
            lock.unlock();
        }
    }
    
    public void inner() {
        lock.lock(); // 同一个线程可以再次获取锁
        try {
            System.out.println("内层方法获取锁,锁计数: " + lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        ReentrancyDemo demo = new ReentrancyDemo();
        demo.outer();
    }
}
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

当然,ReentrantLock 的高级特性也是synchronized所不具备的。

  • 尝试获取锁
public class TryLockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void tryLockMethod() {
        // 尝试获取锁,立即返回结果
        if (lock.tryLock()) {
            try {
                System.out.println("获取锁成功,执行任务");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("获取锁失败,执行其他操作");
        }
    }
    
    public void tryLockWithTimeout() {
        try {
            // 尝试在指定时间内获取锁
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    System.out.println("在2秒内获取锁成功");
                    Thread.sleep(1000);
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("在2秒内获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
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
  • 公平锁与非公平锁
public class FairLockDemo {
    // 创建公平锁
    private final ReentrantLock fairLock = new ReentrantLock(true);
    // 创建非公平锁(默认)
    private final ReentrantLock nonFairLock = new ReentrantLock(false);
    
    public void fairLockMethod() {
        fairLock.lock();
        try {
            System.out.println("公平锁: " + Thread.currentThread().getName());
        } finally {
            fairLock.unlock();
        }
    }
    
    public void nonFairLockMethod() {
        nonFairLock.lock();
        try {
            System.out.println("非公平锁: " + Thread.currentThread().getName());
        } finally {
            nonFairLock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 适合使用 ReentrantLock 的场景:

  1. 需要尝试获取锁:使用 tryLock() 避免死锁
  2. 需要可中断的锁获取:使用 lockInterruptibly()
  3. 需要公平性:要求线程按申请顺序获取锁
  4. 需要多个条件变量:使用多个 Condition 实现精细等待/通知
  5. 需要知道锁的状态:使用 isLocked(), getHoldCount() 等方法

# 锁加final的原因

在 Java 并发编程中,将 Lock 和 Condition 声明为 final 是一种重要的最佳实践,这主要是出于线程安全、内存可见性和代码可靠性的考虑。

# 防止重新赋值

final 关键字确保引用不会被重新指向另一个对象,这避免了在多线程环境下可能出现的竞态条件:

// 不安全:锁引用可能被改变
private ReentrantLock lock = new ReentrantLock();

public void unsafeMethod() {
    // 可能被另一个线程改变锁引用
    lock.lock();
    try {
        // 临界区代码
    } finally {
        lock.unlock(); // 可能解锁的是不同的锁对象!
    }
}

// 线程可能执行这样的代码
public void changeLock() {
    lock = new ReentrantLock(); // 改变了锁引用
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果锁引用不是 final 的,一个线程可能在另一个线程获取锁后但释放锁前改变锁引用,导致线程尝试释放一个不同的锁对象。

# 避免空指针异常

final 字段必须在构造过程中初始化,这避免了空指针异常:

public class Container {
    private final ReentrantLock lock; // 必须在构造函数中初始化
    
    public Container() {
        lock = new ReentrantLock(); // 必须初始化
    }
    
    // 如果没有final,可能会忘记初始化
}
1
2
3
4
5
6
7
8
9

# 内存可见性

非 final 字段不能保证在不同线程间的可见性:

public class VisibilityIssue {
    private ReentrantLock lock; // 非final
    
    public void init() {
        lock = new ReentrantLock(); // 初始化
    }
    
    public void useLock() {
        // 其他线程可能看不到初始化后的lock,看到的是null
        lock.lock(); // 可能抛出NullPointerException
        try {
            // ...
        } finally {
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在 Java 内存模型中,final 字段的初始化保证对所有线程立即可见,无需额外的同步。

上次更新: 2025/08/31, 14:50:15
线程顺序执行与等待和通知
线程池与任务调度

← 线程顺序执行与等待和通知 线程池与任务调度→

最近更新
01
Spring中Bean的生命周期
09-03
02
数据不丢失与准确类
09-01
03
线程池与任务调度
08-31
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式