Java面试题~从一个线程并发安全场景谈谈Java的锁、CAS算法以及ABA问题

摘要:

对于“并发安全”,想必各位小伙伴都有所耳闻,它指的是“系统中某一时刻多个线程并发访问一段或者一个有线程安全性问题的代码、方法后,最终出现不正确的结果或者并非最初所预料的结果”。

有问题并有对应的解决方案,而“并发安全性问题”对应的解决方案也无非是根据项目所处的环境而采取相应的措施,如单体环境下采用加synchronized同步锁、原子操作锁atomicXXX 以及 lock等方式;分布式环境下采取分布式锁(基于Redis、ZooKeeper、Redisson…)等方式加以解决。

本文我们将以一个简单的场景:“模拟 网站访问的计数统计”为案例,介绍在多线程并发的场景下出现的结果及其相应的解决方案,包括synchronized、atomicXXX、lock以及介绍CAS算法和ABA相关的问题。

内容:

所谓的“网站访问计数器”,其实也很好理解,就是统计用户在前端浏览器访问网站的次数,当然啦,在本文中,我们将尽量写一个简化版的“网站访问计数器”,用于演示“并发安全” 出现的场景。废话不多说,下面进入代码实战环节

  • 模拟“网站访问计数器”代码实战

(1)如下代码所示,我们直接在类中编写“访问计数”的核心代码(其实就是一个静态变量的共享、数值叠加罢了):

public class WebAccessTotal {
    //方式一
    private static int total;

    public void incrementTotal(){
        total++;
    }
    public static void main(String[] args) throws InterruptedException {
        WebAccessTotal testCount = new WebAccessTotal();
        //开启5个线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //每个线程都让total增200,模拟访问 200次
                for (int j = 0; j < 200; j++) {
                    testCount.incrementTotal();                
}
            }).start();
        }
        sleep(2000);

        //正确的情况下会输出1000
        System.out.println(total);
    }
}

在上述代码中,我们采用了一个静态变量total模拟 统计网站的访问量(虽然粗糙了点~~~),右键点击运行main方法,观察控制台输出的结果,会发现其结果竟然并非我们所预料的1000,而是 < 1000,如下图所示,输出结果竟然是 412,有点令人大跌眼镜:

(2)不过,这个结果却又在情理之中,因为incrementTotal() 方法中的这段代码:total++在多线程并发访问时存在安全性 的问题,具体原因在于 ++ 操作将会被拆分为多个步骤执行,即:int tmp=total+1; total=tmp; 两个步骤:

假设total的初始值为 8,当并发的两个线程A,B同时调用 incrementTotal() 方法时,此时很有可能 A.total=8 ,B.total=8,在执行完该方法之后,A.tmp=9,B.tmp=9,最终该方法的输出结果为:9,而实际上其输出结果应该为:10(因为调用了两次哦)

         而这个过程正是出现“线程不安全”的源头,既然出现了问题,那么就应当想办法进行解决,下面我们进入问题解决方案的实战!

  • 基于Synchronized同步代码块 保证线程安全

(1)首先,我们先采用 Synchronized 关键字同步代码块,试试其最终输出的结果,其源代码如下所示:

//方式二
private static int total;

public synchronized void incrementTotal(){
    total++;
}
public static void main(String[] args) throws InterruptedException {
    WebAccessTotal testCount = new WebAccessTotal();
    //开启5个线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //每个线程都让total增200,模拟访问 200次
            for (int j = 0; j < 200; j++) {
                testCount.incrementTotal();
            }
        }).start();
    }
    sleep(2000);

    //正确的情况下会输出1000
    System.out.println("输出结果:"+total);
}

点击运行,查看控制台的输出结果,会发现结果如我们所料,正是 1000 ,不多也不少:

(2)而加了 Synchronized 关键字 之所以能起到这种效果,主要在于它限定了瞬时访问该方法时多个线程的“同步操作”,即当并发的多个线程要想访问该方法时,需要获取得到该方法的“锁”,只有获取到了“锁”才能执行方法里面的代码逻辑,而同一时刻也就只能有一个线程可以获取得到(其他没获取到的需要堵塞式等待),从而也就导致了 total++ 变成了 一个 “原子性操作”,即要么都做了,要买都不做。

         对于 Synchronized 关键字,我们都知道它是 Java内置的锁,属于悲观、同步锁的一种,其加锁的粒度是相当粗的,在高并发场景下其性能也不是很好。

更多请见:http://www.mark-to-win.com/tutorial/51083.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值