转自:http://ajava.org/course/java/13565.html
一、Java多线程互斥及同步控制和通信
多个线程的执行是并发的,也就是在逻辑上“同时”,而不管是否是物理上的“同时”。如果系统只有一个CPU,那么真正的“同时”是不可能的,但是由于CPU的速度非常快,用户感觉不到其中的区别,因此我们也不用关心它,只需要设想各个线程是同时执行即可。
多线程和传统的单线程在程序设计上最大的区别在于,由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,由此带来的线程调度,同步等问题。
1、线程互斥与线程同步
线程互斥:并发执行的多个线程在某一时间内只允许一个线程在执行以访问数据。
线程同步:并发执行的多个线程之间互相发送消息进行合作、互相等待,按一定速度配合执行。
2、线程互斥
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
(1)应用的场合:
当两个或多个线程同时访问共享数据时,可能对数据进行读、写、修改、删除等操作时,应保证同时只有一个线程访问共享数据。
(2)线程互斥的实现机制
① 监视器(同步锁定):
线程进入监视器后其它线程则不能再进入监视器,直到被锁定的线程退出监视器,下一个线程才能进入监视器被执行。
如何产生监视器(同步互斥方法):通过在被多个线程所共享的方法前加上synchronized关键字定义出同步互斥方法,每一个拥有synchronized方法的对象都含有一个独立的监视器,只有某一个线程的synchronized方法执行完后其它线程的synchronized方法才能被执行。
- public synchronized void myFun()
- {
- }
② 编程要点:
l 应将访问共享数据的代码设计为synchronized方法;
l synchronized可以用来限定一个方法或一小段语句或整个类(可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问),即类方法,实例方法,一个方法中的任何代码块。
l 由于可以通过 private 关键字来保证数据对象只能被方法访问,所以只需针对方法提出一套同步锁定机制。通过synchronized 方法来控制对类中的成员变量(共享数据)的访问。
(3)synchronized 关键字
在有些应用中,可能会出现两个线程访问同一个对象的情况。但是,在大多数有用的程序中,线程之间通常有信息流。试考虑一个金融应用程序,它有一个 Account 对象,如下例中所示:
一个银行中的多项活动
- public class Account
- { String holderName;
- float amount;
- public Account(String name, float amt)
- { holderName = name;
- amount = amt;
- }
- public void deposit(float amt)
- { amount += amt;
- }
- public void withdraw(float amt)
- { amount -= amt;
- }
- public float checkBalance()
- { return amount;
- }
- }
在此代码样例中潜伏着一个错误。如果此类用于单线程应用程序,不会有任何问题。但是,在多线程应用程序的情况中,不同的线程就有可能同时访问同一个 Account 对象,比如说一个联合帐户的所有者在不同的 ATM 上同时进行访问。
在这种情况下,存入和支出就可能以这样的方式发生:一个事务被另一个事务覆盖。这种情况将是灾难性的。但是,Java 编程语言提供了一种简单的机制来防止发生这种覆盖。每个对象在运行时都有一个关联的锁。这个锁可通过为方法添加关键字 synchronized 来获得。这样,修订过的 Account 对象(如下所示)将不会遭受像数据损坏这样的错误:
对一个银行中的多项活动进行同步处理
- public class Account
- { String holderName;
- float amount;
- public Account(String name, float amt)
- { holderName = name;
- amount = amt;
- }
- public synchronized void deposit(float amt)
- { amount += amt;
- }
- public synchronized void withdraw(float amt)
- { amount -= amt;
- }
- public float checkBalance()
- { return amount;
- }
- }
deposit() 和 withdraw() 函数都需要这个锁来进行操作,所以当一个函数运行时,另一个函数就被阻塞。请注意, checkBalance() 未作更改,它严格是一个读函数。因为 checkBalance() 未作同步处理,所以任何其他方法都不会阻塞它,它也不会阻塞任何其他方法,不管那些方法是否进行了同步处理。
(4)synchronized代码块
synchronized 方法的主要缺陷在于,若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized方法 ,然后在主方法中调用该synchronized方法来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
通过 synchronized关键字来声明synchronized 块。语法如下:
- synchronized(syncObject)
- {
- //允许访问控制的代码
- }
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (同步对象,可以是类实例或类)的锁方能执行。由于synchronized 块可以针对任意的代码块,且可任意指定上锁的对象,故灵活性较高。
(5)线程互斥的实例:
编程模拟银行计算利息,由线程管理50个出纳(50个线程),计算全部出纳付出的总利息,其中每个出纳处理10笔钱,每笔款额为1000元,利息为0.1。
- import java.applet.Applet; //未采用同步方法时的程序
- import java.awt.*;
- import java.awt.event.*;
- public class tongbu extends Applet implements Runnable,ActionListener
- { int Accounts=1000; //每笔款额为1000元
- int money,interest,totalMoney;
- List list=new List(); //定义并产生一个List控件
- Button startButton=new Button("开始计算");
- public void init()
- { list.resize(size().width,size().height-50); //设定List控件的大小与Applet相匹配
- setLayout(new BorderLayout()); //方位布局
- add("Center",list); //定位List控件的位置
- startButton.addActionListener(this);
- add("South",startButton); //在Applet中加入一个“开始计算”按钮
- }
- public void actionPerformed(ActionEvent evt) //响应用户单击“开始计算”按钮的事件
- { if(evt.getSource()==startButton)
- { for(int i=1;i<=50;i++) //产生并启动50个线程(50个出纳)
- new Thread(this).start();
- }
- }
- // public synchronized void run()
- public void run()
- { for(int i=0;i<10;i++)
- { totalMoney=totalMoney+Accounts;//统计及显示必须“一气合成”地执行,不允许打断。
- interest=interest+(int)(Accounts*0.1);
- }
- try
- { Thread.sleep(10); //目的是适应高档主机
- }
- catch(InterruptedException e)
- {
- }
- String result=String.valueOf(totalMoney)+"--"+String.valueOf(interest);
- list.addItem(result);
- }
- }
计算结果有错(不是逐个累加状态),其原因是所创建的50个线程都要调用calculate()及addlist()方法(注意:一个线程的调用还未结束,另一个线程的调用已经可能开始!),而每执行一次calculate(都修改totalMoney 及interest值;每当两个线程同时调用calculate(),由于CPU的分时执行效果,两个线程交差修改totalMoney 及interest值,非同步执行导致totalMoney的值与interest值不匹配(让一个线程顺序执行完毕后再让另一个线程顺序执行完毕,在某一时刻只允许一个线程访问共享资源)。
改进的措施:将run()方法设置为同步方法(是否可以将calculate() 及addlist()设置为同步方法 ?)
要点:
区分实例对象监视器(在同一个类中的所有非静态同步方法都使用同一个实例对象监视器,一旦某一个对象的同步方法被执行,该对象即获得监视器,只有当该对象的同步方法被完整地执行完毕,其他对象才能获得监视器)和类监视器(在同一个类中的所有静态同步方法都使用同一个类监视器,当同步方法要求访问静态成员数据时应将此同步方法设计为静态同步方法,此时在任意时刻只允许有一个静态同步方法被执行)的不同。
3、线程间同步通讯---线程的阻塞:
多线程除互斥外,在某些应用场合下还应考虑线程间同步通讯----线程的阻塞(一个线程等待另外一个线程的执行并根据该线程的执行结果自己再执行,也即阻塞指的是暂停一个线程的执行以等待某个条件发生,如某资源就绪等)。
因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对线程间同步通讯----线程的阻塞机制的支持。
线程在继续执行前需要等待一个条件时,仅有 synchronized 关键字是不够的。虽然 synchronized 关键字阻止并发更新一个对象,但它没有实现线程间同步通讯。
Object 类为此提供了三个函数:wait()、notify() 和 notifyAll()。
(1)线程同步应用的场合:
在“生产者---消费者”式的问题中。
(2)生产者---消费者的问题:
生产者每生产出一件产品(创建出数据),消费者都能及时地消费它(使用数据),不能提前或落后;否则将不匹配(消费者获取得数据将不是生产者产生的数据)。
(3)实现的手段:
利用 wait()(释放同步锁,进入等待队列)、notify()(唤醒等待队列中的第一个线程,并把它移人同步锁申请队列)及notifyAll()方法(它们只能在synchronized同步方法中被调用)。
(4)等待—通知机制:
通过使用wait(),一个线程可以等待对象里的一些条件的改变,而通过notify()和notifyAll(),一个线程也可以通知等待对象条件的所有线程,告诉它们条件已经改变,可以继续执行。通常的一种情况是一个线程产生对象的数据,而另一个线程使用对象的数据。
(5)实例1:(线程与进程之间通讯)
问题:实时跟踪显示用户移动鼠标时的落点位置(将用户移动鼠标时的落点位置作为生产者传递给线程,由线程显示出鼠标坐标位置)。
- import java.awt.*;
- import java.applet.*;
- public class ThreadCom extends Applet implements Runnable
- { Thread myThread;
- List ConsumeList=new List(),ProduceList=new List();
- int x=0,y=0;
- private boolean isAvailable=false; //定义一个标置变量
- public void init() //定义两个List控件并重置其大小
- { ConsumeList.resize(size().width/4,size().height);
- ProduceList.resize(size().width/4,size().height);
- setLayout(new BorderLayout());//定位两个List控件的位置及布局
- add("East",ConsumeList);
- add("West",ProduceList);
- if(myThread ==null)
- { myThread=new Thread(this);
- myThread.start(); //产生并启动一个线程
- }
- ProduceList.addItem("Produce"+"("+x+","+y+")");
- }
- public synchronized void run() //同步方法 线程 !
- { while(myThread ==Thread.currentThread())
- { //在List控件中显示出鼠标坐标位置值
- ConsumeList.addItem("Consume"+"("+x+","+y+")");
- while(isAvailable ==false)
- {//让线程处于等待状态(暂停)直到被notify() 或notifyAll()唤醒
- 因wait()定义有异常,所以应捕获它
- try
- {wait();
- }
- catch(Exception e)
- { System.out.println(e);
- }
- }
- isAvailable=false; //设置消费数字完毕标置
- notify(); //释放监视器
- }
- }
- public synchronized boolean mouseMove(Event evt, int x, int y)
- {//鼠标移动的响应函数
- while(isAvailable ==true)
- //等待消费者获得新的数字(等待消费者释放监视器)
- { try
- { wait();
- }
- catch(InterruptedException e)
- {
- }
- }
- isAvailable=true; //设置生产数字完毕标置
- notify(); //释放监视器以唤醒处于等待状态的线程
- this.x=x; //获得鼠标位置 进程
- this.y=y;
- ProduceList.addItem("Produce"+"("+x+","+y+")");
- return true;
- }
- /*
- public void run()
- { while(myThread ==Thread.currentThread())
- { ConsumeList.addItem("Consume"+"("+x+","+y+")");
- }
- }
- public boolean mouseMove(Event evt, int x, int y)
- { this.x=x;
- this.y=y;
- ProduceList.addItem("Produce"+"("+x+","+y+")");
- return true;
- }
- */
- }
程序执行结果有错,生产者(移动鼠标)与消费者(线程)未同步,不匹配,改为同步方法。
实例2:(线程与线程之间通讯):
问题:生产者线程产生数据,消费者线程获得数据并显示其值(消费它)
- import java.io.*;
- import java.applet.*;
- class Producer extends Thread //生产者线程
- { private Box box;
- public Producer(Box box)
- { this.box=box;
- }
- public void run()
- { for(int i=0;i<10;i++)
- { box.put(i); //产生数据
- System.out.println("Producer put:"+i);
- try
- { sleep((int)(Math.random()*100));
- }
- catch(InterruptedException e) { }
- }
- }
- }
- class Consumer extends Thread //消费者线程
- { private Box box;
- public Consumer(Box box)
- { this.box=box;
- }
- public void run()
- { int value;
- for(int i=0;i<10;i++)
- { value=box.get(); //获得数据并显示其值
- System.out.println("Consumer got:"+value);
- }
- }
- }
- class Box //共用类并提供get()、put()方法
- { private int contents;
- private boolean isAvailable=false; //定义一个标置变量
- public synchronized int get() //同步方法
- { while(isAvailable ==false)
- { try //等待生产者生产出新的数字(等待生产者释放监视器)
- { wait();
- }catch(InterruptedException e)
- {
- }
- }
- isAvailable=false; //设置消费数字完毕标置
- notify(); //释放监视器
- return contents;
- }
- public synchronized void put(int value)
- { while(isAvailable ==true)
- //等待消费者获得新的数字(等待消费者释放监视器)
- { try
- { wait();
- }catch(InterruptedException e)
- {
- }
- }
- contents=value;
- isAvailable=true; //设置生产数字完毕标置
- notify(); //释放监视器
- }
- }
- public class PrdCons //main()所在的类
- { public static void main(String args[])
- { Box b=new Box();
- Producer p=new Producer(b); //产生生产者、消费者线程
- Consumer c=new Consumer(b);
- p.start(); //启动生产者、消费者线程
- c.start();
- }
- }
(6)suspend() / resume() 方法和wait() / notify() 方法的对比
l suspend()/resume() 方法
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
l wait()/notify() 方法
两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
l 两者的对比
区别的核心在于,suspend()/resume() 方法,阻塞时都不会释放占用的锁(如果占用了的话),而wait()/notify() 方法则相反。
首先,suspend()/resume() 方法都隶属于 Thread 类,而wait()/notify() 方法却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。
其次,suspend()/resume() 方法都可在任何位置调用,但是wait()/notify() 方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
关于 wait()/notify() 方法的两点说明:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
4、线程优先级
(1)优先级(共10级):
它们决定线程执行的先后次序(优先级高者先执行)并可以通过Thread类中的setPriority()和getPriority()方法来改变和获取优先级。
(2)典型的优先级码
Thread.MIN_PRIORITY (1级)、Thread.MAX_PRIORITY(10级)、Thread.NORM_PRIORITY(5级)
(3)调度规则:
优先级高者的线程先执行,然后再执行优先级低者;但对相同优先级的线程的执行次序将取决于目标操作系统的设计方式(时间片设计方式,即抢先式----轮循执行;非时间片设计方式,即非抢先式---先创建者先执行,然后再另一个。此时容易产生“自私”线程)。
- class ThreadTest
- { public static void main(String args[])
- { Thread ThreadOne=new MyThread(“Thread One”);
- ThreadOne.setPriority(Thread.MIN_PRIORITY);
- ThreadOne.start();
- Thread ThreadTwo=new MyThread(“Thread Two”);
- ThreadTwo.setPriority(Thread.MAX_PRIORITY);
- ThreadTwo.start();
- Thread ThreadThree=new MyThread(“Thread Three”);
- ThreadThree.setPriority(Thread.MAX_PRIORITY);
- ThreadThree.start();
- }
- }
- class MyThread extends Thread
- { String threadName;
- MyThread(String nameString)
- { threadName=nameString ;
- }
- public void run()
- { for(int I=0; I<3; I++)
- { System.out.println(threadName+” “+getPriority() );
- }
- }
- }
5、线程组
利用它可以将多个线程集合为一个单独的对象,到达对这些线程共同操作(如统一启动一组线程或挂起一组线程等),创建出线程组并加入指定的线程成员(也可以是子线程组,形成树状层次结构)。
线程是被个别创建的,但可以将它们归类到线程组中,以便于调试和监视。只能在创建线程的同时将它与一个线程组相关联。在使用大量线程的程序中,使用线程组组织线程可能很有帮助。可以将它们看作是计算机上的目录和文件结构。
线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。
在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。在 Java 中,除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。
Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。
- ThreadGroup aThreadGroup=new ThreadGroup(“线程组名”);
- Thread aThread=new Thread(aThreadGroup, “线程名”);
- aThread.getThreadGroup(); //返回线程组对象aThreadGroup
(1)ThreadGroup类成员函数
(2)影响组内各个线程的组函数:resume()、stop()、suspend()。
6、守护线程
守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。
可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。