java 多线程的等待通知

 一.等待和通知机制

        等待和通知机制其实就是同步机制,帮助线程之间通信,让一个线程通知另外一个线程某种特定的条件发生了。java 多线程中的等待唤醒,有两种实现方法

       ①通过wait和notify,notifyAll方法来配合完成的

       通过线程锁(ReentrantLock)、线程通信状态(Condition)

二.synchronized、wait和notify

synchronized

用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,它获得了对象锁, 其它线程对该对象所有同步代码部分的访问都被暂时阻塞。    

void wait()

等待某种条件的发生。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。

void wait(long timeout)

等待一个条件的发生。但是,如果在设定的时间内没有收到通知,就返回。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。

void notify()

通知线程其等待的条件已经发生了。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。

wait() / notify() 都是Object类的方法。每一个Java对象都是直接或间接继承Object类,所以任何Java对象都支持这种机制。所以等待和通知机制存在于Java系统中每一个对象里。

        等待和通知机制并不指定该条件到底是什么。事实上,一般情况下也不需要指定。程序员决定一个线程在执行到代码的某处时在某个对象上调用wait()方法进行等待,另一个线程执行一段代码后认为条件满足了,则在同一个对象上调用notify()方法,通知等待该对象的线程。

        当我们使用wait(),notify(),notifyAll()方法时,很容易出现java.lang.IllegalMonitorStateException异常。这个异常的原因就是当前线程没有获得对象的锁,或者说,当前调用的对象不是锁对象。所以我们必须明白调用wait(),notify(),notifyAll()的对象必须是当前锁对象。实例程序如下:

import java.util.Random;

public class SynchronizedTest implements Runnable {
    private Student stu;
    private boolean flag = true; 
    public SynchronizedTest(Student student){
        stu = student;
    } 
    public synchronized void setName() throws InterruptedException{
        System.out.println(Thread.currentThread().getName() + " get into setName().");
        if(flag == true){
            flag = false;
            this.wait();          
        }
        stu.setName("宜一一" + new Random().nextInt(100));
        if(flag == false){
            Thread.sleep(5000);
        }
        System.out.println(Thread.currentThread().getName() + " set success,the name is reset to:" + stu.getName());
        notify();
    }
    public void setGrade(){
        synchronized (this) {
            stu.setGrade("11");
        }        
    }  
    public void setAge(){
        synchronized (stu) {
            stu.setAge(11);
        }
    }

    @Override
    public void run() {  
        try {
            Thread.sleep(200);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try {
            setName();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

public class SynchronizedMainTest {

    public static void main(String[] args) {
        Student s1 = new Student("0","0","0",0);
        Student s2 = new Student("00","00","00",10);
        SynchronizedTest st1 = new SynchronizedTest(s1);
        SynchronizedTest st2 = new SynchronizedTest(s2);
        
        //当runnable task是同一个对象时,那些同步方法才起作用,synchronized关键字修饰的方法本质上是获得该对象的对象锁。
        Thread t1 = new Thread(st1);
        Thread t2 = new Thread(st1);
        t1.start();
        
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

其运行结果如下:

Thread-0 get into setName().
Thread-1 get into setName().
Thread-1 set success,the name is reset to:宜一一22
Thread-0 set success,the name is reset to:宜一一5

Student类作为一个资源类属性如下, getter和setter方法省略 :

public class Student {

        private String name;
        private String stuId;
        private String grade;
        private int age;

可以看到,在 setName()  方法里面,首先打印 System.out.println(Thread.currentThread().getName() + " get into setName().");  之后修改 SynchronizedTest  对象的标志位flag,进入wait状态,代码如下:       

 if(flag == true){
            flag = false;
            this.wait();          
        }

与Thread.sleep()方法不同的是,调用this.wait()将放弃该线程持有的对象锁,其他在等待该对象锁的线程可以进入到该同步方法内,此时标志位已经修改,所以其他线程可以继续向下执行,当线程执行到 最后已经正确修改Student对象的名字时,调用了 notify()方法来通知其他还在等待该对象锁的线程对象,也就是一开始就放弃对象锁的那个线程。之前的线程被唤醒后继续之前没有做完的工作,修改Student对象的名字后退出同步方法区。

         值得一提的是在使用wait()和notify()方法时,必须是在获得该对象的对象锁的时候使用,之前有将this.wait()修改为Thread.currentThread().wait()时,程序输出结果如下:

Thread-0 get into setName().
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:503)
    at com.thread.locktest.SynchronizedTest.setName(SynchronizedTest.java:19)
    at com.thread.locktest.SynchronizedTest.run(SynchronizedTest.java:52)
    at java.lang.Thread.run(Thread.java:745)
Thread-1 get into setName().
Thread-1 set success,the name is reset to:宜一一44

所以在使用wait()和notify()方法时,一定要清楚当前线程持有的是哪一个对象的对象锁。在 setAge() 里获得的是Student对象锁,所以初始化 SynchronizedTest  对象的时候,如果传入的是同一个Student对象时,不同的线程对象进入 setAge()  方法也必须等待Student的对象锁。

三.线程锁(ReentrantLock)
        多线程并发索取某一资源,要求该资源线程安全(即线程同步),每一线程在使用资源时候会需要检查资源状态,如果状态不符合立即通知资源的维护服务进行维护,维护完毕后发布通知表示该资源可以继续被索取使用
        所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作。

3.1什么是reentrantlock

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多 线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

reentrant 锁意味着什么呢?简 单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

3.2 ReentrantLock与synchronized的比较

相同:ReentrantLock提供了synchronized类似的功能和内存语义。

不同:

(1)ReentrantLock 功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合 适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个 Condition实例,所以更有扩展性。

(2)ReentrantLock 的性能比synchronized会好点。

(3)ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

3.3 ReentrantLock扩展的功能

3.3.1 实现可轮询的锁请求 

在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误恢复机制,可以规避死锁的发生。 
如果你不能获得所有需要的锁,那么使用可轮询的获取方式 使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状 态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下:

  1. Lock lock = ...;   
  2. if (lock.tryLock()) {   
  3. try {   
  4. // manipulate protected state   
  5. } finally {   
  6. lock.unlock();   
  7. }   
  8. } else {   
  9. // perform alternative actions   
  10. }   
3.3.2 实现可定时的锁请求 

当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活 
动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。 

3.3.3 实现可中断的锁获取请求 

可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

3.4 ReentrantLock不好与需要注意的地方

(1) lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放

(2) 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

       Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能.Lock对象的使用如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {

    private static Lock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        
        Thread t1 = new Thread(new Runnable() {
             
            @Override
            public void run() {    
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " has got the lock." );
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has done the job." );
                lock.unlock();
           }
        });
         
        Thread t2 = new Thread(new Runnable() {        
            @Override
            public void run() {          
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " has got the lock." );
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " has done the job." );
                lock.unlock();
            }
        });     
        t2.start();
        t1.start();
    }

}

运行结果如下:

Thread-0 has got the lock.
Thread-0 has done the job.
Thread-1 has got the lock.
Thread-1 has done the job.

 

转载于:https://my.oschina.net/cain1507/blog/735881

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值