有时,多个线程需要共享一个对象。如果多个线程同时访问同一个对象,必然会产生对于共享数据的访问冲突。例如一个线程在更新该对象的同时,另一个线程也试图更新或读取该对象,这样将破坏数据的一致性。为避免多个线程同时访问一个共享对象,这里引入同步的概念。
在某个时刻只允许一个线程独占性访问该共享对象,而其他线程只能处于阻塞状态;只有该当线程访问操作结束后,才允许其他线程访问。这称为相互排斥或线程同步。
Java使用synchronized关键字控制对共享信息的并发访问,实现线程同步。Synchron-
ized( )方法相当于一条封装了该方法的整个方法体的同步语句,例如:
Synchronized void method(){
//对共享对象的操作
}
在程序设计中,如果只想实现对某一段代码的同步,可以使用同步代码的方式。
void methodB() {
//Object obj=new Object();
synchronized(obj){
//对共享对象的操作
}
}
这种同步方式使程序运行速度和可读性都相对较差,但为处理线程同步提供了较灵活的方式。实际上对方法的synchronized 也是通过锁定对象实现的,相当于:
void methodA(){
synchronized(this){ //对共享对象的操作}
}
【例9-11】用synchronized 关键字实现代码段同步的示例。
public class BookShelf
{
String bookName;
int amount;
public BookShelf (String name, int amt)
{
bookName = name;
amount = amt;
}
public synchronized void putIn(int amt)
{
amount += amt;
}
public synchronized void withdraw(int amt)
{
amount -= amt;
}
public int checkRemainder ()
{
return amount;
}
}
Java使用监控器(monitor)来实现对共享数据操作的同步。共享对象都有一个监控器(对象锁)。监控器一次只允许一个线程执行对象的同步块。通过在程序进入同步块时就将对象锁住(获得锁),只有当同步块完成时,监控器才会打开该对象的锁(释放锁),让其他有最高优先级的阻塞线程处理它的同步块。如果一个程序内有两个或以上的方法使用synchronized 关键字,则它们在同一个“对象互斥锁”管理之下。
多线程间除了需要解决对共享数据操作的同步问题,还需要进行线程通信来协调线程间的运行进度问题。Java提供了java.lang.Object类的wait ( )和notify( )/notifyAll( )方法来协调线程间的运行进度(读取)关系。
线程已获得某个对象的锁,若该线程调用wait( )方法,则退出所占用的处理器,打开该对象的锁,转为阻塞状态,并允许其他同步语句获得对象锁。当执行条件满足后,将调用notify( )方法,唤醒这个处于阻塞的线程,线程转为可运行状态,又有机会获得该对象的锁。
如果一个线程通过调用wait( )方法进入对该共享对象的等待状态,应确保有一个独立的线程最终将会调用notify( )方法,以使等待共享对象的线程回到可运行状态。
【例9-12】wait( )与notify( )方法的例子。
public class WaitTest {
public static void main(String [] args) {
ThreadB b = new ThreadB();
b.start();
System.out.println("Total b is: " + b.getTotal());
}
}
class ThreadB extends Thread {
int total;
public void run() {
synchronized(this) {
for(int i=0;i<10000;i++) {
total += i;
}
System.out.println("In ThreadB total is: " + total);
notify();
}
}
synchronized public int getTotal() {
try{
wait();
}catch(InterruptedException e) {}
return total;
}
}
运行结果如下:
In ThreadB total is:
49995000
Total b is:49995000
例9-13是一个线程通信的实例,它说明了如何使用wait( )和notify( )方法来协调线程对共享数据的读写访问。
【例9-13】在本例中,用synchronized来同步对共享数据的读写,读写代码依赖于一个同步对象,需在其上执行wait( )和notify( )方法。如果线程在等待这个同步对象,就需要一个独立的类Queue表示共享对象,并在其中使用notify( )方法。
public class Queue {//共享数据结构——队列
protected Object[] data;
protected int writeIndex;
protected int readIndex;
protected int count;
public Queue(int size) {
data = new Object[size];
}
public synchronized void write(Object value) {//同步对共享数据的写操作
while(count >= data.length) {
try{
wait(); //阻塞,等待共享数据的同步读操作唤醒
}catch(InterruptedException e) {}
}
data[writeIndex++] = value;
System.out.println("write data is: " + value);
writeIndex %= data.length;
count += 1;
notify(); //唤醒处于阻塞状态的同步读操作
}
public synchronized void read() {//同步对共享数据的读操作
while(count <= 0){
try{
wait(); //阻塞,等待共享数据的同步写操作唤醒
}catch(InterruptedException e) {}
}
Object value = data[readIndex++];
System.out.println("read data is: " + value);
readIndex %= data.length;
count =-1;
notify(); //唤醒处于阻塞状态的同步读操作
}
public static void main(String[] args) {
Queue q = new Queue(5);
new Writer(q); //实例化并启动写线程
new Reader(q); //实例化并启动读线程
}
}
class Writer implements Runnable{//写线程
Queue queue;
Writer(Queue target){
queue = target;
new Thread(this).start();
}
public void run(){//线程体
int i = 0;
while(i<5){
queue.write(new Integer(i));
i++;
}
}
}
class Reader implements Runnable{//读线程
Queue queue;
Reader(Queue source){
queue = source;
new Thread(this).start();
}
public void run(){//线程体
int i=0;
while(i<5){
queue.read();
}
}
}
运行结果如下:
the end of main()
write data is: 0
write data is: 1
write data is: 2
write data is: 3
write data is: 4
read data is: 0
read data is: 1
read data is: 2
read data is: 3
read data is: 4
因此,有必要设计具有良好行为的线程,并使用wait()和notify()进行通信。注意:run()方法保证了在执行暂停或终止之前,共享数据处于一致的状态,这是非常重要的。不应这样来设计程序:随意地创建和处理线程,或创建无数个对话框线程和socket 端点。因为每个线程都会消耗一定的系统资源。
线程间因为相互等待对方的资源而不能继续执行的情况称为死锁。Java 线程的同步非常容易导致死锁现象,如例9-14所示。
【例9-14】程序示例。
Public class DeadlockRisk{
Private static class Resource{
Public int value;
}
private Resource resourceA-new Resource();
private Resource resourceB-new Resource();
public int read(){
synchronized(resourceA) {//这里出现死锁
sysnchronized(resourceB){
return resourceB.value+resourceA.value;}
}
}
public void write(int a, int b){
synchronized(resourceB){//这里出现死锁
synchronized(resourceA){
resourceA.value-a;
resourceB.value-b;}
}
}
}
持有一个锁并试图获取另一个锁时,就有死锁的危险。死锁是由资源的无序使用而带来的,应该在设计程序时尽量避免出现造成死锁的条件。
解决死锁问题的方案是:给资源排序。将所有的线程排列次序并始终遵照这个次序获取锁。例如,如果有3个资源A、B、C,并有一个线程要获得其中任何一个资源。线程1和线程2都必须确保它在获取B 的锁之前先获得A的锁,以此类推。释放锁时,按照与获取相反的次序。
协调两个需要存取公共数据的线程可能会变得非常复杂。需保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程等待这把锁的时候释放合适的锁,所以必须保证所编写的程序不发生死锁。