Java 线程同步与线程间通信

Java 线程同步与线程间通信

线程同步概述

核心问题:竞争状态与线程安全

当多个线程同时访问共享资源(临界资源)时,可能导致资源数据被破坏,这种现象称为竞争状态。若一个类的对象在多线程环境中不会引发竞争状态,则该类为线程安全的。

示例:未同步的账户存款问题

多个线程向同一账户存入1元,因未同步导致最终余额小于预期(理论100,实际可能远小)。

package com.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Jing61
 */
public class AccountWithoutSync {
    //临界资源
    private static Account account = new Account();
    
    private static class Account {
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        //为了故意放大数据破坏的可能性,采用下列语句,其实可用 balance += amount;代替
        public void deposit(int amount) {
            int newBalance = balance + amount;
            try {
                // 模拟处理时间
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance = newBalance;
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++) executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated());
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

可能的运行结果:
image

问题原因

  • 线程1读取余额(0)→ 计算新余额(1)→ 休眠。
  • 线程2读取相同余额(0)→ 计算新余额(1)→ 休眠。
  • 线程1唤醒后写入余额(1)→ 线程2唤醒后覆盖写入(1)。
  • 最终两个线程的操作仅生效一次,导致数据错误。

image
在步骤1中,任务1从账户中获取余额数目。在步骤2中,任务2从账户中获取同样数目的余额。在步骤3中,任务1向账户写入一个新余额。在步骤4中,任务2也向该账户写入一个新余额。这个场景的效果就是任务1什么都没做,因为在步骤4中,任务4覆盖了任务1的结果。很明显,问题是任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程中的一个普遍问题,称为竞争状态(race condition)。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的(thread-safe)。

线程同步实现方式

方式一:synchronized 关键字

synchronized 用于标记临界区(需同步的代码/方法),通过加锁机制保证同一时间只有一个线程访问临界区,分为同步方法和同步代码块。

同步方法

直接在方法上添加 synchronized,锁对象为当前实例(实例方法)或类对象(静态方法)。

package com.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Jing61
 */
public class AccountWithSync {
    //临界资源
    private static Account account = new Account();
    
