线程锁与等待通知机制

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中。

你可能感兴趣的:(synchronized,volatile,wait,线程锁,等待-通知机制)