详解并发编程的优缺点

一直以来并发编程对于刚入行的小白来讲老是以为高深莫测,因而乎,就诞生了想写点东西记录下,以提高理解和堆并发编程的认知。为何须要用的并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具备哪些缺点?以及在进行并发编程时应该了解和掌握的概念是什么?这篇文章主要以这三个问题来谈一谈。java

一.为何要用到并发


一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为何会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并非一种天然法则或者是物理定律,它只是基于认为观测数据后,对将来的一种预测。按照所预测的速度,咱们的计算能力会按照指数级别的速度增加,不久之后会拥有超强的计算能力,正是在畅想将来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,而后在2004年秋季,Intel宣布完全取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。可是,聪明的硬件工程师并无中止研发的脚步,他们为了进一步提高计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一块儿,也就是造成了多核CPU。短短十几年的时间,家用型CPU,好比Intel i7就能够达到4核心甚至8核心。而专业服务器则一般能够达到几个独立的CPU,每个CPU甚至拥有多达8个以上的内核。所以,摩尔定律彷佛在CPU核心扩展上继续获得体验。所以,多核的CPU的背景下,催生了并发编程的趋势,经过并发编程的形式能够将多核CPU的计算能力发挥到极致,性能获得提高程序员

顶级计算机科学家Donald Ervin Knuth如此评价这种状况:在我看来,这种现象(并发)或多或少是因为硬件设计者机关用尽了致使的,他们将摩尔定律的责任推给了软件开发者。面试

另外,在特殊的业务场景下先天的就适合于并发编程。好比在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将全部的像素遍历一边都须要很长的时间,面对如此复杂的计算量就须要充分利用多核的计算的能力。又好比当咱们在网上购物时,为了提高响应速度,须要拆分,减库存,生成订单等等这些操做,就能够进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。正是由于这些优势,使得多线程技术可以获得重视,也是一名CS学习者应该掌握的:算法

  • 充分利用多核CPU的计算能力;
  • 方便进行业务拆分,提高应用性能

二. 并发编程有哪些缺点


多线程技术有这么多的好处,难道就没有一点缺点么,就在任何场景下就必定适用么?很显然不是。sql

2.1 频繁的上下文切换

时间片是CPU分配给各个线程的时间,由于时间很是短,因此CPU不断经过切换线程,让咱们以为多个线程是同时执行的,时间片通常是几十毫秒。而每次切换时,须要保存当前的状态起来,以便可以进行恢复先前状态,而这个切换时很是损耗性能,过于频繁反而没法发挥出多线程编程的优点。一般减小上下文切换能够采用无锁并发编程,CAS算法,使用最少的线程和使用协程。数据库

  • 无锁并发编程:能够参照concurrentHashMap锁分段的思想,不一样的线程处理不一样段的数据,这样在多线程竞争的条件下,能够减小上下文切换的时间。编程

  • CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,能够有效的减小一部分没必要要的锁竞争带来的上下文切换安全

  • 使用最少线程:避免建立不须要的线程,好比任务不多,可是建立了不少的线程,这样会形成大量的线程都处于等待状态bash

  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换服务器

因为上下文切换也是个相对比较耗时的操做,因此在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可使用Lmbench3测量上下文切换的时长 vmstat测量上下文切换次数

2.2 线程安全

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的状况,一旦产生死锁就会形成系统功能不可用。

public class DeadLockDemo {
	private static String resource_a = "A";
	private static String resource_b = "B";
	public static void main(String[] args) {
		deadLock();
	}
	public static void deadLock() {
		Thread threadA = new Thread(new Runnable() {
			@Override
			            public void run() {
				synchronized (resource_a) {
					System.out.println("get resource a");
					try {
						Thread.sleep(3000);
						synchronized (resource_b) {
							System.out.println("get resource b");
						}
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
		);
		Thread threadB = new Thread(new Runnable() {
			@Override
			            public void run() {
				synchronized (resource_b) {
					System.out.println("get resource b");
					synchronized (resource_a) {
						System.out.println("get resource a");
					}
				}
			}
		}
		);
		threadA.start();
		threadB.start();
	}
}
复制代码

在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。所以threadA,threadB出现线程安全的问题,造成死锁。一样能够经过jps,jstack证实这种推论:

"Thread-1":
  waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),
  which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
        at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)
        - waiting to lock <0x00000007d5ff53a8(a java.lang.String)
        - locked <0x00000007d5ff53d8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
"Thread-0":
        at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)
        - waiting to lock <0x00000007d5ff53d8(a java.lang.String)
        - locked <0x00000007d5ff53a8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
Found 1 deadlock.
复制代码

如上所述,彻底能够看出当前死锁的状况。

那么,一般能够用以下方式避免死锁的状况:

  • 避免一个线程同时得到多个锁;
  • 避免一个线程在锁内部占有多个资源,尽可能保证每一个锁只占用一个资源;
  • 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
  • 对于数据库锁,加锁和解锁必须在一个数据库链接里,不然会出现解锁失败的状况

因此,如何正确的使用多线程编程技术有很大的学问,好比如何保证线程安全,如何正确理解因为JMM内存模型在原子性,有序性,可见性带来的问题,好比数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程当中也会让你收获颇丰。

三. 应该了解的概念


3.1 同步VS异步

同步和异步一般用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。好比,在超时购物,若是一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就相似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干吗就干吗去了,当货物到达后你收到通知去取就好。

3.2 并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,若是系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能经过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出如今拥有多个CPU的系统中。

3.3 阻塞和非阻塞

阻塞和非阻塞一般用来形容多线程间的相互影响,好比一个线程占有了临界区资源,那么其余线程须要这个资源就必须进行等待该资源的释放,会致使等待的线程挂起,这种状况就是阻塞,而非阻塞就刚好相反,它强调没有一个线程能够阻塞其余线程,全部的线程都会尝试地往前运行。

3.4 临界区

临界区用来表示一种公共资源或者说是共享数据,能够被多个线程使用。可是每一个线程使用时,一旦临界区资源被一个线程占有,那么其余线程必须等待。

读者福利

分享免费学习资料

针对于Java程序员,我这边准备免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

为何某些人会一直比你优秀,是由于他自己就很优秀还一直在持续努力变得更优秀,而你是否是还在知足于现状心里在窃喜!但愿读到这的您能点个小赞和关注下我,之后还会更新技术干货,谢谢您的支持!

资料领取方式:加入Java技术交流群963944895点击加入群聊,私信管理员便可免费领取

如何成为一个有逼格的Java架构师

怎么提升代码质量?——来自阿里P8架构师的研发经验总结

阿里P8分享Java架构师的学习路线,第六点尤其重要

每一个Java开发者应该知道的八个工具

想面试Java架构师?这些最基本的东西你都会了吗?

画个图来找你的核心竞争力,变中年危机为加油站

哪有什么中年危机,不过是把定目标当成了有计划

被裁人不是寒冬重点,重点是怎么破解职业瓶颈

相关文章
相关标签/搜索