五、JAVA多线程:线程间通信(wait、notify、notifyAll、wait set、自定义锁 BooleanLock )

       我们在开发多线程程序的时候,往往不会只存在一个独立的线程,相反大多数情况下是需要多个线程之间进行协同工作的,如何在多个线程之间进行通信,是本章学习的重点。另外,本章的最后部分将会分析synchronized关键字的缺陷,我们手动实现了一个显式锁(BooleanLock)可以解决synchronized所不具备的功能,其中也需要用到线程间通信的知识。

同步阻塞与异步非阻塞

 

异步非阻塞消息处理

单线程间通讯

初识wait和notify

代码样例:

import java.util.LinkedList ;
import static java.lang.Thread.currentThread ;


public class EventQueue {
    private final int max ;

    static  class Event {
        int i ;

        public Event(int i){
            this.i = i ;
        }


    }

    private final LinkedList<Event> eventQueue = new LinkedList<>();

    private final static int DEFAULT_MAX_EVENT = 10 ;


    public EventQueue(){
        this(DEFAULT_MAX_EVENT);
    }

    public EventQueue (int max){
        this.max = max;
    }

    public void offer(Event event){
        synchronized (eventQueue){
          if(eventQueue.size() >= max ){
              try{
                  console("the queue is full ...");
                  eventQueue.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }

          }

          console("the new event is submitted ");
          eventQueue.addLast(event);
          eventQueue.notify();

        }
    }

    public Event take(){
        synchronized (eventQueue){
            if(eventQueue.isEmpty()){

                try {
                    console("the queue is empty . ");
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

            Event event = eventQueue.removeFirst();
            this.eventQueue.notify();
            console("the event " + event.i + "  is handled . ");

            return event ;
        }

    }


    private void console(String message){
        System.out.printf("%s:%s \n", currentThread().getName() , message);
    }
}
import java.util.concurrent.TimeUnit;

public class EventClient {
    public static void main(String[] args) {

        final EventQueue eventQueue = new EventQueue() ;
        new Thread(() -> {
            for (int i = 0 ; i < 100000; i++){

                eventQueue.offer(new EventQueue.Event(i));
            }


        },"Producer"  ).start();


        new Thread(() -> {
            for (;;){
                eventQueue.take();

                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }

        },"Consumer " ).start(); ;


    }


}

运行截图:

wait和notify方法详解

wait和notify方法并不是Thread特有的方法,而是Object中的方法。也就是说JDK中每一个类都拥有这两个方法。

先看wait的三个重载方法:

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout,  int nanos) throws InterruptedException

说明:

1.wait方法这三个重载方法都将调用wait(long timeout)方法 , wait()方法 等价于 wait(0), 0 代表永不超时。

2.Object的wait(long timeout)方法会导致当前线程进入阻塞,知道有其他线程调用了Object的nofify或者notifyAll方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。

3.wait方法必须拥有该对象的monitor,也就是waite方法必须在同步方法中使用。

4.当前线程执行了该对象的wai方法之后,将会放弃对该monitor的所有权,并进进入到与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,他就会释放对该对象的monitor的所有权, 其他线程也会有机会继续争抢该monitor的所有权。

notify解析

public final native void notify();

唤醒单个正在执行该对象的wait方法的线程

如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。

被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行

关于wait和notify的注意事项

1.wait方法是可中断方法,一旦调用了wait方法案进入了阻塞状态, 其他线程可以使用interrupt方法将其打断。

可中断方法被打断后会受到中断异常InterruptedException,同时interrupt标识也会被擦除。

2.线程执行了某个对象的wait方法之后,会加入与之对应的wait set中, 每一个对象的monitor都会有一个与之关联的wait set

3.当前程进入wait set之后, notify方法可以将其进行唤醒, 也就是从wait set中弹出,同时,中断wait中的线程,也就将其唤醒。

4.必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法monitor的所有权。

wait和sleep

1.wait和sleep方法都可以使线程进入阻塞状态。

2.wait和sleep方法都是可中断方法,被中断之后,都会受到中断异常

3.wait是Object的方法, sleep是Thread中的特有方法

4.wait方法的执行必须要同步方法中进行,则sleep不需要

5.线程在同步方法中执行sleep方法是,并不释放monitor锁,而wait方法则会释放

6.sleep方法短暂休眠后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断之后才能退出阻塞

多线程间通讯

生产者和消费者

1.notifyAll方法

多线程间通讯需要用到Object的notifyAll方法,该方法与notify比较相似,都可以唤醒由于调用了wait方法而阻塞的线程,但是notify方法只能唤醒其中的一个线程,而notifyAll方法则可以同时唤醒全部的阻塞线程,同时被唤醒的线程仍然需要继续争抢monitor的锁。

代码样例:

package com.zl.step5.eventQueue2;

import java.util.LinkedList;

import static java.lang.Thread.currentThread;


public class EventQueue {
    private final int max ;

