synchronization 线程同步

为什么要使用线程同步?

假设有这么一个场景:

小明银行账户有1000元

有一个 线程A 正在向 小明 银行账户里面存入100块钱,为此 A线程 需要做以下几个步骤:

  1. 读取小明账户余额 存入 临时变量 balance

  2. 令balance = balance + 100

  3. 将更新后的 balance 写入小明银行账户

不巧好巧, 有一个线程B 同时 也从小明银行账户里面取200块钱,为此 B 线程 需要做以下几个步骤:

  1. 读取小明账户余额 存入 临时变量 balance

  2. 令balance = balance - 200

  3. 将更新后的 balance 写入小明银行账户

但我们说过, 多线程的执行顺序是随机的,是由操作系统动态决定的。假设 A B线程是按照这样的顺序执行的:

  1. A线程 读取小明账户余额 存入 临时变量 balance(此时 balance 为 1000)

  2. A线程 令balance = balance + 100 (此时 balance为1100)

  3. B线程 读取小明账户余额 存入 临时变量 balance(此时balance为1000)

  4. A线程 将更新后的 balance 写入小明银行账户 (小明银行账户更新为1000)

  5. B线程 令balance = balance - 200 (此时balance为800)

  6. B线程 将更新后的 balance 写入小明银行账户 (小明银行账户更新为800)

问题来了: 存100, 取200,余额应该是900的,为什么会变为800?

根源在于上面的第5步, B线程读取的 balance 为存入100块之前的 余额,而此时已经存入了100块,余额已经改变了,B 线程没有来得及更新!

解决方法

我们在存钱和取钱的时候,禁止其他线程访问当前的账户余额。换句话说,在 A线程 进行存钱操作时, 禁止B线程的运行,等A线程运行完以后,才能执行B 线程。

代码演示

我们编写了一个类 Bank, 主要有两个方法,一个是 saveMoney 存钱,一次存入100;另外一个是 withdrawMoney 取钱, 一次取200。 我们在 saveMoney 函数声明时加入了 synchronization 关键字, 表示当前函数运行时, 不允许被别的线程抢占; 在 withdrawMoney 函数内部使用 synchronization(this){代码块} 表示在当前对象内的代码块执行时,不允许被其他线程抢占。 特别的是,我们在存钱和取钱过程中, 故意 sleep 1000毫秒,用来模拟当前线程被别的线程抢占的情况。

public class Bank {

    private String account;// 账号
    private int balance;// 账户余额

    public Bank(String account, int balance) {
        this.account = account;
        this.balance = balance;
    }

    //获取账户信息
    public String getAccount() {
        return account;
    }

    //获取余额
    public int getBalance() {
        return balance;
    }

    //设置余额
    public void setBalance(int balance) {
        this.balance = balance;
    }


    // 模拟存款操作,一次存款100元, 使用线程同步
    //对当前函数进行上锁,函数内的资源不允许被其他线程访问
    public synchronized void saveMoney() {
        // 获取当前账户余额
        int balance = this.getBalance();

        // 修改余额,加100元
        balance = balance + 100;

        // 故意让进程阻塞1秒,用来引诱调用另一个线程
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // 修改账户余额
        this.setBalance(balance);

        // 输出现在的余额
        System.out.println("存款100后账户余额为 : " + this.getBalance());

    }

    // 模拟取款操做, 一次取200, 使用线程同步
    public void withdrawMoney() {
        //synchronized(obj){代码块}
        //对obj对象内的 资源 进行上锁, 其他线程不允许访问

        synchronized (this) {
            // 获取当前账户余额
            int balance = this.getBalance();

            balance = balance - 200;

            // 故意让进程阻塞1秒,用来引诱调用另一个线程
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // 修改账户余额
            this.setBalance(balance);

            // 输出现在的余额
            System.out.println("取款200后账户余额为 : " + this.getBalance());
        }

    }

    @Override
    public String toString() {
        return "Bank [账户 : " + account + ", 余额 : " + balance + "]";
    }
    
}

我们编写了 SaveThread 用来模拟存钱的线程:

//对银行账户进行存款操作 的 线程
public class SaveThread implements Runnable{

    Bank bank;

    public SaveThread (Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        this.bank.saveMoney();
    }
}

我们编写了 WithdrawThread 用来模拟取钱的线程:

// 从银行账户中 取款的线程
public class WithdrawThread implements Runnable{
    Bank bank;

    public WithdrawThread(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        this.bank.withdrawMoney();
    }

}

最后,我们编写了一个测试类 BankTest 。我们特意对存款,取款线程使用了 join 函数,其目的是让存款线程和取款线程优于 主线程执行,最后执行主线程打印账户余额信息。

public class BankTest {
    public static void main(String[] args) {
        //创建账户, 给定金额为1000
        Bank bank=new Bank("123456789", 1000);

        //创建存款, 取款 对象
        SaveThread save= new SaveThread(bank);
        WithdrawThread withdraw= new WithdrawThread(bank);

        //创建存款, 取款 线程
        Thread saveThread=new Thread(save);
        Thread withdrawThread= new Thread(withdraw);

        saveThread.start();
        withdrawThread.start();

        //这里用join表示希望先存取款,然后再打印银行余额
        try {
            saveThread.join();
            withdrawThread.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println(bank);
    }

}

运行结果:

存款100后账户余额为 : 1100
取款200后账户余额为 : 900
Bank [账户 : 123456789, 余额 : 900]

无论我们运行多少次,最后结果依然正确。

Last updated