1. synchronized 关键字
- 基本用法和特点: synchronized 用于实现线程同步,保证同一时刻只有一个线程能访问被同步的代码块或方法。它可修饰方法和代码块,如 synchronized (锁对象) { } ,锁对象可以是任意对象。
- 可重入性:指的是同一个线程在持有锁的情况下,再次进入被该锁同步的代码块或方法是允许的。例如,一个递归方法被 synchronized 修饰,在递归调用过程中,线程不需要重新获取锁。
- 死锁问题
- 死锁场景举例
- 一个线程连续两次尝试获取同一把锁,会导致死锁。
- 两个线程分别持有一把锁,并且都尝试获取对方持有的锁,比如线程1持有锁a,尝试获取锁b;线程2持有锁b,尝试获取锁a ,就会陷入死锁。
- M 个线程竞争 N 把锁( M > N ),也可能出现死锁。经典的哲学家就餐问题就是此类场景,多个哲学家(线程)竞争有限的筷子(锁)。
- 死锁的必要条件:互斥、不可抢占、请求和保持、循环等待 。只有当这四个条件同时满足时,才会发生死锁。
2. volatile 关键字
- 作用:主要解决内存可见性引起的线程安全问题。当一个变量被声明为 volatile ,它会禁止编译器对该变量相关操作的优化,保证不同线程对该变量的读取是最新值,而不是从各自的缓存中读取旧值。
- 举例:假设有一个共享变量 boolean flag = false ,线程A修改 flag 为 true ,如果没有 volatile 修饰,线程B可能无法及时感知到 flag 的变化,继续使用旧值。使用 volatile 修饰 flag 后,就能保证线程B能及时看到线程A对 flag 的修改。
3. wait 和 notify 方法
- 所属类及原理: wait 和 notify 是 Object 类的方法,用于协调线程执行顺序,避免线程饿死。 wait 方法使当前线程进入等待状态,并且会释放持有的锁 ; notify 方法用于唤醒在同一个对象上等待的某个线程, notifyAll 则唤醒所有等待的线程。
- 使用示例
public class WaitNotifyExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程1开始等待");
lock.wait();
System.out.println("线程1被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2准备唤醒其他线程");
lock.notify();
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
- 注意事项: wait 方法必须在 synchronized 代码块内使用,否则会抛出 IllegalMonitorStateException 异常 。因为 wait 方法内部做的第一件事就是释放锁,所以需要先持有锁才能释放。
4. 单例模式
- 饿汉式单例
- 实现原理:在类加载时就创建实例,天生是线程安全的。
- 代码示例
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 懒汉式单例
- 普通懒汉式(线程不安全):在第一次调用 getInstance 方法时创建实例,但在多线程环境下可能创建多个实例。
- 代码示例
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
- 线程安全的懒汉式(双重检查锁定):通过双重检查和 synchronized 关键字保证线程安全,同时延迟实例创建。
- 代码示例
public class SingletonLazySafe {
private static volatile SingletonLazySafe instance;
private static final Object locker = new Object();
private SingletonLazySafe() {}
public static SingletonLazySafe getInstance() {
if (instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazySafe();
}
}
}
return instance;
}
}
- 加入 volatile 的原因:防止指令重排。在创建对象时,可能会出现指令重排情况,导致还未完成对象初始化就被其他线程获取到,使用 volatile 修饰 instance 可以禁止指令重排,保证对象初始化的完整性和可见性。
5. 指令重排序
- 概念:是编译器优化的一种手段,在确保逻辑一致的前提下,调整代码的顺序以提高效率。但在多线程环境下,指令重排序可能会导致线程安全问题。
- 举例:创建对象的操作可以抽象为申请内存空间、在内存空间上进行初始化(构造方法)、将内存地址保存到引用变量中这三个步骤。指令重排序可能会使顺序变为1 - 3 - 2 ,如果此时另一个线程判断对象引用不为空就直接使用,而对象还未完成初始化,就会出错。在单例模式的双重检查锁定实现中,使用 volatile 修饰实例变量就是为了防止指令重排序带来的问题。
这些知识点在Java多线程编程和设计模式中非常关键,理解并正确应用它们能有效避免多线程环境下的各种问题,设计出高效、安全的程序。