您的位置:首页 > 其它

线程锁与等待通知机制

2014-10-22 17:43 344 查看

synchronized关键词:

在一个多线程程序中,多个线程通过共同协作来完成指定任务。在协作过程中,各个线程通过共享内存的方式来进行通信。一般CPU采用时间片轮转等不同算法来对线程进行调度,这个情况下,一个线程所做的操作对于其它线程并不一定可见,来看代码:

public class IdGenerator {
private int value = 0;
public int getNext() {
return value++;
}
}


上面这一段代码对于一个单线程程序来说,每次对getNext方法的调用都可以保证得到不重复的值。而对于多线程程序来说,却不能保证每次取得的值都不一样。因为上面这一段代码中至少存在取值和赋值两个操作,再具体到cpu指令的话应该会有更多个指令序列,它并不是一个原子操作。

要解决这个问题,我们可以使用关键词synchronized对其添加线程锁:

public class IdGenerator {
private int value = 0;
public synchronized int getNext() {
return value++;
}

public int getNext2() {
synchronized(this) {
return value++;
}
}
}


synchronized关键词可以添加在方法或代码块之上,关键词内包含的代码块在同一时刻只允许有一个线程访问。对于声明为synchronized的方法,静态方法对应的监视器对象是所在Java类对应的Class类的对象所关联的监视器对象,而实例方法使用的是当前对象实例所关联的监视器对象。对于synchronized代码块,对应的监视器对象是synchronized代码块声明中的对象所关联的监视器对象。

volatile关键词:

在Java中,对于long型和double型64位的域的读取和写入操作也不是原子操作,Java一次只能操作32位的数据也就是说,在读取或者写入long和double型的时候也可能会被其它线程所打断。因此在多线程程序中使用long型和double型的共享变量时,需要把变量声明为volatile,以保证读取和写入操作的完整性。如:

volatile double d = 0;


将变量声明为volatile相当于为单个变量的读取和写入添加了同步操作。但是volatile在使用时不需要利用锁机制,因此性能上要优于synchronized关键词。但是在上面的IdGenerator类中,如果只是把value声明为volatile,这样是不够的,因为写入的value的正确值依赖于value的当前值。

等待-通知机制

Object类中有wait、notify和notifyAll方法:

package java.lang;
public class Object {
...
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}

wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
...
}


在多线程程序中,单个线程可能会需要满足某些逻辑条件才能继续运行。当线程所要求的条件不满足时,线程进入等待状态,等待由于其他线程的运行而使条件得到满足;其他线程则负责在合适的时机发出通知来唤醒处于等待状态的线程。对于这种场景,可以使用while循环和volatile变量来处理。但是这种做法的本质是让线程处于忙等待的状态,并通过轮询的方式来判断条件是否满足。处于忙等待的线程仍然占用CPU时间,对性能造成影响。更好的做法是使用Object类提供的wait、notify和notifyAll方法:

package com.multithread;

public class maintest {

static private class Lock {}
private static Lock lock = new Lock();

public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
synchronized(lock) {
while(true) {
System.out.println("我是R");
try {
lock.wait();
} catch (InterruptedException e) {}
System.out.println("我是R wait之后的代码段");
}
}
}
};
Thread t = new Thread(r);
t.start();

Runnable r1 = new Runnable() {
@Override
public void run() {
synchronized(lock) {
while(true) {
System.out.println("我是R1");
try {
lock.wait();
} catch (InterruptedException e) {}
System.out.println("我是R1 wait之后的代码段");
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Thread.currentThread().sleep(1000);
System.out.println("----------------------notify-------------------------");
synchronized(lock) {
System.out.println("获得lock锁");
lock.notify();
}
Thread.currentThread().sleep(1000);
System.out.println("---------------------notifyAll-----------------------");
synchronized(lock) {
System.out.println("获得lock锁");
lock.notifyAll();
}
}
}


运行该段代码,显示结果如下:

我是R
我是R1
----------------------notify-------------------------
获得lock锁
我是R wait之后的代码段
我是R
---------------------notifyAll-----------------------
获得lock锁
我是R wait之后的代码段
我是R
我是R1 wait之后的代码段
我是R1


成功调用wait方法的先决条件是当前线程获取到监视器对象上的锁。如果没有锁,则抛出java.lang.IllegalMonitorStateException异常;如果有锁,那么当前线程被添加到对象所关联的等待集合中,并释放其持有的监视器对象上的锁。当前线程被阻塞,无法继续执行,直到被从对象所关联的等待集合中移除。

对应的notify和notifyAll方法用来通知线程离开等待状态。调用一个对象的notify方法会从该对象关联的等待集合中选择一个线程来唤醒。如果等待集合中有多个线程,具体选择哪个线程唤醒由虚拟机实现来决定,不能保证唤醒的顺序和开发者所预计的一样。而notifyAll则是唤醒等待集合中的所有线程,所以当等待集合中包含多个线程的话,使用notifyAll可以保证程序的正确性,代价是会造成一定的性能影响。

书上只说了wait方法的成功调用需要当前线程持有监视器对象上的锁,因此wait方法的调用需要放在使用synchronized关键词声明的方法或代码块中。但是博主实际测试的结果是notify和notifyAll也必须放到synchronized中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: