并发程序三大要素

2022-03-24 02:39:36 来源:网络整理 作者:管理员

原标题:并发程序三大要素

主题:并发编程三大要素

目标:用例子讲解3要素;刻意练习:细致完整

目标读者:需要了解并发知识的人

并发编程三大要素

并发即多个线程同时运行。所谓一个和尚挑水喝,两个和尚抬水喝,三个和尚没水喝。做事的人一多,就容易出幺蛾子,程序也不例外。所以,为了保证最后结果的正确性,需要保证下面的三大要素。

可见性(visibility)

一个线程对共享变量进行修改,另外的线程能立马看到

先看下面一个小例子:

public class Code3 {

private static /*volatile*/ int num = 0;

public static void main(String[] args) throws InterruptedException {

System.out.println("程序执行");

new Thread(() -> {

System.out.println("子线程开始");

while (num == 0) {

// System.out.println(num);

}

System.out.println("子线程结束");

}).start();

TimeUnit.SECONDS.sleep(1);

num = 1;

}

}

上面程序的执行结果是:

并发程序三大要素

“子线程结束”一直没有输出出来,意味着,对于子线程来说num一直都是等于0的,循环一直没有结束。但我们在主线程,也就是main方法里明明把num改成1了呀。为什么会这样呢?

这是因为线程在执行的时候会读取出一份共享变量的拷贝到线程本地的缓存中,所以线程们对这个变量的修改,互相之间是不可见的。

解决这个问题的一个办法,就是给变量加上volatile关键字,这个关键字的作用之一,就是保证变量的更新,对所有的线程都是可见的。

我在第9行注释掉的那句打印,也可以解决可见性的问题,因为println()方法里加了synchronized,它也能变量的可见性。

并发程序三大要素

有序性(ordering)

程序执行的顺序和代码的顺序保持一致

程序在实际的执行过程中,不一定是严格按照代码的顺序执行的。

为了提高效率,可能会发生指令重排。

比如,有两句话

  1. 等待用户输入变量y的值
  2. 计算x+1

因为CPU的执行速度很快,在等待语句1执行的过程中,我可以先把语句2给算出来。而不是空在那里等着,因为两句话没什么前后关联。

当然,如果两句话换成了

  1. 等待用户输入变量x的值
  2. 计算x+1

这就肯定不能重排了。

所以,对于单线程来说,就有一个特性叫as if serial,像是顺序执行一样。只要保证单线程结果的最终一致性就可以了。

但,对于多线程来说就可能出现问题。比如下面这个程序。

public class Code3 {

private static int num = 0;

private static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {

System.out.println("程序执行");

new Thread(() -> {

System.out.println("子线程开始");

while (flag) {

}

System.out.println(num);

}).start();

TimeUnit.SECONDS.sleep(1);

num = 1;

flag = false;

}

}

因为15行和16行可能出现重排的现象,flag=false先执行,再执行num=1,就可能导致最终11行子线程输出的num值为0,而不是1。

当然这个做实验做很多次也不一定能做得出来,只是有可能发生。

解决这个问题已经可以使用volatile关键字,volatile的另一个作用就是禁止重排序。

具体的实现机制是增加内存屏障(Memory Barrier)

  • LoadLoadBarrier,load1;屏障;load2,即屏障的前后都是读指令,则load2必须等待load1执行完毕。
  • StoreStoreBarrier,store1;屏障;store2,即屏障的前后都是写指令,则store2必须等待store1执行完毕。
  • LoadStoreBarrier,load;屏障;store,即屏障的前是读指令,屏障后是写指令,则store必须等待load执行完毕。
  • StoreLoadBarrier,store;屏障;load,即屏障的前是写指令,屏障后是读指令,则load必须等待store执行完毕。
  1. 写屏障(即volatile写之前都不能写,volatile写之后才可以读) StoreStoreBarrier volatile写(store) StoreLoadBarrier
  2. 读屏障(即volatile读之后,才可以读写) volatile读(load) LoadLoadBarrier LoadStoreBarrire

原子性(atomicity)

不可分割的操作,要么都成功,要么都失败

还是先来一段小代码:

public class Code3 {

private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {

Thread[] threads = new Thread[100];

CountDownLatch countDownLatch = new CountDownLatch(threads.length);

for (int i = 0; i < threads.length; i++) {

threads[i] = new Thread(() -> {

for (int j = 0; j < 100; j++) {

num++;

}

countDownLatch.countDown();

});

}

for (Thread thread : threads) {

thread.start();

}

countDownLatch.await();

System.out.println(num);

}

}

