1.为什么要使用并发?长期以来,硬件的发展是极其迅猛的,还有一个非常著名的“摩尔定律”。讨论为什么涉及并发编程可能会很奇怪。硬件的发展,它们之间的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律不是自然法则或物理定律,它只是根据观察到的数据对未来的预测。按照预测的速度,我们的算力将呈指数级增长,不久的将来我们将拥有超强的算力。就在我们畅想未来的时候,2004年,英特尔宣布将4GHz芯片的计划推迟到2005年。随后在2004年秋天,英特尔宣布全面取消4GHz计划,这意味着摩尔定律对半个多世纪戛然而止。不过,智能硬件工程师并没有停止研发。为了进一步提高计算速度,他们不再追求单独的计算单元,而是将多个计算单元集成在一起,即形成多核CPU。仅仅十几年的时间,家用CPU,比如Inteli7,就可以达到4核甚至8核。专业的服务器通常有几个独立的CPU,每个CPU甚至有8个以上的核心。因此,摩尔定律似乎继续在CPU核心缩放方面得到应用。因此,在多核CPU的背景下,诞生了并发编程的趋势。通过并发编程的形式,可以最大限度地发挥多核CPU的计算能力,提高性能。顶级计算机科学家DonaldErvinKnuth评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者的无奈,他们将摩尔定律的责任推给了软件开发者。另外,它天生适合特殊业务场景下的并发编程。例如在图像处理领域,一张1024X768像素的图片包含了786000多个像素。即便遍历所有像素需要很长时间,面对如此复杂的计算,也需要充分利用多核的计算能力。又比如我们在网上购物时,为了提高响应速度,需要拆分,减少库存,生成订单等,这些都可以使用多线程技术拆分完成。面对复杂的业务模型,并行程序比串行程序更适合业务需求,并发编程更符合这种业务拆分。正是因为有这些优点,多线程技术才得以被重视,也是一个CS学习者应该掌握的:充分利用多核CPU的计算能力;方便业务拆分,提高应用性能2.并发编程有什么缺点多线程技术有那么多好处,难道就没有缺点,一定适用于任何场景吗?很明显不是。2.1频繁上下文切换时间片是CPU分配给各个线程的时间。因为时间很短,CPU一直在切换线程,让我们感觉是多个线程在同时执行。时间片一般为几十毫秒。而且每次切换都需要保存当前状态,以便恢复之前的状态,而且这种切换是非常耗性能的,而且如果过于频繁,会无法发挥多线程的优势编程。通常,减少上下文切换可以通过使用无锁并发编程、CAS算法、使用最少的线程数和使用协程来实现。无锁并发编程:可以参考concurrentHashMap锁分段的思想,不同的线程处理不同的数据段,这样在多线程竞争的情况下,可以减少上下文切换的时间。CAS算法使用Atomic下的CAS算法更新数据,并使用乐观锁,可以有效减少不必要的锁竞争带来的上下文切换。使用最少的线程:避免创建不必要的线程,比如任务很少,但是创建了很多线程,会导致大量线程处于等待状态。协程:在单线程中实现多任务调度,在单线程中维护多个任务之间的切换。因为上下文切换也是比较耗时的操作,所以在《Java并发编程的艺术》一书中有个实验,并发的积累不一定比串行的积累快。可以使用Lmbench3来衡量上下文切换的时长,使用vmstat来衡量上下文切换的次数。2.2线程安全在多线程编程中最难掌握的就是临界区线程安全问题。一不留神,就会出现死锁。一旦发生死锁,系统就会被破坏。功能不可用。publicclassDeadLockDemo{privatestaticStringresource_a="A";privatestaticStringresource_b="B";publicstaticvoidmain(String[]args){deadLock();}publicstaticvoiddeadLock(){ThreadthreadA=newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(resource_a){System.out.println("getresourcea");try{Thread.sleep(3000);同步(resource_b){System.out.println("getresourceb");}}catch(InterruptedExceptione){e.printStackTrace();}}}});ThreadthreadB=newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(resource_b){System.out.println("获取资源b");synchronized(resource_a){System.out.println("获取资源a");}}}});threadA.start();threadB.start();}}复制代码上面的demo中,开启了两个线程threadA和threadB,其中threadA占用resource_a,等待threadB释放resource_b。线程B占用资源_b,正在等待线程A释放资源_a。所以threadA和threadB存在线程安全问题,形成死锁。这个推导也可以通过jps和jstack来证明:"Thread-1":waitingtolockmonitor0x000000000b695360(object0x00000007d5ff53a8,ajava.lang.String),被"Thread-0"持有"Thread-0":waitingtolockmonitor0x000000000b697c10(object0x00000007d5ff53d8,ajava.lang.String),由"Thread-1"保存上面列出的线程的Java堆栈信息:================================================================”Thread-1”:在学习死锁演示$2。run(DeadLockDemo.java:34)-等待锁定<0x00000007d5ff53a8(ajava.lang.String)-锁定<0x00000007d5ff53d8(ajava.lang.String)atjava.lang.Thread.run(Thread.java:722)"Thread-0”:在learn.DeadLockDemo$1.run(DeadLockDemo.java:20)-等待锁定<0x00000007d5ff53d8(ajava.lang.String)-在java.lang锁定<0x00000007d5ff53a8(ajava.lang.String)。Thread.run(Thread.java:722)发现1个死锁。复制代码如上所述,我们完全可以看出当前的死锁情况。那么,通常可以通过以下方式避免死锁:避免一个线程同时获取多个锁;避免一个线程在锁内部占用多个资源,尽量保证每个锁只占用一个资源;尽量使用定时锁,使用lock.tryLock(timeOut),当前线程等待超时时不会阻塞;对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会出现解锁失败所以,如何正确使用多线程编程技术有很多学问,比如如何保证线程安全,如何正确理解JMM内存模型的原子性、有序性和可见性带来的问题,如数据脏读、DCL等(后续章节会介绍)。而且在学习多线程编程技术的过程中,你也会收获很多。3.应该理解的概念3.1同步VS异步同步和异步通常用来描述一个方法调用。在同步方法调用开始时,调用者必须等待被调用方法结束,调用者后面的代码才能执行。异步调用是指无论被调用方法是否完成,调用者都会继续执行后面的代码,并在被调用方法完成时通知调用者。比如加班购物,如果一件东西不见了,你要等仓库人员给你调货,你可以在收银台继续付款,直到仓库人员把货给你送过去,类似到同步调用。而异步调用,就像网购一样,在线支付下单后,什么都不用管,只管做自己该做的,货到了,通知你去取货.3.2并发和并行并发和并行是非常容易混淆的概念。并发是指多个任务交替执行,而并行是指真正意义上的“同时执行”。实际上,如果系统中只有一个CPU,采用多线程,在真实的系统环境下是无法进行并行执行的,只能通过切换时间片的方式交替执行,成为并发执行任务。真正的并行只能发生在具有多个CPU的系统中。3.3阻塞和非阻塞阻塞和非阻塞通常用来描述多线程之间的交互。例如,如果一个线程占用了临界区资源,那么其他线程需要等待资源被释放,这会导致等待线程挂起。从一开始,这种情况就是阻塞的,非阻塞正好相反。它强调任何线程都不能阻塞其他线程,所有线程都会尝试向前运行。3.4临界区临界区用于表示可被多个线程使用的公共资源或共享数据。但是每个线程在使用时,一旦临界区资源被一个线程占用,其他线程就必须等待。
