# 《并发设计模式》第03章-不可变模式-有哪些方法能够解决并发问题?

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:掌握解决并发问题的多种方法,能够从有锁和无锁两个角度深入理解并发问题的解决方案,能够结合自身实际项目思考并发问题出现的原因及其解决方案,并将解决并发问题的方案灵活应用到自身实际项目中。

大家好,我是冰河~~

“原来我之前写的代码存在严重的并发问题,这下我可要好好学学并发编程了,经过老大的耐心讲解,我已经知道了之前代码出现并发问题的原因了,也就是多个线程同时读写共享变量时,会将共享变量复制到各自的工作内存中进行处理,这样就会导致缓存不一致的问题。那怎么解决问题呢?看来还是要向老大请教才行呀!”,小菜认真的思考着。

# 一、情景再现

小菜开发的统计调用商品详情接口次数的功能代码存在严重的线程安全问题,会导致统计出来的结果数据远远低于预期结果,这个问题困扰了小菜很长时间,经过老王的耐心讲解,小菜已经明白了出现线程安全问题的原因。但是,作为211、985毕业的高材生,小菜并不会止步于此,他可是立志要成为像老王一样的牛人。所以,他也在思考着解决这些线程安全问题的方案。

# 二、寻求帮助

尽管小菜思想上很积极,也很主动,但是对于一个刚刚毕业的应届生来说,很多知识不够系统,也不够全面,在网上搜索对应的解决方案时,也不知道哪些信息是正确的,哪些是模棱两可的。于是,小菜决定还是要请教自己的直属领导老王。

这天,小菜还是早早的来到了公司等老王的到来。过了一会儿,他看到老王来到了公司,便主动走到老王的工位说:“老大,我现在知道我写的代码为什么会出现线程安全的问题了,但是有哪些方案可以解决这些问题,我现在还不太清楚,可以给我讲讲吗?”。

“可以,你拿上笔和本子,我们还是到会议室说吧”,说着,老王便拿起了电脑,与小菜一起向会议室走去。

# 三、并发问题解决方案

“我们先来从整体上了解下解决并发问题存在哪些方案,其实,总体上来说,解决并发问题可以分为有锁方案和无锁方案”,说着老王便打开电脑画了一张解决并发问题解决方案的图,如图3-1所示。


老王接着说:“看这张图,解决并发问题的方案总体上可以分成有锁方案和无锁方案,有锁方案可以分成synchronized锁和Lock锁两种方案,无锁方案可以分成局部变量、CAS原子类、ThreadLocal和不可变对象等几种方案。小菜你先把这张图记一下,接下来,我们再一个个讲一下这些方案”。

“好的”,小菜回应道。

# 四、加锁方案

“好了,我们继续讲,这里,我们一起讲synchronized锁和Lock锁,它们统称为加锁方案”,老王说道,“像synchronized锁和Lock锁,都是采用了悲观锁策略,实现的功能类似,只不过synchronized锁是通过JVM层面来实现加锁和释放锁,在使用时,不需要我们自己手动释放锁。而Lock锁是通过编码方式实现加锁和释放锁,在使用时,需要我们自己在finally代码块中释放锁,我们先来看一段代码”。说着,老王便在IDEA中噼里啪啦的敲了一段代码,这段代码的类是SynchronizedLockCounter。

SynchronizedLockCounter类的源码详见:concurrent-design-patterns-immutable工程下的io.binghe.concurrent.design.right.SynchronizedLockCounter。

public class SynchronizedLockCounter {
    private int count;
    private Lock lock = new ReentrantLock();

    public void lockMethod(){
        lock.lock();
        try{
            this.add();
        }finally {
            lock.unlock();
        }
    }

    public synchronized void synchronizedMethod(){
        this.add();
    }

    private void add(){
        count++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

“看这个类,lockMethod()使用了Lock加锁和释放锁,并且是我们自己在finally代码块中手动释放了锁。而使用synchronized加锁时,并没有手动释放锁,两个方法都具备原子性。这点明白吗?”。

“明白”,小菜说道。

“好,那接下来,我们再分析下上面的代码,其实,在执行count++操作时,还是会分成三个步骤”。

1.从主内存读取count的值。

2.将count的值进行加1操作。

3.将count的值写回主内存。

“使用synchronized和Lock对方法加锁,都会保证上面三个步骤的原子性,那是怎么保证的呢?我们再来看一张图”,说着老王又画了一张图,如图3-2所示。

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码