Java多线程问题探讨

来源:互联网 发布:linux用户加入组 编辑:程序博客网 时间:2024/05/18 00:54

本文使将x-spirit关于Java多线程同步的一系列文章进行了整理,原文地址:http: //www.blogjava.net/zhangwei217245/ 


众所周知,在Java多线程编程中,一个非常重要的方面就是线程的同步问题。

关于线程的同步,一般有以下解决方法:
1. 在需要同步的方法的方法签名中加入synchronized关键字。
2. 使用synchronized块对需要进行同步的代码段进行同步。
3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。

另外,为了解决多个线程对同一变量进行访问时可能发生的安全性问题,我们不仅可以采用同步机制,更可以通过JDK 1.2中加入的ThreadLocal来保证更好的并发性。
本篇中,将详细的讨论Java多线程同步机制,并对ThreadLocal做出探讨。

大致的目录结构如下:


一、线程的先来后到——问题的提出:为什么要有多线程同步?Java多线程同步的机制是什么?
二、给我一把锁,我能创造一个规矩——传统的多线程同步编程方法有哪些?他们有何异同?
三、Lock来了,大家都让开—— Java并发框架中的Lock详解。
四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll)
五、你有我有全都有—— ThreadLocal如何解决并发安全性?
六、总结——Java线程安全的几种方法对比。

一、线程的先来后到

我们来举一个Dirty的例子:某餐厅的卫生间很小,几乎只能容纳一个人如厕。为了保证不受干扰,如厕的人进入卫生间,就要锁上房门。我们可以把卫生间想象成是共享的资源,而众多需要如厕的人可以被视作多个线程。假如卫生间当前有人占用,那么其他人必须等待,直到这个人如厕完毕,打开房门走出来为止。这就好比多个线程共享一个资源的时候,是一定要分出先来后到的。

有人说:那如果我没有这道门会怎样呢?让两个线程相互竞争,谁抢先了,谁就可以先干活,这样多好阿?但是我们知道:如果厕所没有门的话,如厕的人一起涌向厕所,那么必然会发生争执,正常的如厕步骤就会被打乱,很有可能会发生意想不到的结果,例如某些人可能只好被迫在不正确的地方施肥……

正是因为有这道门,任何一个单独进入如厕的人都可以顺利的完成他们的如厕过程,而不会被干扰,甚至发生以外的结果。这就是说,如厕的时候要讲究先来后到。

那么在Java 多线程程序当中,当多个线程竞争同一个资源的时候,如何能够保证他们不会产生“打架”的情况呢?有人说是使用同步机制。没错,像上面这个例子,就是典型的同步案例,一旦第一位开始如厕,则第二位必须等待第一位结束,才能开始他的如厕过程。一个线程,一旦进入某一过程,必须等待正常的返回,并退出这一过程,下一个线程才能开始这个过程。这里,最关键的就是卫生间的门。其实,卫生间的门担任的是资源锁的角色,只要如厕的人锁上门,就相当于获得了这个锁,而当他打开锁出来以后,就相当于释放了这个锁。

也就是说,多线程的线程同步机制实际上是靠锁的概念来控制的。那么在Java程序当中,锁是如何体现的呢?

让我们从JVM的角度来看看锁这个概念:

在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:
1)保存在堆中的实例变量
2)保存在方法区中的类变量

这两类数据是被所有线程共享的。
(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)

在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。
对于对象来说,相关联的监视器保护对象的实例变量。

对于类来说,监视器保护类的类变量。

(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。) 
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。

但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)

类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减1,当计数器值为0时,锁就被完全释放了。

java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。

在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java虚拟机都会自动锁上对象或者类。

看到这里,我想你们一定都疲劳了吧?o(∩_∩)o...哈哈。让我们休息一下,但是在这之前,请你们一定要记着:
当一个有限的资源被多个线程共享的时候,为了保证对共享资源的互斥访问,我们一定要给他们排出一个先来后到。而要做到这一点,对象锁在这里起着非常重要的作用。

在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。

本篇中,我们来看一看传统的同步实现方式以及这背后的原理。

很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多人看到这个东西会感到困惑:“都说同步机制是通过对象锁来实现的,但是这么一个关键字,我也看不出来Java程序锁住了哪个对象阿?“

二、给我一把锁,我能创造一个规矩


没错,我一开始也是对这个问题感到困惑和不解。不过还好,我们有下面的这个例程:
 public class ThreadTest extends Thread {
     private int threadNo;
     public ThreadTest(int threadNo) {
         this.threadNo = threadNo;
     } 
    public static void main(String[] args) throws Exception {
         for (int i = 1; i < 10; i++) {
             new ThreadTest(i).start();
             Thread.sleep(1);
         }
     } 
     @Override
     public synchronized void run() {
         for (int i = 1; i < 10000; i++) {
             System.out.println("No." + threadNo + ":" + i);
        }
     }
 }

