并发程序三大要素
主题:并发编程三大要素
目标:用例子讲解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)
程序执行的顺序和代码的顺序保持一致
程序在实际的执行过程中,不一定是严格按照代码的顺序执行的。
为了提高效率,可能会发生指令重排。
比如,有两句话
- 等待用户输入变量y的值
- 计算x+1
因为CPU的执行速度很快,在等待语句1执行的过程中,我可以先把语句2给算出来。而不是空在那里等着,因为两句话没什么前后关联。
当然,如果两句话换成了
- 等待用户输入变量x的值
- 计算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执行完毕。
- 写屏障(即volatile写之前都不能写,volatile写之后才可以读) StoreStoreBarrier volatile写(store) StoreLoadBarrier
- 读屏障(即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多。
说明一个什么问题呢,就是自增这个操作不是原子性的,因为它可能中间过程被打断。
假设自增有三步:
- 把num值取出来
- 把num值加一
- 把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··················
相关文章
-
英语专业退学自学编程,结果36岁当上阿里巴巴合伙人,阿里良将如何批量养成?
-
数据赋能营销 品牌破局之道 ZOL科技互联网生态研讨会盛世将启
-
苹果宣布取消AirPower项目;阿里证实全资收购Teambition;「网约车第一股」Lyft上市首日收涨逾8%
-
【虎嗅早报】特朗普“封杀”中兴华为?外交部:安全问题最好用事实来说话;锤子投资人:罗永浩没有卸任CEO
-
营销观察|游戏业务承压之下,腾讯广告的快与慢
-
玩家EA账号创建原始邮箱被改,EA账号频繁多次被盗的安全问题
-
Signal 表示它不会遵守澳大利亚的反加密法案
-
解析企业微信crm系统作用
-
王健林瘦身、张近东增肥,苏宁易购吃下万达百货
-
热点|拼多多增加补贴5亿元联合百大品牌促“正品下乡”
-
阿里最强AI芯片发布!1秒处理7.8万张照片,一块顶10块GPU
-
腾讯新推短视频「哈皮」,想要PK抖音仍有难度
-
德国工业可穿戴设备初创公司ProGlove获得3600万欧元投资
-
互联网的2018:巨头掉头 齐向供给侧数字化
-
36氪独家|微信朋友圈第三条广告全量开放,商业化变现时隔一年再次提速
-
金立原总裁加入小米,滴滴上线金融服务 | 天下网事1.03
-
美食电商定位左右摇摆,三心二意的环球捕手能走多远?
-
中国研发全球首个对多种变异株均有效疫苗;新加坡再爆客工宿舍感染群
-
焦点分析丨与特斯拉终有一战,蔚来打算先用自己的方式先追赶它
-
小红书爆款标题的套路!99%的博主都在用,一看就会
-
收购苹果不开玩笑?网友建议老罗踏实做好眼前事
-
苹果iPhone手机安装台服英雄联盟手游及台服lol苹果ios手机怎么下载安装?
-
全能E家分享:铝塑管和PB管有何区别
-
最前线|百度搜索引擎已死?王小川:欢迎使用搜狗
-
消息称比特大陆再裁员,AI业务成重灾区
-
从12张对比照片看:外国版“求大神PS”系列的幽默之处
-
AI如何融入实体经济?听听大咖怎么讲
-
小程序和新零售,双双涉入深水区
-
陷艾滋传播争议 同性社交软件Blued宣布关停整改一周