起100个线程,同时对num这个变量做100次自增操作,理想的结果应该是100*100=10000。但我的机器上测试,实际的结果是9000多。

说明一个什么问题呢,就是自增这个操作不是原子性的,因为它可能中间过程被打断。

假设自增有三步:

  1. 把num值取出来
  2. 把num值加一
  3. 把num值放回去

就可能出现,当前num=0,线程1把0取出来了,并且完成了加一,把值变成了1,这时候线程2也来了,它取出来的也是0,并且把值从0改成1,并且把1的值写了回去。然后这时候线程1开始执行第3步,又一次把1写了回去。这就导致了数据不一致的结果。如果自增操作不可打算的话,两个线程执行完的结果应该是2,而不是1。

解决这个问题的办法,就是上锁。

上锁的本质:让并发的程序序列化,即把原本同时执行的程序,改成前后顺序执行。

悲观锁

认为这个操作一定会被打断,所以不管三七二十一,先锁上再说。通过synchronized实现。(第10行)

public class Code3 {

private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {

Thread[] threads = new Thread[100];

CountDownLatch countDownLatch = new CountDownLatch(threads.length);

for (int i = 0; i < threads.length; i++) {

threads[i] = new Thread(() -> {

for (int j = 0; j < 100; j++) {

synchronized (Code3.class) {

num++;

}

}

countDownLatch.countDown();

});

}

for (Thread thread : threads) {

thread.start();

}

countDownLatch.await();

System.out.println(num);

}

}

乐观锁

认为这个操作不会被打断,所以先不上锁,在写入的时候验证原数据是否被修改,如果被修改了,就读取新的值,再重试一遍,直到成功为止。通过CAS(Compare And Swap/Set)实现。

java自带有CAS方式的整形类AtomicInteger

public class Code3 {

private static AtomicInteger num = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {

Thread[] threads = new Thread[100];

CountDownLatch countDownLatch = new CountDownLatch(threads.length);

for (int i = 0; i < threads.length; i++) {

threads[i] = new Thread(() -> {

for (int j = 0; j < 100; j++) {

num.incrementAndGet();

}

countDownLatch.countDown();

});

}

for (Thread thread : threads) {

thread.start();

}

countDownLatch.await();

System.out.println(num);

}

}

锁类型的选择

并不是乐观锁看着名字比较积极就无脑选择乐观锁比较好。

因为乐观锁会一直频繁的重试,直到成功为止,这个重试的过程也是会消耗cpu资源的。

而悲观锁通过等待队列的方式实现,在等待锁的过程中不消耗资源,所以可以视情况而定。

如果锁内部执行的时间较长,且排队人数很多,就可以选择悲观锁。

如果锁内部执行时间很多,且排队人数不多,就可以选择乐观锁。

字数:不统计

耗时:2小时45分

··················END··················

显示全文
为您推荐
钱姓起源和来历:活了八百岁的彭祖竟然是钱氏祖先
钱姓起源和来历:活了八百岁的彭祖竟然是钱氏祖先

钱姓排于百家姓中的第二位,虽然起源和来历并不多,但是钱姓人却分布广泛,成为了中国姓氏中的第二大姓。钱姓的主要来源就是彭祖的儿子篯孚,彭祖的真名叫做篯铿,是因为被尧封于大彭而得姓-彭,而他的儿子篯孚因为掌管着钱财,任职钱府上士,所以便为“钱”姓,所以彭钱是一家。...

发布时间:2024-09-20 20:03:27

胡服最早由谁引入中原?赵武灵王为推行骑射改革服饰
胡服最早由谁引入中原?赵武灵王为推行骑射改革服饰

胡服最早是由春秋战国时的赵武灵王-赵雍引入中原的,因为当时赵国的地形和地理位置都非常不好,常年深受战争威胁,加上赵武灵王继位之后,时局不稳,周边国家都想要攻打赵国,所以赵武灵王便下令让全员都推行胡服,并且学习骑射,相较于宽大的汉服更加便于行动。...

发布时间:2024-09-20 19:05:44

赛艇运动起源于哪里?英国船工无聊时竞赛(造就贵族运动)
赛艇运动起源于哪里?英国船工无聊时竞赛(造就贵族运动)

赛艇运动起源于英国的泰晤士河上,当时还是17世纪,英国泰晤士河上的船工在工作之余经常会举办一些赛船,久而久之就形成了一种习俗,在1715年的时候,为了庆祝英王的卫冕,所以第一次正式举办了赛艇比赛,最终在1775年发展为一个正式的运动项目,并且成立了相应的运动俱乐部。...

发布时间:2024-09-20 18:04:28

