## 一、什么时候会出现线程不同步? 当两个线程同时访问一个资源时,若数据更新不够迅速,则会出现线程之间修改数据后数据不同步,示例 定义Card类,有balance属性代表余额,写takeMoney方法模拟取钱 ```java public class Card { private int balance = 2000; public int takeMoney(int num) { if (num > 0) { if (this.balance >= num) { this.balance -= num; return this.balance; } } return -1; } } ``` 创建取钱线程CardThread类,用来实施多线程取钱操作 ```java import java.util.Random; import java.util.concurrent.Callable; public class CardThread implements Callable { private final Card card; public CardThread(Card card) { this.card = card; } @Override public String call() { Random r = new Random(); int num = r.nextInt(2000); int balance = this.card.takeMoney(num); if (balance >= 0) { return Thread.currentThread().getName() + "取钱成功:" + num + ",剩余:" + balance; } return Thread.currentThread().getName() + "取钱失败:" + num; } } ``` 创建测试TestCardThread类,编写主函数代码 ```java import Card; import CardThread; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TestCardThread { public static void main(String[] args) { Card card = new Card(); FutureTask task1 = new FutureTask<>(new CardThread(card)); FutureTask task2 = new FutureTask<>(new CardThread(card)); Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); try { System.out.println(task1.get()); System.out.println(task2.get()); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } } ``` 运行时有极小概率会取出大于余额的钱数(如下图),很明显线程出现了不同步  ## 二、同步关键字(线程锁) 为了防止上述情况发生,可以在Card类takeMoney方法中添加`synchronized`关键字实现线程同步。 #### 1、直接添加在方法名 ```java public class Card { private int balance = 2000; // 防止多线程同时操作数值导致有未同步问题,则使用关键字synchronized实现同一时间只允许一个线程访问 public synchronized int takeMoney(int num) { if (num > 0) { if (this.balance > num) { this.balance -= num; return this.balance; } } return -1; } } ``` #### 2、使用synchronized代码块 ```java public class Card { private int balance = 2000; private static final Object lock = new Object(); // 线程锁 // 防止多线程同时操作数值导致有未同步问题,则使用关键字synchronized实现同一时间只允许一个线程访问 public int takeMoney(int num) { if (num > 0) { if (this.balance >= num) { synchronized (lock){ this.balance -= num; } // 线程锁释放 return this.balance; } } return -1; } } ``` ## 三、线程死锁 #### 1、什么是线程死锁? **线程死锁**是指两个或两个以上的线程在执行过程中,由于**竞争资源**或**彼此通信不畅**而造成的一种**相互等待**的现象。若无外力干涉,这些线程都将无法继续执行下去。 用个**小故事**说明: 五位哲学家围坐在一张圆桌旁,他们只做两件事:思考和吃饭。每两位哲学家之间有一根筷子。哲学家要想吃饭,必须同时拿起他左边和右边的筷子。如果每位哲学家都先拿起左边的筷子,然后等待右边的筷子,就会导致所有人都拿着一根筷子,等待另一根,从而所有人都无法吃饭。这就是死锁。 必然出现线程死锁的代码示例: ```java public class CertainDeadlockDemo { private static final Object lockA = new Object(); private static final Object lockB = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lockA) { // 1. 线程1获取锁A System.out.println("Thread1 持有 lockA"); try { Thread.sleep(10); // 2. 休眠10ms,确保线程2能拿到锁B } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { // 5. 线程1尝试获取锁B(但锁B已被线程2持有,所以等待) System.out.println("Thread1 同时持有 lockA 和 lockB"); } } }); Thread thread2 = new Thread(() -> { synchronized (lockB) { // 3. 线程2获取锁B System.out.println("Thread2 持有 lockB"); try { Thread.sleep(10); // 4. 休眠10ms,确保线程1能拿到锁A } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA) { // 6. 线程2尝试获取锁A(但锁A已被线程1持有,所以等待) System.out.println("Thread2 同时持有 lockB 和 lockA"); } } }); thread1.start(); thread2.start(); // 程序将永远卡住,不会结束,因为发生了死锁。 } } ``` #### 2、线程死锁出现的条件 | 条件 | 描述 | 上述代码示例中的体现 | | :--------- | :----------------------------------------------------------------------------- | :-------------------------------------------------------------------- | | 互斥使用 | 一个资源每次只能被一个线程占用。 | 锁`lockA` 和 `lockB` 都是互斥的,一次只能被一个线程持有。 | | 不可抢占 | 线程获得的资源在未使用完毕之前,不能被其他线程强行夺走,只能由该线程主动释放。 | 线程1不能强行从线程2手中抢走`lockB`,只能等待线程2释放。 | | 请求和保持 | 一个线程在持有一个资源的同时,又去请求另一个资源。 | 线程1在持有`lockA` 的同时,又去请求 `lockB`。线程2亦然。 | | 循环等待 | 存在一个线程-资源的环形链。例如:T1等待T2占用的资源,T2等待T1占用的资源。 | 线程1等待线程2释放`lockB`,线程2等待线程1释放 `lockA`,形成循环等待。 | 注意:只要**破坏以上四个条件中的任意一个**,死锁就不会发生。 #### 3、如何避免线程死锁? 避免死锁的核心策略就是**破坏死锁的四个必要条件**。由于“互斥使用”和“不可抢占”是锁的基本特性,我们通常无法改变,所以重点在于破坏“请求和保持”和“循环等待”。 ##### ①破坏“请求和保持”条件:一次性申请所有资源 **思路**:让线程在开始执行前,一次性申请它所需要的所有资源,如果无法全部满足,则一个资源都不分配。 **实现方式**:在 Java 中,这通常意味着**按照一个全局固定的顺序去获取所有需要的锁**,并且在获取下一个锁之前,不持有任何锁。 ```java public class AvoidDeadlockByOrder { private static final Object lockA = new Object(); private static final Object lockB = new Object(); // 定义一个全局的锁获取顺序。例如,总是先获取id小的锁。 public static void getLocks(Object firstLock, Object secondLock) { // 确定哪个锁应该先被获取 Object lock1 = firstLock; Object lock2 = secondLock; // 通过hashCode或自定义id来排序,确保所有线程都遵循相同的顺序。 if (System.identityHashCode(firstLock) > System.identityHashCode(secondLock)) { lock1 = secondLock; lock2 = firstLock; } synchronized (lock1) { synchronized (lock2) { // 执行需要持有两把锁的代码 System.out.println(Thread.currentThread().getName() + " 执行任务"); } } } public static void main(String[] args) { Thread t1 = new Thread(() -> getLocks(lockA, lockB)); Thread t2 = new Thread(() -> getLocks(lockA, lockB)); // 两个线程都按A->B的顺序获取锁 t1.start(); t2.start(); } } ``` **关键点**:无论线程需要哪些锁,它们都遵循一个**全局一致的顺序**(例如,按对象的哈希值或一个唯一的ID排序)。这样就不会出现线程1先拿A再拿B,而线程2先拿B再拿A的情况,从而破坏了循环等待条件。 ##### ②破坏“循环等待”条件:使用超时等待 **思路**:使用显式锁(`java.util.concurrent.locks.Lock`)的 `tryLock()` 方法,为锁的获取设置一个超时时间。如果在规定时间内无法获取所有需要的锁,就**释放已经持有的锁**,进行回退,并等待一段时间后重试。 ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AvoidDeadlockByTimeout { private static final Lock lock1 = new ReentrantLock(); private static final Lock lock2 = new ReentrantLock(); public static void doWorkWithLocks() { while (true) { // 重试机制 boolean acquiredLock1 = false; boolean acquiredLock2 = false; try { // 尝试获取锁,设置超时时间 acquiredLock1 = lock1.tryLock(100, TimeUnit.MILLISECONDS); acquiredLock2 = lock2.tryLock(100, TimeUnit.MILLISECONDS); if (acquiredLock1 && acquiredLock2) { // 成功获取两把锁,执行任务 System.out.println(Thread.currentThread().getName() + " 成功获取所有锁,执行任务"); break; // 任务完成,退出循环 } else { // 如果没有成功获取所有锁,就释放已经获取的锁 System.out.println(Thread.currentThread().getName() + " 获取锁失败,进行回退并重试"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 处理中断 } finally { // 在finally块中确保锁被释放 if (acquiredLock1) { lock1.unlock(); } if (acquiredLock2) { lock2.unlock(); } } // 等待随机时间后重试,避免活锁(多个线程同时重试) try { Thread.sleep((long) (Math.random() * 100)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } public static void main(String[] args) { new Thread(() -> doWorkWithLocks()).start(); new Thread(() -> doWorkWithLocks()).start(); } } ``` **关键点**:通过 `tryLock` 和超时机制,我们破坏了“不可抢占”条件(间接地,因为超时后释放锁相当于允许了抢占),并避免了无限期的等待。结合随机等待,可以有效防止死锁。 ## 四、利用缓冲区实现同步线程 #### 1、工厂和买家之间的关系 约定工厂和买家之间有个仓库,仓库用于存放工厂生产的货物,当仓库满的时候,工厂想要存放生产的货物时就必须等买家把货物买走,此时工厂主动停止生产,等买家购买货物后主动跟工厂说开始生产才生产,反之同理。 #### 2、定义仓库(有界缓冲区) ```java public class Storage { private int count = 0; public int getCount() { return count; } // 负责生产的方法 public synchronized void set() { // 如果库存>=10时,停止生产 if (count >= 10) { try { System.out.println("工厂停工"); this.wait(); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } count++; this.notify(); } // 负责消费的方法 public synchronized void get() { // 如果库存<=0时,停止购买 if (count <= 0) { try { System.out.println("买家不买"); this.wait(); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } count--; this.notify(); } } ``` #### 3、生产、购买线程 ```java // 多线程的启动是调用类的start()方法 public class Factory implements Runnable { private Storage storage; public Factory(Storage storage) { this.storage = storage; } @Override public void run() { for (int i = 0; i < 50; i++) { this.storage.set(); System.out.println("工厂生产,剩余:" + this.storage.getCount() + ",总生产量:" + (i + 1)); } } } ``` ```java // 多线程的启动是调用类的start()方法 public class Buyer implements Runnable { private Storage storage; public Buyer(Storage storage) { this.storage = storage; } @Override public void run() { for (int i = 0; i < 50; i++) { this.storage.get(); System.out.println("买家购买,剩余:" + this.storage.getCount() + ",总购买量:" + (i + 1)); } } } ``` #### 4、测试函数 ```java public static void main(String[] args) { Storage storage = new Storage(); Factory factory = new Factory(storage); Buyer buyer = new Buyer(storage); Thread t1 = new Thread(factory); Thread t2 = new Thread(buyer); t1.start(); t2.start(); } ``` ## 五、volatile关键字 若多个线程需要同时访问同一变量修改其数据时,可以给变量加`volatile`关键字来保证每个线程修改之间的数据同步。 ```java private int volatile balance = 2000; ``` Loading... ## 一、什么时候会出现线程不同步? 当两个线程同时访问一个资源时,若数据更新不够迅速,则会出现线程之间修改数据后数据不同步,示例 定义Card类,有balance属性代表余额,写takeMoney方法模拟取钱 ```java public class Card { private int balance = 2000; public int takeMoney(int num) { if (num > 0) { if (this.balance >= num) { this.balance -= num; return this.balance; } } return -1; } } ``` 创建取钱线程CardThread类,用来实施多线程取钱操作 ```java import java.util.Random; import java.util.concurrent.Callable; public class CardThread implements Callable<String> { private final Card card; public CardThread(Card card) { this.card = card; } @Override public String call() { Random r = new Random(); int num = r.nextInt(2000); int balance = this.card.takeMoney(num); if (balance >= 0) { return Thread.currentThread().getName() + "取钱成功:" + num + ",剩余:" + balance; } return Thread.currentThread().getName() + "取钱失败:" + num; } } ``` 创建测试TestCardThread类,编写主函数代码 ```java import Card; import CardThread; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TestCardThread { public static void main(String[] args) { Card card = new Card(); FutureTask<String> task1 = new FutureTask<>(new CardThread(card)); FutureTask<String> task2 = new FutureTask<>(new CardThread(card)); Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); try { System.out.println(task1.get()); System.out.println(task2.get()); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } } ``` 运行时有极小概率会取出大于余额的钱数(如下图),很明显线程出现了不同步  ## 二、同步关键字(线程锁) 为了防止上述情况发生,可以在Card类takeMoney方法中添加`synchronized`关键字实现线程同步。 #### 1、直接添加在方法名 ```java public class Card { private int balance = 2000; // 防止多线程同时操作数值导致有未同步问题,则使用关键字synchronized实现同一时间只允许一个线程访问 public synchronized int takeMoney(int num) { if (num > 0) { if (this.balance > num) { this.balance -= num; return this.balance; } } return -1; } } ``` #### 2、使用synchronized代码块 ```java public class Card { private int balance = 2000; private static final Object lock = new Object(); // 线程锁 // 防止多线程同时操作数值导致有未同步问题,则使用关键字synchronized实现同一时间只允许一个线程访问 public int takeMoney(int num) { if (num > 0) { if (this.balance >= num) { synchronized (lock){ this.balance -= num; } // 线程锁释放 return this.balance; } } return -1; } } ``` ## 三、线程死锁 #### 1、什么是线程死锁? **线程死锁**是指两个或两个以上的线程在执行过程中,由于**竞争资源**或**彼此通信不畅**而造成的一种**相互等待**的现象。若无外力干涉,这些线程都将无法继续执行下去。 用个**小故事**说明: 五位哲学家围坐在一张圆桌旁,他们只做两件事:思考和吃饭。每两位哲学家之间有一根筷子。哲学家要想吃饭,必须同时拿起他左边和右边的筷子。如果每位哲学家都先拿起左边的筷子,然后等待右边的筷子,就会导致所有人都拿着一根筷子,等待另一根,从而所有人都无法吃饭。这就是死锁。 必然出现线程死锁的代码示例: ```java public class CertainDeadlockDemo { private static final Object lockA = new Object(); private static final Object lockB = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lockA) { // 1. 线程1获取锁A System.out.println("Thread1 持有 lockA"); try { Thread.sleep(10); // 2. 休眠10ms,确保线程2能拿到锁B } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { // 5. 线程1尝试获取锁B(但锁B已被线程2持有,所以等待) System.out.println("Thread1 同时持有 lockA 和 lockB"); } } }); Thread thread2 = new Thread(() -> { synchronized (lockB) { // 3. 线程2获取锁B System.out.println("Thread2 持有 lockB"); try { Thread.sleep(10); // 4. 休眠10ms,确保线程1能拿到锁A } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA) { // 6. 线程2尝试获取锁A(但锁A已被线程1持有,所以等待) System.out.println("Thread2 同时持有 lockB 和 lockA"); } } }); thread1.start(); thread2.start(); // 程序将永远卡住,不会结束,因为发生了死锁。 } } ``` #### 2、线程死锁出现的条件 | 条件 | 描述 | 上述代码示例中的体现 | | :--------- | :----------------------------------------------------------------------------- | :-------------------------------------------------------------------- | | 互斥使用 | 一个资源每次只能被一个线程占用。 | 锁`lockA` 和 `lockB` 都是互斥的,一次只能被一个线程持有。 | | 不可抢占 | 线程获得的资源在未使用完毕之前,不能被其他线程强行夺走,只能由该线程主动释放。 | 线程1不能强行从线程2手中抢走`lockB`,只能等待线程2释放。 | | 请求和保持 | 一个线程在持有一个资源的同时,又去请求另一个资源。 | 线程1在持有`lockA` 的同时,又去请求 `lockB`。线程2亦然。 | | 循环等待 | 存在一个线程-资源的环形链。例如:T1等待T2占用的资源,T2等待T1占用的资源。 | 线程1等待线程2释放`lockB`,线程2等待线程1释放 `lockA`,形成循环等待。 | 注意:只要**破坏以上四个条件中的任意一个**,死锁就不会发生。 #### 3、如何避免线程死锁? 避免死锁的核心策略就是**破坏死锁的四个必要条件**。由于“互斥使用”和“不可抢占”是锁的基本特性,我们通常无法改变,所以重点在于破坏“请求和保持”和“循环等待”。 ##### ①破坏“请求和保持”条件:一次性申请所有资源 **思路**:让线程在开始执行前,一次性申请它所需要的所有资源,如果无法全部满足,则一个资源都不分配。 **实现方式**:在 Java 中,这通常意味着**按照一个全局固定的顺序去获取所有需要的锁**,并且在获取下一个锁之前,不持有任何锁。 ```java public class AvoidDeadlockByOrder { private static final Object lockA = new Object(); private static final Object lockB = new Object(); // 定义一个全局的锁获取顺序。例如,总是先获取id小的锁。 public static void getLocks(Object firstLock, Object secondLock) { // 确定哪个锁应该先被获取 Object lock1 = firstLock; Object lock2 = secondLock; // 通过hashCode或自定义id来排序,确保所有线程都遵循相同的顺序。 if (System.identityHashCode(firstLock) > System.identityHashCode(secondLock)) { lock1 = secondLock; lock2 = firstLock; } synchronized (lock1) { synchronized (lock2) { // 执行需要持有两把锁的代码 System.out.println(Thread.currentThread().getName() + " 执行任务"); } } } public static void main(String[] args) { Thread t1 = new Thread(() -> getLocks(lockA, lockB)); Thread t2 = new Thread(() -> getLocks(lockA, lockB)); // 两个线程都按A->B的顺序获取锁 t1.start(); t2.start(); } } ``` **关键点**:无论线程需要哪些锁,它们都遵循一个**全局一致的顺序**(例如,按对象的哈希值或一个唯一的ID排序)。这样就不会出现线程1先拿A再拿B,而线程2先拿B再拿A的情况,从而破坏了循环等待条件。 ##### ②破坏“循环等待”条件:使用超时等待 **思路**:使用显式锁(`java.util.concurrent.locks.Lock`)的 `tryLock()` 方法,为锁的获取设置一个超时时间。如果在规定时间内无法获取所有需要的锁,就**释放已经持有的锁**,进行回退,并等待一段时间后重试。 ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AvoidDeadlockByTimeout { private static final Lock lock1 = new ReentrantLock(); private static final Lock lock2 = new ReentrantLock(); public static void doWorkWithLocks() { while (true) { // 重试机制 boolean acquiredLock1 = false; boolean acquiredLock2 = false; try { // 尝试获取锁,设置超时时间 acquiredLock1 = lock1.tryLock(100, TimeUnit.MILLISECONDS); acquiredLock2 = lock2.tryLock(100, TimeUnit.MILLISECONDS); if (acquiredLock1 && acquiredLock2) { // 成功获取两把锁,执行任务 System.out.println(Thread.currentThread().getName() + " 成功获取所有锁,执行任务"); break; // 任务完成,退出循环 } else { // 如果没有成功获取所有锁,就释放已经获取的锁 System.out.println(Thread.currentThread().getName() + " 获取锁失败,进行回退并重试"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 处理中断 } finally { // 在finally块中确保锁被释放 if (acquiredLock1) { lock1.unlock(); } if (acquiredLock2) { lock2.unlock(); } } // 等待随机时间后重试,避免活锁(多个线程同时重试) try { Thread.sleep((long) (Math.random() * 100)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } public static void main(String[] args) { new Thread(() -> doWorkWithLocks()).start(); new Thread(() -> doWorkWithLocks()).start(); } } ``` **关键点**:通过 `tryLock` 和超时机制,我们破坏了“不可抢占”条件(间接地,因为超时后释放锁相当于允许了抢占),并避免了无限期的等待。结合随机等待,可以有效防止死锁。 ## 四、利用缓冲区实现同步线程 #### 1、工厂和买家之间的关系 约定工厂和买家之间有个仓库,仓库用于存放工厂生产的货物,当仓库满的时候,工厂想要存放生产的货物时就必须等买家把货物买走,此时工厂主动停止生产,等买家购买货物后主动跟工厂说开始生产才生产,反之同理。 #### 2、定义仓库(有界缓冲区) ```java public class Storage { private int count = 0; public int getCount() { return count; } // 负责生产的方法 public synchronized void set() { // 如果库存>=10时,停止生产 if (count >= 10) { try { System.out.println("工厂停工"); this.wait(); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } count++; this.notify(); } // 负责消费的方法 public synchronized void get() { // 如果库存<=0时,停止购买 if (count <= 0) { try { System.out.println("买家不买"); this.wait(); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } count--; this.notify(); } } ``` #### 3、生产、购买线程 ```java // 多线程的启动是调用类的start()方法 public class Factory implements Runnable { private Storage storage; public Factory(Storage storage) { this.storage = storage; } @Override public void run() { for (int i = 0; i < 50; i++) { this.storage.set(); System.out.println("工厂生产,剩余:" + this.storage.getCount() + ",总生产量:" + (i + 1)); } } } ``` ```java // 多线程的启动是调用类的start()方法 public class Buyer implements Runnable { private Storage storage; public Buyer(Storage storage) { this.storage = storage; } @Override public void run() { for (int i = 0; i < 50; i++) { this.storage.get(); System.out.println("买家购买,剩余:" + this.storage.getCount() + ",总购买量:" + (i + 1)); } } } ``` #### 4、测试函数 ```java public static void main(String[] args) { Storage storage = new Storage(); Factory factory = new Factory(storage); Buyer buyer = new Buyer(storage); Thread t1 = new Thread(factory); Thread t2 = new Thread(buyer); t1.start(); t2.start(); } ``` ## 五、volatile关键字 若多个线程需要同时访问同一变量修改其数据时,可以给变量加`volatile`关键字来保证每个线程修改之间的数据同步。 ```java private int volatile balance = 2000; ``` 最后修改:2025 年 10 月 13 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 👆请简单粗暴地爱我。