【漫画】JAVA并发编程 如何解决原子性问题

原创声明:本文转载自公众号【胖滚猪学编程】,转载务必注明出处!java

并发编程BUG源头文章中,咱们初识了并发编程的三个bug源头:可见性、原子性、有序性。在如何解决可见性和原子性文章中咱们大体了解了可见性和有序性的解决思路,今天轮到最后一个大bug,那就是原子性。编程

知识回顾

_1

锁模型

_2
_3

JAVA中的锁模型

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。微信

  • synchronized 是独占锁/排他锁(就是有你没个人意思),可是注意!synchronized并不能改变CPU时间片切换的特色,只是当其余线程要访问这个资源时,发现锁还未释放,因此只能在外面等待。
  • synchronized必定能保证原子性,由于被 synchronized 修饰某段代码后,不管是单核 CPU 仍是多核 CPU,只有一个线程可以执行该代码,因此必定能保证原子操做
  • synchronized也可以保证可见性和有序性。根据前第二篇文章:Happens-Before 规则之管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。即前一个线程的解锁操做对后一个线程的加锁操做可见。综合 Happens-Before 的传递性原则,咱们就能得出前一个线程在临界区修改的共享变量(该操做在解锁以前),对后续进入临界区(该操做在加锁以后)的线程是可见的。- synchronized 关键字能够用来修饰静态方法,非静态方法,也能够用来修饰代码块

理论说完了,来点实际的吧!首先咱们用synchronized 修饰非静态方法来改写第一章中原子性问题的那段代码:并发

private long count = 0;
    
    // 修饰非静态方法 当修饰非静态方法的时候,锁定的是当前实例对象 this。
    // 当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,若是这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不一样方法,可是他们使用同一个对象来调用,那么这些方法的锁就是同样的,就是这个对象,那么会形成阻塞。若是多个线程经过不一样的对象来调用方法,那么他们的锁就是不同的,不会形成阻塞。
    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized2 test = new TestSynchronized2();
        // 建立两个线程,执行 add() 操做
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // 启动两个线程
        th1.start();th2.start();
        // 等待两个线程执行结束
        th1.join();th2.join();
        System.out.println(test.count);
    }

运行一下吧!你会发现永远均可以达到咱们想要的效果了~
除了上面代码中修饰非静态方法,还能够修饰静态方法和代码块app

// 修饰静态方法 当修饰静态方法的时候,锁定的是当前类的 Class 对象,即TestSynchronized2.class 。这个范围就比对象锁大。这里就算是不一样对象,可是只要是该类的对象,就使用的是同一把锁。
    synchronized static void bar() {
        // 临界区
    }
    // 修饰代码块 java中经典的双重锁检查机制
    private volatile static TestSynchronized2 instance;
    public static TestSynchronized2 getInstance() {
        if (instance == null) {
            synchronized (TestSynchronized2.class) {
                if (instance == null) {
                    instance = new TestSynchronized2();
                }
            }
        }
        return instance;
    }

明确锁和资源的关系

深刻分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。受保护资源和锁之间的关联关系是 N:1 的关系。若是一个资源用N个锁,那确定出问题的,就好像一个厕所坑位,你有10把钥匙,那不是能够10我的同时进了?性能

如今给出两段错误代码,想想到底为啥错了吧?this

static long value1 = 0L;

    synchronized long get1() {
        return value1;
    }

    synchronized static void addOne1() {
        value1 += 1;
    }
long value = 0L;

    long get() {
        synchronized (new Object()) {
            return value;
        }
    }

第一段错误缘由:
由于咱们说过synchronized修饰普通方法 锁定的是当前实例对象 this 而修饰静态方法 锁定的是当前类的 Class 对象
因此这里有两把锁 分别是 this 和 TestSynchronized3.class
因为临界区 get() 和 addOne() 是用两个锁保护的,所以这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就致使并发问题了。atom

第二段错误缘由:
加锁本质就是在锁对象的对象头中写入当前线程id,可是synchronized (new Object())每次在内存中都是新对象,因此加锁无效。线程

问:刚刚的例子都是多个锁保护一个资源,这样百分百是不行的。那么一个锁保护多个资源,就必定能够了吗?3d

答:若是多个资源彼此之间是没有关联的,那能够用一个锁来保护。若是有关联的话,那是不行的。好比说银行转帐操做,你给我转帐,我帐户多100,你帐户少100,我不能用个人锁来保护你,就像现实生活中个人锁是不能保护你的财产的。

划重点!要区分多个资源是否有关联!可是一个锁保护多个没关联的资源,未免性能太差了哦,好比我听歌和玩游戏能够同时进行,你非得让我作完一个再作另外一个,岂不是要双倍时间。因此即便一个锁能够保护多个没关联的资源,可是通常而已,会各自用不一样的锁,可以提高性能。这种锁还有个名字,叫细粒度锁。

问:刚刚说到银行转帐的案例,那么假如某天在某银行同时发生这样一个事,柜员小王须要完成A帐户给B帐户转帐100元,柜员小李须要完成B帐户给A帐户转帐100元,请问如何实现呢?

答:其实用两把锁就实现了,转出一把,转入另外一把。只有当二者都成功时,才执行转帐操做。

public static void main(String[] args) throws InterruptedException {
        Account a = new Account(200); //A的初始帐户余额200
        Account b = new Account(300); //B的初始帐户余额200
        Thread threadA = new Thread(()->{
            try {
                transfer(a,b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                transfer(b,a,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        threadB.start();
    }

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        synchronized (source) {
            log.info("持有锁{} 等待锁{}",source,target);
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

至此,恭喜你,一波问题解决了,但是遗憾的告诉你:又致使了另外一个bug。这段代码是有可能发生死锁的!并发编程中要注意的东西可真是多哟。我们先把死锁这个名词记住!持续关注【胖滚猪学编程】公众号!在咱们后面的文章中找答案!

如何保证原子性

如今咱们已经知道互斥锁能够保证原子性,也知道了如何使用synchronized来保证原子性。但synchronized 并非JAVA中惟一能保证原子性的方案。

若是你粗略的看一下J.U.C(java.util.concurrent包),那么你能够很显眼的发现它俩:
image.png

一个是lock包,一个是atomic包,只要你英语过了四级。。我相信你均可以立刻判定,它们能够解决原子性问题。

因为这两个包比较重要,因此会放在后面的模块单独说,持续关注【胖滚猪学编程】公众号吧!

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!形象来源于微信表情包【胖滚家族】喜欢能够下载哦~

相关文章
相关标签/搜索