线程之间的通信

使用wait和notify

关于wait()方法,并不是与sleep一样用于阻塞的,而是在synchronized中配合着notify()或者notifyAll()来使用的。

它的目的,就是用于线程之间的通讯。

所谓线程之间的通讯,就是两个线程能够对一个共享变量有协作地改变。

一般来说,都是你改你的,我改我的,处于一种混乱的状态,但是它们可以通过代码的控制完成交流。

package thread_communication;

public class IncreaseAndDecrease {

    public static void main(String[] args) {
        ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {

                connectionBtwThread.increase();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {

                connectionBtwThread.decrease();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {

                connectionBtwThread.increase();
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {

                connectionBtwThread.decrease();
            }
        }, "D").start();
    }
}

class ConnectionBtwThread {
    private int num = 0;

    public synchronized void increase() {
        while (num != 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + ++num);

        notifyAll();
    }


    public synchronized void decrease() {
        while (num == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + --num);

        notifyAll();
    }


}

资源类是ConnectionBtwThread,共享变量是num

为什么用 while (num != 0)而非if(num != 0)?因为wait存在着虚假唤醒。也就是说,你没有notify,人家自己醒来了,这显然不是我们想要的。

这样的话,即使有虚假唤醒,也要通过num != 0的检测。

我们可以看一下javadoc的描述:

在这里插入图片描述

main方法中我开了4条线程来
进行加减。如果将while改成if的话,这是会有问题的(读者可以尝试)。

结果:

A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0



那么,为什么要使用synchronized 呢?

在这里插入图片描述

如果并非持有锁的线程调用wait方法,异常会抛出。

使用condition

新的lock有着和以前并发一样的功能,lock能像synchronized一样锁,也有wait和notify一样的功能。

package thread_communication;

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

public class IncreaseAndDecrease {

    public static void main(String[] args) {
        ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                connectionBtwThread.increase();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                connectionBtwThread.decrease();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {

                connectionBtwThread.increase();
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                connectionBtwThread.decrease();
            }
        }, "D").start();
    }
}

class ConnectionBtwThread {
    private int num = 0;

    ReentrantLock lock = new ReentrantLock();

    Condition condition = lock.newCondition();

    public void increase() {
        lock.lock();
        try {
            while (num != 0) {
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + " : " + ++num);

            condition.signalAll();
        } catch (Exception ex) {
            ex.getStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void decrease() {
        lock.lock();
        try {
            while (num == 0) {
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + " : " + --num);

            condition.signalAll();
        } catch (Exception ex) {
            ex.getStackTrace();
        } finally {
            lock.unlock();
        }
    }


}

如果你懂之前wait和notify的代码,那么这里的代码也一定会懂,因为逻辑是一样的,只是换了api。

如果只是实现同样的功能的话,新的lock锁又为何出现呢?所以,这里的功能会更加强大。

新的需求:

线程A打印5次,然后线程B打印10次,然后线程C打印15次,再循环……
次序不能变。

这种唤醒叫做精准唤醒,因为A打印完成之后,他要唤醒的是B,而不是其他的。

condition能够做到这一点。

package thread_communication;

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

public class IncreaseAndDecrease {

    public static void main(String[] args) {
        ConnectionBtwThread connectionBtwThread = new ConnectionBtwThread();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                connectionBtwThread.print5();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                connectionBtwThread.print10();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {

                connectionBtwThread.print15();
            }
        }, "C").start();

    }
}

class ConnectionBtwThread {
    private int num = 1;

    private ReentrantLock lock = new ReentrantLock();

    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            while (num != 1) {
                condition1.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + i);
            }

            num = 2;

            condition2.signal();
        } catch (Exception ex) {
            ex.getStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void print10() {
        lock.lock();
        try {
            while (num != 2) {
                condition2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + i);
            }

            num = 3;

            condition3.signal();
        } catch (Exception ex) {
            ex.getStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            while (num != 3) {
                condition3.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + i);
            }

            num = 1;

            condition1.signal();
        } catch (Exception ex) {
            ex.getStackTrace();
        } finally {
            lock.unlock();
        }
    }


}

这里 condition2.signal();明确唤醒了print10,即B线程。

结果大概是这样的:

A1
A2
A3
A4
A5
B1
B2
B3
B4
B5
B6
B7
B8
B9
B10
C1
C2
C3
C4
C5
C6
C7
C8
C9
C10
C11
C12
C13
C14
C15
A1
A2
A3
A4
A5
……

使用CountDownLatch

闭锁的countDown()await()方法的组合也可以完成线程的通信。

只有数到0,才能都通过await()方法执行它下面的代码。

package thread_communication;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.Assert.assertEquals;

public class HandsOff {
    public static void main(String[] args) throws InterruptedException {
        //prepare the thread pool
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        AtomicInteger sharedState = new AtomicInteger();
        CountDownLatch latch = new CountDownLatch(1);

        //producer task
        Runnable producer = ()->{
            Integer producedElement = ThreadLocalRandom
                    .current()
                    .nextInt();
            sharedState.set(producedElement);
            System.out.println("saving an element : " + producedElement + " to the exchange point");

            latch.countDown();
        };

        //consumer task
        Runnable consumer = ()->{
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Integer consumedElement = sharedState.get();
            System.out.println("consuming an element : " + consumedElement + " from the exchange point");
        };

        //execute
        executorService.execute(producer);
        executorService.execute(consumer);

        executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
        executorService.shutdown();

        assertEquals(latch.getCount(),0);
    }

}

