​并发编程是提升程序性能的有效手段。不过,你是否真的了解并发编程......

1、并发编程 Bug 的根源是什么?
2、volatile 实质上是解决什么问题?
3、什么是Happens-Before 规则?
4、什么是管程?
5、程序一不小心死锁了怎么办? 怎么避免死锁?

目录:

一、为什么要并发编程
二、并发编程的核心要点
三、并发编程需要注意的问题
四、并发编程Bug的源头

为什么要并发编程

虽然说并发编程的第一原则是不要写并发程序。但是,随着硬件的驱动和国内互联网行业的飞速发展,对软件系统的并发量要求越来越高,传统的中间件和数据库已经成为性能的瓶颈。并发编程已经成为绕不开的话题,也慢慢成为软件工程师的必备技能。

并发编程可以提升对CPU的使用效率,降低系统的响应时间,提升系统的容错能力。总结起来就是提升系统性能,提高工作效率。再往高一层是满足人们日益增长的物质文化需求。

并发编程的核心要点

并发编程可以总结抽象为三个核心的要点:分工,同步,互斥。

所谓分工,指的是如何高效地拆解任务并分配给线程。现实中有很多分工的例子,和现实对比有助于理解。如项目经理拆解任务,分派给各项目成员,项目成员并行处理任务。再如,跟分工相关的设计模式——生产-消费者模式,可以类比餐馆大厨和服务员。大厨是生产者,负责炒菜,做完放在出菜口,服务员是消费者,负责把菜端给客户。

同步,指线程间如何协作。还是上面的例子,项目组各成员在执行任务的过程中,任务之间可能存在依赖的关系,一个任务结束以后,依赖它的后续任务就可以开工了,那后续的任务要怎么知道可以开工了呢?这就靠沟通协作了,现实中可能发邮件、发微信、或者口头传达。在并发编程领域,线程之间的协作靠管程来解决。线程协作基本可以描述为等待-唤醒机制,当某个条件不满足时,线程等待,当某个条件满足时,线程被唤醒继续执行。

互斥,则是保证同一时刻只允许一个线程访问共享的资源。并发程序里,多个线程同时访问同一个共享变量时,结果是不确定的。不确定就会引发线程安全的问题,解决线程安全问题的核心方案是互斥,实现互斥的核心技术是锁。
线程安全例子:

public class ThreadTest1 {
private static long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() throws InterruptedException {
final ThreadTest1 test = new ThreadTest1();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(new Runnable(){
@Override
public void run() {
test.add10K();
}
});
Thread th2 = new Thread(new Runnable(){
@Override
public void run() {
test.add10K();
}
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
public static void main(String[] args) throws InterruptedException{
for(int i=0;i<20;i++){
calc();
System.out.println("第"+(i+1)+"次计算结果:"+count);
count = 0;
}
}
}

上面的程序,第一眼看过去,觉得结果应该是20000,在单线程里调用两次add10K()方法,count的值就是20000,但实际上calc()的执行结果是 10000 到 20000 之间的随机数。因为当多个线程同时调用add10K()时,存在数据竞争。互斥,是解决数据竞争的有效手段。

程序运行结果:

并发编程需要注意的问题

并发编程需要注意的问题很多,主要包括:安全性问题、活跃性问题、性能问题。

安全性问题

我们经常说或者听说这个方法不是线程安全的,前面讲互斥也提到了线程安全,那什么是线程安全了?

线程安全的本质是正确性,即程序按照我们期望的执行,不会产生不确定的结果。对于一个程序,首先要保证的肯定是程序的正确性。当多个线程同时访问同一数据时,如果不采取防护措施,就会出现并发 Bug,专业术语叫数据竞争(Data Race),比如上面说到的累加操作。

对上面add10K()方法稍微改进一下,是否就不存在并发 Bug 了呢?

synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1);
}
}

修改后的代码,所有访问共享变量的地方都加了互斥锁,此时不存在数据竞争。但是,set(get()+1)依赖get()的执行结果。除了数据竞争,还可能存在竞态条件(Race Conditon)。竞态条件,指程序的执行结果依赖线程的执行顺序。

现假设count=0,当两个线程同时执行到set中的get方法时,get被锁串行化,第一个线程拿到 count 等于0,还没来得及执行完 set(0+1) 操作,第二个线程执行get()方法拿到的count 也是0(此时线程一已经执行完 get() 方法,释放了锁,所以线程二可以进入get() 方法)。这时,两个线程的执行结果都是1,将结果写入内存,得到 count = 1,而我们期望的是2,这就是竞态条件导致的并发 Bug。

将锁加在 add10K()方法能解决该问题,但是会引发下一个问题:性能问题。

性能问题

安全性问题的解决方案是加锁。锁的本质是使并行的操作串行化,如果串行化的范围过大,就没有发挥多线程的优势。而我们之所以搞多线程就是为了提升性能。这样一来,线程安全问题和性能问题就成了一对矛盾体,我们只有平衡好这对矛盾体才能设计出安全、高效的并发程序。

