Java 多线程同步问题的探究 (摘自于网上)
Java 多线程同步问题的探究(一、线程的先来后到)
时间:2010-03-29 20:18 来源:未知 作者:admin
关于线程的同步,一般有以下解决方法:
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 多线程同步问题的探究(二、给我一把锁,我能创造一个规矩)
时间:2010-03-29 20:20 来源:未知 作者:admin
核心提示:在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。 本篇中,我们来看一看传统的同步实现方式以及这背后的原理。 很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多
在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。
本篇中,我们来看一看传统的同步实现方式以及这背后的原理。
很多人都知道,在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在向我们说:“给我一把锁,我能创造一个规矩”。
下一篇中,我们将看到JDK 5提供的新的同步机制,也就是大名鼎鼎的Doug Lee提供的Java Concurrency框架。
Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)
文章分类:Java编程
在上一节中,
我们已经了解了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! " );
}
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
运行结果:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 76
thread a first read age is: 76
thread a second read age is: 76
a release lock2@Step2!
c got lock1@Step1!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 35
thread c first read age is: 35
thread c second read age is: 35
c release lock2@Step2!
b got lock1@Step1!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 91
thread b first read age is: 91
thread b second read age is: 91
b release lock2@Step2!
成功生成(总时间: 30 秒)
显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁,所以JVM优先使刚刚释放该锁的线程重新获得该锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是 30秒。
我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这两个对象锁应该选择那些被所有线程所共享的对象。
那么好。我们把第二个同步块中的对象锁改为student(此处略去代码,读者自己修改),程序运行结果为:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 73
thread a first read age is: 73
c got lock1@Step1!
thread a second read age is: 73
a release lock2@Step2!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 15
thread c first read age is: 15
b got lock1@Step1!
thread c second read age is: 15
c release lock2@Step2!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 19
thread b first read age is: 19
thread b second read age is: 19
b release lock2@Step2!
成功生成(总时间: 21 秒)
从修改后的运行结果来看,显然,由于同步块的对象锁不同了,三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟的时间是两个线程同时工作的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线程执行第二个同步块的动作。相比较第一个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/2n,其中n为线程数。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个 synchronized块,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m- 1)n+1)/mn那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而这种方案至少具备数学上的可行性理论支持。)
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
可见,使用不同的对象锁,在不同的同步块中完成任务,可以使性能大大提升。
很多人看到这不禁要问:这和新的Lock框架有什么关系?
别着急。我们这就来看一看。
synchronized块的确不错,但是他有一些功能性的限制:
1. 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。
2.synchronized 块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些更适合使用非块结构锁定的情况。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
JDK 官方文档中提到:
ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ”
简单来说,ReentrantLock有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
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! " );
}
}
}
从上面这个程序我们看到:
对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。并且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
这说明两点问题:
1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码人员自行掌握。
2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。
3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而在finally中使用unlock方法。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
细心的读者又发现了:
在我们的例程中,创建ReentrantLock实例的时候,我们的构造函数里面传递的参数是false。那么如果传递 true又回是什么结果呢?这里面又有什么奥秘呢?
Java 多线程同步问题的探究(四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll))
文章分类:Java编程
Java监视器支持两种线程:互斥和协作。
前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。
举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。
这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。
在JVM中,此种监视器被称为等待并唤醒监视器。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。
请看下面的代码:
public class NotifyTest {
private String flag = " true " ;
class NotifyThread extends Thread{
public NotifyThread(String name) {
super (name);
}
public void run() {
try {
sleep( 3000 ); // 推迟3秒钟通知
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = " false " ;
flag.notify();
}
};
class WaitThread extends Thread {
public WaitThread(String name) {
super (name);
}
public void run() {
while (flag != " false " ) {
System.out.println(getName() + " begin waiting! " );
long waitTime = System.currentTimeMillis();
try {
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
waitTime = System.currentTimeMillis() - waitTime;
System.out.println( " wait time : " + waitTime);
}
System.out.println(getName() + " end waiting! " );
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println( " Main Thread Run! " );
NotifyTest test = new NotifyTest();
NotifyThread notifyThread = test. new NotifyThread( " notify01 " );
WaitThread waitThread01 = test. new WaitThread( " waiter01 " );
WaitThread waitThread02 = test. new WaitThread( " waiter02 " );
WaitThread waitThread03 = test. new WaitThread( " waiter03 " );
notifyThread.start();
waitThread01.start();
waitThread02.start();
waitThread03.start();
}
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException,根本不是你想要的结果。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
请注意以下几个事实:
1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。
2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。
3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常。
4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。
也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
1: 执行对象的某个同步实例方法
2: 执行对象对应的同步静态方法
3: 执行对该对象加同步锁的同步块
显然,在上面的例程中,我们用第三种方法比较合适。
于是我们将上面的wait和notify方法调用包在同步块中。
1 . synchronized (flag) {
2 . flag = " false " ;
3 . flag.notify();
4 . }
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
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 . }
但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag="false";
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。
我们不妨将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 boolean isEmpty() {
return hambergs.isEmpty();
}
// 判断容器内汉堡的个数
public int size() {
return hambergs.size();
}
// 返回容器的最大容量
public int getMaxSize() {
return 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.size() < pool.getMaxSize()) {
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] " );
}
}
}
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
最后,我们构造汉堡店,让这个故事发生:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
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。有的读者会问:如
Java 多线程同步问题的探究(五、你有我有全都有—— ThreadLocal如何解决并发安全性?)
文章分类:Java编程
前面我们介绍了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(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个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());
}
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器当中把Student对象set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视为HashMap
而在每一个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。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
于是,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());
}
}
可见,要正确使用ThreadLocal,必须注意以下几点:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
1. 总是对ThreadLocal中的initialValue()方法进行覆盖。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
2. 当使用set()或get()方法时牢记这两个方法是对当