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

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

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

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

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

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

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

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

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

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

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

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

#p#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 
 
 
 
  1. public class ThreadLocalUsage extends Thread {  
  2.     public User user = new User();  
  3.    
  4.     public User getUser() {  
  5.         return user;  
  6.     }  
  7.    
  8.     @Override 
  9.     public void run() {  
  10.         this.user.set("var1");  
  11.    
  12.         while (true) {  
  13.             try {  
  14.                 sleep(1000);  
  15.             } catch (InterruptedException e) {  
  16.             }  
  17.             System.out.println(this.user.get());  
  18.         }  
  19.     }  
  20.    
  21.     public static void main(String[] args) {  
  22.    
  23.         ThreadLocalUsage thread = new ThreadLocalUsage();  
  24.         thread.start();  
  25.    
  26.         try {  
  27.             sleep(4000);  
  28.         } catch (InterruptedException e) {  
  29.         }  
  30.    
  31.         thread.user.set("var2");  
  32.    
  33.     }  
  34. }  
  35.    
  36. class User {  
  37.    
  38.     private static ThreadLocal enclosure = new ThreadLocal(); // is it must be static?  
  39.    
  40.     public void set(Object object) {  
  41.         enclosure.set(object);  
  42.     }  
  43.    
  44.     public Object get() {  
  45.         return enclosure.get();  
  46.     }  
  47. 上面的例子会一直打印var1,而不会打印var2,就是因为不同线程中的ThreadLocal是互相独立的。

    用jstack工具可以找到锁相关的信息,如果线程占有锁,但是由于执行到wait方法时处于wait状态暂时释放了锁,会打印waiting on的信息:

     
     
     
     
    1. "Thread-0" prio=6 tid=0x02bc4400 nid=0xef44 in Object.wait() [0x02f0f000]  
    2.    java.lang.Thread.State: WAITING (on object monitor)  
    3.         at java.lang.Object.wait(Native Method)  
    4.         - waiting on <0x22a7c3b8> (a Wait)  
    5.         at java.lang.Object.wait(Object.java:485)  
    6.         at Wait.run(Wait.java:8)  
    7.         - locked <0x22a7c3b8> (a Wait) 

    如果程序持续占有某个锁(例如sleep方法在sleep期间不会释放锁),会打印locked的信息:

     
     
     
     
    1. "Thread-0" prio=6 tid=0x02baa800 nid=0x1ea4 waiting on condition [0x02f0f000]  
    2.    java.lang.Thread.State: TIMED_WAITING (sleeping)  
    3.         at java.lang.Thread.sleep(Native Method)  
    4.         at Wait.run(Wait.java:8)  
    5.         - locked <0x22a7c398> (a Wait) 

    而如果是线程希望进入某同步块,而在等待锁的释放,会打印waiting to的信息:

     
     
     
     
    1. "main" prio=6 tid=0x00847400 nid=0xf984 waiting for monitor entry [0x0092f000]  
    2.    java.lang.Thread.State: BLOCKED (on object monitor)  
    3.         at Wait.main(Wait.java:23)  
    4.         - waiting to lock <0x22a7c398> (a Wait) 

    #p#

    JDK 1.4

    在2002年4月发布的JDK1.4中,正式引入了NIO。JDK在原有标准IO的基础上,提供了一组多路复用IO的解决方案。

    通过在一个Selector上挂接多个Channel,通过统一的轮询线程检测,每当有数据到达,触发监听事件,将事件分发出去,而不是让每一个channel长期消耗阻塞一个线程等待数据流到达。所以,只有在对资源争夺剧烈的高并发场景下,才能见到NIO的明显优势。

    相较于面向流的传统方式这种面向块的访问方式会丢失一些简易性和灵活性。下面给出一个NIO接口读取文件的简单例子(仅示意用):

     
     
     
     
    1. import java.io.FileInputStream;  
    2. import java.io.IOException;  
    3. import java.nio.ByteBuffer;  
    4. import java.nio.channels.FileChannel;  
    5.    
    6. public class NIO {  
    7.    
    8.     public static void nioRead(String file) throws IOException {  
    9.         FileInputStream in = new FileInputStream(file);  
    10.         FileChannel channel = in.getChannel();  
    11.    
    12.         ByteBuffer buffer = ByteBuffer.allocate(1024);  
    13.         channel.read(buffer);  
    14.         byte[] b = buffer.array();  
    15.         System.out.println(new String(b));  
    16.         channel.close();  
    17.     }  

    JDK 5.0

    2004年9月起JDK 1.5发布,并正式更名到5.0。有个笑话说,软件行业有句话,叫做“不要用3.0版本以下的软件”,意思是说版本太小的话往往软件质量不过关——但是按照这种说法,JDK的原有版本命名方式得要到啥时候才有3.0啊,于是1.4以后通过版本命名方式的改变直接升到5.0了。

    JDK 5.0不只是版本号命名方式变更那么简单,对于多线程编程来说,这里发生了两个重大事件,JSR 133和JSR 166的正式发布。

    JSR 133

    JSR 133重新明确了Java内存模型,事实上,在这之前,常见的内存模型包括连续一致性内存模型和先行发生模型。

    对于连续一致性模型来说,程序执行的顺序和代码上显示的顺序是完全一致的。这对于现代多核,并且指令执行优化的CPU来说,是很难保证的。而且,顺序一致性的保证将JVM对代码的运行期优化严重限制住了。

    但是JSR 133指定的先行发生(Happens-before)使得执行指令的顺序变得灵活:

    • 在同一个线程里面,按照代码执行的顺序(也就是代码语义的顺序),前一个操作先于后面一个操作发生
    • 对一个monitor对象的解锁操作先于后续对同一个monitor对象的锁操作
    • 对volatile字段的写操作先于后面的对此字段的读操作
    • 对线程的start操作(调用线程对象的start()方法)先于这个线程的其他任何操作
    • 一个线程中所有的操作先于其他任何线程在此线程上调用 join()方法
    • 如果A操作优先于B,B操作优先于C,那么A操作优先于C

    而在内存分配上,将每个线程各自的工作内存(甚至包括)从主存中独立出来,更是给JVM大量的空间来优化线程内指令的执行。主存中的变量可以被拷贝到线程的工作内存中去单独执行,在执行结束后,结果可以在某个时间刷回主存:

    但是,怎样来保证各个线程之间数据的一致性?JLS给的办法就是,默认情况下,不能保证任意时刻的数据一致性,但是通过对 synchronized、volatile和final这几个语义被增强的关键字的使用,可以做到数据一致性。要解释这个问题,不如看一看经典的 DCL(Double Check Lock)问题:

     
     
     
     
    1. public class DoubleCheckLock {  
    2.     private volatile static DoubleCheckLock instance; // Do I need add "volatile" here?  
    3.     private final Element element = new Element(); // Should I add "final" here? Is a "final" enough here? Or I should use "volatile"?  
    4.    
    5.     private DoubleCheckLock() {  
    6.     }  
    7.    
    8.     public static DoubleCheckLock getInstance() {  
    9.         if (null == instance)  
    10.             synchronized (DoubleCheckLock.class) {  
    11.                 if (null == instance)  
    12.                     instance = new DoubleCheckLock();  
    13.                     //the writes which initialize instance and the write to the instance field can be reordered without "volatile"  
    14.             }  
    15.    
    16.         return instance;  
    17.     }  
    18.    
    19.     public Element getElement() {  
    20.         return element;  
    21.     }  
    22.    
    23. }  
    24.    
    25. class Element {  
    26.     public String name = new String("abc");  

    在上面这个例子中,如果不对instance声明的地方使用volatile关键字,JVM将不能保证getInstance方法获取到的 instance是一个完整的、正确的instance,而volatile关键字保证了instance的可见性,即能够保证获取到当时真实的 instance对象。

    但是问题没有那么简单,对于上例中的element而言,如果没有volatile和final修饰,element里的name也无法在前文所述的instance返回给外部时

    文章题目:Java多线程发展简史
    URL分享:http://www.hantingmc.com/qtweb/news43/274043.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联