第五章 线程间协作--《java多线程编程实战指南-核心篇》

5.1 等待与通知:wait/notify

wait()的作用是使其执行线程被暂停,该方法可以用来实现等待;notify()的作用是唤醒一个被暂停的线程,调用该方法可实现通知。

由于一个线程只有在持有一个对象的内部锁的情况下才能调用该对象wait方法,因此wait()调用总是放在响应对象所引导的临界区之中。

执行someObject.wait()而被暂停的线程称为对象someObject上等待的线程。由于同一个对象的该方法可以被多个线程执行,因此一个对象可能存在多个等待线程。someObject上的等待线程可以通过其他线程执行someObject.notify()来唤醒。someObject..wait()会以原子操作的方法使其执行线程(当前线程)暂停并使该线程释放其持有的someObject对应的内部锁。当前线程被暂停的时候其对someObject.wait()的调用并未返回。其他线程在该线程所需的保护条件成立的时候执行响应的notify方法,即someObject..notify()可以唤醒someObject上的一个等待线程。被唤醒的等待线程在其占用处理器继续运行的时候,需要再次申请someObject对应的内部锁。被唤醒的线程在其再次持有someObject对应的内部锁的情况下继续执行someObject.wait()中剩余的指令,直到wait方法返回。

//wait实现等待的模板方法
synchronized(someObject){
    while(保护条件不成立){
        //调用Object.wait()暂停当前线程
        someObject.wait();
    }
    //代码执行到这里说明保护条件已经满足
    doAction();
}

//notify实现通知的模板方法
synchronized(someObject){
    //更新等待线程的保护条件涉及的共享变量
    updateSharedState();
    //唤醒其他线程
    someObject.notify();
}

等待线程对保护条件的判断、Object.wait()的调用总是应该方法响应对象所引导的临界区中的一个循环语句之中。

等待线程对保护条件的判断、Object.wait()的执行以及目标动作的执行必须放在同一个对象(内部锁)所引导的临界区之中。

Object.wait()暂停当前线程时释放的锁只是与该wait方法所属对象的内部锁。当前线程所持有的其他内部所、显示锁并不会因此而被释放。

由于一个线程只有在持有一个对象的内部锁的情况下才能够执行该对象的notify方法,因此Object.notify()调用总是放在相应对象内部锁所引导的临界区之中。也正是由于Object.notify()要求其执行线程必须持有该方法所属对象的内部锁,因此Object.wait()在暂停其执行线程的同时必须释放相应的内部锁;否则通知线程无法获得相应的内部锁,也就是无法执行相应对象的notify方法来通知等待线程!Object.notify()的执行线程持有的响应对象的内部锁只有在Object.notify()调用所在的临界区代码执行结束后才会被释放,而Object.notify()本身并不会将这个内部锁释放。因此,为了使等待线程在其被唤醒之后能够尽快获得相应的内部锁,我们要尽可能的将Object.notify()调用放在靠近临界区结束的地方。等待线程被唤醒之后占用处理器继续运行时,如果有其他线程持有了相应对象的内部锁,那么这个等待线程可能又会再次被暂停,以等待再次获得相应内部锁的机会,而这会导致上下文切换。

调用Object.notify()所唤醒的线程仅是响应对象上的一个任意等待线程,所以这个被唤醒的线程可能不是我们真正想要唤醒的那个线程。因此,有时候我们需要借助Object.notifyAll(),它可以唤醒响应对象上的所有等待线程。由于等待线程和通知线程在其实现等待和通知的时候必须是调用同一个对象的wait方法、notify方法,而这两个方法都要求其执行线程必须持有该方法所属对象的内部锁,因此等待线程和通知线程是同步在同一对象之上的两种线程。

此处个人理解:notify和wait方法都是针对对象的,所以加锁的地方需要注意!!

java虚拟机会为每一个对象维护一个入口集用于存储申请该对象内部锁的线程。此外,java虚拟机还会为每个对象维护一个被称为等待集的队列,该队列用户存储该对象上的等待线程。Object.wait()将当前线程暂停并释放相应内部锁的同时会将该线程存入该方法所属对象的等待集中。执行一个对象的notify方法会使该对象的等待集中的一个任意线程被唤醒。被唤醒的线程仍然会停留在相应对象的等待集中,直到该线程再次持有相应内部锁的时候Object.wait()会使当前线程从其所在的等待集移除,接着Object.wait()调用就返回了。

package JavaCoreThreadPatten.capter05;

import java.util.Random;