这个程序其实就是让10个线程在控制台上数数,从1数到9999。理想情况下,我们希望看到一个线程数完,然后才是另一个线程开始数数。但是这个程序的执行过程告诉我们,这些线程还是乱糟糟的在那里抢着报数,丝毫没有任何规矩可言。

但是细心的读者注意到:run方法还是加了一个synchronized关键字的,按道理说,这些线程应该可以一个接一个的执行这个run方法才对阿。

但是通过上一篇中,我们提到的,对于一个成员方法加synchronized关键字,这实际上是以这个成员方法所在的对象本身作为对象锁。在本例中,就是以ThreadTest类的一个具体对象,也就是该线程自身作为对象锁的。一共十个线程,每个线程持有自己 线程对象的那个对象锁。这必然不能产生同步的效果。换句话说,如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的!

我们来看下面的例程:
public class ThreadTest2 extends Thread { 
     private int threadNo;
     private String lock;
     public ThreadTest2(int threadNo, String lock) {
         this.threadNo = threadNo;
         this.lock = lock
     } 
     public static void main(String[] args) throws Exception {
         String lock = new String("lock");
         for (int i = 1; i < 10; i++) {
             new ThreadTest2(i, lock).start();
             Thread.sleep(1);
         }
     }
     public void run() {
         synchronized (lock) {
             for (int i = 1; i < 10000; i++) {
                 System.out.println("No." + threadNo + ":" + i);
             }
        }
     }
 }
我们注意到,该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的同一个区域,即存放main函数中的lock变量的区域。
程序将原来run方法前的synchronized关键字去掉,换用了run方法中的一个synchronized块来实现。这个同步块的对象锁,就是main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!

于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。
 
再来看下面的例程:

 public class ThreadTest3 extends Thread {
     private int threadNo;
     private String lock;
     public ThreadTest3(int threadNo) {
         this.threadNo = threadNo;
     }
     public static void main(String[] args) throws Exception {
         //String lock = new String("lock");
         for (int i = 1; i < 20; i++) {
             new ThreadTest3(i).start();
             Thread.sleep(1);
         }
     } 
     public static synchronized void abc(int threadNo) {
         for (int i = 1; i < 10000; i++) {
                System.out.println("No." + threadNo + ":" + i);   
         }
     }
     public void run() {
         abc(threadNo);
     }
 }

细心的读者发现了:这段代码没有使用main方法中创建的String对象作为这10个线程的线程锁。而是通过在run方法中调用本线程中一个静态的同步方法abc而实现了线程的同步。我想看到这里,你们应该很困惑:这里synchronized静态方法是用什么来做对象锁的呢?

我们知道,对于同步静态方法,对象锁就是该静态方法所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例,就是唯一的ThreadTest3.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!

这样我们就知道了:

1、对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;
2、如果采用method级别的同步,则对象锁即为method所在的对象,如果是静态方法,对象锁即指method所在的
Class对象(唯一);
3、对于代码块,对象锁即指synchronized(abc)中的abc;
4、因为第一种情况,对象锁即为每一个线程对象,因此有多个,所以同步失效,第二种共用同一个对象锁lock,因此同步生效,第三个因为是
static因此对象锁为ThreadTest3的class 对象,因此同步生效。
如上述正确,则同步有两种方式,同步块和同步方法(为什么没有wait和notify?这个我会在补充章节中做出阐述)

如果是同步代码块,则对象锁需要编程人员自己指定,一般有些代码为synchronized(this)只有在单态模式才生效;

(本类的实例有且只有一个)

如果是同步方法,则分静态和非静态两种。
静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法(不用担心是否单例)。
所以说,在Java多线程编程中,最常见的synchronized关键字实际上是依靠对象锁的机制来实现线程同步的。

我们似乎可以听到synchronized在向我们说:“给我一把锁,我能创造一个规矩”。


三、Lock来了,大家都让开【1. 认识重入锁】


在上一节中,

我们已经了解了Java多线程编程中常用的关键字synchronized,以及与之相关的对象锁机制。这一节中,让 我们一起来认识JDK 5中新引入的并发框架中的锁机制。

我想很多购买了《Java程序员面试宝典》之类图书的朋友一定对下面 这个面试题感到非常熟悉:

问:请对比synchronized与java.util.concurrent.locks.Lock 的异同。
答案:主要相同点:Lock能完成synchronized所实现的所有功能
     主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放 锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

恩,让我们先鄙视一下应试教育。

言归正传,我们先来看一个多线程程序。它使用多个线程对一个Student对象进行访问,改变其中的变量值。 我们首先用传统的synchronized 机制来实现它:

public class ThreadDemo implements Runnable {    class Student {        private int age = 0;        public int getAge() {            return age;        }        public void setAge(int age) {            this.age = age;        }    }    Student student = new Student();    int count = 0;    public static void main(String[] args) {        ThreadDemo td = new ThreadDemo();        Thread t1 = new Thread(td, "a");        Thread t2 = new Thread(td, "b");        Thread t3 = new Thread(td, "c");        t1.start();        t2.start();        t3.start();    }    public void run() {        accessStudent();    }    public void accessStudent() {        String currentThreadName = Thread.currentThread().getName();        System.out.println(currentThreadName + " is running!");        synchronized (this) {//(1)使用同一个ThreadDemo对象作为同步锁            System.out.println(currentThreadName + " got lock1@Step1!");            try {                count++;                Thread.sleep(5000);            } catch (Exception e) {                e.printStackTrace();            } finally {                System.out.println(currentThreadName + " first Reading count:" + count);            }        }               System.out.println(currentThreadName + " release lock1@Step1!");        synchronized (this) {//(2)使用同一个ThreadDemo对象作为同步锁            System.out.println(currentThreadName + " got lock2@Step2!");            try {                Random random = new Random();                int age = random.nextInt(100);                System.out.println("thread " + currentThreadName + " set age to:" + age);                this.student.setAge(age);                System.out.println("thread " + currentThreadName + " first  read age is:" + this.student.getAge());                Thread.sleep(5000);            } catch (Exception ex) {                ex.printStackTrace();            } finally{                System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());            }        }        System.out.println(currentThreadName + " release lock2@Step2!");    }}

运行结果:

a is running!a got lock1@Step1!b is running!c is running!a first Reading count:1a release lock1@Step1!a got lock2@Step2!thread a set age to:76thread a first  read age is:76thread a second read age is:76a release lock2@Step2!c got lock1@Step1!c first Reading count:2c release lock1@Step1!c got lock2@Step2!thread c set age to:35thread c first  read age is:35thread c second read age is:35c release lock2@Step2!b got lock1@Step1!b first Reading count:3b release lock1@Step1!b got lock2@Step2!thread b set age to:91thread b first  read age is:91thread b second read age is:91b release lock2@Step2!成功生成(总时间:30 秒)

显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁,所以JVM优先使刚刚释放该锁的线程重新获得该 锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是 30秒。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 
我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这 两个对象锁应该选择那些被所有线程所共享的对象。

那么好。我们把第二个同步块中的对象锁改为student(此处略去代码,读 者自己修改),程序运行结果为:

a is running!a got lock1@Step1!b is running!c is running!a first Reading count:1a release lock1@Step1!a got lock2@Step2!thread a set age to:73thread a first  read age is:73c got lock1@Step1!thread a second read age is:73a release lock2@Step2!c first Reading count:2c release lock1@Step1!c got lock2@Step2!thread c set age to:15thread c first  read age is:15b got lock1@Step1!thread c second read age is:15c release lock2@Step2!b first Reading count:3b release lock1@Step1!b got lock2@Step2!thread b set age to:19thread b first  read age is:19thread b second read age is:19b release lock2@Step2!成功生成(总时间:21 秒)

从 修改后的运行结果来看,显然,由于同步块的对象锁不同了,三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之 后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟 的时间是两个线程同时工作的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线程执行第二个同步块的动作。相比较第一 个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/2n,其中n为 线程数。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个 synchronized块,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m- 1)n+1)/mn那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我 们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而 这种方案至少具备数学上的可行性理论支持。)

可见,使用不同的对象锁,在不同的同步块中完成任务,可以使性能大大提升。

很多人看到这不禁要问:这和新的Lock框 架有什么关系?

别着急。我们这就来看一看。

synchronized块的确不错,但是他有一些功能性的限制:
1. 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。
2.synchronized 块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些更适合使用 非块结构锁定的情况。

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 

JDK 官方文档中提到:
ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized  方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ”

简单来说,ReentrantLock有一个与锁相关的获取计 数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。 

ReentrantLock  类(重入锁)实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性 能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。) 

我们把 上面的例程改造一下:

public class ThreadDemo implements Runnable {    class Student {        private int age = 0;        public int getAge() {            return age;        }        public void setAge(int age) {            this.age = age;        }    }    Student student = new Student();    int count = 0;    ReentrantLock lock1 = new ReentrantLock(false);    ReentrantLock lock2 = new ReentrantLock(false);    public static void main(String[] args) {        ThreadDemo td = new ThreadDemo();        for (int i = 1; i <= 3; i++) {            Thread t = new Thread(td, i + "");            t.start();        }    }    public void run() {        accessStudent();    }    public void accessStudent() {        String currentThreadName = Thread.currentThread().getName();        System.out.println(currentThreadName + " is running!");        lock1.lock();//使用重入锁        System.out.println(currentThreadName + " got lock1@Step1!");        try {            count++;            Thread.sleep(5000);        } catch (Exception e) {            e.printStackTrace();        } finally {            System.out.println(currentThreadName + " first Reading count:" + count);            lock1.unlock();            System.out.println(currentThreadName + " release lock1@Step1!");        }        lock2.lock();//使用另外一个不同的重入锁        System.out.println(currentThreadName + " got lock2@Step2!");        try {            Random random = new Random();            int age = random.nextInt(100);            System.out.println("thread " + currentThreadName + " set age to:" + age);            this.student.setAge(age);            System.out.println("thread " + currentThreadName + " first  read age is:" + this.student.getAge());            Thread.sleep(5000);        } catch (Exception ex) {            ex.printStackTrace();        } finally {            System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());            lock2.unlock();            System.out.println(currentThreadName + " release lock2@Step2!");        }    }}

从上面这个 程序我们看到:

对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。并 且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的。

这说明两点问题:
1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码 人员自行掌握。
2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。
3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而 在finally中使用unlock方法。

细心的读者又发现了:

在我们的例程中,创建ReentrantLock实例的时候,我们的构造函数里面传递的参数是false。那么如果传递 true又回是什么结果呢?这里面又有什么奥秘呢?

请看本节的续 ———— Fair or Unfair? It is a question...


三、Lock来了,大家都让开【2. Fair or Unfair? It is a question...】

我们继续前面有关ReentrantLock的话题。
首先,ReentrantLock有一个带布尔型参数的构造函数,在JDK官方文档中对它是这样描述的:
“此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的
一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock  方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。 ”


简单来讲:公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。 

观察采用公平锁和非公平锁的例程运行效果发现:线程获得锁的顺序发生了一些变化(见下表)。


Unfair:


1 is running!

1 got lock1@Step1!

3 is running!

2 is running!

1 first Reading count:1

1 release lock1@Step1!

3 got lock1@Step1!

1 got lock2@Step2!

thread 1 set age to:18

thread 1 first read age is:18

3 first Reading count:2

3 release lock1@Step1!

2 got lock1@Step1!

thread 1 second read age is:18

1 release lock2@Step2!

3 got lock2@Step2!

thread 3 set age to:34

thread 3 first read age is:34

2 first Reading count:3

2 release lock1@Step1!

thread 3 second read age is:34

3 release lock2@Step2!

2 got lock2@Step2!

thread 2 set age to:72

thread 2 first read age is:72

thread 2 second read age is:72

2 release lock2@Step2!

成功生成(总时间:20 秒)

Fair:


1 is running!

1 got lock1@Step1!

2 is running!

3 is running!

1 first Reading count:1

1 release lock1@Step1!

1 got lock2@Step2!

thread 1 set age to:82

thread 1 first read age is:82

2 got lock1@Step1!

2 first Reading count:2

2 release lock1@Step1!

3 got lock1@Step1!

thread 1 second read age is:82

1 release lock2@Step2!

2 got lock2@Step2!

thread 2 set age to:65

thread 2 first read age is:65

3 first Reading count:3

3 release lock1@Step1!

thread 2 second read age is:65

2 release lock2@Step2!

3 got lock2@Step2!

thread 3 set age to:31

thread 3 first read age is:31

thread 3 second read age is:31

3 release lock2@Step2!

成功生成(总时间:20 秒)



这样的变化告诉我们:

采用非公平的锁时,当一个线程释放了第一个锁以后,由于线程的抢占,刚刚被释放的锁马上被下一个线程占有。采用公平锁时,由于公平锁倾向于将访问权授予等待时间最长的线程,所以,当第一个锁被第一个线程释放以后,第二个锁马上将访问权授予第一个线程,而第一个锁将访问权授予了第二个线程。这里,公平锁在平衡分配方面耗费了一定的时间,这使得第一个线程获得第二个锁的时间优先于第二个线程获得第一个锁。这样,采用不同的锁,就出现了两种不同的结果。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 
为了看到公平锁和非公平锁性能上的差异,我们不妨将其中线程的睡眠时间设定为1毫秒,然后把循环产生的线程数提高到5000(修改后的代码已忽略,自行修改),可以发现,由于公平锁要维持锁分配的均衡性,所以,采用公平锁的程序总运行时间更长一些。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/ 
根据运行环境的差异,有些朋友可能并不一定能很直观的从运行结果中看到两种不同的锁带来的性能差异。不妨引用IBM开发者社区的一组测试结果来看一看就行有什么样的差异吧:
4CPU情况下的同步、非公平锁和公平锁吞吐量比较:


单CPU情况下,同步、非公平锁和公平锁的吞吐量:


可以看到,同步和公平锁的吞吐量都是最低的,公平锁更低一些。但是同步内置的监控器锁是不公平的,而且永远都是不公平的。而JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。

既然Lock这么近乎完美,那我们也许可以忘却synchronized了。

但是任何事物都是有两面性的。
1.使用Lock,你必须手动的在finally块中释放锁。锁的获得和释放是不受JVM控制的。这要求编程人员更加细心。
2.当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

Lock提供了在多线程争用的情况下更好的并发性,但这是以牺牲一定的可维护性为代价的。

所以说,当大量线程发生争用的时候,Lock来了,大家都让开。

四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll)


Java监视器支持两种线程:互斥和协作。

前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。

举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。

这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。

在JVM中,此种监视器被称为等待并唤醒监视器。

在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。

请看下面的代码:

 1. public class NotifyTest {    2.     private  String flag = "true";    3.     4.     class NotifyThread extends Thread{    5.         public NotifyThread(String name) {    6.             super(name);    7.         }    8.         public void run() {         9.             try {   10.                 sleep(3000);//推迟3秒钟通知   11.             } catch (InterruptedException e) {   12.                 e.printStackTrace();   13.             }   14.                15.                 flag = "false";   16.                 flag.notify();   17.         }   18.     };   19.    20.     class WaitThread extends Thread {   21.         public WaitThread(String name) {   22.             super(name);   23.         }   24.    25.         public void run() {   26.                27.                 while (flag!="false") {   28.                     System.out.println(getName() + " begin waiting!");   29.                     long waitTime = System.currentTimeMillis();   30.                     try {   31.                         flag.wait();   32.                     } catch (InterruptedException e) {   33.                         e.printStackTrace();   34.                     }   35.                     waitTime = System.currentTimeMillis() - waitTime;   36.                     System.out.println("wait time :"+waitTime);   37.                 }   38.                 System.out.println(getName() + " end waiting!");   39.                40.         }   41.     }   42.    43.     public static void main(String[] args) throws InterruptedException {   44.         System.out.println("Main Thread Run!");   45.         NotifyTest test = new NotifyTest();   46.         NotifyThread notifyThread =test.new NotifyThread("notify01");   47.         WaitThread waitThread01 = test.new WaitThread("waiter01");   48.         WaitThread waitThread02 = test.new WaitThread("waiter02");   49.         WaitThread waitThread03 = test.new WaitThread("waiter03");   50.         notifyThread.start();   51.         waitThread01.start();   52.         waitThread02.start();   53.         waitThread03.start();   54.     }   55.    56. }  

这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。

运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException,根本不是你想要的结果。

请注意以下几个事实:
   1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。
   2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。
   3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常。
   4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。

也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。

通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法:

1: 执行对象的某个同步实例方法
2: 执行对象对应的同步静态方法
3: 执行对该对象加同步锁的同步块

显然,在上面的例程中,我们用第三种方法比较合适。

于是我们将上面的wait和notify方法调用包在同步块中。

   1.             synchronized (flag) {    2.                 flag = "false";    3.                 flag.notify();    4.             }  


   1.             synchronized (flag) {    2.                 while (flag!="false") {    3.                     System.out.println(getName() + " begin waiting!");    4.                     long waitTime = System.currentTimeMillis();    5.                     try {    6.                         flag.wait();    7.                     } catch (InterruptedException e) {    8.                         e.printStackTrace();    9.                     }   10.                     waitTime = System.currentTimeMillis() - waitTime;   11.                     System.out.println("wait time :"+waitTime);   12.                 }   13.                 System.out.println(getName() + " end waiting!");   14.             }  
但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。


我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag="false";


显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。


我们不妨将flag封装到JavaBean或者数组中去,这样用JavaBean对象或者数组对象进行同步,就可以达到既能修改里面参数又不耽误同步的目的。

1. private   String flag[] = {"true"}; 

   1.         synchronized (flag) {    2.             flag[0] = "false";    3.             flag.notify();    4.         }  

  1.                  synchronized (flag) {    2.                 flag[0] = "false";    3.                 flag.notify();    4.             }synchronized (flag) {    5.                 while (flag[0]!="false") {    6.                     System.out.println(getName() + " begin waiting!");    7.                     long waitTime = System.currentTimeMillis();    8.                     try {    9.                         flag.wait();   10.                            11.                     } catch (InterruptedException e) {   12.                         e.printStackTrace();   13.                     }  

运行这个程序,看不到异常了。但是仔细观察结果,貌似只有一个线程被唤醒。利用jconsole等工具查看线程状态,发现的确还是有两个线程被阻塞的。这是为啥呢?

程序中使用了flag.notify()方法。只能是随机的唤醒一个线程。我们可以改用flag.notifyAll()方法。这样,所有被阻塞的线程都会被唤醒了。

最终代码请读者自己修改,这里不再赘述。

好了,亲爱的读者们,让我们回到开篇提到的汉堡店大赛问题当中去,来看一看厨师、服务生和顾客是怎么协作进行这个比赛的。

首先我们构造故事中的三个次要对象:汉堡包、存放汉堡包的容器、服务生
public class Waiter {//服务生,这是个配角,不需要属性。}

    class Hamberg {        //汉堡包        private int id;//汉堡编号        private String cookerid;//厨师编号        public Hamberg(int id, String cookerid){            this.id = id;            this.cookerid = cookerid;            System.out.println(this.toString()+"was made!");        }        @Override        public String toString() {            return "#"+id+" by "+cookerid;        }            }

    class HambergFifo {        //汉堡包容器        List<Hamberg> hambergs = new ArrayList<Hamberg>();//借助ArrayList来存放汉堡包        int maxSize = 10;//指定容器容量        //放入汉堡        public <T extends Hamberg> void push(T t) {            hambergs.add(t);        }        //取出汉堡        public Hamberg pop() {            Hamberg h = hambergs.get(0);            hambergs.remove(0);            return h;        }        //判断容器是否为空        public synchronized boolean isEmpty() {            return hambergs.isEmpty();        }        //判断容器内汉堡的个数        public synchronized int size() {            return hambergs.size();        }        //返回容器的最大容量        public synchronized int getMaxSize() {            return this.maxSize;        }        //判断容器是否已满,未满为真        public synchronized boolean isNotFull(){            return hambergs.size() < this.maxSize;        }    }
接下来构造厨师对象:

    class Cooker implements Runnable {        //厨师要面对容器        HambergFifo pool;        //还要面对服务生        Waiter waiter;        public Cooker(Waiter waiter, HambergFifo hambergStack) {            this.pool = hambergStack;            this.waiter = waiter;        }        //制造汉堡        public void makeHamberg() {            //制造的个数            int madeCount = 0;            //因为容器满,被迫等待的次数            int fullFiredCount = 0;            try {                                while (true) {                    //制作汉堡前的准备工作                    Thread.sleep(1000);                    if (pool.isNotFull()) {                        synchronized (waiter) {                            //容器未满,制作汉堡,并放入容器。                            pool.push(new Hamberg(++madeCount,Thread.currentThread().getName()));                            //说出容器内汉堡数量                            System.out.println(Thread.currentThread().getName() + ": There are "                                           + pool.size() + " Hambergs in all");                            //让服务生通知顾客,有汉堡可以吃了                            waiter.notifyAll();                            System.out.println("### Cooker: waiter.notifyAll() :"+                                      " Hi! Customers, we got some new Hambergs!");                        }                    } else {                        synchronized (pool) {                            if (fullFiredCount++ < 10) {                                //发现容器满了,停止做汉堡的尝试。                                System.out.println(Thread.currentThread().getName() +                                         ": Hamberg Pool is Full, Stop making hamberg");                                System.out.println("### Cooker: pool.wait()");                                //汉堡容器的状况使厨师等待                                pool.wait();                            } else {                                return;                            }                        }                    }                                        //做完汉堡要进行收尾工作,为下一次的制作做准备。                    Thread.sleep(1000);                }            } catch (Exception e) {                madeCount--;                e.printStackTrace();            }        }        public void run() {            makeHamberg();        }    }

接下来构造顾客对象:

    class Customer implements Runnable {        //顾客要面对服务生        Waiter waiter;        //也要面对汉堡包容器        HambergFifo pool;        //想要记下自己吃了多少汉堡        int ateCount = 0;        //吃每个汉堡的时间不尽相同        long sleeptime;        //用于产生随机数        Random r = new Random();        public Customer(Waiter waiter, HambergFifo pool) {            this.waiter = waiter;            this.pool = pool;        }        public void run() {            while (true) {                try {                    //取汉堡                    getHamberg();                    //吃汉堡                    eatHamberg();                } catch (Exception e) {                    synchronized (waiter) {                        System.out.println(e.getMessage());                        //若取不到汉堡,要和服务生打交道                        try {                            System.out.println("### Customer: waiter.wait():"+                                        " Sorry, Sir, there is no hambergs left, please wait!");                            System.out.println(Thread.currentThread().getName()                                         + ": OK, Waiting for new hambergs");                            //服务生安抚顾客,让他等待。                            waiter.wait();                            continue;                        } catch (InterruptedException ex) {                            ex.printStackTrace();                        }                    }                }            }        }        private void eatHamberg() {            try {                //吃每个汉堡的时间不等                sleeptime = Math.abs(r.nextInt(3000)) * 5;                System.out.println(Thread.currentThread().getName()                         + ": I'm eating the hamberg for " + sleeptime + " milliseconds");                                Thread.sleep(sleeptime);            } catch (Exception e) {                e.printStackTrace();            }        }        private void getHamberg() throws Exception {            Hamberg hamberg = null;            synchronized (pool) {                try {                    //在容器内取汉堡                    hamberg = pool.pop();                    ateCount++;                    System.out.println(Thread.currentThread().getName()                                + ": I Got " + ateCount + " Hamberg " + hamberg);                    System.out.println(Thread.currentThread().getName()                                 + ": There are still " + pool.size() + " hambergs left");                } catch (Exception e) {                    pool.notifyAll();                    System.out.println("### Customer: pool.notifyAll()");                    throw new Exception(Thread.currentThread().getName() +                             ": OH MY GOD!!!! No hambergs left, Waiter![Ring the bell besides the hamberg pool]");                }            }        }    }

最后构造汉堡店,让这个故事发生:

public class HambergShop {    Waiter waiter = new Waiter();    HambergFifo hambergPool = new HambergFifo();    Customer c1 = new Customer(waiter, hambergPool);    Customer c2 = new Customer(waiter, hambergPool);    Customer c3 = new Customer(waiter, hambergPool);    Cooker cooker = new Cooker(waiter, hambergPool);    public static void main(String[] args) {        HambergShop hambergShop = new HambergShop();        Thread t1 = new Thread(hambergShop.c1, "Customer 1");        Thread t2 = new Thread(hambergShop.c2, "Customer 2");        Thread t3 = new Thread(hambergShop.c3, "Customer 3");        Thread t4 = new Thread(hambergShop.cooker, "Cooker 1");        Thread t5 = new Thread(hambergShop.cooker, "Cooker 2");        Thread t6 = new Thread(hambergShop.cooker, "Cooker 3");        t4.start();        t5.start();        t6.start();        try {            Thread.sleep(10000);        } catch (Exception e) {        }        t1.start();        t2.start();        t3.start();    }}
运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不知道那些顾客是不是会被撑到。。。


读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。
有的读者会问:如果我用ReentrantLock来代替上面这些例程当中的 synchronized块,是不是也可以呢?感兴趣的读者不妨一试。


但是在这里,我想提前给出结论,就是,
如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized块,那么上面这些程序还是要抛出java.lang.IllegalMonitorStateException异常的,不仅如此,你甚至还会看到线程死锁。原因就是当某个线程调用第三方对象的wait或者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息。但此时,程序流程如果没有用finally来处理unlock方法,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了,你用JConsole这种工具来检测JVM死锁,还检测不出来。


正确的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因为ReentrantLock已经对这种互斥和协作进行了概括。所以,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用。


好了,我们现在明白:
1. 线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。
2. 线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。
3. 线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作。


下一讲当中,我们来看看如何实实在在的解决线程之间抢占共享资源的问题。敬请期待!

五、你有我有全都有—— ThreadLocal如何解决并发安全性?

前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。

在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?

什么是本地线程?
本地线程开玩笑的说:不要迷恋哥,哥只是个传说。

其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实现。

当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。

说了这么多,来看看如何正确使用ThreadLocal。

通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。

还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。
下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:

/** * * @author x-spirit */public class ThreadDemo3 extends Thread{    private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>();    public ThreadDemo3(Student stu){        stuLocal.set(stu);    }    public static void main(String[] args) {        Student stu = new Student();        ThreadDemo3 td31 = new ThreadDemo3(stu);        ThreadDemo3 td32 = new ThreadDemo3(stu);        ThreadDemo3 td33 = new ThreadDemo3(stu);        td31.start();        td32.start();        td33.start();    }    @Override    public void run() {        accessStudent();    }    public void accessStudent() {        String currentThreadName = Thread.currentThread().getName();        System.out.println(currentThreadName + " is running!");        Random random = new Random();        int age = random.nextInt(100);        System.out.println("thread " + currentThreadName + " set age to:" + age);        Student student = stuLocal.get();        student.setAge(age);        System.out.println("thread " + currentThreadName + " first  read age is:" + student.getAge());        try {            Thread.sleep(5000);        } catch (InterruptedException ex) {            ex.printStackTrace();        }        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());    }}

貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器当中把Student对象set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?


带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。


原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视为HashMap<ThreadLocal,Object>。


而在每一个Thread对象中,都有一个ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的set方法就是首先尝试从当前线程中取得ThreadLocalMap(以下简称Map)对象。如果取到的不为null,则以ThreadLocal对象自身为key,来取Map中的value。如果取不到Map对象,则首先为当前线程创建一个ThreadLocalMap,然后以ThreadLocal对象自身为key,将传入的value放入该Map中。


    ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }       public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);    }

而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。

    public T get() {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null) {            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null)                return (T)e.value;        }        return setInitialValue();    }    private T setInitialValue() {        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);        return value;    }       protected T initialValue() {        return null;    }
这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入Student对象,而此时,以ThreadLocal对象为key,Student对象为value的Map是被放入当前的活动线程内的。也就是Main线程。而当我们的3个ThreadDemo3线程运行起来以后,调用get()方法,都是试图从当前的活动线程中取得ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程了,于是,程序最终执行了ThreadLocal原生的initialValue()方法,返回了null。


讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。


于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。
这样,我们就有了下面的正确例程:

/** * * @author x-spirit */public class ThreadDemo3 extends Thread{    private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){        @Override        protected Student initialValue() {            return new Student();        }    };    public ThreadDemo3(){           }    public static void main(String[] args) {        ThreadDemo3 td31 = new ThreadDemo3();        ThreadDemo3 td32 = new ThreadDemo3();        ThreadDemo3 td33 = new ThreadDemo3();        td31.start();        td32.start();        td33.start();    }    @Override    public void run() {        accessStudent();    }    public void accessStudent() {        String currentThreadName = Thread.currentThread().getName();        System.out.println(currentThreadName + " is running!");        Random random = new Random();        int age = random.nextInt(100);        System.out.println("thread " + currentThreadName + " set age to:" + age);        Student student = stuLocal.get();        student.setAge(age);        System.out.println("thread " + currentThreadName + " first  read age is:" + student.getAge());        try {            Thread.sleep(5000);        } catch (InterruptedException ex) {            ex.printStackTrace();        }        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());    }}
********** 补疑 ******************

有的童鞋可能会问:“你这个Demo根本没体现出来,每个线程里都有一个ThreadLocal对象;应该是一个ThreadLocal对象对应多个线程,你这变成了一对一,完全没体现出ThreadLocal的作用。”

那么我们来看一下如何用一个ThreadLocal对象来对应多个线程:

/** * * @author x-spirit */public class ThreadDemo3 implements Runnable{    private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(){        @Override        protected Student initialValue() {            return new Student();        }    };    public ThreadDemo3(){           }    public static void main(String[] args) {        ThreadDemo3 td3 = new ThreadDemo3();        Thread t1 = new Thread(td3);        Thread t2 = new Thread(td3);        Thread t3 = new Thread(td3);        t1.start();        t2.start();        t3.start();    }    @Override    public void run() {        accessStudent();    }    public void accessStudent() {        String currentThreadName = Thread.currentThread().getName();        System.out.println(currentThreadName + " is running!");        Random random = new Random();        int age = random.nextInt(100);        System.out.println("thread " + currentThreadName + " set age to:" + age);        Student student = stuLocal.get();        student.setAge(age);        System.out.println("thread " + currentThreadName + " first  read age is:" + student.getAge());        try {            Thread.sleep(5000);        } catch (InterruptedException ex) {            ex.printStackTrace();        }        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());    }}
这里,多个线程对象都使用同一个实现了Runnable接口的ThreadDemo3对象来构造。这样,多个线程使用的ThreadLocal对象就是同一个。结果仍然是正确的。但是仔细回想一下,这两种实现方案有什么不同呢?


答案其实很简单,并没有本质上的不同。对于第一种实现,不同的线程对象当中ThreadLocalMap里面的KEY使用的是不同的ThreadLocal对象。而对于第二种实现,不同的线程对象当中ThreadLocalMap里面的KEY是同一个ThreadLocal对象。但是从本质上讲,不同的线程对象都是利用其自身的ThreadLocalMap对象来对各自的Student对象进行封装,用ThreadLocal对象作为该ThreadLocalMap的KEY。所以说,“ThreadLocal的思想精髓就是为每个线程创建独立的资源副本。”这句话并不应当被理解成:一定要使用同一个ThreadLocal对象来对多个线程进行处理。因为真正用来封装变量的不是ThreadLocal。就算是你的程序中所有线程都共用同一个ThreadLocal对象,而你真正封装到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同对象。就好比线程就是房东,ThreadLocalMap就是房东的房子。房东通过ThreadLocal这个中介去和房子里的房客打交道,而房东不管要让房客住进去还是搬出来,都首先要经过ThreadLocal这个中介。


所以提到ThreadLocal,我们不应当顾名思义的认为JDK里面提供ThreadLocal就是提供了一个用来封装本地线程存储的容器,它本身并没有Map那样的容器功能。真正发挥作用的是ThreadLocalMap。也就是说,事实上,采用ThreadLocal来提高并发行,首先要理解,这不是一种简单的对象封装,而是一套机制,而这套机制中的三个关键因素(Thread、ThreadLocal、ThreadLocalMap)之间的关系是值得我们引起注意的。


**************** 补疑完毕 ***************************


可见,要正确使用ThreadLocal,必须注意以下几点:


1. 总是对ThreadLocal中的initialValue()方法进行覆盖。


2. 当使用set()或get()方法时牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程!


3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。


运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。


现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。


当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。


把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新的思路。它为每个线程创建一个独立的资源副本,从而将多个线程中的数据隔离开来,避免了同步所产生的性能问题,是一种“以空间换时间”的解决方案。
但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。


好了,本系列到此马上就要划上一个圆满的句号了。不知大家有什么意见和疑问没有。希望看到你们的留言。


下一讲中我们就来对之前的内容进行一个总结,顺便讨论一下被遗忘的volatile关键字。敬请期待。


0 0
原创粉丝点击