既然使用锁会带来性能问题,最好的方案是使用无锁的算法和数据结构。这方面相关的技术有线程本地存储,写入时复制,乐观锁,java并发包的原子类等。其次,减少锁持有的时间,相关的技术包括 ConcurrentHashMap 里的分段锁,读写锁(读无锁,写才会互斥)。

活跃性问题

所谓活跃性问题,指的是某个操作无法执行下去。常见的有”死锁”,其他还有“活锁”和”饥饿”。

线程1和线程2都要访问共享资源 A 和 B,对A、B 加锁以后,如果线程1持有了资源A等待B被其他线程释放,而线程2持有了资源B等待A被其他线程释放,就是进入死等状态。解决死锁的方案只有一个:重启。怎么避免死锁,并发解决方案中会提及。

活锁,指线程虽然没有发生阻塞,但仍然会执行不下去。活锁可以类比生活中一个例子,路人甲从左手边出门,路人乙从右手边进门,为了避免相撞,甲乙相互谦让,甲让路走他的右手边,乙让路走他的左手边,结果又相撞了。

活锁的解决方案比较简单,谦让时,尝试等待一个随机时间就可以了。

饥饿是指线程因无法访问资源而一直等待,无法往下执行的情况。解决饥饿最有效的方式是使用公平锁,线程的等待是有顺序的,排在等待队列前面的线程会优先获取资源。

并发Bug的源头

宏观上,并发编程表现为安全性问题、活跃性问题、性能问题;微观上,并发编程则涉及原子性、可见性、有序性三个问题。而导致这三个问题更深层次的原因是 cup、内存、I/O设备三者之间速度的差异。

为了合理发挥 CPU 的高性能,平衡三者之间的速度差异,计算机体系结构、操作系统、编译程序都做了相应的贡献。

CPU 增加了缓存,用以平衡 CPU 和内存之间的速度差异;操作系统增加了进程、线程,以分时复用CPU,进而平衡CPU与I/O设备的速度差异;编译程序优化指令的执行次序,使得缓存能够得到更加合理地利用。以上的这些努力,使程序的性能得到了很大的提升。不过,凡是皆有利弊,我们享受这些成果的同时,也遭受一些诡异 Bug 的折磨。

源头之一:缓存导致的可见性问题

单核时代不存在可见性问题,因为所有线程操作的同一个CPU缓存。多核时代,每颗CPU都有自己的缓存,当多个线程操作不同的CPU缓存时,就会出现可见性问题。

再次分析上面提到的add10K()方法。我们假设线程 A 和线程 B 同时开始执行,那么第一次都会将 count=0 读到各自的 CPU 缓存里,执行完 count+=1 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,我们会发现内存中是 1,而不是我们期望的 2。之后由于各自的 CPU 缓存里都有了 count 的值,两个线程都是基于 CPU 缓存里的 count 值来计算,所以导致最终 count 的值都是小于 20000 的。这就是缓存的可见性问题。

源头之二:线程切换带来的原子性问题

由于I/O太慢,早期的操作系统就发明了进程,后来经过演化,有了线程。目前的任务切换都是基于线程的。

线程切换带来的原子性问题本质上是高级语言的一条语句对应多条CPU指令导致的。例如一条 count += 1语句,至少对应三条CPU指令。
指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

操作系统做任务切换,可能发生在任何一条CPU指令执行完。对于上面的三条指令来说,我们假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。

源头之三:编译优化带来的有序性问题

首先,看一下Java领域经典的利用双重检查创建单例对象的案例。

public class SingleInstance {
static SingleInstance instance;
static SingleInstance getInstance(){
if (instance == null) {
synchronized(SingleInstance.class) {
if (instance == null)
instance = new SingleInstance();
}
}
return instance;
}
}

上面程序中,new 操作执行路径:
1、分配一块内存M
2、在M上初始化SingleInstance对象
3、M的地址赋给instance

new 操作优化后执行路径:
1、分配一块内存M
2、M的地址赋给instance
3、在M上初始化SingleInstance对象

如果线程A执行完优化后的步骤2之后发生线程切换,切换到线程B,此时线程B调用getInstance()进入第一层判断,发现instance 不为null(内存已经分配了地址),直接返回。而此时并没有初始化instance,这个时候访问instance的成员变量可能触发空指针异常。

总结:

本文主要阐述了并发编程的核心要点,主要问题,以及产生这些问题的根源。针对每个问题都有对应的解决方案,而一个问题的解决方案可能引发其他的问题。所以,要写好并发程序,就要在各个问题之间做好平衡,以满足实际的业务场景。

