并发程序三大要素

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··················

显示全文
为您推荐
太白金星的真身是什么:李长庚(老子的徒弟)
太白金星的真身是什么:李长庚(老子的徒弟)

【导读】 太白金星的真身是什么:李长庚(老子的徒弟),下面是小编为你收集整理的,希望对你有帮助!对于太白金星本人的真身有很多说法,有的说太白金星就是李白,还有的说太白金星就是天上的金星。其实太白金星原名交李长庚,是老子的徒弟,太上老君原本是平凡之人,经过了多年修炼之后,终于领悟到了道德真言,并且......

发布时间:2023-10-02 18:01:15

宋仁宗张贵妃的爱情故事 宋仁宗张贵妃关系如何
宋仁宗张贵妃的爱情故事 宋仁宗张贵妃关系如何

宋仁宗在历史上绝对算得上是一位好皇帝,把宋朝治理的景景有条,皇上都有后宫佳丽三千,会有很多的妃子为皇后传宗接代。宋仁宗也有很多的妃子,但他最爱的就是张贵妃了,两人的爱情故事也被后人传颂。...

发布时间:2023-10-02 18:00:11

滑齿龙:欧洲大型海洋爬行类(体长6米/长有鱼鳍)
滑齿龙:欧洲大型海洋爬行类(体长6米/长有鱼鳍)

【导读】 滑齿龙:欧洲大型海洋爬行类(体长6米/长有鱼鳍),下面是小编为你收集整理的,希望对你有帮助!滑齿龙是一种上龙亚目的海洋爬行动物,生存于1亿7000万年前的侏罗纪中期,外形非常像是蛇颈龙,四肢呈现鱼鳍状,而脖子较短,嘴巴像鳄鱼,它最大的特点就是会利用鼻孔在水中搜寻猎物的气味,体长可达6米,属于......

发布时间:2023-10-02 17:01:17

宋仁宗无子的真实原因 宋仁宗孩子为什么死了那么多
宋仁宗无子的真实原因 宋仁宗孩子为什么死了那么多

宋仁宗没有儿子的原因就是他儿子在没出生多长时间就死了。本来宋仁宗有四个儿子,十二个女儿,没想到后来四个儿子都去世了,十二个女儿也才只剩下四个,这对宋仁宗来说是很大的打击,后来宋仁宗不得不让自己的义子来接替他的皇位。...

发布时间:2023-10-02 17:00:11

QQ象棋分几个级别
QQ象棋分几个级别

【导读】 QQ象棋分几个级别,下面是小编为你收集整理的,希望对你有帮助!QQ普通中国象棋与QQ新中国象棋均为13个级别。QQ普通中国象棋级别由低到高分别如下:草民、县丞、县令、都尉、校尉、常侍、中郎将、太守、刺史、将军、太尉、大将军、王。QQ新中国象棋级别由低到高分别如下......

发布时间:2023-10-02 16:01:13

宋仁宗四个公主是谁 这些公主结果怎么样下场如何
宋仁宗四个公主是谁 这些公主结果怎么样下场如何

宋仁宗的孩子是比较多的,但是有很多都夭折了。宋仁宗的命运是比较坎坷的,自己身为明君,但是并没有好的报应,本来有十三个女儿,最后只活下来四个,有三个儿子全部夭折,当时宋仁宗也是比较痛苦的,宋仁宗的四个闺女是比较优秀的。...

发布时间:2023-10-02 16:00:09

宋仁宗多爱张贵妃 宋仁宗为何唯独偏爱张贵妃
宋仁宗多爱张贵妃 宋仁宗为何唯独偏爱张贵妃

宋仁宗是很爱张贵妃,虽然张贵妃不是皇后,但是宋仁宗对张贵妃是很好的,后宫佳丽三千唯独宠张贵妃一人。张贵妃所拥有的魅力是其他人所没有的。...

发布时间:2023-10-02 15:41:11

旧鲨齿龙:最古老的鲨齿龙科(最长10米/仅出土尾椎骨)
旧鲨齿龙:最古老的鲨齿龙科(最长10米/仅出土尾椎骨)

【导读】 旧鲨齿龙:最古老的鲨齿龙科(最长10米/仅出土尾椎骨),下面是小编为你收集整理的,希望对你有帮助!旧鲨齿龙是一种兽脚亚目下的鲨齿龙科恐龙,也是目前已经发现的最原始的鲨齿龙科恐龙,诞生于侏罗纪的末期,平均体长可以达到8.5-10米,属于大型肉食恐龙的一种,它的第一批化石是在非洲的坦桑尼亚发现的。旧鲨......

发布时间:2023-10-02 15:01:16

宋朝最昏庸的皇帝是谁 他为什么可以成功登位
宋朝最昏庸的皇帝是谁 他为什么可以成功登位

宋朝昏庸的皇帝有着许多,但是最有名的无疑就是宋徽宗。在宋徽宗当政时期,无恶不作并且还进行卖官的买卖。宋徽宗可以说是享受了足够的荣华富贵,所住之地,所做之事都彰显了他追求奢侈生活的追求,他持政对于全国的百姓来说都一种悲哀。...

发布时间:2023-10-02 15:00:13

怎么炒西瓜
怎么炒西瓜

【导读】 怎么炒西瓜,下面是小编为你收集整理的,希望对你有帮助!1、西瓜皮洗净切条,火腿肠切条备用。2、锅内热油炒香辣椒、蒜瓣。3、倒入西瓜皮、火腿肠,加入盐、鸡精翻炒均匀即可。西瓜皮:西瓜皮,别名西瓜翠衣。来源为葫芦科植物西瓜的外层果皮。采制7~8月收集西瓜皮,......

发布时间:2023-10-02 14:01:24

宋朝皇帝能力排名前五名 赵匡胤是宋朝的开国皇帝
宋朝皇帝能力排名前五名 赵匡胤是宋朝的开国皇帝

宋朝在我国历史上是个盛世,经过多个皇帝的治理,每个皇帝都是很优秀的。也有人对宋朝皇帝进行了总排名,看看他们几个到底谁的功劳最大,在宋朝后期自然也出现了很多个昏君,这些就不做评判了,现在主要讲的是功劳大的。...

发布时间:2023-10-02 14:00:16

apm线和地铁什么区别
apm线和地铁什么区别

【导读】 apm线和地铁什么区别,下面是小编为你收集整理的,希望对你有帮助!APM属于中低运量系统,地铁属于大运量公共交通系统。在轨道交通里APM距离短、车身小、中运量载客少、启动和停车加速快,且使用橡胶轮相比钢轨更富弹性,因此,乘坐体感和传统地铁不同。APM浦江线全自动无人......

发布时间:2023-10-02 13:01:16