4-3 解决原子性问题

一个或多个操作在CPU执行的过程中不被中断的特性,称为原子性。
线程出现原子性的问题是因为线程切换导致,同一时刻只能有一个线程操作共享对象才能解决原子性问题。自然而然我们想到加锁方式来搞定原子性问题。线程A持有锁之后才能访问加锁后的资源,其他线程只能等待,直到线程A释放锁后,才有机会抢占持有锁。实现互斥的条件。

王宝令老师举了一个很生动的例子来说明锁模型。一般办公室早高峰是蹲坑的黄金时间,大家都抢着上厕所,无奈坑位有限,运气好的话你去的时候有坑位,这时候你蹲坑锁门,享受一泻千里的快感。其他同事只能在门外苦苦等候,直到你打开门出来后,其他人才有机会进入这个坑位。这个例子中,我们锁的是不是就是坑位,保护的是我们正常拉屎的隐私和权力。换到代码中,是不是就是锁的是共享变量的访问,保护的是共享变量,这是我们理解锁的关键。

在上一节中已经提到synchronized,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块。他的使用方法如下:

public class Synchronized {
    private final Object LOCK = new Object();

    //  修饰非静态方法
    public synchronized void lockMethod() {
        // 受锁保护的资源,临界区
    }
    //  修饰静态方法
    public synchronized static void lockStaticMethod() {
        // 受锁保护的资源,临界区
    }

    public void  method() {
        // 修饰代码块
        synchronized (LOCK) {
            // 受锁保护的资源,临界区
        }
    }
}

synchronized 的加锁lock() 和解锁 unlock()是由java编译器在修饰方法或代码块前后自动添加,无需我们手动进行加锁和解锁步骤。在上面代码中synchronized 修饰代码块时可以看到锁的是LOCK对象,而synchronized 修饰静态方法锁的是当前类的Class对象,即我们的类Synchronized;当修饰非静态方法时锁的是当前的实例对象this。

也就是说下面代码中lockMethod方法和lockStaticMethod方法是由两个不同的锁this,Synchronized.class保护资源i,不会存在互斥关系,会导致并发问题。一个资源只能由同一把锁保护,同一把锁可以保护N个资源。

public class Synchronized {

    static int i = 0;

    //  修饰非静态方法
    public synchronized void lockMethod() {
        // 受锁保护的资源,临界区
        i++;

    }
    //  修饰静态方法
    public synchronized static int lockStaticMethod() {
        // 受锁保护的资源,临界区
       return i;
    }
}

当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。

  1. 资源间没有关联关系

    比如我们银行卡账户余额balance和银行卡密码pwd两个资源没有直接关联关系。那么我们可以通过balanceLock和pwdLock两个锁来分别管理,不同的资源用不同的锁保护,各自管各自的。当然你也可以用同一把锁来管理这两个资源,只不过会造成不必要的性能浪费,因为这会导致操作串行化。用不同的锁对受保护资源进行精细化管理,能够提升性能,尽量使用细粒度锁是保证性能的关键所在。

public class Account {
    private Double balance;
    private String pwd;
    private final Object balanceLOCK = new Object();
    private final Object pwdLOCK = new Object();

    // 取款
    public void withdraw(Double amt) {
        synchronized (balanceLOCK) {
            if (this.balance > amt) {
                this.balance -= amt;
            }
        }
    }

    // 查看余额
    public Double getBalance() {
        synchronized (balanceLOCK) {
            return balance;
        }
    }
    // 更新密码
    public void updatePassword(String pwd) {
        synchronized (pwdLOCK) {
            this.pwd = pwd;
        }
    }

    // 查看余额
    public String getPwd() {
        synchronized (pwdLOCK) {
            return pwd;
        }
    }
}
  1. 资源间存在关联关系

假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。

public class Account {

    private Double balance;
    private String name;
    public Account(String name, Double balance) {
        this.name = name;
        this.balance = balance;
    }


    public void transfer(Account target, Double money) {
        synchronized (Account.class) {
            if (this.balance > money) {
                this.balance -= money;
                target.balance += money;
            }
        }

    }

我们通过Account.class作为共享锁来实现,Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。但是如果使用class对象作为锁的话,会导致所有的操作都变成串行,降低了执行效率。因此,并不是最合适的选择。

前面我们提到了锁尽可能小的范围细粒度锁,对于上面转账操作A账户转账到B账户,分别加锁。效率肯定就提升了。我们用代码来实现一下A,B两个账户分别加锁:

转账操作
public class Account {
    //账号
    private String accountName;
    // 余额
    private int balance;
    public Account(String accountName,int balance){
        this.accountName = accountName;
        this.balance = balance;
    }
  // 省略get/set方法
}
public class AccountMain implements Runnable {
    //转出账户
    public Account fromAccount;
    //转入账户
    public Account toAccount;
    //转出金额
    public int amount;

    public AccountMain(Account fromAccount,Account toAccount,int amount){
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }
    @Override
    public void run(){
        while(true){
            //获取fromAccount对象的锁
            synchronized(fromAccount){
                //获取toAccount对象的锁
                synchronized(toAccount){
                    //转账进行的条件:判断转出账户的余额是否大于0
                    if(fromAccount.getBalance() <= 0){
                        System.out.println(fromAccount.getAccountName() + "账户余额不足!");
                        return;
                    }else{
                        //更新转出账户的余额:
                        fromAccount.setBalance(fromAccount.getBalance() - amount);
                        //更新转入账户的余额:
                        toAccount.setBalance(toAccount.getBalance() + amount);
                    }
                }
            }
       System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
            System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
        }
    }

    public static void main(String[] args) {
      
        Account fromAccount = new Account("张三",200000);
        Account toAccount = new Account("李四",200000);

        //  每次转出2元.
        Thread a = new Thread(new AccountMain(fromAccount,toAccount,2));
        Thread b = new Thread(new AccountMain(toAccount,fromAccount,2));

        a.start();
        b.start();
    }
}

当我们按照思路写完执行发现,等了好久都没有等到程序结束。尴尬的发现死锁了。



6. 使用synchronized实现死锁

我们来分析下这个例子死锁是怎么造成的,线程a在执行转账操作张三->李四的同一时刻,线程b也执行账户 李四 转账户 张三 的操作。两个线程同时执行到了(1)处,此时线程a的fromAccount是不是就是张三,而对于b线程来说fromAccount就是李四了。此时执行到(2),a 试图获取账户 李四 的锁时,发现账户 李四 已经被锁定(被 b线程 锁定),所以 a 开始等待;b 则试图获取账户 张三 的锁时,发现账户 张三 已经被锁定(被 a线程 锁定),所以 b 也开始等待。于是 a和 b 会无期限地等待下去,最终造成了死锁。

  synchronized(fromAccount){(1)
                //获取toAccount对象的锁
                synchronized(toAccount){(2)
    }
}

总结

这节我们了解到通过加锁互斥的方式可以解决原子性问题,以及synchronized不同加锁方法,加锁的对象。在通过转账例子使用细粒度锁的时候又碰到了死锁问题。下一节来整理下如何避免死锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容