Java多线程发展简史

这篇文章,大部分内容,是周五我做的一个关于如何进行Java多线程编程的Knowledge Sharing的一个整理,我希望能对Java从第一个版本开始,在多线程编程方面的大事件和发展脉络有一个描述,并且提及一些在多线程编程方面常见的问题。对于Java程序员来说,如果从历史的角度去了解一门语言一个特性的演进,或许能有不同收获。

 

引言

首先问这样一个问题,如果提到Java多线程编程,你会想到什么?

 

  • volatile、synchronized关键字?
  • 竞争和同步?
  • 锁机制?
  • 线程安全问题?
  • 线程池和队列?

好吧,请原谅我在这里卖的关子,其实这些都对,但是又不足够全面,如果我们这样来谈论Java多线程会不会全面一些:

  1. 模型:JMM(Java内存模型)和JCM(Java并发模型)
  2. 使用:JDK中的并发包
  3. 实践:怎样写线程安全的代码
  4. 除错:使用工具来分析并发问题
  5. ……

可是,这未免太死板了,不是么?

不如换一个思路,我们少谈一些很容易查到的语法,不妨从历史的角度看看Java在多线程编程方面是怎样进化的,这个过程中,它做了哪些正确的决定,犯了哪些错误,未来又会有怎样的发展趋势?

另外,还有一点要说是,我希望通过大量的实例代码来说明这些事情。Linus说:“Talk is cheap, show me the code.”。下文涉及到的代码我已经上传,可以在此打包下载。

 

诞生

Java的基因来自于1990年12月Sun公司的一个内部项目,目标设备正是家用电器,但是C++的可移植性和API的易用性都让程序员反感。旨在解决这样的问题,于是又了Java的前身Oak语言,但是知道1995年3月,它正式更名为Java,才算Java语言真正的诞生。

 

JDK 1.0

1996年1月的JDK1.0版本,从一开始就确立了Java最基础的线程模型,并且,这样的线程模型再后续的修修补补中,并未发生实质性的变更,可以说是一个具有传承性的良好设计。

抢占式和协作式是两种常见的进程/线程调度方式,操作系统非常适合使用抢占式方式来调度它的进程,它给不同的进程分配时间片,对于长期无响应的进程,它有能力剥夺它的资源,甚至将其强行停止(如果采用协作式的方式,需要进程自觉、主动地释放资源,也许就不知道需要等到什么时候了)。Java语言一开始就采用协作式的方式,并且在后面发展的过程中,逐步废弃掉了粗暴的stop/resume/suspend这样的方法,它们是违背协作式的不良设计,转而采用wait/notify/sleep这样的两边线程配合行动的方式。

一种线程间的通信方式是使用中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InterruptCheck extends Thread {
 
     @Override
     public void run() {
         System.out.println( "start" );
         while ( true )
             if (Thread.currentThread().isInterrupted())
                 break ;
         System.out.println( "while exit" );
     }
 
     public static void main(String[] args) {
         Thread thread = new InterruptCheck();
         thread.start();
         try {
             sleep( 2000 );
         } catch (InterruptedException e) {
         }
         thread.interrupt();
     }
}

这是中断的一种使用方式,看起来就像是一个标志位,线程A设置这个标志位,线程B时不时地检查这个标志位。另外还有一种使用中断通信的方式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class InterruptWait extends Thread {
     public static Object lock = new Object();
 
     @Override
     public void run() {
         System.out.println( "start" );
         synchronized (lock) {
             try {
                 lock.wait();
             } catch (InterruptedException e) {
                 System.out.println(Thread.currentThread().isInterrupted());
                 Thread.currentThread().interrupt(); // set interrupt flag again
                 System.out.println(Thread.currentThread().isInterrupted());
                 e.printStackTrace();
             }
         }
     }
 
     public static void main(String[] args) {
         Thread thread = new InterruptWait();
         thread.start();
         try {
             sleep( 2000 );
         } catch (InterruptedException e) {
         }
         thread.interrupt();
     }
}

在这种方式下,如果使用wait方法处于等待中的线程,被另一个线程使用中断唤醒,于是抛出InterruptedException,同时,中断标志清除,这时候我们通常会在捕获该异常的地方重新设置中断,以便后续的逻辑通过检查中断状态来了解该线程是如何结束的

在比较稳定的JDK 1.0.2版本中,已经可以找到Thread和ThreadUsage这样的类,这也是线程模型中最核心的两个类。整个版本只包含了这样几个包:java.io、 java.util、java.net、java.awt和java.applet,所以说Java从一开始这个非常原始的版本就确立了一个持久的线程模型

值得一提的是,在这个版本中,原子对象AtomicityXXX已经设计好了,这里给出一个例子,说明i++这种操作时非原子的,而使用原子对象可以保证++操作的原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.atomic.AtomicInteger;
 
