并行编程的革命
我就不说最初那个单核CPU时代了,我们从多进程编程开始讲。在引入多线程概念前,多进程是并发编程的唯一解决方案;多进程在解决并发问题的同时带来了一些问题:主要有以下几点,多线程也就是正因为多进程有许多不足才被设计出来:
多进程的特点:每个进程都独立拥有数据空间(堆、栈、代码区等),这是多线程跟多进程最本质的区别,这个区别是多线程与多进程优缺点的起因
多进程缺点:
- 进程间数据共享困难
- 进程调试时上下文切换开销大
- 线程给异步设计提供了方便的设计模式
多进程优点:
- 进程与进程间的执行没有相互干扰(暂不考虑进程间通讯问题),进程基本上可以随时终时,所以操作系统会提供给我们任务管理器来强制终止进程,或是提供kill命令。线程这样做往往很危险,线程的终止方法也很特殊,后面我们会讲到;因为强制终止线程不能保证数据的一致性、完整性。
- 进程能非常方便地应用于分布式环境
结论:多线程是不能完全替换多进程,但多线程带来的革命也是明显的,我程序设计中,我们要跟据实际情况,选择合理的并发方案;其实好多时候,我们往往是两者结合来用。好了,对多线程的基本概念讲到这里,我们以下要讲讲Java对多线程的支持。
Java多线程
线程对象设计
如果Java不是一门面象对象语言,那么我们要学习的东西可能少得多,但使用起来并不一定方便。我们先来讲一下Java中多线程的设计。Java中一切都是对像,线程也是,线程是CPU调度的单位,它本身跟具体的程序功能无关;所以Java把程序的功能又独立提取出来,放到Runnable或Callable接口中;最后Java添加了一个管理类ExecotorSerivce,用于管理多个线程的执行。
Thread类
一个Thread类表示一个线程,其实一个线程可以抽象为一个程序指针,保存了程序当前的执行进度;当然,其实一个线程的实现相对复杂,还包括锁的实现等,这个说法仅供理解。
一个thread对象包含了一些关键信息
- 线程的状态:线程栈信息(每个线程都有个独立的栈)、中断标记信息(用于停止线程)、守护线程标记、名字、优先级等
- 线程的一个协作方法:t.join(); //让调用者等待t线程的执行,并阻塞调用线程。
- run(); //重新调用new Runnable().run(); 语法上讲,可以在这个方法里直接添加线程功能,但不推介。
- start(); //启动线程
- currentThread(); //获得当前线程
- sleep(); //让当前线程等待一段时间,些时不释放锁,仅不去抢占CPU。sleep并不直接干扰其它线程的执行。
- yield(); //让当前线程释放一下CPU。
- interrupted(); //返回当前线程的中断状态,并重置中断标记。
Runnable & Callable类
线程执行的具体功能被放在这个接口类里,如果涉及到资源协作(锁或是synchronized),我一般不直接在这两个类里处理这些,因为Runnable及Callable是两个行为类,一般不在这两个类里直接包含资源,所以一般不涉及到资源的锁问题;这样做也是为了让自己的程序更加容易阅读;然而一般退出线程的逻辑却应放在这个类里。
Callable相对于Runnable给我们带来了两个额外功能
- 线程执行返回执行结果 Callable<returnType>
- 可抛出异常,表示取得返回值的过程中遇到了其它状况,而不至于让我们发现返回值莫名其妙地没了
线程返回结果其实在绝大多数情况下其它是没必要的,但这种情况也不算太少数;我想说明的是,相对于Runnable来说Callable并没有带来革命性的更改,所以喜欢优先用哪个,我们可以跟据需要来;我一般会优先用Runnable+ExecutorSerivce;因为即使突然发现需要返回值,把Runnable修改为Callable也不是一件复杂的事。
ExecutorService管理类
相对于Callable,我觉得这个类实用多了;这类有以下特点:
- 个数限制:以池的方式管理线程,用户不再需要自己创建线程,并能对线程的总个数加以限制;
- 线程复用:除此之外,我们知道一般情况下我们都是一个线程对应一个Task,这样的线程是不能复用的,虽然我们说创建线程的开销远小于创建进程,但创建一个线程的开销相对于一般的编程还是要大得多的,默认Thread对象也没有给我们提供支持多个Task的方法,原因是需要满足太多的条件,这样做很危险;而ExecutorSerivce的池机制已经帮我们实现了这个功能。
- 最后,ExecutorService提供了submit方法;这个方法有两个功能1、Runnable与Callable统一创建接口;2、跟踪线程的执行状态Future<return type>。
异常处理
异常是与线程对应的,我们知道当主线程抛出uncatched异常时,会造成jvm挂掉,并打印出栈信息。虽然这个功能也就这样,但至少我们知道自己的程序已经出状况了,需要重新维护。
子线程默认不会通知uncached异常信息,出现异常时,子线程挂掉,但我们什么也不知道。我们可以通过以下方法来外理这种异常
Thread.UncaughtExceptionHandler eh = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // TODO Auto-generated method stub } }; t.setUncaughtExceptionHandler(eh); //一个线程对应一个异常控制器 Thread.setDefaultUncaughtExceptionHandler(eh); //默认所有线线程都用这个异常控制器
其它:守护线程及优先级
设置守护线程方法
t.setDaemon(true);//请注意顺序
t.start();
守护线程概念
有这样一些线程,它们仅给其它线程提供后台服务,它们独立存在的话没有任何意义,我们往往把它们设置为守护线程;我们再说一点,一个Java进程都对应一个运行时的jvm,那么什么时候决定这个java进程已经执行完了可以退出jvm呢?回答是除了守护线程之外已经没有其它线程了,换句话说,程序已经不对外执行计算任务了,那么这个时候进程就可以结束了。
优先级
优先级仅是CPU调度的一个指标,其实影响CPU调度的原因很多。知道这点就可以了。
CPU协调
线程是CPU调度的基本单位,一个线程可以用以下方法直接影响CPU调度,且不涉及到资源及锁。CPU协调就是一般说的CPU竞争,我觉得用协调更好一些,因为现在我们程序员的素质高,不会占着CPU不放了;不像一些软件,比如,卡巴死机。
sleep()方法
sleep原理:创建并启动一个闹钟,并传入回调(回调用于继续执行),然后挂起当前线程;sleep并不释放锁,所以尽量不要在获得锁的时候用sleep,那就是占着茅坑不...了,呵呵。
Thread.sleep(millsec); //让当前线程sleep一会
Yield()方法
暂停当前线程,使其它线程能获得cpu。
Thread.yield();
资源协调
多线程真正有意思的地方我想就是这部分了,我们程序员不是干体力活的;这部分是最能体现我们程序员实力的地方了。
多线程里有句老话,大致这个意思:一切多线程问题的根源是对共享数据的访问。这句话绝对是95%正确的。5%我忽略不计了,算买个保险吧。
Java中多线程间的资源共享是通过“互斥锁”来实现的,它分为两种语法:
- 以synchronized关键字为主的块状锁定语法。它的原理是锁定目标对象对应的monitor,之后我们会比较详细的说明这一部分
- 以Lock接口为基础的显式锁
原子性 & volatile关键字
在讲具体锁之前,我想先讲两个非常重要的,但实际使用时很少使用(至少我是这样的)
原子性
理解原子性的概念非常重要。原子性指一个操作是CPU执行的最小单位,不能被打断。对具有原子性的操作,原理上讲我们是不用给它们加锁的。
volatile关键字作用
Java线程一般会把共享变量等拷贝一份副本到线程“本地栈”,从而提高性能;拷由的时机往往是这样的:在获得锁时拷贝,在释放锁的时候写回(包含wait,notify前后那些隐藏的锁交换);
用volatile修饰的变量表示这样一个意思:不对共享变量进行拷贝,直接访问共享变量,以达到变量值的一致性。
实站:我们很难确定一个操作的原子性,这往往跟不同的JVM实现有关;举两个例子:i++,不是原子的。即使long l=5;这样一个操作在一个32位机子上也不是原子的,我们知道java中的长整型应该是64位的,而32位机器一次仅能处理32位,那么有可能,这个一赋值操作会分为两个原子操作,先赋低32位,再赋高32位。当然如果操作系统做了特殊处理的话,那另当别论。关于volatile也一样,即使加了volatile我们还得考虑原子性,而原子性往往是得不到保障的,所以volatile的使用也比较难。
我觉得一般的做法是不要考虑这两个概念,尽量给共享的资源加上锁。即使在我们认为是原子操作的地方也加上锁,因为原子操作往往非常“精细”,锁定的时间间隔自然也相对较短。
块状隐式锁synchronized
先说一下锁的概念:一个目标锁同时仅能被一个线程占用,线程要访问锁之间的代码的话必须先获取锁;(这里先不考虑特殊的ReadWriteLock)
每个java对象都对应有一个monitor。synchronized就是通过锁定这个monitor对象来达到锁定的目的;相对于显式的lock,synchronized语法的最大好处应该是会自动释放锁。synchronized有以下三种用法
- 在类方法前加锁:表示锁定的是当前类对象(ClassName.class)的monitor;
- 在实例方法前加锁:表示锁定的是当前的实例对象;
- 在代码块里加锁:可以指定具体的目标对象,语法synchronized(o){...}
显式锁Lock
相对于用synchronized方法及语句,Lock提供了更加灵活的、可扩展的锁操作。它提供了更加灵活的语法结构,有的(指Lock的实现类)可能拥有一些特性,如它有可能支持多个与之关联的 Condition 对象
synchronized方式的一些不足
synchronized方法及语句提供了访问隐藏锁的方法,每个对象都关联着一个隐藏锁,但限制了锁的获得及释放必须发生在一个块状结构里:当同时获得多个锁时,这些锁必须以相反的顺序释放,而且,所有的锁的释放与锁的获取必须在同一个文法范围(可以理解为同一层缩进,同一个方法,同一个块里)。
虽然syncronized方法及语句的块式机制让我们能够轻松地运用隐藏锁机制来进行多线程编程,并且让我们避免了锁编程中的一些普遍编程问题。但还是有一些情况你不得不使用更加灵活的Lock方式(显式的Lock)。
一个必须得用显式Lock的实例
比如:一些并行遍历数据结构的算法,需要一种“一步步地”或“链接式的锁定”:先获得节点A的锁,再获得节点B的锁,然后释放对A的锁,获得对C的锁,释放对B的锁,再获得对D的锁 ...。Lock接口的实现类就能够让这些技法的使用变为可能,它实现了让锁能在不同的块结构里获得并释放,并允许多个锁可以以任何顺序获得或释放。
显式锁的特点(还是与实现类有关,并不是说每种显式锁都一定支持这些特点):
- 能关联多个Conditioin对象
- 可以随时随地加锁,锁与锁之前不一定要“嵌套”。
- 获得锁时可以被中断(synchronized在获取锁时是不能被中断的),这点在处理死锁时非常有用(解除死锁仅需发一个中断 )
- 其它还有其它一些有用的方法,这里不一一例举了
继承结构
java.util.concurrent.locks.Lock
`-ReentrantLock
`-ReentrantReadWriteLock
`-ReadLock
`-WriteLock
说实话,我不知道他这里为什么要把类名取为Reentrant(可重入),我觉得这根本与可重入的概念没有什么关系。有关可重入的概念大家要以自行从网上搜索。
wait & notify & notifyAll;
wait概念
由于程序执行需要的资源没有达到程序要求,而主动放弃已经获得的对象锁(在synchronized里对象指的是monitor),此时线程进入目标对象(monitor)的等待队列,让其它线程准备完资源后再重新通知锁定的对象(monitor),并让对象(monitor)进一步通知在等待队列里的线程。
notify & notifyAll
一般情况下,我们都用nofityAll(),它会通知所有等待的线程,都去尝试获得锁;而notify();仅会从等待队列中挑选一个线程,如果被挑选的线程,但如果被挑选的线程此时并没有满足执行的条件,进入wait,那么就可能造成所有的线都都进入了wait状态。
锁的目标对象与资源的关系
其实两者并没有直接的关系,但目标对象的选择必需与想要锁定的资源在逻辑上要能大约等同。这个在Java语言里比较好选择,一般把资源封装在对象里,然后把对象(或对象的monitor)当成目标对象。
线程式局部变量
比如:LocalThread<Integer> locThd = ...;做为一个共享变量的话,它会自动为每个线程自动创建一份拷贝,线程与线程内的数据不影响。这个类在线程内传递参数非常有用,我们可以把一个线程需要的一些公共信息放在LocalThread里。
终止线程
这个是个很有意思的话题,网上有一些终止线程的方法,比如判断标记,用interrupt中断,也有说中断并不能用来终止线程;那么到底是怎么回事?
线程是没法直接强制终止的
直接强制终止一个线程,会造成数据的丢失,不一致等,这对一个程序来说是非常致命的,所以Java里希望线程都能够“主动退出”;
判断标记的缺点
判断标记法自然是终止线程的一个可行的方法,但有一个不足:当线程被堵塞时(sleep, wait, join)线程无法进入判断,更无法即时退出;尤其是当线程进入DeadLock时,就更没有办法了。
再说说interrupt
interrupt有两个作用,一个是给线程添加interrupt标记,另一个是终止正被堵塞的线程,并让它们抛出一个InterruptedException,抛出interruptException之后,线程会自动清除interrupt标记。interrupt虽然不能直接退出所有线程,但至少当前已经堵塞的线程能立马退出,那些未堵塞的线程也能比较顺利地进入一个“判断标记”,然后自动退出线程。所以总的来说,结合interruptException来退出线程是很不错的,也可以直接用isInterrupt()函数来获取interrupt标记直接判断,其它判断标记的添加可以以语镜为准。
如果你使用是的ExecutorService,那么,有两个方法与线程关闭相关
- shutdown(); //表示ExecutorSerivce对象不再继续接受其它对象的执行,但不影响已经提供的线程。
- shutdownNow(); //在shutdown的语义上,再分别对已经提交的线程发起interrupt中断,试图中断每一个线程,线程的最终退出逻辑需线程自己判断interrupt标记。
- awaitTermination(timeout, unit); //使当前线程堵塞并等待ExecutorSerivce执行完所有已经提供的线程,并返回结果。
来个实例
package test;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadDemo {
/**
* 这里我们定义一个线程安全的的ResHolder类
* add 用于添加某个资源,为了保证资源耗尽,我们给它设定一个上限Max,如果大于这个上限,那么就让clear方法重置一下.
* @author Mr.He
*/
static class ResHolder{
public static final int Max = 10000; //read only
private int i = 0;//把这个当成共享变量
/**
* 我们一般不会在这个类里处理interruptedException,因为我们在这里不能确认是否要退出线程.
* 而synchronized等锁操作一般都放在这个类里
* @throws InterruptedException
* @author Mr.He
*/
public synchronized void add() throws InterruptedException{
while(true){
if(i<Max){
i++;
}else{
notifyAll();
wait();
}
}
}
public synchronized void clear() throws InterruptedException{
while(true){
if(i<Max){
wait();
}else{
System.out.println("Clear value from 100 to 0 [Input ENTER to exit]");
i = 0;
notifyAll();
}
}
}
}
/**
* 在Runnable的接口实现类里,是处理Interrupted异常最好的地方.
* @author Mr.He
*/
static class TaskAdd implements Runnable{
private ResHolder resHolder;
private TaskAdd(ResHolder resHolder) {
super();
this.resHolder = resHolder;
}
@Override
public void run() {
try {
resHolder.add();
} catch (InterruptedException e) {
System.out.println("Exit for interrupt! "+Thread.currentThread());
}
}
}
static class TaskClear implements Runnable{
private ResHolder resHolder;
private TaskClear(ResHolder resHolder) {
super();
this.resHolder = resHolder;
}
@Override
public void run() {
try {
resHolder.clear();
} catch (InterruptedException e) {
System.out.println("Exit for interrupt! "+Thread.currentThread());
}
}
}
public static void main(String[] args) throws IOException {
ResHolder resHolder = new ResHolder();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit( new TaskAdd(resHolder) );
exec.submit( new TaskAdd(resHolder) );
exec.submit( new TaskClear(resHolder) );
//退出~
if(System.in.read()!=-1){
exec.shutdownNow();
}
}
}