Java 线程间通信

Java 线程间通信

1 同步阻塞&异步阻塞

同步和异步是相对于执行结果来说,会不会等待结果返回;
阻塞和非阻塞是相对于线程是否被阻塞;
知乎
博客1
博客2

1.1 同步阻塞

一个业务逻辑需要等待上个线程完整的生命周期;
请求量大,程序同一时间受理的数量有限,也就是系统的整体吞吐量不高;
一个线程处理一个请求的方式,会导致频繁创建和销毁线程,会增加系统的额外开销;
业务达到峰值时,大量的业务处理线程阻塞会导致频繁的CPU上下文切换,从而降低系统性能;

1.2 异步阻塞

一个请求来之后会立刻得到一个结果,而系统有若干个业务处理线程,同时处理这个请求,处理结果可从工单等标记获取;
客户端不需要等到程序执行完毕,从而提高了系统的吞吐量和并发量;
服务端线程控制在一定范围内,并进行重复利用,可减少上线程下文切换造成的开销;

2 单个线程间通信

2.1 wait&notify

它们是Object的方法,wait使线程进入阻塞,会释放锁,notify将线程唤醒,仅仅只是通知,不释放锁;

  • wait方法和notify方法都是Object的方法
  • wait()和wait(long,int)最终掉的都是wait(long)这个本地方法
  • notify()和notifyAll()是本地方法
  • 线程A调对象Object的wait方法,线程A进入阻塞状态
  • 线程B调对象Object的notify方法,线程A被唤醒

在这里插入图片描述
wait:
使线程进入到竞争monitor锁的wait Set中,并处于阻塞状态
wait方法的三个重载方法都调用wait(long timeout)这个方法;
wait方法会导致线程进入阻塞状态,只有其他线程调用Object的notify或notifyAll方法或阻塞时间到达后,才可将其唤醒;
wait方法必须拥有该对象的monitor锁,也就是wait方法必须在同步方法中使用;
当前线程执行了该对象的wait方法后,就失去了该对象的monitor锁,并进入其wait set中;

notify:
唤醒竞争monitor锁而陷入阻塞状态的线程;
唤醒单个正在执行对象wait方法的线程;
如果有某个线程由于执行该对象wait方法而进入阻塞则会被唤醒,如果没有则忽略;
被唤醒的线程需要重新竞争该对象所关联的monitor锁才能继续执行;

通过案例熟悉wait&notify的用法,了解其对线程的影响:
事件队列有固定长度,其有两个方法,一个往队列中塞值,错过最大长度则不允许塞值了,一个从队列中取值,如果为空就等着:

/**
 * 事件队列
 */
public class EventQueue {
    private final int max;
    //内部类 只是用作队列中存储的对象	
    static class Event {

    }

    //定义一个事件链表List
    private final LinkedList<Event> eventQueue = new LinkedList<>();
    //最大10个时间
    private final static int DEFAULT_MAX_EVENT = 10;

    public EventQueue() {
        this(DEFAULT_MAX_EVENT);
    }

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

