并发概要
前言
如果你跟我一样,写了一两年的顺序编程,即程序中的所有事物在任何时刻都只能执行一个步骤,那么你应该考虑学习一下并发高级技术。
并发很难,精通并发编程的学习难度基本和精通面向对象编程差不太多,我自己也处在一个学习阶段,所以如果你看了这篇博客可能错误不少,不过没有关系,学习本身就是面向未知,在黑暗中探索。
何为并发
如果你是一个JAVA的使用者,你肯定知道通过复写Runnable接口,在Thread构造方法中New一个Runnable对象,通过start方法便可启用一个线程,并发可以简单的理解为多线程编程。如果你学过操作系统熟悉进程,线程的概念,想必你很清楚并发。可是我们在日常编程中很少使用并发,因为它很难。为何我不断强调它很难,本质上讲:并发"具有可论证的确定性,但是实际上具有不可确定性"。这句话很绕对不对?其实我也不太懂。
为何我们要学习并发
客观原因有一:通过并发线程可以使程序执行得到极大提高,简单说就是可以更多的压榨计算机的性能。当前的摩尔定律多多少少有些过时了,速度的提升更多是以多核cpu的形式而不是速度更快的芯片的形式出现(对,就是日常场景出现的四核八核手机)。
-
提高运行在单处理器的程序性能
此话听起来有点违背直觉,因为并发在单处理器上的运行开销确实比所有程序都顺序执行的开销要大,因为其中增加了所谓上下文切换的代价。问题的核心在于"堵塞",在编程中难免要与I/O操作,网络请求操作导致整个程序停下来,直到外部条件改变。如果使用并发编写程序,那么当其中一个程序阻塞时,程序中的其他程序还可以继续进行。从单核处理器来讲,如果没有任务阻塞,那么在单核cpu也就没有所谓的并发编程。
-
事件驱动的编程
想要编写出流畅的具有可响应界面的用户界面,并发必不可少。考虑一下这样的场景,因为程序在执行某段长期运行的操作,导致用户输入会被忽略掉,从而称为不可响应的程序。如果不使用并发来处理,那么可以产生可响应用户界面的唯一方法就是所有任务都周期性检查用户输入。当我们通过单独创建的执行线程来响应用户的输入,即使这个线程在大多数时间都是阻塞的,但是程序具有一定程度的可响应性。
基本的线程机制
并发编程让我们可以让程序划分为多个分离的.独立运行的任务。通过使用多线程机制,这些独立任务的每一个都由执行线程来驱动。一个线程在进程中是 单一的顺序控制流。单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的CPU。
-
Runnable接口
public class LiftOff implements Runnable { protected int countDown = 10; private static int taskCount = 0; private final int id = taskCount++; public LiftOff() { } public LiftOff(int countDown) { this.countDown = countDown; } @Override public void run() { while (countDown-- > 0) { System.out.println(status()); Thread.yield(); } } public String status() { return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff") + ")."; } }
任务的run方法通常写为某种形式的循环,使任务一直运行下去直到不在需要。通常run方法被写成了无限循环的形式。通常我们会中断线程来结束任务。
-
Thread类
Thread类通过构造方法需要一个Runnable对象,调用Thread类的start方法来执行必要的初始化操作,然后调用Runnable的run方法。
-
使用Executor
在java1.5后引入了java,util.concurrent包中的执行器将为接管Thread对象简化了并发的编程,Executor允许管理异步任务的执行,ExecutorService知道如何构建恰当的上下文执行Runnable对象。
-
newCacheThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
-
newFixThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
-
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
-
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
-
-
Callable
Runnable是执行工作的独立任务,但是它不返回任何值,如果希望任务完成返回一个值,那么可以实现Callable接口而不是Runnable接口,它的类型参数表示的是从方法call(),并且使用ExecutorService.submit()方法调用它。submit方法会产生Future对象,它用Callable返回结果的特定类型。
-
后台线程
所谓后台线程也成 守护线程,是指程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序不可或缺的部分。因此当所有的非后台线程结束,程序也就终止了,同时会杀死进程的所有后台程序。必须在线程启动前调用setDaemon()方法,才能把它设置为后台线程。
-
基本总结
在我开始接触并发的时候,十分疑惑在于,所要执行的任务和去动它的线程是有差异的。Thread类自身不执行任何操作,他只是驱动赋予它的任务。其实我们对于Thread类没有任何的控制,你只能创建任务,通过某种方式将一个线程附着到任务,来使得这个线程可以驱动任务。