一、线程的交互
a、线程交互的基础知识
线程交互知识点需要从java.lang.Object的类的三个方法来学习:
void notify()
唤醒在此对象监视器上等待的单个线程(notify()方法调用的时候,锁并没有被释放)。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法(wait()方法释放当前锁)。
唤醒在此对象监视器上等待的单个线程(notify()方法调用的时候,锁并没有被释放)。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法(wait()方法释放当前锁)。
当然,wait()还有另外两个重载方法:
void wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
以上这些方法是帮助线程传递线程关心的时间状态。
关于wait/notify,要记住的关键点是:
- 必须从同步环境内调用wait()、notify()、notifyAll()方法。
- 线程不能调用对象上的wait或notify的方法,除非它拥有那个对象的锁。
- wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。
下面看个例子就明白了:
/**
* 计算输出其他线程锁计算的数据
*
*
*/
* 计算输出其他线程锁计算的数据
*
*
*/
public class ThreadA { public static void main(String[] args) { ThreadB b = new ThreadB(); //启动计算线程 b.start(); //主线程拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者 synchronized (b) { try { System.out.println("等待对象b完成计算..."); //当前主线程等待 b.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("b对象计算的总和是:" + b.total); } } } /** * 计算1+2+3 ... +100的和 * * */ public class ThreadB extends Thread { int total; public void run() { synchronized (this) { for (int i = 0; i < 101; i++) { total += i; } //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线主线程被唤醒 notify(); } } }
执行结果:
等待对象b完成计算...
b对象计算的总和是:5050
千万注意:
当在对象上调用wait()方法时,执行该代码的线程(主线程)立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用。
b、多个线程在等待一个对象锁时候使用notifyAll()
在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。
下面给个例子:
/**
* 计算线程
*
*
*/
public class Calculator extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
}
//通知在此对象上等待的所有线程
notifyAll();
}
}
* 计算线程
*
*
*/
public class Calculator extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
}
//通知在此对象上等待的所有线程
notifyAll();
}
}
/**
* 获取计算结果并输出
*
*
*/
public class ReaderResult extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
public void run() {
synchronized (c) {
try {
System.out.println(Thread.currentThread() + "等待计算结果。。。");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "计算结果为:" + c.total);
}
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
//启动三个线程,分别获取计算结果
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
//启动计算线程
calculator.start();
}
}
* 获取计算结果并输出
*
*
*/
public class ReaderResult extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
public void run() {
synchronized (c) {
try {
System.out.println(Thread.currentThread() + "等待计算结果。。。");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "计算结果为:" + c.total);
}
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
//启动三个线程,分别获取计算结果
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
//启动计算线程
calculator.start();
}
}
运行结果:
Thread[Thread-1,5,main]等待计算结果...
Thread[Thread-2,5,main]等待计算结果...
Thread[Thread-3,5,main]等待计算结果...
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.notifyAll(Native Method)
at threadtest.Calculator.run(Calculator.java:18)
Thread[Thread-1,5,main]计算结果为:5050
Thread[Thread-2,5,main]计算结果为:5050
Thread[Thread-3,5,main]计算结果为:5050
Thread[Thread-2,5,main]等待计算结果...
Thread[Thread-3,5,main]等待计算结果...
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.notifyAll(Native Method)
at threadtest.Calculator.run(Calculator.java:18)
Thread[Thread-1,5,main]计算结果为:5050
Thread[Thread-2,5,main]计算结果为:5050
Thread[Thread-3,5,main]计算结果为:5050
运行结果表明,程序中有异常,并且多次运行结果可能有多种输出结果。这就是说明,这个多线程的交互程序还存在问题。究竟是出了什么问题,需要深入的分析和思考,下面将做具体分析。
实际上,上面这个代码中,我们期望的是读取结果的线程在计算线程调用notifyAll()之前等待即可。 但是,如果计算线程先执行,并在读取结果线程等待之前调用了notify()方法,那么又会发生什么呢?这种情况是可能发生的。因为无法保证线程的不同部分将按照什么顺序来执行。幸运的是当读取线程运行时,它只能马上进入等待状态----它没有做任何事情来检查等待的事件是否已经发生。 ----因此,如果计算线程已经调用了notifyAll()方法,那么它就不会再次调用notifyAll(),----并且等待的读取线程将永远保持等待。这当然是开发者所不愿意看到的问题。
因此,当等待的事件发生时,需要能够检查notifyAll()通知事件是否已经发生。
问题是如何检查通知事件是否发生?
二、线程的调度
a、休眠
Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。
这里要明确的一点,不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。
线程休眠的目的是使线程让出CPU的最简单的做法之一,线程休眠时候,会将CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。
线程休眠的方法是Thread.sleep(long millis) 和Thread.sleep(long millis, int nanos) ,均为静态方法,那调用sleep休眠的哪个线程呢?简单说,哪个线程调用sleep,就休眠哪个线程。
/** * Java线程:线程的调度-休眠 * **/ public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); Thread t2 = new Thread(new MyRunnable()); t1.start(); t2.start(); } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 3; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyRunnable implements Runnable { public void run() { for (int i = 0; i < 3; i++) { System.out.println("线程2第" + i + "次执行!"); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程1第2次执行!
线程2第2次执行!
从上面的运行结果可以看出,无法精准保证线程的执行次序。
b、优先级
与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5。
在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。
/** * Java线程:线程的调度-优先级 * * */ public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); Thread t2 = new Thread(new MyRunnable()); t1.setPriority(10); t2.setPriority(1); t2.start(); t1.start(); } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyRunnable implements Runnable { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程2第" + i + "次执行!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
线程1第0次执行!
线程2第0次执行!
线程2第1次执行!
线程1第1次执行!
线程2第2次执行!
线程1第2次执行!
线程1第3次执行!
线程2第3次执行!
线程2第4次执行!
线程1第4次执行!
线程1第5次执行!
线程2第5次执行!
线程1第6次执行!
线程2第6次执行!
线程1第7次执行!
线程2第7次执行!
线程1第8次执行!
线程2第8次执行!
线程1第9次执行!
线程2第9次执行!
由上面的运行结果可以看出,虽然线程1的优先级较高,在真正这行的时候并没有绝对的优势,只是其获得执行的概率较大。
c、让步
线程的让步含义就是使当前运行着线程让出CPU资源,但是让给谁不知道,仅仅是让出,线程状态回到可运行状态。
线程的让步使用Thread.yield()方法,yield() 为静态方法,功能是暂停当前正在执行的线程对象,并执行其他线程。
/** * Java线程:线程的调度-让步 * * */ public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); Thread t2 = new Thread(new MyRunnable()); t2.start(); t1.start(); } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程1第" + i + "次执行!"); } } } class MyRunnable implements Runnable { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程2第" + i + "次执行!"); Thread.yield(); } } }
运行结果:
线程2第0次执行!
线程2第1次执行!
线程2第2次执行!
线程2第3次执行!
线程1第0次执行!
线程1第1次执行!
线程1第2次执行!
线程1第3次执行!
线程1第4次执行!
线程1第5次执行!
线程1第6次执行!
线程1第7次执行!
线程1第8次执行!
线程1第9次执行!
线程2第4次执行!
线程2第5次执行!
线程2第6次执行!
线程2第7次执行!
线程2第8次执行!
线程2第9次执行!
从上面的执行结果可以看出,让步的线程最后执行完毕。
d、合并
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时可以使用join方法。
join为非静态方法,定义如下:
void join() 等待该线程终止。 void join(long millis) 等待该线程终止的时间最长为 millis 毫秒。 void join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
/** * Java线程:线程的调度-合并 * * */ public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); t1.start(); for (int i = 0; i < 20; i++) { System.out.println("主线程第" + i + "次执行!"); if (i > 2) try { //t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。 t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程1第" + i + "次执行!"); } } }
运行结果:
主线程第0次执行!
主线程第1次执行!
主线程第2次执行!
线程1第0次执行!
主线程第3次执行!
线程1第1次执行!
线程1第2次执行!
线程1第3次执行!
线程1第4次执行!
线程1第5次执行!
线程1第6次执行!
线程1第7次执行!
线程1第8次执行!
线程1第9次执行!
主线程第4次执行!
主线程第5次执行!
主线程第6次执行!
主线程第7次执行!
主线程第8次执行!
主线程第9次执行!
主线程第10次执行!
主线程第11次执行!
主线程第12次执行!
主线程第13次执行!
主线程第14次执行!
主线程第15次执行!
主线程第16次执行!
主线程第17次执行!
主线程第18次执行!
主线程第19次执行!
从上面的运行结果中可以看出,使用join方法添加到主线程后,主线程停止运行,等待子线程运行结束后,主线程继续运行至结束。
e、守护线程
守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
setDaemon方法的详细说明:
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: on - 如果为 true,则将该线程标记为守护线程。 抛出: IllegalThreadStateException - 如果该线程处于活动状态。 SecurityException - 如果当前线程无法修改该线程。 另请参见: isDaemon(), checkAccess()
/** * Java线程:线程的调度-守护线程 * * */ public class Test { public static void main(String[] args) { Thread t1 = new MyCommon(); Thread t2 = new Thread(new MyDaemon()); t2.setDaemon(true); //设置为守护线程 t2.start(); t1.start(); } } class MyCommon extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyDaemon implements Runnable { public void run() { for (long i = 0; i < 9999999L; i++) { System.out.println("后台线程第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
后台线程第0次执行!
线程1第0次执行!
线程1第1次执行!
后台线程第1次执行!
后台线程第2次执行!
线程1第2次执行!
线程1第3次执行!
后台线程第3次执行!
线程1第4次执行!
后台线程第4次执行!
后台线程第5次执行!
后台线程第6次执行!
后台线程第7次执行!
从上面的执行结果可以看出:
前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题。
f、同步方法
线程的同步是保证多线程安全访问竞争资源的一种手段。
线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源、什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些原则问题需要考虑,是否有竞争资源被同时改动的问题?
对于同步,在具体的Java代码中需要完成一下两个操作:
- 把竞争访问的资源标识为private;
- 同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。当然这不是唯一控制并发安全的途径。
synchronized关键字使用说明
synchronized只能标记非抽象的方法,不能标识成员变量。
为了演示同步方法的使用,构建了一个信用卡账户,起初信用额为100,然后模拟透支、存款等多个操作。显然银行账户Account对象是个竞争资源,而多个并发操作的是账户方法takeMoney(int money)和putMoney(int money),当然应该在此方法上加上同步,并将账户的余额设为私有变量,禁止直接访问。
package MultiThread; /** * 此处模拟的为信用卡账户 * */ public class PutTakeMoney { /** * @author donghe * */ static class Account{ private String Id; private int remain; public Account(){ } public Account(String id){ this.setId(id); this.remain=100; } public Account(String id, int initRemain ){ this.setId(id); this.remain=initRemain; } public int getRemain(){ return this.remain; } public void setRemain(int r){ this.remain=r; } /** * 存款的方法 * */ public synchronized void putMoney(int money){ try{ Thread.sleep(10L); this.remain+=money; System.out.println(Thread.currentThread().getName()+"存款后,余额为:"+this.remain); Thread.sleep(10L); }catch(InterruptedException ine){ ine.printStackTrace(); } } /** * 取款的方法 * */ public synchronized void takeMoney(int money){ try{ Thread.sleep(10L); this.remain-=money; System.out.println(Thread.currentThread().getName()+"取款后,余额为:"+this.remain); Thread.sleep(10L); }catch(InterruptedException ine){ ine.printStackTrace(); } } public String getId() { return Id; } public void setId(String id) { Id = id; } } static class putThread implements Runnable{ private Account acc; private int put; public putThread(Account acc,int money){ this.acc=acc; this.put=money; } public void run(){ acc.putMoney(put); } } static class takeThread extends Thread{ private Account acc; private int take; public takeThread(String threadName,Account acc, int money){ super(threadName); this.acc=acc; this.take=money; } public void run(){ acc.takeMoney(take); } } public static void main(String[] args){ final Account acc=new Account("刘德华"); Thread thread1=new Thread(new putThread(acc, 30), "线程A"); Thread thread2=new takeThread("线程B", acc, 50); Thread thread3=new Thread(new putThread(acc, 60), "线程C"); Thread thread4=new takeThread("线程D", acc, 70); Thread thread5=new takeThread("线程E", acc, 20); Thread thread6=new Thread(new putThread(acc, 10), "线程F"); Thread thread7=new Thread(new putThread(acc, 30), "线程G"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); thread6.start(); thread7.start(); } }
运行后的结果:
线程A存款后,余额为:130 线程G存款后,余额为:160 线程F存款后,余额为:170 线程E取款后,余额为:150 线程D取款后,余额为:80 线程C存款后,余额为:140 线程B取款后,余额为:90
反面教材,不同步的情况,也就是去掉putMoney(int money)和takeMoney(int money)方法的synchronized修饰符,然后运行程序,结果如下:
线程B取款后,余额为:10 线程E取款后,余额为:-10 线程F存款后,余额为:0 线程A存款后,余额为:10 线程D取款后,余额为:10 线程G存款后,余额为:90 线程C存款后,余额为:90
很显然,上面的结果是错误的,导致错误的原因是多个线程并发访问了竞争资源acc,并对acc的属性做了改动。可见同步的重要性。
注意:
通过前文可知,线程退出同步方法时将释放掉方法所属对象的锁,但还应该注意的是,同步方法中还可以使用特定的方法对线程进行调度。这些方法来自于java.lang.Object类。
void notify()
唤醒在此对象监视器上等待的单个线程。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
结合以上方法,处理多线程同步与互斥问题非常重要,著名的生产者-消费者例子就是一个经典的例子,任何语言多线程必学的例子。
g、同步块
对于同步,除了同步方法外,还可以使用同步代码块,有时候同步代码块会带来比同步方法更好的效果。
追其同步的根本的目的,是控制竞争资源的正确的访问,因此只要在访问竞争资源的时候保证同一时刻只能一个线程访问即可,因此Java引入了同步代码快的策略,以提高性能。
在上个例子的基础上,对putMoney(int money)和takeMoney(int money)方法做了改动,由同步方法改为同步代码块模式,程序的执行逻辑并没有问题。
改动后的Account类如下:
static class Account{ private String Id; private int remain; public Account(){ } public Account(String id){ this.setId(id); this.remain=100; } public Account(String id, int initRemain ){ this.setId(id); this.remain=initRemain; } public int getRemain(){ return this.remain; } public void setRemain(int r){ this.remain=r; } /** * 存款的方法 * */ public void putMoney(int money){ try{ Thread.sleep(10L); synchronized (this) { this.remain+=money; System.out.println(Thread.currentThread().getName()+"存款后,余额为:"+this.remain); } Thread.sleep(10L); }catch(InterruptedException ine){ ine.printStackTrace(); } } /** * 取款的方法 * */ public void takeMoney(int money){ try{ Thread.sleep(10L); synchronized(this){ this.remain-=money; System.out.println(Thread.currentThread().getName()+"取款后,余额为:"+this.remain); } Thread.sleep(10L); }catch(InterruptedException ine){ ine.printStackTrace(); } } public String getId() { return Id; } public void setId(String id) { Id = id; } }
三、并发协作
a、经典的生产者消费者模型
对于多线程程序来说,不管任何编程语言,生产者和消费者模型都是最经典的。就像学习每一门编程语言一样,Hello World!都是最经典的例子。
实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了。
对于此模型,应该明确一下几点:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产。
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
- 当消费者发现仓储没产品可消费时候会通知生产者生产。
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
此模型将要结合java.lang.Object的wait与notify、notifyAll方法来实现以上的需求。这是非常重要的。
package MultiThread; /** * 此处模拟的为信用卡账户 * */ public class ProducerConsumerModel2 { /** * 并发协作:生产消费者模型 * @author donghe * */ static class Warehouse{ private int max_size=100; //最大库存量 private int currentNum; //当前库存数 public Warehouse(){ } public Warehouse(int capcity){ this.currentNum=capcity; } public int getMax_size() { return max_size; } public void setMax_size(int max_size) { this.max_size = max_size; } public int getCurrentNum() { return currentNum; } public void setCurrentNum(int currentNum) { this.currentNum = currentNum; } /** * 生产指定数量的产品 * */ public synchronized void produce(int needNum){ while(this.currentNum+needNum>this.max_size){ System.out.println("要生产的产品数量"+needNum+"超过仓库剩余库存容量"+(this.max_size-this.currentNum)+",生产任务暂停执行!"); try { //当然生产线程等待 this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //满足生产条件,则进行生产,这里仅仅更改了仓库中的库存量 this.currentNum+=needNum; System.out.println("生产了"+needNum+"个产品,生产后当前仓库产品总数为"+this.currentNum); //唤醒在此对象监视器上等待的所有线程 this.notifyAll(); } /** * 消费执行数量的产品 * */ public synchronized void consume(int needNum){ while(this.currentNum<needNum){ System.out.println("要消费的产品数量"+needNum+"超过仓库当前总的产品数"+this.currentNum+",消费任务暂停执行!"); //当然生产线程等待 try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //满足生产条件,则进行生产,这里仅仅更改了仓库中的库存量 this.currentNum-=needNum; System.out.println("消费了"+needNum+"个产品,消费后当前仓库的产品总数为"+this.currentNum); //唤醒在此对象监视器上等待的所有线程 this.notifyAll(); } } static class Producer extends Thread{ private Warehouse wh; private int put; public Producer(String threadName,Warehouse warehouse,int needNum){ super(threadName); this.wh=warehouse; this.put=needNum; } public void run(){ wh.produce(put); } } static class Consumer extends Thread{ private Warehouse wh; private int take; public Consumer(String threadName,Warehouse warehouse, int needNum){ super(threadName); this.wh=warehouse; this.take=needNum; } public void run(){ wh.consume(take); } } public static void main(String[] args){ //仓库中的初始产品数 final Warehouse wh=new Warehouse(50); Thread p1=new Producer("生产线程1", wh, 60); Thread c1=new Consumer("消费线程1", wh, 20); Thread c2=new Consumer("消费线程2", wh, 80); Thread c3=new Consumer("消费线程3", wh, 60); Thread p2=new Producer("生产线程2", wh, 30); Thread p3=new Producer("生产线程3", wh, 10); Thread p4=new Producer("生产线程4", wh, 50); p1.start(); c1.start(); c2.start(); c3.start(); p2.start(); p3.start(); p4.start(); } }
运行结果:
要消费的产品数量80超过仓库当前总的产品数50,消费任务暂停执行!
生产了50个产品,生产后当前仓库产品总数为100
消费了80个产品,消费后当前仓库的产品总数为20
要消费的产品数量60超过仓库当前总的产品数20,消费任务暂停执行!
生产了60个产品,生产后当前仓库产品总数为80
消费了60个产品,消费后当前仓库的产品总数为20
消费了20个产品,消费后当前仓库的产品总数为0
生产了30个产品,生产后当前仓库产品总数为30
生产了10个产品,生产后当前仓库产品总数为40
说明:
对于本例,要说明的是当发现不能满足生产或者消费条件的时候,调用对象的wait方法,wait方法的作用是释放当前线程的所获得的锁,并调用对象的notifyAll() 方法,通知(唤醒)该对象上其他等待线程,使得其继续执行。这样,整个生产者、消费者线程得以正确的协作执行。
notifyAll() 方法,起到的是一个通知作用,不释放锁,也不获取锁。只是告诉该对象上等待的线程“可以竞争执行了,都醒来去执行吧”。
本例仅仅是生产者消费者模型中最简单的一种表示,然而,如果消费者消费的仓储量达不到满足,而又没有生产者,则程序会一直处于等待状态,如下图所示:
这当然是不对的。实际上可以将此例进行修改,根据消费驱动生产,同时生产兼顾仓库,如果仓不满就生产,并对每次最大消费量做个限制,这样就不存在此问题了,当然这样的例子更复杂,更难以说明这样一个简单模型。
b、死锁
线程发生死锁可能性很小,即使看似可能发生死锁的代码,在运行时发生死锁的可能性也是小之又小。
发生死锁的原因一般是两个对象的锁相互等待造成的。
下面是死锁的一个完整例子:
package MultiThread; /** * 并发协作-死锁 * * */ public class Test { public static void main(String[] args) { DeadlockRisk dead = new DeadlockRisk(); MyThread t1 = new MyThread(dead, 1, 2); MyThread t2 = new MyThread(dead, 3, 4); MyThread t3 = new MyThread(dead, 5, 6); MyThread t4 = new MyThread(dead, 7, 8); t1.start(); t2.start(); t3.start(); t4.start(); } } class MyThread extends Thread { private DeadlockRisk dead; private int a, b; MyThread(DeadlockRisk dead, int a, int b) { this.dead = dead; this.a = a; this.b = b; } @Override public void run() { dead.read(); dead.write(a, b); } } class DeadlockRisk { private static class Resource { public int value; } private Resource resourceA = new Resource(); private Resource resourceB = new Resource(); public int read() { synchronized (resourceA) { System.out.println("read():" + Thread.currentThread().getName() + "获取了resourceA的锁!"); synchronized (resourceB) { System.out.println("read():" + Thread.currentThread().getName() + "获取了resourceB的锁!"); System.out.println("读成功! resourceA.value="+resourceA.value+" resourceB.value="+resourceB.value); return resourceB.value + resourceA.value; } } } public void write(int a, int b) { synchronized (resourceB) { System.out.println("write():" + Thread.currentThread().getName() + "获取了resourceB的锁!"); synchronized (resourceA) { System.out.println("write():" + Thread.currentThread().getName() + "获取了resourceA的锁!"); resourceA.value = a; resourceB.value = b; System.out.println("写成功!"); } } } }
运行结果为:
由运行结果可以看出,写线程Thread-0获得resourceB的锁,读线程获得了Thread-2的锁,两者都因无法获得被对方占有,而不释放的锁,使程序运行进入死锁,运行到图中某处停止。
c、volatile关键字
Java 语言包含两种内在的同步机制:synchronized同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。
谈及到volatile关键字,不得不提的一篇文章是:《Java 理论与实践: 正确使用 Volatile 变量》,这篇文章对volatile关键字的用法做了相当精辟的阐述。
之所以要单独提出volatile这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
首先考虑一个问题,为什么变量需要volatile来修饰呢?
要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入。
同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。
假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
通过这个设想,就不难理解volatile关键字了。
volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的
synchronized
”;与 synchronized
块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized
的一部分。更多的内容,请参看::《Java 理论与实践: 正确使用 Volatile 变量》一文,写得很好。