    /**
     * 往队列中添加Event
     *
     * @param event
     */
    public void offer(Event event) {
        //同步块
        synchronized (eventQueue) {
            //如果超过最大数量 则使当前线程陷入阻塞
            if (eventQueue.size() >= max) {
                try {
                    console("the queue is full");
                    //调用wait方法使当前线程陷入阻塞 并放置到eventQueue's waitSet
                    eventQueue.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            console("the new event is submmit");
            //未超过最大数量 在添加至最后
            eventQueue.addLast(event);
            //调用notify方法唤醒eventQueue's waitSet中的线程
            eventQueue.notify();
        }
    }

    /**
     * 从队列中取Event
     *
     * @return
     */
    public Event take() {
        synchronized (eventQueue) {
            //当队列中为空时 则将线程至为阻塞状态
            if (eventQueue.isEmpty()) {
                try {
                    console("the queue is empty");
                    //调用wait方法使当前线程陷入阻塞 并放置到eventQueue's waitSet
                    eventQueue.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            Event event = eventQueue.removeFirst();
            //调用notify方法唤醒eventQueue's waitSet中的线程
            eventQueue.notify();
            console("the  event " + event + "is handle");
            return event;
        }
    }

    private void console(String str) {
        System.out.printf("%s:%s\n", Thread.currentThread().getName(), str);
    }
}

事件队列客户端,启两个线程,一个不断地往队列中塞值,使其被塞满,一个不停地从队列中取值,只要有就取出来:

/**
 * 事件队列客户端
 */
public class EventClient {
    public static void main(String[] args) {
        final EventQueue eventQueue = new EventQueue();

        /**
         * 不停地往eventQueue中塞event
         */
        new Thread(() -> {
            while (true) {
                eventQueue.offer(new EventQueue.Event());
            }
        }, "Producer").start();


        /**
         * 每5s从eventQueue取出一个Event
         */
        new Thread(() -> {
            while (true) {
                eventQueue.take();

                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


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

    }
}

执行结果:

Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the queue is full
Consumer:the  event com.wxx.thread.waitnotify.EventQueue$Event@16187bcdis handle
Producer:the new event is submmit
Producer:the queue is full
Consumer:the  event com.wxx.thread.waitnotify.EventQueue$Event@18df977eis handle
Producer:the new event is submmit
...
...

分析:

  1. 线程Producer一启动,就不停地往eventQueue塞值,eventQueue不一会就被塞满;
  2. eventQueue被塞满后,eventQueue的wait方法被调用,Producer线程陷入阻塞,并被放置到eventQueue的waitSet中进行等待;
  3. 线程Consumer一启动,就不停地从eventQueue取值,取一个值,同时调用eventQueue的notify方法,使eventQueue的waitSet中等待的线程唤醒,唤醒后的线程重新竞争eventQueue的monitor锁,往eventQueue塞值;
  4. 线程Consumer调用notify后休眠5s,所以eventQueue永不会没有值
  5. 3-4的逻辑不停地重复执行

由以上现象和分析得出如下结论:

  1. nodify唤醒单个正在执行该对象wati方法的线程
  2. 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则忽略
  3. 被唤醒的线程需要重新获取对该对象关联的monitor锁才能继续执行

2.2 wait&notify注意事项

  1. wait是可中断方法,当前线程一旦调用wait方法进入阻塞状态,其他线程调用Intterput方法可将其打断,被打断后会跑出InterruptedException;
  2. 线程执行了某个对象的wait方法后,会加入与之对应的wait set中,每个对象的monitor 锁都有一个与之关联的set;
  3. 当线程进入monitor set后,notify方法可以将其唤醒,也就是从wait set中将其弹出;
  4. 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提是必须持有同步方法的monitor锁;
  5. 同步代码的monitor必须与执行wait notify方法的对象一致;
    在这里插入图片描述

2.3 wait&sleep

  • wait和sleep方法都可以使线程进入阻塞状态;
  • wait和sleep方法都是可中断方法,中断后收到中断异常;
  • wait是Object的方法,sleep是Thread的方法;
  • wait方法必须执行在同步方法中,而sleep不需要;
  • 同步方法中执行sleep不会释放monitor锁,而wait会;

3 多线程间通信

前边讲述的是两个线程间的通信,如果在前边EventQueue案例中同时启多个线程进行offer和take操作,那就是多线程间的通信问题了,需要对其进行修改,因为eventQueue的wait set中可以存有多个等待的线程,与单线程间通信的区别就是notify和notifyAll的区别;

3.1 notifyAll方法

与notify类似,notify是每次唤醒一个线程,notifyAll可同时唤醒全部的阻塞线程,被唤醒后仍需要继续争抢关联对象的monitor锁,刚才的程序如果客户端有多个程序在执行take和offer方法时,会出现问题,这时需要将notify换成notifyAll;

4 线程休息室wait set

在虚拟机规范中存在一个wait set的概念,至于该wait set是怎样的数据结构,JDK官方没有给出明确定义,不同厂家JDK有着不同的实现,甚至相同的JDK厂家也存在着差异,anyway,线程调用了某个对象的wait方法之后都会被加入与该对象monitor关联的wait set中,并且释放monitor的所有权;

若干个 线程调用wait方法后,被加入到了与monitor关联的wait set中

notify:另外一个线程调用该monitor的notify方法后,其中一个线程会从wait set中弹出,至于是随机弹出还是以先进先出的方式弹出,虚拟机规范同样没有强制要求;
notifyAll:另外一个线程调用该monitor的notifyAll方法后,wait set中的所有线程都会被弹出;

5 自定义显示锁

5.1 synchronized的缺陷

synchronized关键字提供了一种排他的数据同步机制,某个线程在获取monitor lock的时候可能会陷入阻塞,这种阻塞有两个很明显的缺陷

  1. 无法控制阻塞时长
    定义一个同步方法,休眠1天,当两个线程竞争该同步方法时,Thread2线程的等待时长完全取决于Thread1何时释放,如果Thread2计划最多1分钟获得执行权,否则就放弃,这个思路显然无法实现;
public class SynchronizedDefect {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDefect synchronizedDefect = new SynchronizedDefect();
        Thread thread1 = new Thread(synchronizedDefect::syncMethod, "Thread1");
        thread1.start();
        TimeUnit.MILLISECONDS.sleep(2);

        Thread thread2 = new Thread(synchronizedDefect::syncMethod, "Thread2");
        thread2.start();
    }

    public synchronized void syncMethod() {
        try {
            TimeUnit.DAYS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  1. Thread2若是因竞争某个monitor lock而陷入阻塞,那么它是无法中断的,虽然可以其的interrupt标识,但是sunchronized不像sleep和wait方法一样可以获取到终端信息
    把刚才的main方法稍作修改:
public static void main(String[] args) throws InterruptedException {
        SynchronizedDefect synchronizedDefect = new SynchronizedDefect();
        Thread thread1 = new Thread(synchronizedDefect::syncMethod, "Thread1");
        thread1.start();
        TimeUnit.MILLISECONDS.sleep(2);

        Thread thread2 = new Thread(synchronizedDefect::syncMethod, "Thread2");
        thread2.start();
        TimeUnit.MILLISECONDS.sleep(2);
        thread2.interrupt();
        System.out.println("Thread2 isInterrupted: "+thread2.isInterrupted());
        System.out.println("Thread2's state: "+thread2.getState());

    }

在这里插入图片描述

5.2 自定义显示锁

构造一个显式的BooleanLock,使其具备synchronized的所有功能,并可中断和lock超时的功能;
Lock接口:

public interface Lock {
    void lock() throws InterruptedException;

    void lock(long mills) throws InterruptedException, TimeoutException;

    void unlock();

    List<Thread> getBlockedThreads();

}

BooleanLock实现:

public class BooleanLock implements Lock {


    private Thread currentThread;

    private boolean locked = false;
    private final List<Thread> blockedList = new ArrayList<>();

    /**
     * lock方法
     * 1.使BooleanLock具有synchronized同步的特效
     * 2.使线程可获取中断信息
     *
     * @throws InterruptedException
     */
    @Override
    public void lock() throws InterruptedException {
        synchronized (this) {
            while (locked) {
                //暂存当前线程
                final Thread tempThread = currentThread();

                try {
                    //防止线程被中断 该线程还在blockedList中
                    if (blockedList.contains(tempThread))
                        blockedList.add(tempThread);
                    this.wait();
                } catch (InterruptedException e) {
                    //如果当前线程在wait时被中断,则从blockedList中将其删除,避免内存泄漏
                    blockedList.remove(tempThread);
                    //继续抛出中断异常
                    throw e;
                }
            }
            blockedList.remove(currentThread());
            this.locked = true;
            this.currentThread = currentThread();
        }
    }

    /**
     * @param mills
     * @throws InterruptedException
     * @throws TimeoutException
     */
    @Override
    public void lock(long mills) throws InterruptedException, TimeoutException {
        synchronized (this) {
            if (mills < 0) {
                this.lock();
            } else {
                long remainingMills = mills;
                long endMills = currentTimeMillis() + remainingMills;
                while (locked) {
                    if (remainingMills <= 0) {
                        throw new TimeoutException("can not get the lock during" + mills);
                    }
                    if (!blockedList.contains(currentThread)) {
                        blockedList.add(currentThread);
                    }
                    this.wait(remainingMills);

                    remainingMills = endMills - currentTimeMillis();
                }
                blockedList.remove(currentThread());
                this.locked = true;
                this.currentThread = currentThread();
            }
        }
    }

    @Override
    public void unlock() {
        synchronized (this) {
            if (currentThread == currentThread()) {
                this.locked = false;
                this.notifyAll();
            }
        }
    }

    @Override
    public List<Thread> getBlockedThreads() {
        return null;
    }
}

客户端调用:

public class LockClient {
    private final Lock lock = new BooleanLock();

    /**
     * 可中断同步方法
     */
    public void syncMethod() {
        try {
            lock.lock();
            int randomInt = current().nextInt(10);
            System.out.println(currentThread() + "get the lock.");
            TimeUnit.SECONDS.sleep(randomInt);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    /**
     * 可超时同步方法
     */
    public void syncMethodTimeoutable() {
        try {
            lock.lock(1000);
            System.out.println(currentThread() + "get the lock.");
            int randomInt = current().nextInt(10);
            TimeUnit.SECONDS.sleep(randomInt);
        } catch (InterruptedException | TimeoutException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //1 lock()方法使BooleanLock具有synchronized同步的效果
        LockClient lockClient = new LockClient();
        IntStream.range(0, 10).mapToObj(i -> new Thread(lockClient::syncMethod)).forEach(Thread::start);

        //2 syncMethod方法捕获了中断异常 使线程可获得中断信息
        LockClient lockClientInterrupt = new LockClient();
        new Thread(lockClientInterrupt::syncMethod, "T1").start();
        TimeUnit.MILLISECONDS.sleep(2);
        Thread t2 = new Thread(lockClientInterrupt::syncMethod, "T2");
        t2.start();
        t2.interrupt();

        //3 syncMethodTimeoutable方法调用lock(long mills) 使线程竞争monitor锁可超时
        LockClient lockClientTimeoutable = new LockClient();
        new Thread(lockClientTimeoutable::syncMethod, "T3").start();
        TimeUnit.MILLISECONDS.sleep(2);
        Thread t4 = new Thread(lockClientTimeoutable::syncMethodTimeoutable, "T4");
        t4.start();
        TimeUnit.MILLISECONDS.sleep(10);

    }


}

第一部分执行结果:
在这里插入图片描述
lock方法使BooleanLock具有synchronized同步的效果,线程按顺序执行:

Thread[Thread-0,5,main]get the lock.
Thread[Thread-9,5,main]get the lock.
Thread[Thread-2,5,main]get the lock.
Thread[Thread-8,5,main]get the lock.
Thread[Thread-1,5,main]get the lock.
Thread[Thread-7,5,main]get the lock.
Thread[Thread-3,5,main]get the lock.
Thread[Thread-6,5,main]get the lock.
Thread[Thread-4,5,main]get the lock.
Thread[Thread-5,5,main]get the lock.

第二部分执行结果,syncMethod方法捕获了中断异常,使程序可获得中断信息:

Thread[T1,5,main]get the lock.
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.wxx.thread.obviouslock.BooleanLock.lock(BooleanLock.java:30)
	at com.wxx.thread.obviouslock.LockClient.syncMethod(LockClient.java:14)
	at java.lang.Thread.run(Thread.java:748)

第三部分执行结果,是线程在竞争monitor锁时可超时:

Thread[T3,5,main]get the lock.
java.util.concurrent.TimeoutException: can not get the lock during1000
	at com.wxx.thread.obviouslock.BooleanLock.lock(BooleanLock.java:65)
	at com.wxx.thread.obviouslock.LockClient.syncMethodTimeoutable(LockClient.java:35)
	at java.lang.Thread.run(Thread.java:748)

参考文献:
[ 1 ] Java高并发编程详解 汪文君著。–北京:机械工业出版社,2018年6月第1版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值