1. 并发的多面性:
并发可以解决两个问题:
一是“速度”:
并发编程可以提高多处理器机器的处理速度;
它通常是提高运行在单处理器上的程序的性能,顺序执行中阻塞是其重要原因。
二是改进代码设计:
一些问题,如仿真,没有并发很难支持。
2. 线程基础:
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中。一个进程可以有多个线程,进程使用不同的内存空间,而线程共享一片相同的内存空间(这里不是指栈内存,栈内存是用来存储本地数据的)。
2.1 Java中实现线程的方法:
方法一:Thread实现;
方法二:Runnbale实现:从Java不能多重继承的角度来看,Runnable要比Thread合适一些;
方法三:使用Executor:
不同类型的Executor:
CachedThreadPool:可缓存的线程池,如果线程池大小超过了处理任务的需要,会回收空闲(60s不执行任务)的线程,线程数量不会限制,大小依赖JVM;
SingleThreadExecutor:同时只有一个线程在工作,异常后创建一个新线程,所有的任务按顺序提交运行;
FixedThreadPool:指定一个最大线程数,每提交一个任务创建一个线程直到达到最大线程数,保持线程数不变;
ScheduledThreadPool:创建一个大小无限的线程池,支持定时周期性执行的任务;
2.2 有返回的的任务:Callable(还可以抛出异常)和Future;
通过Executor.submit(new SubCallable());
Future对象可以保存返回的结果:
get()获取返回的结果,该方法是阻塞的;
isDone():查看是否已经完成;
2.3 休眠:
sleep:可以抛出InterruptedException;
2.4 优先级(MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY):
优先级的实现还是依赖操作系统和底层实现,但是调度器将倾向于优先权最高的线程先进行。但是优先权较低的还是可以得到执行的,而是执行的频率较低(优先权不会导致死锁)。
2.5 让步(yield函数):
这也是“建议”线程调度机制,具有同优先级的线程可以执行。
2.6 后台线程(Thread.setDaemon方法):
只要有非后台线程运行,后台线程就不会终止;
后台线程派生的子线程也是后台线程;
后台线程中finally子句不会执行;
线程不能将自己设置成后台线程,否则会抛出IllegalThreadStateException;
2.7 自管理Runnable:
与直接继承Thread相比,使用组合的方式更灵活和可扩展:
class SelfManagerRunnbale implements Runnable {
private Thread thread = new Thread(this);
public void run() {
//...
}
}
2.8 加入一个线程(join):
在一个线程中对另一个线程调用join(),原线程挂起(可以通过isAlive查看)执行直到join的线程运行结束。 在调用线程中调用join线程的interrupt()方法可以中断join线程。 在catch到InterruptedException后,将清除interrupt标志,调用isInterrupt返回false;
2.9 捕获异常(典型的捕获子线程中逃逸的RuntimeException):
如何在线程外捕获逃逸的异常(RunntimeException);
Java SE5后,可以用Executor解决这个问题——Thread.UncaughtExceptionHandler.uncaughtException();
使用Executor我们可以通过传入特定ThreadFactory的方式设置Thread的特性,这是一种很好的分离,好的设计。
另外,在一个线程中捕获子线程异常,可以通过设置默认异常处理器的方式:
Thread.setDefaultUncaughtExceptionHandler();默认处理器在子线程没有设置异常处理器时调用。
3. 共享受限资源:
超过一个线程处理临界数据,必须使用相关同步方法,每个有操作的方法都必须同步。
存在过个线程共享统一资源时,使用序列化访问共享资源方案,通常使用锁语句,这是一种互斥量。
使用并发时,将域设置成private,防止其他任务直接访问域,用synchronized关键字锁定;
JVM锁跟踪计数,同一任务每进入一次synchronized时计数加1,每出一次synchronized计数减一;
java中lock和synchronized临界区都是可重入的锁;
3.1 synchronized关键字:
Java中对象和类都有对应的锁,可以保证synchronized static在类范围控制并发。
3.2 使用显式的Lock对象:
使用显式的Lock对象可以获得更细粒度的控制力。
ReentrantLock类:与try-finally结合使用
try {
lock.lock();
} finally {
lock.unlock();
}
异常清理: 使用try-finally的好处是可以在发生异常事在finally做清理工作,单单使用synchronized不行,同时保证unlock不会过早发生; 尝试锁定(synchronized无法做到):
try {
captured = lock.trylock(2, timeout);
} finally {
if(caputred)
lock.unlock();
}
实现锁耦合(链式列表遍历节节加锁);
3.3 原子性与易变性:
volatile:保证64位数据类型读写的原子性(防止字撕裂);可见性(数据存入主存,没有本地缓存);不能保证递增/减操作或者某个域受到其他域限制的情况下的线程安全;原子类:Atomic类,用于保证一些读写操作的原子性;
临界区(同步控制块):synchronized(){};
在其他对象上同步:synchronized(object),如果获取了synchronized块上的锁,那么该对象其他synchronized方法和临界区就不能调用;
线程本地存储:一种自动化机制,为使用相同变量的每个不同的线程都创建不同的存储。ThreadLocal<T>,通过get/set方法存取内容;
4. 终结任务:
4.1 正常情况下安全退出的一种自定义方法:使用volatile的布尔值变量作为canceled标志,退出任务;
4.2 在阻塞时终结:
线程状态:
新建(new):创建时短暂的时间处于状态,分配资源,初始化,有资格获取CPU时间;
就绪(Runnable):该状态内,只有CPU分配时间片就可以运行;
阻塞(Blocked):阻塞时不可能获得CPU时间,直到重新进入到就绪状态;
死亡(Dead):run返回或者中断,线程终止;
进入阻塞状态的原因:
1. sleep();
2. wait()挂起,thread.join()挂起;
3. 等待某个输入/输出完成;
4. 等待暂不可用的锁;
4.3 中断:
具体的方法:
(1)interrupt():对于运行状态下的线程调用该方法不会有效果,对于阻塞调用中断,如可中断抛出InterruptedExceptioin;
(2)Thread.interrupted():检查是否中断,并复位跳出run循环,可以不抛出异常,和volatile布尔变量作用类似;
(3)Futrue.cancel(true):传true表示如果对应任务在运行,则中断;
(4)executor.shutdownNow();所有线程一起中断;
(5)自己维护本地布尔变量用于跳出run()循环;
总结:(2)(5)是用来跳出run循环的,对于资源一定要结合try-finally来进行清理;
(1),(3),(4)是用来在阻塞时终止任务的,通过异常来终止,对于资源要在catch子句中进行清理。
interrupt(),Future.cancel(true),executor.shutdownNow()都是向线程发送interrupt()改变该线程的中断标志,如下情况会抛出异常;
中断的影响:
(1)线程阻塞时:sleep,等待IO,等待lock,thread.join()挂起当前线程;中断抛出异常;
(2)非阻塞时对中断状态敏感的方法:
Thread.interrupted()检查并复位,不抛出异常;
已中断时,调用Thread.sleep()如果中断标志产生,调用sleep会抛出InterruptedException;
Lock.lockInterruptibly(),如果已处于中断,调用该方法抛出InterruptedException;
Thread.isInterrupted(),不会复位;
InterruptedExceptioin:抛出后也会复位;
阻塞状态的中断:
(1)sleep产生的阻塞可以被中断;
(2)io、临界区产生的等待阻塞不可以被中断;
(3)对于IO,关闭阻塞的资源可以产生中断;
对于使用NIO,通过futrue.cancel(true)/shutdownNow()产生的中断抛出ClosedByInterruptException;通过资源的close方法关闭抛出AsynchronousCloseException;
(4)使用ReentranLock代替synchronized临界区,可以通过中断终止;
5. 线程间协作:
任务协作:互斥,挂起,唤醒;
5.1 基础知识:
wait(),notify()和notifyAll():
wait(时间参数和无参数两个版本):挂起,释放锁,相比sleep()并不会释放锁,因此sleep可以为Thread的类方法;
notify:唤醒一个挂起线程,但不能决定具体是哪一个线程;
notifyAll:唤醒对应锁上所有挂起的线程;
使用显示Lock和Condition条件对象: JSE 1.5 的新工具,可以从lock对象中获得condition,调用condition对象上的await,signal,signalAll来代替wait,notify,notifyAll,更为安全;
注意:
(1)只能在同步控制块中调用它们;
(2)它们是基于锁的,锁是基于对象的,因此是对象的一部分;
例子和问题:
【1】经典的汽车上漆和抛光例子;这里使用notifyAll,因为有一些任务出于不同的原因在等待锁,必须要使用notifyAll;
【2】什么时候使用notify和notifyAll:
notify只有一个被唤醒的线程,你必须确定是合适的线程;
只要有多个任务在等待不同的条件就必须使用notifyAll;
大部分时候使用notifyAll更为安全;
【3】错失的信号(死锁发生的原因之一):
while(someCondition) {
//这里someCondition可能被其他线程修改,造成了无限的wait
synchronized(shareMonitor) {
sharedMonitor.wait();
}
}
5.2 消费者和生产者:
厨师和服务员这个例子非常经典,也可以看到wait和notifyAll需要比较细致的控制,较难使用:
服务员:
class Waitress implements Runnable {
private final Restaurant restaurant;
public Waitress(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized (this) {
while(restaurant.meal == null) {
wait();
}
}
//got a meal
logger.debug("got meal " + restaurant.meal);
synchronized (restaurant.cook) {
restaurant.meal = null;
restaurant.cook.notifyAll();
}
}
} catch (InterruptedException e) {
logger.error(e);
}
}
}
厨师:
class Cook implements Runnable {
private final Restaurant restaurant;
public Cook(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized (this) {
while (restaurant.meal != null) {
wait();
}
}
//check count
if(++restaurant.count == 10) {
logger.debug("out of food, stop");
restaurant.executorService.shutdownNow();
}
synchronized (restaurant.waitress) {
restaurant.meal = new Meal(restaurant.count);
logger.debug("cooking " + restaurant.meal);
TimeUnit.MILLISECONDS.sleep(500); //here, throw a InterruptedException when shutdownNow
restaurant.waitress.notifyAll();
}
}
} catch (InterruptedException e) {
logger.error(e);
}
}
}
注意在厨师的run(),restaurant.executorService.shutdownNow()后,厨师线程并没有立刻停止;依然可以执行到logger.debug("cooking " + restaurant.meal);直到调用sleep()抛出java.lang.InterruptedException: sleep interrupted;前面也提到了sleep是对中断敏感的方法;
5.3 生产者和消费者与队列:
使用wait/notify的方式较为原始且很难控制,JAVA提供了更高的抽象:同步队列(阻塞队列):
阻塞队列的种类: ArrayBlockingQueue:固定大小,可以实现公平和非公平; LinkedBlockingQueue:基于链表实现的一个阻塞队列,如不指定容量,默认容量为Integer.MAX_VALUE; PriorityBlockingQueue:出队顺序和优先级有关,无界阻塞队列,不进行容量检查; DelayQueue:基于PriorityQueue,只有元素指定的延时到了,才可以获取数据;
重要方法: add:不能传入null,否则抛出NPE,队列满(有界队列)时,抛出异常IllegalStateException;成功返回true; remove:队列为空时抛出异常,成功返回true; offer:插入元素到队尾,成功返回true; peek:返回队首元素,空时返回null; poll:返回并移除队首元素,空时返回努力来; 实现原理:通过Lock,Condition实现,put,take方法的等待条件分别是while(count == length)和while(count==0);
PS:任务间管道I/O: 使用PipedWriter/Reader进行通信; 没有阻塞队列健壮; 可以被中断;
6. 死锁:
发生的条件:
(1)互斥条件;
(2)保持与请求条件;
(3)独占条件(不能剥夺);
(4)循环等待条件;