    static  class Event {


    }

    private final LinkedList<Event> eventQueue = new LinkedList<>();

    private final static int DEFAULT_MAX_EVENT = 10 ;


    public EventQueue(){
        this(DEFAULT_MAX_EVENT);
    }

    public EventQueue (int max){
        this.max = max;
    }

    public void offer(Event event){
        synchronized (eventQueue){
          while(eventQueue.size() >= max ){
              try{
                  console("the queue is full ...");
                  eventQueue.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }

          }

          console("the new event is submitted :" +event);
          eventQueue.addLast(event);
          eventQueue.notifyAll();

        }
    }

    public Event take(){
        synchronized (eventQueue){
            while(eventQueue.isEmpty()){

                try {
                    console("the queue is empty . ");
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

            Event event = eventQueue.removeFirst();
            this.eventQueue.notifyAll();
            console("the event " + event + "  is handled . ");

            return event ;
        }

    }


    private void console(String message){
        System.out.printf("%s:%s \n", currentThread().getName() , message);
    }
}
package com.zl.step5.eventQueue2;

import java.util.concurrent.TimeUnit;

public class EventClient {
    public static void main(String[] args) {

        final EventQueue eventQueue = new EventQueue() ;

        new Thread(() -> {
            for (;;){

                eventQueue.offer(new EventQueue.Event());
            }


        },"Producer-1"  ).start();


        new Thread(() -> {
            for (;;){

                eventQueue.offer(new EventQueue.Event());
            }


        },"Producer-2"  ).start();

        new Thread(() -> {
            for (;;){
                eventQueue.take();

                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }

        },"Consumer " ).start(); ;


    }


}

线程休息室wait set

线程调用了某个对象的wait方法之后,都会被加入该对象的monitor关联的wait set中, 并且释放monitor的所有权

nofify方法:

notifyAll方法

自定义显示锁BooleanLock

synchronized关键字的缺陷

synchronized关键字给提供了一种排他式的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞,

有两个明显的缺陷:

1.无法控制阻塞时长

2.阻塞不可被中断

测试代码:

package com.zl.step5;

import java.util.concurrent.TimeUnit;

public class SynchronizedDefect {

    public synchronized void syncMethod(){

        try {
            System.out.printf("Thread [ %s ] is running .....\n ", Thread.currentThread().getName());
            TimeUnit.HOURS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDefect defect = new SynchronizedDefect();

        Thread t1 = new Thread(defect::syncMethod,"T1");

        t1.start();

        TimeUnit.MILLISECONDS.sleep(2);


        Thread t2 = new Thread(defect::syncMethod,"T2");
        t2.start();


    }



}

package com.zl.step5;

import java.util.concurrent.TimeUnit;

public class SynchronizedDefect2 {

    public synchronized void syncMethod(){

        try {
            System.out.printf("Thread [ %s ] is running .....\n ", Thread.currentThread().getName());
            TimeUnit.HOURS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDefect2 defect = new SynchronizedDefect2();

        Thread t1 = new Thread(defect::syncMethod,"T1");

        t1.start();

        TimeUnit.MILLISECONDS.sleep(2);


        Thread t2 = new Thread(defect::syncMethod,"T2");
        t2.start();

        TimeUnit.MILLISECONDS.sleep(2);

        t2.interrupt();


        System.out.printf("t2 interrupted : %s  \n ", t2.isInterrupted());

        System.out.printf("t2 state : %s  \n ", t2.getState());

    }



}

t2 在竞争资源的时候,处于阻塞状态  ,是无法被打断的。

显示锁BooleanLock

使其具备synchronized关键字所有的功能,同时又具备可中断和lock超时的功能

1.定义Lock接口

import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * 1.lock()方法永远阻塞,除非获取到了锁,这一点和synchronized非常类似
 *   但是该方法是可以被中断的,中断的时候,抛出InterruptedException
 * 2.lock(long mills)  方法除了可以被中断以外,还增加了对应的超时功能
 * 3.unlock() 方法可用来释放锁
 * 4.getBlockedThreads() 用于获取当前有哪些线程被阻塞
 * 
 * 
 */
public interface Lock {

    void lock() throws InterruptedException;

    void lock(long mills) throws  InterruptedException, TimeoutException ;

    void unlock();

    List<Thread> getBlockedThreads();

}

* 1.lock()方法永远阻塞,除非获取到了锁,这一点和synchronized非常类似
* 但是该方法是可以被中断的,中断的时候,抛出InterruptedException
* 2.lock(long mills) 方法除了可以被中断以外,还增加了对应的超时功能
* 3.unlock() 方法可用来释放锁
* 4.getBlockedThreads() 用于获取当前有哪些线程被阻塞

2.实现BooleanLock

BooleanLock是Lock的一个boolean实现,通过控制一个Boolean变量的开关来决定是否允许当前的线程获取该锁。

代码:

package com.zl.step5.booleanlock;

import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * 1.lock()方法永远阻塞,除非获取到了锁,这一点和synchronized非常类似
 *   但是该方法是可以被中断的,中断的时候,抛出InterruptedException
 * 2.lock(long mills)  方法除了可以被中断以外,还增加了对应的超时功能
 * 3.unlock() 方法可用来释放锁
 * 4.getBlockedThreads() 用于获取当前有哪些线程被阻塞
 *
 *
 */
public interface Lock {

    void lock() ;

    void lock(long mills) throws  InterruptedException, TimeoutException ;

    void unlock();

    List<Thread> getBlockedThreads();

}
package com.zl.step5.booleanlock;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;

public class BooleanLock implements Lock {


    // 当前拥有锁的线程
    private Thread lockThread ;

    // ture 该锁已经被某个线程所获得
    // false代表该锁没有被任何线程获得或者已经被释放
    private boolean locked  = false ;

    // 用来存储哪些线程在获取当前线程时计入阻塞状态
    private final List<Thread> blockedList = new ArrayList<>();

    @Override
    public synchronized void lock()  {
//        System.out.println("lock 当前线程:"+currentThread()  );
//        System.out.println("lock 持有锁线程:"+lockThread );

        // 使用lock方法使用同步代码块的方式进行方法同步
        synchronized (this){
            // 如果当前锁已经被某个线程所获得,则该线程加入阻塞队列
            // 并且使当前线程wait释放对this monitor的所有权
            while (locked){
                final Thread tempThread = currentThread();
                try{

                    if(!blockedList.contains(currentThread())){
                        blockedList.add(currentThread());
                    }
                    this.wait();

                }catch (InterruptedException e){

                    blockedList.remove(tempThread) ;

                }

            }
            // 如果当前锁没有被其他线程所获得,则该线程尝试从阻塞队列中删除自己
            blockedList.remove(currentThread());
            // locked 开关指定为true
            this.locked = true ;
            //  记录获取锁的线程
            this.lockThread = currentThread() ;

        }

    }

    @Override
    public void lock(long mills) throws InterruptedException, TimeoutException {
        synchronized (this){

            // 如果mills 不合法, 则默认调用lock方法 ,当然也可以抛出参数非法异常
            if(mills <= 0){
                this.lock();
            }else{
                long remainingMills = mills ;
                long endMills = currentTimeMillis() + remainingMills ;
                while(locked){
                    //如果remainingMills 小于等于0 ,
                    // 则意味着当前线程被其他线程唤醒或者在指定的wait时间到了之后,
                    // 还没有获得锁,则抛出超时异常
                    if(remainingMills <= 0){
                        throw new TimeoutException("can not get  the lock during " + mills) ;
                    }

                    if(!blockedList.contains(currentThread())){
                        blockedList.add(currentThread());
                    }
                    // 调用wait
                    this.wait(remainingMills);

                    // 重新计算remainingMills 时间,因为该线程有可能会被唤醒
                    remainingMills = endMills - currentTimeMillis() ;
                }
                blockedList.remove(currentThread()) ;
                this.locked = true ;
                this.lockThread = currentThread() ;
            }
        }
    }

    @Override
    public  void unlock() {

//        System.out.println("unlock当前线程:"+currentThread()  );
//        System.out.println("unlock持有锁线程:"+lockThread );
        
        synchronized (this){
            //解锁, 那个线程加的锁,只能由该线程进行解锁
            if (lockThread == currentThread()){
                this.locked = false;
                Optional.of(currentThread().getName()+" release the lock." ).ifPresent(System.out::println);
                this.notifyAll();
            }else{
                System.out.println(currentThread() + " unlock fail ... ");
            }
        }

    }

    @Override
    public List<Thread> getBlockedThreads() {
        return Collections.unmodifiableList(blockedList);
    }
}
package com.zl.step5.booleanlock;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.IntStream;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.ThreadLocalRandom.current;

public class BooleanLockTest   {


    private final Lock lock = new BooleanLock();

    public  void syncMethod()  {

        try {
            lock.lock(10 * 1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

        try {

            int randomInt = current().nextInt(3) ;
            System.out.println(currentThread() + " get the lock , is running .... ");
            TimeUnit.SECONDS.sleep(randomInt);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }


    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("mian Thread : " + Thread.currentThread());

        BooleanLockTest blt = new BooleanLockTest();

        IntStream.range(0,10)
                .mapToObj(i -> new Thread(blt::syncMethod))
                .forEach(Thread::start);


//        new Thread(blt::syncMethod,"T1").start();
//
//        TimeUnit.MILLISECONDS.sleep(2);
//
//        Thread t2 =  new Thread(blt::syncMethod,"T2") ;
//
//        t2.start();;
//        TimeUnit.MILLISECONDS.sleep(2);
//        t2.interrupt();



    }



}

本文整理来源于:

《Java高并发编程详解:多线程与架构设计》 --汪文君

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/86006417