线程总结(三):同步线程 - Synchronized

前言

先来一个小例子,假设夫妻俩开了一个共同账户,然后同时去取款,丈夫取了600,妻子取了800,按照之前的思路来写就是

public class TestThread {
    public static void main(String[] args) {

        Account account = new Account(1000);

        GetMoneyThread personA = new GetMoneyThread("丈夫", account, 600);
        GetMoneyThread personB = new GetMoneyThread("妻子", account, 800);

        personA.start();
        personB.start();

    }
}

class Account {
    private int balance;// 账户余额

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }
}

class GetMoneyThread extends Thread {

    private int money;// 每次取钱的金额
    private Account account;// 账户
    private String name;// 取款人姓名

    public GetMoneyThread(String name, Account account, int money) {
        this.name = name;
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
            this.setName(name);
            if (money <= account.getBalance()) {
                System.out.println(this.getName() + "成功取出 " + money + " 元 ");

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance() - money);
                System.out.println("此时账户内余额为 " + account.getBalance() + " 元");
            } else {
                System.out.println(this.getName() + " 取款时余额不足,取款失败");
            }
        }
}

输出结果是

丈夫成功取出 600 元 
妻子成功取出 800 元 
此时账户内余额为 400 元
此时账户内余额为 -400 元

然而账户里一共才有1000块钱,却取出了1400,账户的余额变成了-400,银行岂不是亏死了,究其原因,是因为夫妻(两个线程)俩同时对账户做出操作,而卡内余额没有及时更新造成的。

一个程序中存在多个线程并没有关系,每个线程挨个儿执行,依次去占用 CPU 去执行自己的代码,你走你的阳关道我走我的独木桥,相互不冲突,这本没有问题,但是现实编程往往需要多个线程去访问同一个资源:譬如同一块内存、同一个数据库、同一个文件。

当多个线程同时访问一个资源时,因为线程的执行过程不可控(占用 CPU 的顺序由线程调度器决定以及 I/O 的延时特性),所以很容易导致运算结果和实际结果不符的情况(譬如文章一开始举的例子)。

这个就是线程安全问题,多个线程访问同一个资源,该资源就被成为共享资源,也有人叫它临界资源。而修改这个资源的代码块被成为临界区

为了避免解决这个线程安全问题,就需要引入同步互斥概念了,同步互斥就是在多个线程访问共享资源时,保证共享数据在同一时刻只能被一个线程使用,互斥是因,同步是果。当线程访问资源时,会给资源加一道“锁”,已经被“锁住”的资源别的线程无法访问,当该线程访问完资源之,“取消锁”,别的线程方可继续访问。

我们可以使用两种方法给资源“加锁”:

  • synchronized

  • Lock


synchronized 关键字

synchronized 有两个重要含义:它确保了一次直有一个线程可以执行代码的受保护部分,而且它确保了一个线程更改的数据对于其他线程是可见的。

同步代码块

同步代码块的格式如下:

synchronized(obj){
    ...
    // 此处的代码就是同步代码块
}

其中 obj 可以看作是一个监视器也可以看作是一个“锁”,任何 Java 对象都可以充当,在线程执行到同步代码块的时候,会先锁定“监视器”,再执行代码块中的内容,执行完成之后会释放对监视器的锁定,在监视器处于锁定状态的时候,其余线程无法访问代码块中的内容。

在上面取钱的那个例子中,账户就可以当作是一个监视器,在一个线程操作该账户的时候,其他线程无法同时访问。修改一下 GetMoneyThread 类:

class GetMoneyThread extends Thread {

    private Account account;// 账户
    private int money;// 取的钱数
    private String name;// 取款人姓名

    public GetMoneyThread(Account account, int money, String name) {
        this.account = account;
        this.money = money;
        this.name = name;
    }

    @Override
    public void run() {
        this.setName(name);
        synchronized (account) {// 账户对象作为同步监视器,线程执行前先锁定该对象,使得其他线程无法访问,待同步代码执行完释放锁,其他线程开始锁定同步监视器
            if (account.getBalance() >= money) {
                System.out.println(name + " 取了 " + money + " 元");

                account.setBalance(account.getBalance() - money);

                System.out.println(name + " 取款后,卡内余额为 " + account.getBalance());

            } else {
                System.out.println("卡内余额不足," + name + " 取款失败");
            }
        }
    }
}

执行结果

丈夫 取了 600 元
丈夫 取款后,卡内余额为 400
卡内余额不足,妻子 取款失败

同步方法

也可以直接将方法设置为 synchronized,对于普通的 synchronized 方法,他的同步监视器就是调用该方法的对象,对于静态 synchronized 方法来说,同步监视器是与 Class 对象相关的监控器。

重新修改一下之前的取款代码:

public class TestThread {
    public static void main(String[] args) {

        Account account = new Account(1000);

        GetMoneyThread personA = new GetMoneyThread(account, 600, "丈夫");
        GetMoneyThread personB = new GetMoneyThread(account, 800, "妻子");

        personA.start();
        personB.start();

    }
}

class Account {
    private int balance;// 账户余额

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    public synchronized void getMoney(String name, int money) {
        if (balance >= money) {
            System.out.println(name + " 取了 " + money + " 元");

            balance = balance - money;

            System.out.println("卡内余额为 " + balance);

        } else {
            System.out.println("卡内余额不足," + name + " 取款失败");
        }
    }
}

class GetMoneyThread extends Thread {

    private Account account;// 账户
    private int money;// 取的钱数
    private String name;// 取款人姓名

    public GetMoneyThread(Account account, int money, String name) {
        this.account = account;
        this.money = money;
        this.name = name;
    }

    @Override
    public void run() {
        this.setName(name);
        account.getMoney(name, money);
    }
}

执行结果

妻子 取了 800 元
卡内余额为 200
卡内余额不足,丈夫 取款失败

注意: synchronized 只能修饰代码块或方法,但不能修饰构造方法或者成员变量。

不管是同步代码块还是同步方法,执行逻辑都是 加锁 → 修改 → 释放锁 → 其他线程加锁 →...,在同步代码块中,可以将同步监视器设置为this,这表示该代码块将与这个类中的 synchronized 方法使用同一个锁。

同步监视器的释放

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

  • 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行,当前线程也会释放同步监视器。

  • 当前线程在同步代码块、同步方法中出现了未处理的 Error 或者 Exception,导致了该代码块、该方法异常结束时,当前线程会释放同步监视器。

  • 当前线程执行同步代码或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程进入暂停状态,并释放同步监视器。

Synchronized 注意事项

当编写同步代码时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:

  • Synchronized 代码块应尽可能的简短

  • 不要在 Synchronized 块中调用可能引起阻塞的方法,譬如I/O

  • 在持有锁的时候,尽量不要调用其他对象的方法。

参考

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1