为什么不能用宸字取名?哪些名字普通人扛不起
为什么不能用宸字取名?哪些名字普通人扛不起

人们之所以不愿意或者不敢以“宸”为名,主要是因为在古时候“宸”有着“深邃的房屋”的意思,也就是专指皇帝的居住场所,象征着无上的权力,所以“宸”字自然也就成为了禁忌,不过现代人大多是害怕这个字的气势,普通人的命格扛不住。...

发布时间:2024-09-20 17:00:53

商细蕊原型是谁?原来四位京剧名旦都是灵感来源
商细蕊原型是谁?原来四位京剧名旦都是灵感来源

商细蕊是电视剧《鬓边不是海棠红》中的京剧名旦,很多人都为他和海归商人程凤台的知己故事而动容,但遗憾的是在历史长河中并没有商细蕊真正的原型,只能说他可能是作者根据历史上著名的京剧名旦构建的一个角色,因为他和梅兰芳、程砚秋、尚小云以及徐碧云等四人的经历和性格都有着极为相似之处。...

发布时间:2024-09-20 16:05:01

历史上著名的美女有哪些:杨玉环,君王不早朝(绝世美女)
历史上著名的美女有哪些:杨玉环,君王不早朝(绝世美女)

在中华上下五千年的历史长河中,除了王朝更替的政治生活与文化生活,还有一些美貌的女子点缀着历史的绘卷,那么在悠悠岁月中有哪些著名的美女呢?下面小编就来为大家盘点一下吧!...

发布时间:2024-09-20 15:04:04

历史上妃嫔最少的皇帝:明孝宗朱佑樘,独爱张皇后(童年不幸)
历史上妃嫔最少的皇帝:明孝宗朱佑樘,独爱张皇后(童年不幸)

谈及古代帝王,最让人津津乐道的当属那后宫佳丽三千,不过也不是所有的帝王都有那么充实的后宫,比如历史上有一位皇帝就只有一位妻子,是不是让人有点不敢相信,下面小编就带大家一起了解下吧!...

发布时间:2024-09-20 14:01:05

历史上最长情的皇帝:顺治皇帝,挚爱董鄂妃(为爱出家)
历史上最长情的皇帝:顺治皇帝,挚爱董鄂妃(为爱出家)

自古无情帝王家,说到帝王的爱情多是奢望,很多帝王后宫的妃子换了一个又一个,几乎都是薄情寡义的,但是漫长的历史长河中也有例外,有的皇帝便与众不同,特别的长情,下面小编来为大家介绍一下历史上最长情的皇帝吧!...

发布时间:2024-09-20 13:03:24

武侠剧中的绝世高手有哪些:扫地僧,化险为夷(秒杀高手)
武侠剧中的绝世高手有哪些:扫地僧,化险为夷(秒杀高手)

可以说人的成长伴随着无数的武侠剧,尤其是电视剧中的那些绝世高手更是让人影响深刻,有不少人小时候都幻想过可以武功盖世,一统江湖,那么有人知道武侠剧中有哪些绝世高手吗?下面小编为大家盘点一下吧!...

发布时间:2024-09-20 12:02:19

电视剧中的古装美女:刘亦菲,举止摇曳生姿(天仙下凡)
电视剧中的古装美女:刘亦菲,举止摇曳生姿(天仙下凡)

古装电视剧中有许多的美丽的女子,她们身着美丽的衣裳,一举一动摇曳生姿,美得让人挪不开双眼,可以说是一场视觉盛宴,下面就让小编为大家盘点一下那些古装美女吧!...

发布时间:2024-09-20 11:09:01

清朝古装剧中的美人:刘诗诗,若曦无人超越(顾盼生姿)
清朝古装剧中的美人:刘诗诗,若曦无人超越(顾盼生姿)

近年清朝古装剧收视率暴涨,这些电视剧不仅剧情引人入胜,身着清装的美人也同样吸引了不少目光,有着让人过目不忘的美貌,那么清朝古装剧中有哪些美人呢?下面小编就来为大家盘点一下吧!...

发布时间:2024-09-20 11:02:08

小青龟能长多大:背甲14厘米,体色会变化(濒危物种)
小青龟能长多大:背甲14厘米,体色会变化(濒危物种)

养龟市场上经常可以看到小青龟,这种乌龟乖巧可爱以及性情温顺,所以很多人都会选择小青龟来饲养,不过一直都是见到小青龟都比较小,这不禁令人好奇这种乌龟能长多大?下面小编就带大家一起了解一下小青龟吧!...

发布时间:2024-09-20 10:01:47