solr 高并发_你真的了解并发编程吗?相关推荐

  1. guava 并发_使用Guava对并发应用程序进行基于对象的微锁定

    guava 并发 编写并发Java应用程序时最令人讨厌的问题之一是对线程之间共享的资源的处理,例如Web应用程序的会话和应用程序数据. 结果,如果应用程序的并发级别很低,许多开发人员选择根本不同步这些 ...

  2. mysql 高并发 响应时间_高并发,你真的了解吗?

    摘要:本文介绍高并发系统的度量指标,讲述高并发系统的设计思路,再梳理高并发的关键技术,最后结合作者的经验做一些延伸探讨. 当前,数字化在给企业带来业务创新,推动企业高速发展的同时,也给企业的IT软件系 ...

  3. 多少并发量算高并发_如何理解:程序、进程、线程、并发、并行、高并发?

    作者:大宽宽 链接:http://tinyurl.com/wx5xxho 在这里你可以了解: 为啥大家说的进程的意思有出入? 为啥并发那么难理解? 为啥高并发不仅仅是"高"+&qu ...

  4. 一个springboot能支持多少并发_吃透这篇,你也能搭建出一个高并发和高性能的系统...

    " 什么是高并发?高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,简单点说,就是 QPS(Queries Per Second). 那么我们在谈论高 ...

  5. java设计模式并发_[高并发Java 七] 并发设计模式

    [高并发Java 七] 并发设计模式 [高并发Java 七] 并发设计模式 为什么80%的码农都做不了架构师?>>> 在软件工程中,设计模式(design pattern)是对软件设 ...

  6. java 并发框架源码_某网Java并发编程高阶技术-高性能并发框架源码解析与实战(云盘下载)...

    第1章 课程介绍(Java并发编程进阶课程) 什么是Disruptor?它一个高性能的异步处理框架,号称"单线程每秒可处理600W个订单"的神器,本课程目标:彻底精通一个如此优秀的 ...

  7. 多线程导出excel高并发_用多线程优化Excel表格数据导入校验的接口

    公司的需求,当前某个Excel导入功能,流程是:读取Excel数据,传入后台校验每一条数据,判断是否符合导入要求,返回给前端,导入预览展示.(前端等待响应,难点).用户再点击导入按钮,进行异步导入(前 ...

  8. 高并发,你真的理解透彻了吗?

    转自: 公众号:IT人的职场进阶 作者: 骆俊武 支持原创,喜欢的关注上面的公众号 高并发,几乎是每个程序员都想拥有的经验.原因很简单:随着流量变大,会遇到各种各样的技术问题,比如接口响应超时.CPU ...

  9. java 并发框架源码_Java并发编程高阶技术-高性能并发框架源码解析与实战

    Java并发编程高阶技术-高性能并发框架源码解析与实战 1 _0 Z' @+ l: s3 f6 r% t|____资料3 Z9 P- I2 x8 T6 ^ |____coding-275-master ...

最新文章

  1. linux下测试磁盘的读写IO速度-简易方法
  2. HTML5上传图片,后台使用java
  3. 干货!隐马尔科夫模型
  4. .net 实时通信_【WebSocket】实时多人答题对战游戏
  5. sql查询重复记录、删除重复记录方法大全
  6. POJ1410 Intersection
  7. x86_64 x86 amd64 i386 i686 aarch64等词语含义
  8. 70周年国庆,34个省级行政区前来祝贺
  9. Python 万能代码模版:数据可视化篇
  10. java 检测表情符号_java判断字符串是否是QQ表情
  11. 2022质量员-土建方向-岗位技能(质量员)特种作业证考试题库及模拟考试
  12. Python3 flags
  13. mysql主从同步错误:The slave I/O thread stops because master and slave have equal MySQL server UUIDs
  14. 浙江海盐已经试行“核供暖”,南方到底该不该供暖?南方人顶起~
  15. Python生成汉字字库文字,以及转换为文字图片
  16. android单元测试教程,Android单元测试-Junit
  17. IBM ServerGuide 9.40
  18. 微博只显示来自android,新浪微博手机版五大常见问题解决方法
  19. c语言十进制转八进制递归,C语言之利用递归将十进制转换为二进制
  20. 【数据库系统原理作业】八、集合查询、派生词查询、数据更新、空值的处理、视图

热门文章

  1. 五大法则助你成为更出色的开发者|原力计划
  2. 从疫情中看智慧医疗场景新应用,智慧医疗纵深发展还有哪些可能性?
  3. 黑科技抗疫,Python 开发者大集结!
  4. 让数据库运行在浏览器里?TiDB + WebAssembly 告诉你答案
  5. 亲测!这本 Python 书销量超过13W+原来是这样
  6. 性能提升 3 倍的树莓派 4,被爆设计缺陷!
  7. 标贝科技亮相2019中国互联网大会 解决语音合成定制需求痛点
  8. Windows 3.1 往事:历史上第一个真正占据主导地位的操作系统
  9. 微软将终止支持 Win7;今日头条不与微信竞争;诺基亚芬兰裁员 | 极客头条
  10. 恭喜你,2018 中国开发者有奖大调查“榜上有名”!