/**
 * 告警代理
 */
public class AlarmAgent {

    private static volatile AlarmAgent INSTANCE = null;
    //是否已经连接上服务器
    private volatile boolean connectToServer = false;

    private AlarmAgent(){}

    public static AlarmAgent getINSTANCE(){
        if(null==INSTANCE){
            synchronized (AlarmAgent.class){
                if(null==INSTANCE){
                    INSTANCE = new AlarmAgent();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 初始化操作
     */
    public void init(){
        doConnectionToServer();
        Thread thread = new Thread(new HeartbeartThread());
        thread.setDaemon(true);
        thread.setName("心跳检测后台守护线程");
        thread.start();
    }

    private void doConnectionToServer() {
        //连接操作
        synchronized (this){
            //连接建立成功
            connectToServer = true;
            //唤醒所有在等待的线程
            notifyAll();
        }
    }

    public void sendAlarm(String message){
        synchronized (this){
            while (!connectToServer){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //发送消息
            doSendMessage(message);
        }
    }

    public void doSendMessage(String message){
        //发送消息逻辑
    }


    /**
     * 心跳检测线程
     */
    class HeartbeartThread implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
                while (true){
                    if(checkConnection()){
                        connectToServer = true;
                    }else{
                        connectToServer = false;
                    }
                }
            } catch (InterruptedException e) {
            }
        }
        public boolean checkConnection(){
            //检测连接是否正常
            return new Random().nextBoolean();
        }
    }
}
package JavaCoreThreadPatten.capter05;

import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class TimeOutWaitExample {
    private static final Object lock = new Object();
    private static boolean ready = false;

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for(;;){
                synchronized (lock){
                    ready = new Random().nextInt(100)>50 ? true : false;
                    try {
                        TimeUnit.MILLISECONDS.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(ready){
                        lock.notify();
                    }
                    System.out.println(ready);
                }
            }
        });
        t.setDaemon(true);
        t.start();
        try {
            waiter(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void waiter(long timeout) throws InterruptedException {
        if (timeout<0){
            throw new IllegalArgumentException();
        }
        long start = System.currentTimeMillis();
        long waitTime;
        long now;
        synchronized (lock){
            while (!ready){
                now = System.currentTimeMillis();
                //计算剩余等待时间
                waitTime = timeout-(now-start);
                if(waitTime<=0){
                    break;
                }
                //此处标识当前线程在lock这个对象的入口等待
                lock.wait(waitTime);
            }
            if(ready){
                //做正常的业务流程
                System.out.println("做正常的业务逻辑");
            }else {
                System.out.println("wait time out");
            }
        }
    }
}

wait/notify的问题:

5.2 java条件变量

Condition接口可以作为wait/notify的替代品来实现等待/通知,它为解决过早唤醒问题提供了支持,并解决了Object.wait(long)不能区分其返回是否是由等待超时而导致的问题。

package JavaCoreThreadPatten.capter05;

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

public class ConditionUsage {
    private static final Lock LOCK = new ReentrantLock();
    private static final Condition CONDITION = LOCK.newCondition();

    public void aGuaredMethod() {
        try {
            LOCK.lock();
            boolean condition = true;
            while (condition) {
                try {
                    CONDITION.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //做一些事情
        } finally {
            LOCK.unlock();
        }
    }

    public void anNotificationMethod(){
        LOCK.lock();
        CONDITION.signal();
    }
}
package JavaCoreThreadPatten.capter05;

import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TimeOutWaitExampleWithCondition {
    private static final Lock LOCK = new ReentrantLock();
    private static final Condition CONDITION = LOCK.newCondition();
    private static boolean ready = false;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (; ; ) {
                try {
                    LOCK.lock();
                    ready = new Random().nextInt(100) > 50 ? true : false;
                    try {
                        TimeUnit.MILLISECONDS.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (ready) {
                        CONDITION.signal();
                    }
                    System.out.println(ready);
                } finally {
                    LOCK.unlock();
                }
            }
        });
        t.setDaemon(true);
        t.start();
        try {
            waiter(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void waiter(long timeout) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException();
        }
        long start = System.currentTimeMillis();
        long waitTime;
        long now;
        LOCK.lock();
        while (!ready) {
            now = System.currentTimeMillis();
            //计算剩余等待时间
            waitTime = timeout - (now - start);
            if (waitTime <= 0) {
                break;
            }
            //此处标识当前线程在lock这个对象的入口等待
            CONDITION.awaitUntil(new Date(System.currentTimeMillis()+timeout));
        }
        if (ready) {
            //做正常的业务流程
            System.out.println("做正常的业务逻辑");
        } else {
            System.out.println("wait time out");
        }
    }
}

5.3 倒计时协调器:CountDownLatch

CountDownLatch可以用来实现一个或多个线程等待其他线程完成一组特定操作之后才继续运行。

CountDownLatch内部会维护一个用于表示未完成的先决操作数量计数器。CountDownLatch.countDown()每被执行一次就会使相应实例的计数器值减少1,当计数器值不为0时CountDownLatch.await()的执行线程会被暂停,CountDownLatch.countDown()相当于一个通知方法,它会在计数器达到0的时候唤醒响应实例上的所有等待线程。

package JavaCoreThreadPatten.capter05;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 结果:
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 一个线程执行完毕
 * 十个任务已经执行完毕,开始执行下一阶段任务
 */
public class CountdownLatchExample {
    /**
     * 十个线程的任务执行完毕开始执行接下来的业务
     */
    private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(10);

    public static void main(String[] args){
        try {
            for(int i=0;i<10;i++){
                new Thread(new Handler()).start();
                TimeUnit.SECONDS.sleep(1);
            }
            COUNT_DOWN_LATCH.await();
            System.out.println("十个任务已经执行完毕,开始执行下一阶段任务");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class Handler implements Runnable{

        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                System.out.println("一个线程执行完毕");
            } catch (InterruptedException e) {
            }finally {
                //注意,此处为了保证计数器能够释放,应当放在finally中释放
                COUNT_DOWN_LATCH.countDown();
            }
        }
    }
}

如果CountDownLatch内部计数器由于程序的错误而无法达到0,那么相应实例上的线程会一直处于WAITING状态,因此我们首先应当将countDown()方法的调用放在finally语句块中,确保能够被调用到;其次可以调用await(long,TimeUnit)允许指定一个超时时间,如果超过指定时间计数器的值仍未达到0,那么所有执行该实例的await()方法的ixancheng都会被唤醒。

5.4 栅栏(CyclicBarrier)

有时候多个线程可能需要相互等待对方执行到代码中的某个地方(集合点),这时这些线程才能够继续执行。

CyclicBarrier实例是可以重复使用的。

使用CyclicBarrier实现等待的县城被称为参与方。参与方只需要执行CyclicBarrier.await()就可以实现等待。尽管从应用代码的角度来看,参与方是并发执行CyclicBarrier.await()的,但是,CyclicBarrier内部维护了一个显式锁,这使得其总是可以在所有参与方中分出一个最后执行CyclicBarrier.await()的线程,该线程被称为最后一个线程。除最后一个线程外的任何参与方执行CyclicBarrier.await()都会导致该线程被暂停。最后一个线程执行CyclicBarrier.await()会使得使用相应CyclicBarrier实例的其他所有参与方被唤醒。

package JavaCoreThreadPatten.capter05;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 栅栏功能:模拟100个人爬山,爬山一共分为两段,第一段结束等人全到开始爬第二段,第二段结束全部到达后宣布胜利,团建结束
 */
public class CyclicBarrierExample {
    private static final CyclicBarrier CYCLIC_BARRIER = new CyclicBarrier(100);
    private static final CyclicBarrier CYCLIC_BARRIER2 = new CyclicBarrier(100);
    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(1);

    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(new Person(i + "帅哥", CYCLIC_BARRIER)).start();
        }
    }

    static class Person implements Runnable {

        private final String name;
        private final CyclicBarrier cyclicBarrier;

        public Person(String name, CyclicBarrier cyclicBarrier) {
            this.name = name;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                //第一阶段爬山
                climb();
                //等别人都到达第一阶段终点
                //这里使用存在问题
                CYCLIC_BARRIER.await();
                //第二阶段爬山开始
                ATOMIC_INTEGER.set(2);
                climb();
                //等别人都到达第二阶段终点
                CYCLIC_BARRIER2.await();
                //所有人都到达终点,庆祝开始
                System.out.println("游戏结束," + name + "打开了香槟!!!");
            } catch (InterruptedException e) {
            } catch (BrokenBarrierException e) {
            }
        }

        public void climb() {
            try {
                TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
                int s = new Random().nextInt(10);
                TimeUnit.SECONDS.sleep(s);
                System.out.println(name + "第" + ATOMIC_INTEGER.get() + "阶段爬山结束了");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


CyclicBarrier内部实现是基于条件变量的,因此CyclicBarrier的开销与条件变量的开销相似,其主要开销在可能产生的上下文切换。

CyclicBarrier内部使用了一个条件变量trip来实现等待/通知。CyclicBarrier内部实现使用了分代的概念用于表示CyclicBarrier实例是可以重复使用的。除最后一个线程以外的任何一个参与方都相当于一个等待线程,这些线程所使用的保护条件是当前分代内,尚未执行await方法的参与方个数(parties)为0。当前分代的初始状态是parties等于参与方总数。CyclicBarrier.await()每被执行一次会使相应实例的parties值减少1.最后一个线程相当于通知线程,它执行CyclicBarrier.await()会使相应实例的parties值变为0,此时该线程会限制性barrierAction.run(),然后再执行trip.signalAll()来唤醒所有等待线程。接着,开始下一个分代,即使得CyclicBarrier的parties值又重新恢复为其初始值。

 CyclicBarrier的应用场景:1.使迭代算法并发化;2.在测试代码中模拟高并发。

5.5 生产者-消费者模式

由于线程之间无法像函数调用那样通过参数直接传递数据,因此生产者和消费者之间需要一个用于传递产品的传输通道。

JDK1.5中引入的接口java.util.concurrent.BlockingQueue定义线程安全的阻塞队列,队列按照其存储容量是否受限制分为有界队列和无界队列。

有界队列可以使用ArrayBlockingQueue或者LinkedBlockingQueue来实现。ArrayBlockingQueue内部使用一个数组作为其存储空间,而数组的存储空间是预先分配的,因此ArrayBlockingQueue的put操作、take操作本身并不会增加垃圾回收的负担。ArrayBlockingQueue的缺点是其内部在实现put、take操作的时候使用的是同一个锁(显式锁),从而可能导致锁的高争用,进而导致较多的上下文切换。

LinkedBlockingQueue即能实现无界队列,也能实现有界队列。LinkedBlockingQueue的其中一个构造器允许我们创建队列得时候指定队列容量。LinkedBlockingQueue的优点是其内部在实现put、take操作的时候分别使用了两个显式锁(putLock和takeLock),这降低了锁争用的可能性。LinkedBlockingQueue的内部存储空间是一个链表,而链表节点(对象)所需的存储空间是动态分配的,put操作、take操作都会导致链表节点的动态创建和移除,因此LinkedBlockingQueue的缺点是它可能增加垃圾回收的负担。另外,由于LinkedBlockingQueue的put、take操作使用的是两个锁,因此LinkedBlockingQueue维护其队列的当前长度(size)时无法使用一个普通int型变量而是使用了一个原子变量(AtomicInteger)。这个原子变量可能会被生产者和消费者线程争用,因此它可能导致额外的开销。

SynchronousQueue是一个特殊的有界队列,生产者执行SynchronousQueue.put(E)时如果没有消费者线程执行SynchronousQueue.take(),那么该生产者线程会被暂停,直到有消费者线程执行了SynchronousQueue.take();类似的,消费者线程执行SynchronousQueue.take()时如果没有生产者执行了SynchronousQueue.put(E),那么该消费者线程会被暂停,直到有生产者线程执行了SynchronousQueue.put(E)。SynchronousQueue适合于在消费者处理能力与生产者处理能力相差不大的情况下使用。否则,由于生产者线程执行执行put操作时没有消费者线程执行take操作,或者消费者线程执行take操作的时候没有生产者线程执行put操作的概率比较大,从而可能导致较多的等待。(意味着上下文切换)

ArrayBlockingQueue和SynchronousQueue都既支持非公平调度也支持公平调度,而LinkedBlockingQueue仅支持非公平调度

如果生产者线程和消费者线程之间的并发程度比较大,那么这些线程对传输通道内部所使用的锁的争用可能性也随之增加。这时,有界队列的实现适合选用LinkedBlockingQueue,否则我们可以考虑使用ArrayBlockingQueue。

LinkedBlockingQueue适合在生产者线程和消费者线程之间并发程度比较大的情况下使用。

ArrayBlockingQueue适合在生产者线程和消费者线程之间的并发程度比较低的情况下使用。

SynchronousQueue适合在消费者处理能力与生产者处理能力相差不大的情况下使用。

信号量:Semaphore

java.util.concurrent.Semaphore可以用来实现流量控制。

Semaphore.acquire()/release()分别用于申请配额和返还配额。Semaphore.acquire()在成功获得一个配额后会立即返回。如果当前的可用额度不足,那么Semaphore.acquire()会使其执行线程暂停。Semaphore内部会维护一个等待队列用于存储这些被暂停的线程。Semaphore.acquire()在其返回之前总是会将当前的可用配额减少1。Semaphore.release()会使当前可用配额增加1,并唤醒响应Semaphore实例的等待队列中的一个任意等待线程。

package JavaCoreThreadPatten.capter05;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Semaphore;

/**
 * 基于Semaphore的支持流量控制的传输通道实现
 * @param <P>
 */
public class SemaphoreBasedChannel<P>  {
    private final BlockingQueue<P> queue;
    private final Semaphore semaphore;

    /**
     *
     * @param queue 无界队列
     * @param flowlimit 流量数限制
     * @param fair 是否是公平
     */
    public SemaphoreBasedChannel(BlockingQueue<P> queue, int flowlimit,boolean fair) {
        this.queue = queue;
        this.semaphore = new Semaphore(flowlimit,fair);
    }

    public P take(){
        try {
            return queue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void put(P product){
        //获取凭证
        try {
            semaphore.acquire();
            queue.put(product);
        } catch (InterruptedException e) {
        }finally {
            //释放凭证
            semaphore.release();
        }
    }

}

Semaphore.acquire()和Semaphore.release()总是配对使用。

Semaphore.release()调用总是应该放在一个finally块中,以避免虚拟资源访问出现异常情况下当前线程所获得的配额无法返回。

在问题规模一定的情况下,产品的粒度过细会导致产品再传输通道上的移动次数增大;产品的粒度稍微大些可以减少产品再传输通道上的移动次数,但是产品所占用的资源也会随之增加。

5.6 线程中断机制

java平台会为每个线程维护一个被称为中断标记的布尔型状态变量用于表示相应线程是否接收到了中断,中断标记值为true表示响应线程收到了中断。目标线程可以通过Thread.currentThread().isInterrupted()调用来获取该线程的中断标记值,也可以通过Thread.interrupted()来获取并重置中断标记值,即Thread.interrupted()会返回当前线程的中断标记值并将当前线程中断标记重置为false。调用一个线程的interrupt()相当于将该线程的中断标记为true;线程被中断后会抛出InterruptedException异常。

优雅的中断线程:光使用专门的实例变量来作为线程停止标记仍然不够,这是由于当线程停止标记为true的时候,目标线程可能因为执行了一些阻塞方法而被暂停,因此,这时线程停止标记压根不会对目标线程产生任何影响!由此可见,为了使线程停止标记的设置能够起作用,我们可能还需要给目标线程发送中断以将其唤醒,使之得以判断线程停止标记。

package JavaCoreThreadPatten.capter05;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 可优雅中断的任务处理器
 */
public class TerminatableTaskRunnable<T extends Runnable> {
    protected final BlockingQueue<T> queue;
    private Thread workThread;
    //表示是否需要中断,true表示不需要中断
    private volatile boolean isUse = true;
    //表示当前需要进行的任务数量
    private final AtomicLong taskNum = new AtomicLong(0);


    public TerminatableTaskRunnable(BlockingQueue<T> queue) {
        this.queue = queue;
        this.workThread = new WorkThred();
    }

    public void init(){
        workThread.start();
    }

    /**
     * 当没有中断的情况下,继续接受任务
     * @param task
     * @throws InterruptedException
     */
    public void submit(T task) throws InterruptedException {
        if(isUse){
            queue.put(task);
            taskNum.incrementAndGet();
        }else {
            System.err.println("拒绝接受任务");
        }
    }

    /**
     * 中断线程
     */
    public void shutdown(){
        isUse = false;
        //唤醒线程,检查中断标识
        workThread.interrupt();
    }


    class WorkThred extends Thread{
        @Override
        public void run() {
            for(;;){
                //中断且当前待执行的任务书为0
                if(!isUse && taskNum.get()<=0){
                    break;
                }
                try {
                    queue.take().run();
                } catch (InterruptedException e) {
                }
                taskNum.decrementAndGet();
            }
        }
    }
}

在生产者-消费者模式中,生产者线程需要先于消费者线程停止,否则生产者所生产的产品会无法被处理。在单生产者-单消费者模式中,停止生产者、消费者线程有一种简单的额方法;生产者线程在其中指前往传输通道中存入一个特殊产品作为消费者线程的线程停止标志,消费者线程取出这个产品之后就可以退出run方法而终止了。

发布了35 篇原创文章 · 获赞 3 · 访问量 5964
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章