    private static class Account{
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        /**
         * 临界区:整个deposit
         *     synchronized防止多个线程同时今日临界区——一次只有一个线程可以访问该方法
         * @param amount 金额
         * 
         */
        public synchronized void deposit(int amount) {
            //可以使用balance += amount;
            int newBalance = balance + amount;
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance = newBalance;
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++)
            executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated()) {}
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

同步代码块

仅同步方法中的部分代码,锁对象可自定义(需保证多个线程使用同一锁),灵活性更高,并发能力更强。

package com.thread;

/**
 * @author Jing
 */
public class CustomThreadTest {
    public static void main(String[] args) {
        //创建10个线程,代表10个售票窗口
        for(var i = 0; i < 10; i++) {
            var thread = new CustomThread("窗口-" + (i + 1));
            thread.start();//启动线程
        }
    }

}
/**
 * 由于Thread类实现了Runnable接口,
 * 所以可以定义一个类扩展自Thread类,并且覆盖run方法。
 *
 */
class CustomThread extends Thread{
    private static int ticket = 100;//共享资源,存在线程安全问题
    public CustomThread() {}
    public CustomThread(String name) {
        super(name);//Thread(String):指定线程名称
    }
    @Override
    public void run() {
        try {
            while(true) {
                synchronized (CustomThread.class) { // 必须是锁住类,不能锁住对象,锁住对象只是锁住当前对象
                    if(ticket <= 0)
                        break;
                    //Thread.currentThread():获取当前运行的线程
                    System.out.println(Thread.currentThread().getName() + "正在售出编号为 " + ticket-- + " 的火车票");
                    Thread.sleep(10);//模拟销售一张火车票的时间为1ms
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized 锁机制说明

  • 实例方法:锁为调用方法的对象,不同对象的锁相互独立。
  • 静态方法:锁为当前类的 Class 对象,所有实例共享同一锁。
  • 同步代码块:锁为括号内的对象(需确保多线程共用该对象)。
  • 线程获取锁后执行临界区,其他线程需阻塞等待锁释放。

方式二:显式锁(Lock 接口)

JDK 5 提供的 java.util.concurrent.locks.Lock 接口,是 synchronized 的增强版,解决了 synchronized 的局限性(单一锁类型、锁只能在代码块/方法首尾获取释放、线程只能阻塞等待)。

核心优势

  • 支持多种锁类型(如读写锁)。
  • 可在一个方法加锁、另一个方法解锁。
  • 提供 tryLock() 方法,获取不到锁时可选择等待、回退或超时放弃。

主要实现类

  • ReentrantLock:重入锁,功能与 synchronized 类似,灵活性更高。
  • ReentrantReadWriteLock:读写锁,读操作共享、写操作排他,适合读多写少场景。

代码示例:ReentrantLock 实现账户同步

package com.lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Jing61
 */
public class AccountWithLock {
    //临界资源
    private static Account account = new Account();
    
    private static class Account{
        private static Lock lock = new ReentrantLock();
        private int balance = 0;

        public int getBalance() {
            return balance;
        }
        
        /**
         * 锁方式
         */
        public void deposit(int amount) {
            lock.lock();//获取锁
            try {
                //可以使用balance += amount;
                int newBalance = balance + amount;
                Thread.sleep(5);
                balance = newBalance;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//释放锁
            }
            
        }
    }
    
    /**
     * 该任务向账户存入指定金额
     *
     */
    private static class AddPenyTask implements Runnable{
        @Override
        public void run() {
            account.deposit(1);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for(int i = 0; i < 100; i++)
            executor.execute(new AddPenyTask());
        
        executor.shutdown();//关闭执行器,不再接收新的任务
        //等待线程池中所有的任务完毕
        while(!executor.isTerminated()) {}
        
        System.out.println("账户余额:" + account.getBalance());
    }
}

注意事项

  • 获得锁后一定要释放锁。
  • 锁的释放最好放在 finally 块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。

线程间通信

线程间通信用于协调相互依赖的线程执行(如“生产者-消费者”模式),主要通过“锁+条件”或传统的内置监视器实现。

方式一:Lock + Condition(推荐)

ConditionLock 接口的配套工具,通过 Lock.newCondition() 创建,提供线程等待/唤醒的精细化控制。

核心方法

  • await():当前线程释放锁并进入等待状态,直到被唤醒。
  • signal():唤醒一个等待该条件的线程。
  • signalAll():唤醒所有等待该条件的线程。

示例:存款与取款线程协作

取款线程需等待存款线程存入足够金额后再执行,存款线程存入后通知取款线程。

package com.lock;

import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程间相互协作——通过条件实现线程间的通信
 *     条件是通过调用Lock对象的newCondition()方法创建的对象
 *     await():让当前线程进入等待,直到条件发生
 *     signal():唤醒一个等待的线程
 *     signalAll():唤醒所有等待的线程
 *     
 * 启动两个任务:一个任务向账户中存钱,另一个任务向账户中提款。当取钱金额大于账户余额,提款线程必须等待。
 * 不管什么时候,只要向账户中存入一笔钱,存钱线程必须通知提款线程重新尝试
 * 
 * 使用一个有条件的锁 newDeposit 。一个条件对应一个锁。在等待和通知状态之前,线程必须先获取该条件的锁。   
 * @author Jing61
 */
public class ThreadCooperation {
    private static Account account = new Account();
    
    private static class Account{
        //创建锁
        private static Lock lock = new ReentrantLock();
        //创建条件
        private static Condition newDeposit = lock.newCondition();
        //余额
        private int balance = 0;
        
        public void withdraw(int amount) {
            lock.lock();
            try {
                while(balance < amount) {// 当余额小于取款金额,必须等待存款任务条件的发生
                    System.out.println("等待存款任务的完成.....");
                    newDeposit.await();
                }
                balance -= amount;
                System.out.println("取款完成,取款金额:" + amount);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        
        public void deposit(int amount) {
            lock.lock();
            try {
                balance += amount;
                System.out.println("存入一笔金额:" + amount);
                newDeposit.signalAll();
            } finally {
                lock.unlock();
            }
        }

        /*
         * public int getBalance() { return balance; }
         */
    }
    
    /**
     * 存款任务
     *
     */
    private static class DepositTask implements Runnable{
        @Override
        public void run() {
            try {
                while(true) {
                    account.deposit((int)(Math.random() * 100) + 1);
                    Thread.sleep(5);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static class WithdrawTask implements Runnable{
        @Override
        public void run() {
            while(true) {
                account.withdraw((int)(Math.random() * 100) + 1);
            }
        }
    }
    
    public static void main(String[] args) {
        var executor = Executors.newFixedThreadPool(2);
        executor.execute(new WithdrawTask());
        executor.execute(new DepositTask());
        executor.shutdown();
        
    }
}

方式二:传统内置监视器(wait/notify)

JDK 5 之前的线程通信方式,基于 synchronized 锁的内置监视器实现,依赖 Object 类的 wait()notify()notifyAll() 方法。

核心规则

  • 需在 synchronized 方法/代码块中调用,否则抛出 IllegalMonitorStateException
  • wait():释放当前对象的锁,线程进入等待队列。
  • notify():唤醒等待队列中的一个线程,使其重新竞争锁。
  • notifyAll():唤醒等待队列中的所有线程。

示例:传统方式实现存款取款协作

package com.lock;

import java.util.concurrent.Executors;


/**
 * @author Jing61
 */
public class TraditionalCooperation {
    private static Account account = new Account();
    public static class Account{
        private int balance;
        
        public synchronized void withdraw(int amount) {
            try {
                while(amount > balance) {
                    System.out.println("余额不足,终止当前线程并且释放对象的锁");
                    wait(); //必须先获取锁(在同步代码块或者方法中进行调用,否则IllegalMonitorStateException)
                }
                balance -= amount;
                System.out.println("取款金额:" + amount + "当前余额:" + balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public synchronized void deposit(int amount) {
            balance += amount;
            System.out.println("存款金额:" + amount + "当前余额:" + balance);
            notify();//通知一个等待的线程重新获取锁并恢复执行
        }
        
        /**
         * 取款任务
         *
         */
        private static class WithdrawTask implements Runnable{

            @Override
            public void run() {
                while(true) {
                    int amount = (int)(Math.random() * 10000 + 1);
                    account.withdraw(amount);
                }
            }
        }
        
        private static class DepositTask implements Runnable{

            @Override
            public void run() {
                try {
                    while(true) {
                        int amount = (int)(Math.random() * 10000 + 1);
                        account.deposit(amount);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public static void main(String[] args) {
            var executor = Executors.newFixedThreadPool(2);
            executor.execute(new DepositTask());
            executor.execute(new WithdrawTask());
            
            executor.shutdown();
        }
    }
}

两种通信方式对比

方式 优点 缺点
Lock + Condition 支持多条件、唤醒精准、灵活性高 代码稍复杂,需手动释放锁
synchronized + wait/notify 代码简洁、无需手动释放锁 仅支持单条件、唤醒不精准
posted @ 2025-11-13 16:37  Jing61  阅读(7)  评论(0)    收藏  举报