最近写关于并发的小应用,才发现真的该好好的正视java的多线程了。之前没有深入的掌握,用起来也是那么的吃力。作为J2SE里面为 数不多的重要难点之一,多线程应用一直是我以敬畏的心态去尽量避开的,只是通过一些实例掌握一些简单的应用。这段时间会多用点时间 去掌握,有需要写下来的我也会通过这种方式既分享又加深理解。
首先这篇只涉及基础的知识整理,对于并发包java.util.concurrent内的线程池和锁我会看情况在之后的总结中写点东西。对于进程的 概念我们都很熟悉,它是应用程序级的隔离,不同的应用程序之间的进程几乎不共享任何资源。而线程则可以说是应用程序内的隔离,一种 相对低级别的隔离。一个进程可以有多个线程,它们之间隔离的内容大致包括:a.自身的堆栈,b.程序计数器,c.局部变量;共享应用的内 容大致包括:a.内存,b.文件句柄,c.进程状态等。线程不是Java自身的概念,它是操作系统底层的概念。Java作为一种应用语言把线程的 操作通过API提升到应用开发的支持,但是在并发性的支持上并不是那么美好。
Java在设计时,每个对象都有一个隐式的锁,这个锁的使用则是通过synchronized关键字来显式的使用。在JDK5.0以后引用了 java.util.concurrent.ReentrantLock作为synchronized之外的选择,配和Condition可以以一种条件锁的机制来管理并发的线程,之后的 总结再介绍。提到synchronized,多数的初学者都知道Object的 wait(),notify(),notifyAll()是配和其使用的,但是为什么要在同步内 才能用对象的这些方法呢(不然抛 IllegalMoniTorStateException)?
我想因为没有synchronized让对象的隐式锁发挥作用,那么方法或者方法块内的线程在同一时间可能存在多个,假设wait()可用,它会 把这些线程统统的加到wait set中等待被唤醒,这样永远没有多余的线程去唤醒它们。每个对象管理调用其wait(),notify()的线程,使得 别的对象即使想帮忙也帮不上忙。这样的结果就是多线程永远完成不了多任务,基于此Java在设计时使其必须与synchronized一起使用,这 样获得隐式锁的线程同一时间只有一个,当此线程被对象的wait()扔到wait set中时,线程会释放这个对象的隐式锁等待被唤醒的机会,这 样的设计会大大降低死锁。另外同一个对象隐式锁作用下的多个方法或者方法块在没有锁的限制下可以同时允许多个线程在不同的方法内 wait和notify,严重的竞争条件使得死锁轻而易举。所以Java设计者试图通过MoniTor Object模式解决这些问题,每个对象都是MoniTor用 于监视拥有其使用权的线程。
但是synchronized这种获得隐式锁的方式本身也是有隐患问题的:a.不能中断正在试图获得锁的线程,b.试图获得锁时不能设定超时, c.每个锁只有一个条件太少。对于最后一项的设计前面提到的JDK5的方案是可以弥补的,一个ReentrantLock可以有多个Condition,每个条 件管理获得对象锁满足条件的线程,通过await(),signalAll()使只关于Condition自己放倒的线程继续运行,或者放倒一些线程,而不是全 部唤醒等等。但对于前两者的极端情况会出现死锁。下面的这个例子:
Java代码
class DeadLockSample{ public final Object lock1 = new Object(); public final Object lock2 = new Object(); public void methodOne(){ synchronized(lock1){ ... synchronized(lock2){...} } } public void methodTwo(){ synchronized(lock2){ ... synchronized(lock1){...} } }}
假设场景:线程A调用methodOne(),获得lock1的隐式锁后,在获得lock2的隐式锁之前线程B进入运行,调用 methodTwo(),抢先获得了 lock2的隐式锁,此时线程A等着线程B交出lock2,线程B等着lock1进入方法块,死锁就这样被创造出来了。
以上的例子不直观的话,再看一个实例顺便看看wait()的缺陷:
Java代码
import java.util.LinkedList;import java.util.List;/** * User: yanxuxin * Date: Dec 9, 2009 * Time: 5:58:39 PM */public class DeadLockSample { public static void main(String[] args) { final WaitAndNotify wan = new WaitAndNotify(); Thread t1 = new Thread(new Runnable(){ public void run() { wan.pop(); } }); Thread t2 = new Thread(new Runnable(){ public void run() { wan.push("a"); } }); t1.start(); t2.start(); }}class WaitAndNotify { final List list = new LinkedList(); public synchronized void push(String x) { synchronized(list) { list.add(x); notify(); } } public synchronized Object pop() { synchronized(list) { if(list.size() <= 0) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return list.size(); } }}
上面的这个例子也会出现死锁,为什么呢?首先看WaitAndNotify这个类,在push和pop方法上有synchronized关键字,方法内部也有 synchronized,那么当WaitAndNotify实例化时会有两个对象的隐式锁,一个是WaitAndNotify对象自身的,作用在方法上;另一个就是方法 内部同步用到的list的。主线程开启两个线程t1和t2,t1进入pop方法此时list为空,它先后获得了wan和 list的隐式锁,接着就被wait扔 进wait set等待去了。注意这个wait()方法是谁的?答案是wan的,所以它释放了wan的隐式锁,但是把list的死死的抓着不放。此时t2终于 得到了 wan的隐式锁进入push方法,但是不幸的是list的隐式锁它这辈子也得不到了。。。
就是由于wait的设计是针对对象管理线程的,而又没有其他的可以类似栈的方式层层释放锁,导致死锁的杯具了。关于synchronized和 wait(),notify(),notifyAll()的故事,我想我能瞎编的暂时这么多了,不然滔滔江水就又杯具了,哈哈。下面简单的讲讲 java.lang.Thread的特色小故事。(待续…)
PS:”多线程基础总结六”算是本文的补遗了,呵呵。
都成为命途中美丽的点缀,看天,看雪,安安静静,不言不语都是好风景。