jdk1.5原子包介绍(基于volatile)
包的特色:
1, 普通原子数值类型AtomicInteger, AtomicLong提供一些原子操作的加减运算.
2, 使用了解决脏数据问题的经典模式-"比对后设定", 即 查看主存中数据是否与预期提供的值一致,如果一致,才更新.
3, 使用AtomicReference可以实现对所有对象的原子引用及赋值.包括Double与Float,
但不包括对其的计算.浮点的计算,只能依靠同步关键字或Lock接口来实现了.
4, 对数组元素里的对象,符合以上特点的, 也可采用原子操作.包里提供了一些数组原子操作类
AtomicIntegerArray, AtomicLongArray等等.
5, 大幅度提升系统吞吐量及性能.
具体使用, 详解java doc.
1.区别ThreadLocal 与 synchronized
• ThreadLocal是一个线程隔离(或者说是线程安全)的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现;
• synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。
两者的性质、表现及设计初衷不同,因此没有可比较性。
2.理解ThreadLocal中提到的变量副本
事实上,我们向ThreadLocal中set的变量不是由ThreadLocal来存储的,而是Thread线程对象自身保存。当用户调用ThreadLocal对象的set(Object o)时,该方法则通过Thread.currentThread()获取当前线程,将变量存入Thread中的一个Map内,而Map的Key就是当前的ThreadLocal实例。请看源码,这是最主要的两个函数,能看出ThreadLocal与Thread的调用关系:
Java代码
1. public void set(T value) {
2. Thread t = Thread.currentThread();
3. ThreadLocalMap map = getMap(t);
4. if (map != null)
5. map.set(this, value);
6. else
7. createMap(t, value);
8. }
9.
10. ThreadLocalMap getMap(Thread t) {
11. return t.threadLocals;
12. }
(有兴趣的朋友可以阅读Java的ThreadLocal源码)因此,我们可以知道,所谓的变量副本,即是对Object Reference(对象引用)的拷贝。
3.理解Thread和 ThreadLocal对变量的引用关系
实际上Thread和ThreadLocal对变量引用关系就像是坐标系中的X轴和Y轴,是从两个维度上来组织对变量的引用的。
• 首先说Thread。 我们知道一个ThreadOne的执行会贯穿多个方法MethodA、MethodB、MethodC这些方法可能分布于不同的类实例。假设,这些方法分别使用了ThreadLocalA、ThreadLocalB、ThreadLocalC来保存线程本地变量,那么这些变量都存于ThreadOne的Map中,并使用各自的ThreadLocal实例作为key。 因此,可以认为,借助ThreanLocal的set方法,在X轴上,Thread横向关联同一线程上下文中来自多个Method的变量引用副本。
• 接着说ThreadLocal。 一个MethodA中的X变量将被多个线程ThreadOne、ThreadTwo、ThreadThree所访问。假设MethodA使用ThreadLocal存储X,通过set方法,以ThreadLocal作为key值,将不同线程来访时的不同的变量值引用保存于ThreadOne、ThreadTwo、ThreadThree的各自线程上下文中,确保每个线程有自己的一个变量值。因此,可以认为,ThreadLocal是以Method为Y轴,纵向关联了处于同一方法中的不同线程上的变量。
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
下面来看一个hibernate中典型的ThreadLocal的应用:
Java代码
1. private static final ThreadLocal threadSession = new ThreadLocal();
2.
3. public static Session getSession() throws InfrastructureException {
4. Session s = (Session) threadSession.get();
5. try {
6. if (s == null) {
7. s = getSessionFactory().openSession();
8. threadSession.set(s);
9. }
10. } catch (HibernateException ex) {
11. throw new InfrastructureException(ex);
12. }
13. return s;
14. }
可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。
显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。
试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了,不知道除了这些还有什么其他的好处。
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。
ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
四种方式 sychronized关键字
1. sychronized method(){}
2. sychronized (objectReference) {/*block*/}
3. static synchronized method(){}
4. sychronized(classname.class)
其中1和2是代表锁当前对象,即一个对象就一个锁,3和4代表锁这个类,即这个类的锁
要注意的是sychronized method()不是锁这个函数,而是锁对象,即:如果这个类中有两个方法都是sychronized,那么只要有两个线程共享一个该类的reference,每个调用这两个方法之一,不管是否同一个方法,都会用这个对象锁进行同步。锁类的3和4类推,即该类的不同reference调用了sychronized区段的咚咚就会受类锁的控制
还有,如果两个函数调用的先后顺序不能被打断,那么可以有个专门的锁对象来完成这个任务:
class MyLock
{
synchronized getLock()
{
//####还没写完
}
}
五个等级 参见effective java Item 52 : Document thread safety
1. immutable 不可变对象
2. thread-safe 线程安全的,可以放心使用,如java.util.Timer
3. conditionally thread-safe 条件线程安全的,如Vector和Hashtable,一般是安全的,除非存在几个方法调用之间的顺序不能被打断,这时可以用额外的锁来完成
4. thread-compatible 可以使用synchronized (objectReference)来协助完成对线程的调用
5. thread-hostile 不安全的
wait & notifyAll
在循环中使用wait 使用notifyAll而不是notify
pipe
java中也有pipe的,四个类:PipedInputStream, PipedInputReader, PipedOutputStream, PipedOutputWriter 下面是一段生产者消费者的代码(摘自core javaII):
/* set up pipes */
PipedOutputStream pout1 = new PipedOutputStream();
PipedInputStream pin1 = new PipedInputStream(pout1);
PipedOutputStream pout2 = new PipedOutputStream();
PipedInputStream pin2 = new PipedInputStream(pout2);
/* construct threads */
Producer prod = new Producer(pout1);
Filter filt = new Filter(pin1, pout2);
Consumer cons = new Consumer(pin2);
/* start threads */
prod.start(); filt.start(); cons.start();
注意
long 和double是简单类型中两个特殊的咚咚:java读他们要读两次,所以需要同步
死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程 "A" 获得了刀,而线程 "B" 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。虽然要探测或推敲各种情况是非常困难的,但只要按照下面几条规则去设计系统,就能够避免死锁问题:
* 让所有的线程按照同样的顺序获得一组锁。这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题。
*
* 将多个锁组成一组并放到同一个锁下。前面死锁的例子中,可以创建一个银器对象的锁。于是在获得刀或叉之前都必须获得这个银器的锁。
*
* 将那些不会阻塞的可获得资源用变量标志出来。当某个线程获得银器对象的锁时,就可以通过检查变量来判断是否整个银器集合中的对象锁都可获得。如果是,它就可以获得相关的锁,否则,就要释放掉银器这个锁并稍后再尝试。
*
* 最重要的是,在编写代码前认真仔细地设计整个系统。多线程是困难的,在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。
Volatile 变量. volatile 关键字是 Java 语言为优化编译器设计的。以下面的代码为例:
class VolatileTest {
public void foo() {
boolean flag = false;
if(flag) {
//this could happen
}
}
}
一个优化的编译器可能会判断出 if 部分的语句永远不会被执行,就根本不会编译这部分的代码。如果这个类被多线程访问, flag 被前面某个线程设置之后,在它被 if 语句测试之前,可以被其他线程重新设置。用 volatile 关键字来声明变量,就可以告诉编译器在编译的时候,不需要通过预测变量值来优化这部分的代码。
无法访问的线程 有时候虽然获取对象锁没有问题,线程依然有可能进入阻塞状态。在 Java 编程中 IO 就是这类问题最好的例子。当线程因为对象内的 IO 调用而阻塞时,此对象应当仍能被其他线程访问。该对象通常有责任取消这个阻塞的 IO 操作。造成阻塞调用的线程常常会令同步任务失败。如果该对象的其他方法也是同步的,当线程被阻塞时,此对象也就相当于被冷冻住了。其他的线程由于不能获得对象的锁,就不能给此对象发消息(例如,取消 IO 操作)。必须确保不在同步代码中包含那些阻塞调用,或确认在一个用同步阻塞代码的对象中存在非同步方法。尽管这种方法需要花费一些注意力来保证结果代码安全运行,但它允许在拥有对象的线程发生阻塞后,该对象仍能够响应其他线程。
调用 yield() 方法能够将当前的线程从处理器中移出到准备就绪队列中。另一个方法则是调用 sleep() 方法,使线程放弃处理器,并且在 sleep 方法中指定的时间间隔内睡眠。
正如你所想的那样,将这些方法随意放在代码的某个地方,并不能够保证正常工作。如果线程正拥有一个锁(因为它在一个同步方法或代码块中),则当它调用 yield() 时不能够释放这个锁。这就意味着即使这个线程已经被挂起,等待这个锁释放的其他线程依然不能继续运行。为了缓解这个问题,最好不在同步方法中调用 yield 方法。将那些需要同步的代码包在一个同步块中,里面不含有非同步的方法,并且在这些同步代码块之外才调用 yield。
另外一个解决方法则是调用 wait() 方法,使处理器放弃它当前拥有的对象的锁。如果对象在方法级别上使同步的,这种方法能够很好的工作。因为它仅仅使用了一个锁。如果它使用 fine-grained 锁,则 wait() 将无法放弃这些锁。此外,一个因为调用 wait() 方法而阻塞的线程,只有当其他线程调用 notifyAll() 时才会被唤醒。
在进行多线程编程时,经常要使用同步互斥机构,但java本身没有提供的同步互斥机构,仅提供了两个与同步互斥有关的方法:wait()和notify(),可以用来设计信号量类:mySemaphore,它是按照Dijkstra提出的计数信号量的思想设计的。
mySemaphore有两个最重要的成员方法:P()和V()。这两个方法实际就实现了信号量的P操作和V操作。具体描述如下:
public synchronized void P(){
semaphore--;
if(semaphore<0){
try{
wait();
}catch(InterruptedException ie){}
}
}
public synchronized void V(){
semaphore++;
if(semaphore<=0)
notify();
}
其中,semaphore变量记录了信号量的状态,wait()方法相当于block原语,用于阻塞线程的执行,notify()方法相当于wakeup原语,用于唤醒线程恢复运行。由于这两个方法定义为synchronized,这样java虚拟机可保证这两个方法的原子执行,从而实现了P、V操作。
二、管道
并发程序的多个线程之间的通讯通常是使用管道进行,jdk提供了两个管道类:PipedInpuStream和PipedOutputStream,前者用于输入,后者用于输出。这两种管道应该是能够多次连接和关闭,在实现过程中,却发现它们在关闭后,不能重新建立连接。经过仔细调试后,发现jdk的源代码在处理关闭时释放资源存在着缺陷,因此需要编写自己的管道类:MyPipedInputStream和MyPipedOutputStream。这两个类直接从InputStream和OutputStream继承而来,其成员方法与实现基本与PipedInputStream和 PipedOutputStream一致,只是在处理关闭时,将类中的成员变量的值恢复成未连接时的初始值。另外,原有的管道了提供的管道容量只有 1024个字节,在传输数据量较大时,可能会发生溢出,而在自己的管道类中可以任意设置管道容量,例如可以根据需要把管道容量设为64KB。以下仅给出了相应的关闭例程:
1.MyPipedInputStream
public void close() throws IOException {
in = -1;
out = 0;
closedByReader = true;
connected = false;
closed = true;
buffer = new byte[PIPE_SIZE];
}
2.MyPipedOutputStream
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
sink.closed = true;
}
sink = null;
connected = false;
}
包的特色:
1, 普通原子数值类型AtomicInteger, AtomicLong提供一些原子操作的加减运算.
2, 使用了解决脏数据问题的经典模式-"比对后设定", 即 查看主存中数据是否与预期提供的值一致,如果一致,才更新.
3, 使用AtomicReference可以实现对所有对象的原子引用及赋值.包括Double与Float,
但不包括对其的计算.浮点的计算,只能依靠同步关键字或Lock接口来实现了.
4, 对数组元素里的对象,符合以上特点的, 也可采用原子操作.包里提供了一些数组原子操作类
AtomicIntegerArray, AtomicLongArray等等.
5, 大幅度提升系统吞吐量及性能.
具体使用, 详解java doc.
1.区别ThreadLocal 与 synchronized
• ThreadLocal是一个线程隔离(或者说是线程安全)的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现;
• synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。
两者的性质、表现及设计初衷不同,因此没有可比较性。
2.理解ThreadLocal中提到的变量副本
事实上,我们向ThreadLocal中set的变量不是由ThreadLocal来存储的,而是Thread线程对象自身保存。当用户调用ThreadLocal对象的set(Object o)时,该方法则通过Thread.currentThread()获取当前线程,将变量存入Thread中的一个Map内,而Map的Key就是当前的ThreadLocal实例。请看源码,这是最主要的两个函数,能看出ThreadLocal与Thread的调用关系:
Java代码
1. public void set(T value) {
2. Thread t = Thread.currentThread();
3. ThreadLocalMap map = getMap(t);
4. if (map != null)
5. map.set(this, value);
6. else
7. createMap(t, value);
8. }
9.
10. ThreadLocalMap getMap(Thread t) {
11. return t.threadLocals;
12. }
(有兴趣的朋友可以阅读Java的ThreadLocal源码)因此,我们可以知道,所谓的变量副本,即是对Object Reference(对象引用)的拷贝。
3.理解Thread和 ThreadLocal对变量的引用关系
实际上Thread和ThreadLocal对变量引用关系就像是坐标系中的X轴和Y轴,是从两个维度上来组织对变量的引用的。
• 首先说Thread。 我们知道一个ThreadOne的执行会贯穿多个方法MethodA、MethodB、MethodC这些方法可能分布于不同的类实例。假设,这些方法分别使用了ThreadLocalA、ThreadLocalB、ThreadLocalC来保存线程本地变量,那么这些变量都存于ThreadOne的Map中,并使用各自的ThreadLocal实例作为key。 因此,可以认为,借助ThreanLocal的set方法,在X轴上,Thread横向关联同一线程上下文中来自多个Method的变量引用副本。
• 接着说ThreadLocal。 一个MethodA中的X变量将被多个线程ThreadOne、ThreadTwo、ThreadThree所访问。假设MethodA使用ThreadLocal存储X,通过set方法,以ThreadLocal作为key值,将不同线程来访时的不同的变量值引用保存于ThreadOne、ThreadTwo、ThreadThree的各自线程上下文中,确保每个线程有自己的一个变量值。因此,可以认为,ThreadLocal是以Method为Y轴,纵向关联了处于同一方法中的不同线程上的变量。
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
下面来看一个hibernate中典型的ThreadLocal的应用:
Java代码
1. private static final ThreadLocal threadSession = new ThreadLocal();
2.
3. public static Session getSession() throws InfrastructureException {
4. Session s = (Session) threadSession.get();
5. try {
6. if (s == null) {
7. s = getSessionFactory().openSession();
8. threadSession.set(s);
9. }
10. } catch (HibernateException ex) {
11. throw new InfrastructureException(ex);
12. }
13. return s;
14. }
可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。
显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。
试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了,不知道除了这些还有什么其他的好处。
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。
ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
四种方式 sychronized关键字
1. sychronized method(){}
2. sychronized (objectReference) {/*block*/}
3. static synchronized method(){}
4. sychronized(classname.class)
其中1和2是代表锁当前对象,即一个对象就一个锁,3和4代表锁这个类,即这个类的锁
要注意的是sychronized method()不是锁这个函数,而是锁对象,即:如果这个类中有两个方法都是sychronized,那么只要有两个线程共享一个该类的reference,每个调用这两个方法之一,不管是否同一个方法,都会用这个对象锁进行同步。锁类的3和4类推,即该类的不同reference调用了sychronized区段的咚咚就会受类锁的控制
还有,如果两个函数调用的先后顺序不能被打断,那么可以有个专门的锁对象来完成这个任务:
class MyLock
{
synchronized getLock()
{
//####还没写完
}
}
五个等级 参见effective java Item 52 : Document thread safety
1. immutable 不可变对象
2. thread-safe 线程安全的,可以放心使用,如java.util.Timer
3. conditionally thread-safe 条件线程安全的,如Vector和Hashtable,一般是安全的,除非存在几个方法调用之间的顺序不能被打断,这时可以用额外的锁来完成
4. thread-compatible 可以使用synchronized (objectReference)来协助完成对线程的调用
5. thread-hostile 不安全的
wait & notifyAll
在循环中使用wait 使用notifyAll而不是notify
pipe
java中也有pipe的,四个类:PipedInputStream, PipedInputReader, PipedOutputStream, PipedOutputWriter 下面是一段生产者消费者的代码(摘自core javaII):
/* set up pipes */
PipedOutputStream pout1 = new PipedOutputStream();
PipedInputStream pin1 = new PipedInputStream(pout1);
PipedOutputStream pout2 = new PipedOutputStream();
PipedInputStream pin2 = new PipedInputStream(pout2);
/* construct threads */
Producer prod = new Producer(pout1);
Filter filt = new Filter(pin1, pout2);
Consumer cons = new Consumer(pin2);
/* start threads */
prod.start(); filt.start(); cons.start();
注意
long 和double是简单类型中两个特殊的咚咚:java读他们要读两次,所以需要同步
死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程 "A" 获得了刀,而线程 "B" 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。虽然要探测或推敲各种情况是非常困难的,但只要按照下面几条规则去设计系统,就能够避免死锁问题:
* 让所有的线程按照同样的顺序获得一组锁。这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题。
*
* 将多个锁组成一组并放到同一个锁下。前面死锁的例子中,可以创建一个银器对象的锁。于是在获得刀或叉之前都必须获得这个银器的锁。
*
* 将那些不会阻塞的可获得资源用变量标志出来。当某个线程获得银器对象的锁时,就可以通过检查变量来判断是否整个银器集合中的对象锁都可获得。如果是,它就可以获得相关的锁,否则,就要释放掉银器这个锁并稍后再尝试。
*
* 最重要的是,在编写代码前认真仔细地设计整个系统。多线程是困难的,在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。
Volatile 变量. volatile 关键字是 Java 语言为优化编译器设计的。以下面的代码为例:
class VolatileTest {
public void foo() {
boolean flag = false;
if(flag) {
//this could happen
}
}
}
一个优化的编译器可能会判断出 if 部分的语句永远不会被执行,就根本不会编译这部分的代码。如果这个类被多线程访问, flag 被前面某个线程设置之后,在它被 if 语句测试之前,可以被其他线程重新设置。用 volatile 关键字来声明变量,就可以告诉编译器在编译的时候,不需要通过预测变量值来优化这部分的代码。
无法访问的线程 有时候虽然获取对象锁没有问题,线程依然有可能进入阻塞状态。在 Java 编程中 IO 就是这类问题最好的例子。当线程因为对象内的 IO 调用而阻塞时,此对象应当仍能被其他线程访问。该对象通常有责任取消这个阻塞的 IO 操作。造成阻塞调用的线程常常会令同步任务失败。如果该对象的其他方法也是同步的,当线程被阻塞时,此对象也就相当于被冷冻住了。其他的线程由于不能获得对象的锁,就不能给此对象发消息(例如,取消 IO 操作)。必须确保不在同步代码中包含那些阻塞调用,或确认在一个用同步阻塞代码的对象中存在非同步方法。尽管这种方法需要花费一些注意力来保证结果代码安全运行,但它允许在拥有对象的线程发生阻塞后,该对象仍能够响应其他线程。
调用 yield() 方法能够将当前的线程从处理器中移出到准备就绪队列中。另一个方法则是调用 sleep() 方法,使线程放弃处理器,并且在 sleep 方法中指定的时间间隔内睡眠。
正如你所想的那样,将这些方法随意放在代码的某个地方,并不能够保证正常工作。如果线程正拥有一个锁(因为它在一个同步方法或代码块中),则当它调用 yield() 时不能够释放这个锁。这就意味着即使这个线程已经被挂起,等待这个锁释放的其他线程依然不能继续运行。为了缓解这个问题,最好不在同步方法中调用 yield 方法。将那些需要同步的代码包在一个同步块中,里面不含有非同步的方法,并且在这些同步代码块之外才调用 yield。
另外一个解决方法则是调用 wait() 方法,使处理器放弃它当前拥有的对象的锁。如果对象在方法级别上使同步的,这种方法能够很好的工作。因为它仅仅使用了一个锁。如果它使用 fine-grained 锁,则 wait() 将无法放弃这些锁。此外,一个因为调用 wait() 方法而阻塞的线程,只有当其他线程调用 notifyAll() 时才会被唤醒。
在进行多线程编程时,经常要使用同步互斥机构,但java本身没有提供的同步互斥机构,仅提供了两个与同步互斥有关的方法:wait()和notify(),可以用来设计信号量类:mySemaphore,它是按照Dijkstra提出的计数信号量的思想设计的。
mySemaphore有两个最重要的成员方法:P()和V()。这两个方法实际就实现了信号量的P操作和V操作。具体描述如下:
public synchronized void P(){
semaphore--;
if(semaphore<0){
try{
wait();
}catch(InterruptedException ie){}
}
}
public synchronized void V(){
semaphore++;
if(semaphore<=0)
notify();
}
其中,semaphore变量记录了信号量的状态,wait()方法相当于block原语,用于阻塞线程的执行,notify()方法相当于wakeup原语,用于唤醒线程恢复运行。由于这两个方法定义为synchronized,这样java虚拟机可保证这两个方法的原子执行,从而实现了P、V操作。
二、管道
并发程序的多个线程之间的通讯通常是使用管道进行,jdk提供了两个管道类:PipedInpuStream和PipedOutputStream,前者用于输入,后者用于输出。这两种管道应该是能够多次连接和关闭,在实现过程中,却发现它们在关闭后,不能重新建立连接。经过仔细调试后,发现jdk的源代码在处理关闭时释放资源存在着缺陷,因此需要编写自己的管道类:MyPipedInputStream和MyPipedOutputStream。这两个类直接从InputStream和OutputStream继承而来,其成员方法与实现基本与PipedInputStream和 PipedOutputStream一致,只是在处理关闭时,将类中的成员变量的值恢复成未连接时的初始值。另外,原有的管道了提供的管道容量只有 1024个字节,在传输数据量较大时,可能会发生溢出,而在自己的管道类中可以任意设置管道容量,例如可以根据需要把管道容量设为64KB。以下仅给出了相应的关闭例程:
1.MyPipedInputStream
public void close() throws IOException {
in = -1;
out = 0;
closedByReader = true;
connected = false;
closed = true;
buffer = new byte[PIPE_SIZE];
}
2.MyPipedOutputStream
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
sink.closed = true;
}
sink = null;
connected = false;
}