public class Atomicity {
 
     private static volatile int nonAtomicCounter = 0 ;
     private static volatile AtomicInteger atomicCounter = new AtomicInteger( 0 );
     private static int times = 0 ;
 
     public static void caculate() {
         times++;
         for ( int i = 0 ; i < 1000 ; i++) {
             new Thread( new Runnable() {
                 @Override
                 public void run() {
                     nonAtomicCounter++;
                     atomicCounter.incrementAndGet();
                 }
             }).start();
         }
 
         try {
             Thread.sleep( 1000 );
         } catch (InterruptedException e) {
         }
     }
 
     public static void main(String[] args) {
         caculate();
         while (nonAtomicCounter == 1000 ) {
             nonAtomicCounter = 0 ;
             atomicCounter.set( 0 );
             caculate();
         }
 
         System.out.println( "Non-atomic counter: " + times + ":"
                 + nonAtomicCounter);
         System.out.println( "Atomic counter: " + times + ":" + atomicCounter);
     }
}

上面这个例子你也许需要跑几次才能看到效果,使用非原子性的++操作,结果经常小于1000。

对于锁的使用,网上可以找到各种说明,但表述都不够清晰。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class Lock {
     private static Object o = new Object();
     static Lock lock = new Lock();
 
     // lock on dynamic method
     public synchronized void dynamicMethod() {
         System.out.println( "dynamic method" );
         sleepSilently( 2000 );
     }
 
     // lock on static method
     public static synchronized void staticMethod() {
         System.out.println( "static method" );
         sleepSilently( 2000 );
     }
 
     // lock on this
     public void thisBlock() {
         synchronized ( this ) {
             System.out.println( "this block" );
             sleepSilently( 2000 );
         }
     }
 
     // lock on an object
     public void objectBlock() {
         synchronized (o) {
             System.out.println( "dynamic block" );
             sleepSilently( 2000 );
         }
     }
 
     // lock on the class
     public static void classBlock() {
         synchronized (Lock. class ) {
             System.out.println( "static block" );
             sleepSilently( 2000 );
         }
     }
 
     private static void sleepSilently( long millis) {
         try {
             Thread.sleep(millis);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 
     public static void main(String[] args) {
 
         // object lock test
         new Thread() {
             @Override
             public void run() {
                 lock.dynamicMethod();
             }
         }.start();
         new Thread() {
             @Override
             public void run() {
                 lock.thisBlock();
             }
         }.start();
         new Thread() {
             @Override
             public void run() {
                 lock.objectBlock();
             }
         }.start();
 
         sleepSilently( 3000 );
         System.out.println();
 
         // class lock test
         new Thread() {
             @Override
             public void run() {
                 lock.staticMethod();
             }
         }.start();
         new Thread() {
             @Override
             public void run() {
                 lock.classBlock();
             }
         }.start();
 
     }
}

上面的例子可以反映对一个锁竞争的现象,结合上面的例子,理解下面这两条,就可以很容易理解synchronized关键字的使用:

  • 非静态方法使用synchronized修饰,相当于synchronized(this)。
  • 静态方法使用synchronized修饰,相当于synchronized(Lock.class)。

 

JDK 1.2

1998年年底的JDK1.2版本正式把Java划分为J2EE/J2SE/J2ME三个不同方向。在这个版本中,Java试图用Swing修正在AWT中犯的错误,例如使用了太多的同步。可惜的是,Java本身决定了AWT还是Swing性能和响应都难以令人满意,这也是Java桌面应用难以比及其服务端应用的一个原因,在IBM后来的SWT,也不足以令人满意,JDK在这方面到JDK 1.2后似乎反省了自己,停下脚步了。值得注意的是,JDK高版本修复低版本问题的时候,通常遵循这样的原则:

  1. 向下兼容。所以往往能看到很多重新设计的新增的包和类,还能看到deprecated的类和方法,但是它们并不能轻易被删除。
  2. 严格遵循JLS(Java Language Specification),并把通过的新JSR(Java Specification Request)补充到JLS中,因此这个文档本身也是向下兼容的,后面的版本只能进一步说明和特性增强,对于一些最初扩展性比较差的设计,也会无能为力。这个在下文中关于ReentrantLock的介绍中也可以看到。

在这个版本中,正式废除了这样三个方法:stop()、suspend()和resume()。下面我就来介绍一下,为什么它们要被废除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Stop extends Thread {
     @Override
     public void run() {
         try {
             while ( true )
                 ;
         } catch (Throwable e) {
             e.printStackTrace();
         }
     }
 
     public static void main(String[] args) {
         Thread thread = new Stop();
         thread.start();
 
         try {
             sleep( 1000 );
         } catch (InterruptedException e) {
         }
 
         thread.stop( new Exception( "stop" )); // note the stack trace
     }
}

从上面的代码你应该可以看出两件事情:

  1. 使用stop来终止一个线程是不讲道理、极其残暴的,不论目标线程在执行任何语句,一律强行终止线程,最终将导致一些残缺的对象和不可预期的问题产生。
  2. 被终止的线程没有任何异常抛出,你在线程终止后找不到任何被终止时执行的代码行,或者是堆栈信息(上面代码打印的异常仅仅是main线程执行stop语句的异常而已,并非被终止的线程)。

很难想象这样的设计出自一个连指针都被废掉的类型安全的编程语言,对不对?再来看看suspend的使用,有引起死锁的隐患:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Suspend extends Thread {
     @Override
     public void run() {
         synchronized ( this ) {
             while ( true )
                 ;
         }
     }
 
     public static void main(String[] args) {
         Thread thread = new Suspend();
         thread.start();
 
         try {
             sleep( 1000 );
         } catch (InterruptedException e) {
         }
 
         thread.suspend();
 
         synchronized (thread) { // dead lock
             System.out.println( "got the lock" );
             thread.resume();
         }
     }
}

从上面的代码可以看出,Suspend线程被挂起时,依然占有锁,而当main线程期望去获取该线程来唤醒它时,彻底瘫痪了。由于suspend在这里是无期限限制的,这会变成一个彻彻底底的死锁。

相反,看看这三个方法的改进品和替代品:wait()、notify()和sleep(),它们令线程之间的交互就友好得多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Wait extends Thread {
     @Override
     public void run() {
         System.out.println( "start" );
         synchronized ( this ) { // wait/notify/notifyAll use the same
                                 // synchronization resource
             try {
                 this .wait();
             } catch (InterruptedException e) {
                 e.printStackTrace(); // notify won't throw exception
             }
         }
     }
 
     public static void main(String[] args) {
         Thread thread = new Wait();
         thread.start();
         try {
             sleep( 2000 );
         } catch (InterruptedException e) {
         }
         synchronized (thread) {
             System.out.println( "Wait() will release the lock!" );
             thread.notify();
         }
     }
}

在wait和notify搭配使用的过程中,注意需要把它们锁定到同一个资源上(例如对象a),即:

  1. 一个线程中synchronized(a),并在同步块中执行a.wait()
  2. 另一个线程中synchronized(a),并在同步块中执行a.notify()

再来看一看sleep方法的使用,回答下面两个问题:

  1. 和wait比较一下,为什么sleep被设计为Thread的一个静态方法(即只让当前线程sleep)?
  2. 为什么sleep必须要传入一个时间参数,而不允许不限期地sleep?

如果我前面说的你都理解了,你应该能回答这两个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Sleep extends Thread {
     @Override
     public void run() {
         System.out.println( "start" );
         synchronized ( this ) { // sleep() can use (or not) any synchronization resource
             try {
                 /**
                  * Do you know: <br>
                  * 1. Why sleep() is designed as a static method comparing with
                  * wait?<br>
                  * 2. Why sleep() must have a timeout parameter?
                  */
                 this .sleep( 10000 );
             } catch (InterruptedException e) {
                 e.printStackTrace(); // notify won't throw exception
             }
         }
     }
 
     public static void main(String[] args) {
         Thread thread = new Sleep();
         thread.start();
         try {
             sleep( 2000 );
         } catch (InterruptedException e) {
         }
         synchronized (thread) {
             System.out.println( "Has sleep() released the lock!" );
             thread.notify();
         }
     }
}

在这个JDK版本中,引入线程变量ThreadLocal这个类:

Java多线程发展简史_第1张图片

每一个线程都挂载了一个ThreadLocalMap。ThreadLocal这个类的使用很有意思,get方法没有key传入,原因就在于这个key就是当前你使用的这个ThreadLocal它自己。ThreadLocal的对象生命周期可以伴随着整个线程的生命周期。因此,倘若在线程变量里存放持续增长的对象(最常见是一个不受良好管理的map),很容易导致内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ThreadLocalUsage extends Thread {
     public User user = new User();
 
     public User getUser() {
         return user;
     }
 
     @Override
     public void run() {
         this .user.set( "var1" );
 
         while ( true ) {
             try {
                 sleep( 1000 );
             } catch (InterruptedException e) {
             }
             System.out.println( this .user.get());
         }
     }
 
     public static void main(String[] args) {
 
         ThreadLocalUsage thread = new ThreadLocalUsage();
         thread.start();
 
         try {
             sleep( 4000 );
         } catch (InterruptedException e) {
         }
 
         thread.user.set( "var2" );
 
     }
}
 
class User {
 
     private static ThreadLocal<Object> enclosure = new ThreadLocal<Object>(); // is it must be static?
 
     public void set(Object object) {
        

你可能感兴趣的:(Java多线程发展简史)