Java多线程
一.线程的基本概念
1.线程概述
2.线程给我们带来的好处
3.Java的线程模型
二.Runnable接口与Thread类
1.Runnable接口
2.Thread类
3.两种创建线程方法的比较
三. 线程的控制与调度
1.线程的生命周期
2.线程调度与优先级
2.1线程的调度模型
2.2改变线程状态
四、线程的同步机制
1.共享数据的线程“互斥”锁定
1.1线程间的数据共享
1.2 Synchronized
2.传送数据的线程同步运行
2.1线程间传送数据
2.2synchronized与“互斥锁”标志
3.线程间通讯
4.死锁问题
一、线程的基本概念
1.线程概述
线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程。
进程和线程是现代操作系统中两个必不可少的运行模型。在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程);一个进程中可以有一个或多个线程。进程和进程之间不共享内存,也就是说系统中的进程是在各自独立的内存空间中运行的。而一个进程中的线可以共享系统分派给这个进程的内存空间。
线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈, 是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。
注意:任何一个线程在建立时都会执行一个函数,这个函数叫做线程执行函数。也可以将这个函数看做线程的入口点(类似于程序中的main函数)。无论使用什么语言或技术来建立线程,都必须执行这个函数(这个函数的表现形式可能不一样,但都会有一个这样的函数)。如在Windows中用于建立线程的API函数CreateThread的第三个参数就是这个执行函数的指针。
在操作系统将进程分成多个线程后,这些线程可以在操作系统的管理下并发执行,从而大大提高了程序的运行效率。虽然线程的执行从宏观上看是多个线程同时执行,但实际上这只是操作系统的障眼法。由于一块CPU同时只能执行一条指令,因此,在拥有一块CPU的计算机上不可能同时执行两个任务。而操作系统为了能提高程序的运行效率,在一个线程空闲时会撤下这个线程,并且会让其他的线程来执行,这种方式叫做线程调度。我们之所以从表面上看是多个线程同时执行,是因为不同线程之间切换的时间非常短,而且在一般情况下切换非常频繁。假设我们有线程A和B。在运行时,可能是A执行了1毫秒后,切换到B后,B又执行了1毫秒,然后又切换到了A,A又执行1毫秒。由于1毫秒的时间对于普通人来说是很难感知的,因此,从表面看上去就象A和B同时执行一样,但实际上A和B是交替执行的。
2.线程给我们带来的好处
如果能合理地使用线程,将会减少开发和维护成本,甚至可以改善复杂应用程序的性能。如在GUI应用程序中,还以通过线程的异步特性来更好地处理事件;在应用服务器程序中可以通过建立多个线程来处理客户端的请求。线程甚至还可以简化虚拟机的实现,如Java虚拟机(JVM)的垃圾回收器(garbage collector)通常运行在一个或多个线程中。因此,使用线程将会从以下五个方面来改善我们的应用程序:
1) 充分利用CPU资源
现在世界上大多数计算机只有一块CPU。因此,充分利用CPU资源显得尤为重要。当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状态。这将造成大量的计算资源的浪费。而在程序中使用多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。这样CPU就很难有空闲的时候。因此,CPU资源就得到了充分地利用。
2)简化编程模型
如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。但要完成多项任务,如果还使用单线程的话,那就得在在程序中判断每项任务是否应该执行以及什么时候执行。如显示一个时钟的时、分、秒三个指针。使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。这样有助于开发人员对程序的理解和维护。
3) 简化异步事件的处理
当一个服务器应用程序在接收不同的客户端连接时最简单地处理方法就是为每一个客户端连接建立一个线程。然后监听线程仍然负责监听来自客户端的请求。如果这种应用程序采用单线程来处理,当监听线程接收到一个客户端请求后,开始读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,也就是说,这个线程将无法再监听客户端请求了。而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O。但使用异步I/O方式比使用同步I/O更难以控制,也更容易出错。因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。
4)使GUI更有效率
使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。
在现代的GUI框架(如SWING、AWT和SWT)中都使用了一个单独的事件分派线程(event dispatch thread,EDT)来对GUI事件进行扫描。当我们按下一个按钮时,按钮的单击事件函数会在这个事件分派线程中被调用。由于EDT的任务只是对GUI事件进行扫描,因此,这种方式对事件的反映是非常快的。
5) 节约成本
提高程序的执行效率一般有三种方法:
①增加计算机的CPU个数。
②为一个程序启动多个进程
③程序中使用多进程。
第一种方法是最容易做到的,但同时也是最昂贵的。这种方法不需要修改程序,从理论上说,任何程序都可以使用这种方法来提高执行效率。第二种方法虽然不用购买新的硬件,但这种方式不容易共享数据,如果这个程序要完成的任务需要必须要共享数据的话,这种方式就不太方便,而且启动多个线程会消耗大量的系统资源。第三种方法恰好弥补了第一种方法的缺点,而又继承了它们的优点。也就是说,既不需要购买CPU,也不会因为启太多的线程而占用大量的系统资源(在默认情况下,一个线程所占的内存空间要远比一个进程所占的内存空间小得多),并且多线程可以模拟多块CPU的运行方式,因此,使用多线程是提高程序执行效率的最廉价的方式。
3.Java的线程模型
由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。Java通过Thread类将线程所必须的功能都封装了起来。要想建立一个线程,必须要有一个线程执行函数,这个线程执行函数对应Thread类的run方法。Thread类还有一个start方法,这个方法负责建立线程,相当于调用Windows的建立线程函数CreateThread。当调用start方法后,如果线程建立成功,并自动调用Thread类的run方法。因此,任何继承Thread的Java类都可以通过Thread类的start方法来建立线程。如果想运行自己的线程执行函数,那就要覆盖Thread类的run方法。
在Java的线程模型中除了Thread类,还有一个标识某个Java类是否可作为线程类的接口Runnable,这个接口只有一个抽象方法run,也就是Java线程模型的线程执行函数。因此,一个线程类的唯一标准就是这个类是否实现了Runnable接口的run方法,也就是说,拥有线程执行函数的类就是线程类。
从上面可以看出,在Java中建立线程有两种方法,一种是继承Thread类,另一种是实现Runnable接口,并通过Thread和实现Runnable的类来建立线程,其实这两种方法从本质上说是一种方法,即都是通过Thread类来建立线程,并运行run方法的。但它们的大区别是通过继承Thread类来建立线程,虽然在实现起来更容易,但由于Java不支持多继承,因此,这个线程类如果继承了Thread,就不能再继承其他的类了,因此,Java线程模型提供了通过实现Runnable接口的方法来建立线程,这样线程类可以在必要的时候继承和业务有关的类,而不是Thread类。
二、Runnable接口与Thread类
1.Runnable接口
创建线程的最简单的方法就是创建一个实现Runnable 接口的类。Runnable抽象了一个执行代码单元。你可以通过实现Runnable接口的方法创建每一个对象的线程。为实现 Runnable 接口,一个类仅需实现一个run()的简单方法,该方法声明如下:
public void run( )
Runnable接口中的run方法只是一个未实现的方法。一个线程对象必须实现run方法完成线程的所有活动,已实现的run方法称为该对象的线程体。任何实现Runnable接口的对象都可以作为一个线程的目标对象。在run()中可以定义代码来构建新的线程。理解下面内容是至关重要的:run()方法能够像主线程那样调用其他方法,引用其他类,声明变量。仅有的不同是run()在程序中确立另一个并发的线程执行入口。当run()返回时,该线程结束。
在你已经创建了实现Runnable接口的类以后,你要在类内部实例化一个Thread类的对象。Thread 类定义了好几种构造函数。我们会用到的如下:
Thread(Runnable threadOb, String threadName)
该构造函数中,threadOb是一个实现Runnable接口类的实例。这定义了线程执行的起点。新线程的名称由threadName定义。
建立新的线程后,它并不运行直到调用了它的start()方法,该方法在Thread 类中定义。本质上,start() 执行的是一个对run()的调用。 Start()方法声明如下:
void start( )
通过实现Runnable创建线程
下面是创建一个新的线程并开始运行一个例子:
// Create a new thread.
class NewThread implements Runnable {
Thread t;
NewThread() {
// Create a new, second thread
t = new Thread(this, "Demo Thread");
System.out.println("Child thread: " + t);
t.start(); // Start the thread
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
// Let the thread sleep for a while.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
public class ThreadDemo {
public static void main(String args[]) {
new NewThread(); // create a new thread
try {
for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
通过前面的语句this 表明在this对象中你想要新的线程调用run()方法。然后,start() 被调用,以run()方法为开始启动了线程的执行。这使子线程for 循环开始执行。调用start()之后,NewThread 的构造函数返回到main()。当主线程被恢复,它到达for 循环。两个线程继续运行,共享CPU,直到它们的循环结束。
这将产生以下结果:
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting
2.Thread类
Thread类将Runnable接口中的run方法实现为空方法,并定义许多用于创建和控制线程的方法。格式为:
public class Thread extends Object implements Runnable
构造方法:
public Thread( )
public Thread(String name)
public Thread(Runnable target)
public Thread(Runnable target,String name)
public Thread(ThreadGroup group,Runnable target)
public Thread(ThreadGroup group,String name)
public Thread(ThreadGroup group,Runnable target,String name)
Thread类的静态方法:
public static Thread currentThread( ) //返回当前执行线程的引用对象
public static intactiveCount() //返回当前线程组中活动线程个数
public static enumerate(Thread[] tarray) //将当前线程组中的活动线程拷贝到tarray数组中,包括子线程
Thread类的实例方法:
public final String getName() //返回线程名
public final void setName(String name) //设置线程的名字为name
public void start() //启动已创建的线程对象
public final boolean isAlive() //返回线程是否启动的状态
public final ThreadGroup getThreadGroup() //返回当前线程所属的线程组名
public String toString() //返回线程的字符穿信息
通过扩展Thread创建线程:
创建线程的另一个途径是创建一个新类来扩展Thread类,然后创建该类的实例。当一个类继承Thread时,它必须重载run()方法,这个run()方法是新线程的入口。它也必须调用start()方法去启动新线程执行下面是重写扩展线程前面的程序:
// Create a second thread by extending Thread
class NewThread extends Thread {
NewThread() {
// Create a new, second thread
super("Demo Thread");
System.out.println("Child thread: " + this);
start(); // Start the thread
}
// This is the entry point for the second thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
// Let the thread sleep for a while.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
public class ExtendThread {
public static void main(String args[]) {
new NewThread(); // create a new thread
try {
for(int i = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
这将产生以下结果:
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.
到目前为止,我们仅用到两个线程:主线程和一个子线程。然而,你的程序可以创建所需的更多线程。例如,下面的程序创建了三个子线程:
// Create multiple threads.
class NewThread implements Runnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: " + t);
t.start(); // Start the thread
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + "Interrupted");
}
System.out.println(name + " exiting.");
}
}
class MultiThreadDemo {
public static void main(String args[]) {
new NewThread("One"); // start threads
new NewThread("Two");
new NewThread("Three");
try {
// wait for other threads to end
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting.");
}
}
程序输出如下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.
如你所见,一旦启动,所有三个子线程共享CPU。注意main()中对sleep(10000)的调用。这使主线程沉睡十秒确保它最后结束。
3.两种创建线程方法的比较
比较两者的特点和应用领域:
直接继承线程Thread类。该方法编写简单,可以直接操作线程,适用于单重继承情况,因而不能再继承其他类
实现Runnable接口。当一个线程已继承了另一个类时,就只能用实现Runnable接口的方法来创建线程,且便于保持程序风格的一致性。
到这里,你一定会奇怪为什么Java有两种创建子线程的方法,哪一种更好呢。所有的问题都归于一点。Thread类定义了多种方法可以被派生类 重载。对于所有的方法,惟一的必须被重载的是run()方法。这当然是实现Runnable接口所需的同样的方法。很多Java程序员认为类仅在它们被加 强或修改时应该被扩展。因此,如果你不重载Thread的其他方法时,最好只实现Runnable 接口。这当然由你决定。
扩展知识(线程组):
线程组 每个线程都是一个线程组的成员,线程组把多个线程集成为一个对象,通过线程组可以同时对其中的多个线程进行操作,如启动一个线程组的所有线程等。Java的线程组由java.lang包中的Thread——Group类实现。 ThreadGroup类用来管理一组线程,包括:线程的数目,线程间的关系,线程正在执行的操作,以及线程将要启动或终止时间等。线程组还可以包含线程组。在Java的应用程序中,最高层的线程组是名位main的线程组,在main中还可以加入线程或线程组,在mian的子线程组中也可以加入线程和线程组,形成线程组和线程之间的树状继承关系。 |
三、 线程的控制与调度
1.线程的生命周期
线程的状态表示线程正在进行的活动以及在此时间段内所能完成的任务。线程有新建状态、就绪状态、运行状态、阻塞、死亡五中状态。一个具有生命的线程,总是处于这五种状态之一。
1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程状态图
2.线程调度与优先级
2.1线程的调度模型
同一时刻如果有多个线程处于可运行状态,则他们需要排队等待CPU资源。此时每个线程自动获得一个线程的优先级(priority),优先级的高低反映线程的重要或紧急程度。可运行状态的线程按优先级排队,线程调度依据优先级基础上的“先到先服务”原则。
线程调度管理器负责线程排队和CPU在线程间的分配,并由线程调度算法进行调度。当线程调度管理器选种某个线程时,该线程获得CPU资源而进入运行状态。
线程调度是先占式调度,即如果在当前线程执行过程中一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。先占式调度分为:独占式和分时方式。
独占方式下,当前执行线程将一直执行下去,直到执行完毕或由于某种原因主动放弃CPU,或CPU被一个更高优先级的线程抢占
分时方式下,当前运行线程获得一个时间片,时间到时,即使没有执行完也要让出CPU,进入可运行状态,等待下一个时间片的调度。系统选中其他可运行状态的线程执行
分时方式的系统使每个线程工作若干步,实现多线程同时运行
线程的优先级用1~10表示,1表示优先级最高,默认值是5。每个优先级对应一个Thread类的公用静态常量。如:
public static final int NORM_PRIORITY=5
public static final int MIN_PRIORITY=1
public static final int MAX_PRIORITY=10
2.2改变线程状态
①线程睡眠sleep( )
public static void sleep(long millis)throw InterruptedException
当前线程睡眠(停止执行)若干豪秒,线程由运行中的状态进入不可运行状态,睡眠时间过后进程再进入可运行状态。
②暂停线程 yield( )
public static void yield( )
yield()暂停当前线程执行,允许其他线程执行。该线程仍处于可运行状态,不转为阻塞状态。此时,系统选择其他同优先级线程执行,若无其他同优先级程,则选中该线程继续执行。
③连接线程join()
join( )方法使当前暂停执行,等待调用该方法的线程结束后再继续执行本线程。它有三种调用方法:
public final void join() throws InterruptedException
public final void join(long mills) throws InterruptedException
public final void join(long mills,int nanos) throws InterruptedException
等待调用该方法的线程结束,或者最多等待millis毫秒+nanos纳秒后,再继续执行本线程。
如果需要在一个线程中等待,直到另一个线程消失,可以调用join()方法。如果当前线程另一线程中断,join()方法会抛出InterruptedException异常。
④中断线程interrupt()
public void interrupt( )
public boolean isInterrupted( )
public static boolean interrupted( )
interrupt()方法为线程设置一个中断标记,以便于run()方法运行时使用isInterrupted()方法能够检测到,此时,线程在sleep()方法抛出一个Interr——uptedException异常,然后捕获这个异常以处理超时。
例. 改变线程状态。
本例演示线程对象的生命周期从创建到结束的过程,其间使用new()、start()、sleep()、interrupt()等方法改变线程的状态。本例综合运用内部类、图形界面、线程等多方面技术实现设计思想。程序如下:
import java.awt.*;
import java.awt.event.*;
public class Welcome extends WindowAdapter implements ActionListener
{
Frame f;
static Welcome.Thread3 wt1,wt2;
public static void main(String arg[])
{
Welcome w= new Welcome();
w.display();
wt1=w.new Thread3("Welcome!");
wt2=w.new Thread3("How are you?");
wt2.start();
wt2.setButton(); //设置按钮状态
}
public void display()
{
f = new Frame("Welcome");
f.setSize(400,240);
f.setLocation(200,140);
f.setBackground(Color.lightGray);
f.setLayout(new GridLayout(4,1));
f.addWindowListener(this);
f.setVisible(true);
}
public class Thread3 extends Thread
{
Panel p1;
Label lb1;
TextField tf1,tf2;
Button b1,b2;
int sleeptime = (int)(Math.random()*100);
public Thread3(String str)
{
super(str);
for(int i=0;i<100;i++)
str = str + " ";
tf1 = new TextField(str);
f.add(tf1);
p1 = new Panel();
p1.setLayout(new FlowLayout(FlowLayout.LEFT));
lb1 = new Label("sleep");
tf2 = new TextField(""+sleeptime);
p1.add(lb1);
p1.add(tf2);
b1 = new Button("启动");
b2 = new Button("中断");
p1.add(b1);
p1.add(b2);
b1.addActionListener(new Welcome());
b2.addActionListener(new Welcome());
f.add(p1);
f.setVisible(true);
}
public void run()
{
String str;
while (this.isAlive() && !this.isInterrupted())
{ //线程活动且没中断时
try
{
str = tf1.getText();
str = str.substring(1)+ str.substring(0,1);
tf1.setText(str);
this.sleep(sleeptime);
}
catch(InterruptedException e)
{ //中断时抛出
System.out.println(e);
break; //退出循环
}
}
}
public void setButton() //设置按钮状态
{
if (this.isAlive()) b1.setEnabled(false);
if (this.isInterrupted()) b2.setEnabled(false);
}
}//线程
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
public void actionPerformed(ActionEvent e)
{ //单击按钮时触发
if ((e.getSource()==wt1.b1) || (e.getSource()==wt1.b2))
actionPerformed(e,wt1);
if ((e.getSource()==wt2.b1) || (e.getSource()==wt2.b2))
actionPerformed(e,wt2);
}
public void actionPerformed(ActionEvent e,Thread3 wt1)
{ //重载
if(e.getSource()==wt1.b1) //启动
{
wt1.sleeptime=Integer.parseInt(wt1.tf2.getText());
wt1.start();
}
if(e.getSource()==wt1.b2) //中断
wt1.interrupt();
wt1.setButton(); //设置按钮状态
}
}
四、线程的同步机制
前面所提到的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其它线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization)。像你所看到的,Java为此提供了独特的,语言水平上的支持。同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程。
如果你用其他语言例如C或C++时用到过同步,你会知道它用起来有一点诡异。这是因为很多语言它们自己不支持同步。相反,对同步线程,程序必须利用操作系统源语。幸运的是Java通过语言元素实现同步,大多数的与同步相关的复杂性都被消除。
1.共享数据的线程“互斥”锁定
1.1线程间的数据共享
为解决操作的不完整性问题,在Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
例. 银行帐户的存取款线程设计。
本例设计三个类,银行帐户类Account1、存款线程类Save1和取款线程类Fetch1。
程序如下:
class Account1 //帐户缓冲区
{
private String name;
private int value;
void put(int i) //欲存入金额i
{
value = value + i; //存入时,value值增加
}
int get(int i) //欲取金额i,返回实际取到金额
{
if (value>i)
value = value - i; //取走时,value值减少
else //帐户金额不够所取时
{
i = value;
value = 0; //取走全部所余金额
}
return i;
}
int howmatch() //查看帐户上现有金额
{
return value;
}
}
class Save1 extends Thread //存款线程
{
private Account1 a1;
private int amount;
public Save1(Account1 a1,int amount)
{
this.a1 = a1;
this.amount = amount;
}
public void run()
{
int k = a1.howmatch();
try
{
sleep(1); //花费时间
}
catch(InterruptedException e)
{
System.out.println(e);
}
a1.put(amount);
System.out.println("现有"+k+", 存入"+amount+
", 余额"+a1.howmatch());
}
public static void main (String args[])
{
Account1 a1 = new Account1();
(new Save1(a1,100)).start();
(new Save1(a1,200)).start();
(new Fetch1(a1,500)).start();
}
}
class Fetch1 extends Thread //取款线程
{
private Account1 a1;
private int amount;
public Fetch1(Account1 a1,int amount)
{
this.a1 = a1 ;
this.amount = amount;
}
public void run()
{
int k = a1.howmatch();
try
{
sleep(1); //花费时间
}
catch(InterruptedException e)
{
System.out.println(e);
}
System.out.println("现有"+k+", 取走"+a1.get(amount)+
", 余额"+a1.howmatch());
}
}
程序运行结果:
现有0, 存入100, 余额100
现有0, 存入200, 余额300
现有0, 取走300, 余额0
1.2 Synchronized
Synchronized锁定一段代码,称为创建一个代码临界区,使得线程必须等候特定资源的所有权。
当第一个线程执行这段代码时,它获取特定的对象的所有权,即拥有该对象的锁。此时,如果有第二个线程对同一个对象也要执行这段代码时,它试图获取该对象的所有权,但因该对象已被锁定,则第二个线程必须等待,直到锁被释放为止。第一个线程执行完<语句>后,自动释放了锁,接下去第二个线程获得锁并可运行。这样就形成了多个线程对同一个对象的“互斥”使用方式,该对象称为“同步对象”。
这种锁定方式是针对某个特定对象而言的。如果有两个线程同时对两个不同的对象进行操作,则没有锁定,它们可以同时进入代码的临界区
例. 带锁定的存取款线程设计。
class Save2 extends Thread //存款线程
{
private Account1 a1;
private int amount;
public Save2(Account1 a1,int amount)
{
this.a1 = a1;
this.amount = amount;
}
public void run()
{
synchronized (a1) //锁定帐户对象
{
int k = a1.howmatch();
try
{
sleep(1); //花费时间
}
catch(InterruptedException e)
{
System.out.println(e);
}
a1.put(amount);
System.out.println("现有"+k+", 存入"+amount+
", 余额"+a1.howmatch());
}
}
public static void main (String args[])
{
Account1 a1 = new Account1();
(new Save2(a1,100)).start();
(new Save2(a1,200)).start();
(new Fetch2(a1,500)).start();
}
}
class Fetch2 extends Thread //取款线程
{
private Account1 a1;
private int amount;
public Fetch2(Account1 a1,int amount)
{
this.a1 = a1 ;
this.amount = amount;
}
public void run()
{
synchronized (a1) //锁定帐户对象
{
int k = a1.howmatch();
try
{
sleep(1); //花费时间
}
catch(InterruptedException e)
{
System.out.println(e);
}
System.out.println("现有"+k+", 取走"+a1.get(amount)+
", 余额"+a1.howmatch());
}
}
}
程序运行结果:
现有0, 存入100, 余额100
现有100, 存入200, 余额300
现有300, 取走300, 余额0
2.传送数据的线程同步运行
2.1线程间传送数据
如果多个线程同时运行,而且相互间需要传送数据,则必须使线程运行的步调一致,才能保证传送的数据及时准确收到,这称为线程同步问题。
2.2 synchronized与“互斥锁”标志
如果将Buffer1类中的put和get方法声明为“互斥”方法:
synchronized void put(int){ }
synchronized int get(){}
则意味着任一时刻只能有一个线程访问put或get方法。但可以有两个线程同时分别访问put和get方法。所以必须增加称为“互斥锁标志”(lock flag)的信号量。这样在任一时刻只有一个线程可以更改共享数据,从而保证数据的完整性和一致性。
3.线程间通讯
考虑经典的排队问题,其中一个线程正在生产一些数据,另一个是消费它。为了使问题更有趣,假设生产者必须等待,直到它会产生更多的数据消费完毕之前。在一个轮询系统,消费者会浪费大量的CPU周期,而它等待着生产者生产。一旦生产结束了,就开始轮询,浪费更多的CPU周期等待消费者完成,依此类推。显然,这种情况是不希望的。
为了避免轮询,Java包括通过下面的方法优雅的进程间通信机制:
wait( ): 这个方法告诉调用线程放弃监视器和进入睡眠状态,直到其他线程进入同一监视器和调用notify()。
notify( ): 这种方法唤醒第一个线程调用wait()在同一个对象上。
notifyAll( ): 这种方法唤醒所有调用wait()的同一个对象上的线程。最高优先级的线程将首先运行。
这些方法被实现为final的方法在Object,因此所有的类都有它们。这三种方法都只能从一个同步的上下文中被调用。这些方法的对象中声明。各种形式的wait( ) 存在,使可以指定一段时间等待。
例子:
下面的示例程序包括四个类:Q,想同步队列,Producer,也就是生产队列的条目线程对象;Consumer,即消耗队列的条目线程对象和PC,tiny类创建一个Q,生产者和消费者。
写这个程序在Java中正确的方法是使用wait()和notify()方法,以在两个方向的信号,如下所示:
class Q {
int n;
boolean valueSet = false;
synchronized int get() {
if(!valueSet)
try {
wait();
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
System.out.println("Got: " + n);
valueSet = false;
notify();
return n;
}
synchronized void put(int n) {
if(valueSet)
try {
wait();
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
this.n = n;
valueSet = true;
System.out.println("Put: " + n);
notify();
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
new Thread(this, "Producer").start();
}
public void run() {
int i = 0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
new Thread(this, "Consumer").start();
}
public void run() {
while(true) {
q.get();
}
}
}
public class PCFixed {
public static void main(String args[]) {
Q q = new Q();
new Producer(q);
new Consumer(q);
System.out.println("Press Control-C to stop.");
}
}
内部的get(),wait()调用。这将导致其执行暂停,直到生产者通知有些数据已准备就绪。
当发生这种情况,执行里面的get( ) 恢复。该数据已获得后,get() 调用notify()。这告诉生产者,这都是可以的把更多的数据在队列中。
内部的put(),wait()暂停执行,直到消费者已经从队列中删除的项目。当恢复执行,数据的下一个项目被放入队列中,notify()被调用。这就告诉消费者,它现在应该将其删除。
下面是这个程序,它显示了干净的同步行为的一些输出:
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
4.死锁问题
多线程在使用互斥机制实现同步的同时,存在“死锁”的潜在危险。如果多个线程都处于等待状态而无法被唤醒时,就构成死锁(deallock)此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自己的资源,由于系统资源有限程序停止运行。
死锁是很难调试的错误,因为:
· 通常,它极少发生,只有到两线程的时间段刚好符合时才能发生。
· 它可能包含多于两个的线程和同步对象(也就是说,死锁在比刚讲述的例子有更多复杂的事件序列的时候可以发生)。
为充分理解死锁,观察它的行为是很有用的。下面的例子生成了两个类,A和B,分别有 foo( )和bar( )方法。这两种方法在调用其他类的方法前有一个短暂的停顿。主类,名为Deadlock,创建了A和B的实例,然后启动第二个线 程去设置死锁环境。foo( )和bar( )方法使用sleep( )强迫死锁现象发生。
// An example of deadlock.
class A {
synchronized void foo(B b) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered A.foo");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("A Interrupted");
}
System.out.println(name + " trying to call B.last()");
b.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class B {
synchronized void bar(A a) {
String name = Thread.currentThread().getName();
System.out.println(name + " entered B.bar");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("B Interrupted");
}
System.out.println(name + " trying to call A.last()");
a.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class Deadlock implements Runnable {
A a = new A();
B b = new B();
Deadlock() {
Thread.currentThread().setName("MainThread");
Thread t = new Thread(this, "RacingThread");
t.start();
a.foo(b); // get lock on a in this thread.
System.out.println("Back in main thread");
}
public void run() {
b.bar(a); // get lock on b in other thread.
System.out.println("Back in other thread");
}
public static void main(String args[]) {
new Deadlock();
}
}
运行程序后,输出如下:
MainThread entered A.foo
RacingThread entered B.bar
MainThread trying to call B.last()
RacingThread trying to call A.last()
因为程序死锁,你需要按CTRL-C来结束程序。在PC机上按CTRL- BREAK(或在Solaris下按CTRL-/)你可以看到全线程和管程缓冲堆。你会看到RacingThread在等待管程a时占用管程b,同 时,MainThread占用a等待b。该程序永远都不会结束。像该例阐明的,你的多线程程序经常被锁定,死锁是你首先应检查的问题。Java技术即不能发现死锁也不能避免死锁。所以程序员编程时应注意死锁问题,尽量避免。避免死锁的有效方法是:
①线程因为某个条件未满足而受阻,不能让其继续占有资源
②如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