这是一个生产者消费者模型,这种模型是线程通信的典型实现。

latch变为0之后才能执行 Integer consumedElement = sharedState.get();

下面,我们使用同步队列完成同样的效果。

使用SynchronousQueue

同步队列中只能放一个元素,因此put之后会阻塞,他在等take,只有take之后,才能继续put。这样也完美的实现了先生产后消费的思路,当然,这就是线程的通信(协作)。

package thread_communication;

import java.util.concurrent.*;

import static org.junit.Assert.assertEquals;

public class HandsOffV2 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();

        Runnable producer = () -> {
            Integer randomInt = ThreadLocalRandom.current().nextInt();
            try {
                synchronousQueue.put(randomInt);
                System.out.println("saving the sharedNum " + randomInt);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Runnable consumer = () -> {
            try {
                Integer take = synchronousQueue.take();
                System.out.println("consuming the sharedNum " + take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        executorService.execute(producer);
        executorService.execute(consumer);

        try {
            executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        executorService.shutdown();

        assertEquals(synchronousQueue.size(), 0);

    }
}

生产者消费者模型及应用

上面已经讲到这种模型了,并且分别用了闭锁和同步队列来实现。我们现在用阻塞队列写一个。

package thread_communication;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;


public class Setup {
    public static void main(String[] args) {
        BlockingQueue q = new ArrayBlockingQueue(2);
        Producer producer = new Producer(q);
        Consumer consumer1 = new Consumer(q);
        Consumer consumer2 = new Consumer(q);

        new Thread(producer).start();
        new Thread(consumer1).start();
        new Thread(consumer2).start();
    }
}

class Producer implements Runnable {

    private final BlockingQueue queue;

    Producer(BlockingQueue q) {
        queue = q;
    }


    @Override
    public void run() {
        try {
            while (true) {

                queue.put(produce());

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

    private Object produce() {
        return null;
    }
}

class Consumer implements Runnable {

    private final BlockingQueue queue;

    Consumer(BlockingQueue q) {
        queue = q;
    }

    @Override
    public void run() {
        try {
            while (true) {
                consume(queue.take());
            }
        } catch (InterruptedException e) {
            e.getStackTrace();
        }
    }

    private void consume(Object take) {

    }
}

这是Oracle官方的例子,是一个生产者消费者的基本骨架。

那么,生产者消费者模型有什么具体的用途呢?

先生产后消费是其特点。

比如,播放器的预先加载,只有加载完成,才能播放。

为什么灰色的进度条有时候加载加载就停了呢,因为阻塞队列满了,只有当蓝色的播放条追上灰色的时候它才会再次预加载。

我们即将实现的应用,就是异步日志。

什么叫异步呢?就是在主线程之外再开一条线程来写日志,这样就不会阻塞主线程。

日志内容会放进阻塞队列中,在工作线程内,会有一个死循环,一直在询问:“有没有日志信息?”如果有就拿走并写入文件,如果没有就阻塞。

Logger.java

package thread_communication;

import java.io.PrintWriter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 一个异步日志的实现
 *
 * 使用基于阻塞队列的生产消费者模型
 *
 * author:Ocean
 */
public class Logger {

    private BlockingQueue<String> blockingQueue;
    private PrintWriter printWriter;

    //初始化阻塞队列
    public Logger() {
        blockingQueue = new LinkedBlockingQueue<>(10000);
    }

    //打开文件
    public void open() {
        try {
            printWriter = new PrintWriter("log.txt");
            //开一条线程去工作(异步)
            //不妨碍主线程做自己的事
            Thread writeFileThread = new Thread() {

                @Override
                public void run() {
                    //不断地往文件中写日志
                    while (true) {
                        writeFile();
                    }
                }
            };
            writeFileThread.start();


        } catch (Exception e) {
            e.getStackTrace();
        }
    }
    //写日志
    private void writeFile() {
        try {
            //如果阻塞队列中有内容,则取出,否则就阻塞
            String content = blockingQueue.take();
            printWriter.println(content);
            printWriter.flush();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    //模拟日志的产生
    public void putFile(String log){
        try {
            //如果队列满了,则阻塞,否则put
            blockingQueue.put(log);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //关闭流
    public void close() {
        if (printWriter != null) {
            printWriter.close();
        }
    }

}

测试:

package thread_communication;

import java.util.concurrent.TimeUnit;

/**
 * 测试类
 */
public class TestLogger {
    public static void main(String[] args) {
        Logger logger = new Logger();

        //打开文件,不断地问:“有没有日志?”
        logger.open();

        //准备日志信息(模拟)
        for (int i = 0; i < 1000; i++) {
            logger.putFile("I LOVE YOU");

        }

        //让主线程等待日志的写入完成
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        logger.close();
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值