用生活的故事总结一下线程通信

线程之间通信,就像人与人之间通信一样重要。

我觉得理解一个问题,就要建立在我们已有的认知上,就会容易很多。

我将列举几个生活的例子,然后再对比的线程之间通信的例子。

 

# # 线程通信 就像是人和人通信

  先用直白的话来讲,线程通信的目的,就和人之间的通信的目的一样。为了交换信息,为了通知消息。

 

# #   场景一,约好一起做

  比方说,你周末想约隔壁的王阿姨,一起去买菜。不要问为什么是和王阿姨,因为一个人去买菜有点孤独。这就要以信息的方式先通知到王阿姨。比方你想 约王阿姨十点去买菜,你可以给王阿姨发一个短信,可以发一个微信,可以打一个电话,还可以在门口等王阿姨回家的时候约一下她。

  那么线程为什么要通信呢?仔细想一下,是不是有这些场景需要通知到对方?比方说,线程A要约线程B一起去买菜,A线程已经出门了(执行了一些操作),要等等B线程来了再一起操作。

  这个通信的方式就有很多个已经实现好的。

 # #   场景一,方案一:Thread 的 join方法。

join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。

有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。

如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。

示例代码:

/**
 * @author angus
 * @create 2020-04-18 12:38
 */
public class ObjThread {
	public static void main(String[] args) throws InterruptedException {
		Object obj = new Object();
		AtomicReference<String> temp = new AtomicReference<>();
		Thread thread = new Thread(()->{
			System.out.println("开始:线程A");
			try {
				Thread.sleep(3);
				System.out.println("休息完成");
				temp.set("10");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		thread.start();
		//thread.join();
		System.out.println("main线程要执行");
		System.out.println(temp.get());
		
	}


}

  输出的结果,如果不加 join,那么main线程就会有 

注意join()方法有两个重载方法,一个是join(long), 一个是join(long, int)。

实际上,通过源码你会发现,join()方法及其重载方法底层都是利用了wait(long)这个方法。

对于join(long, int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理。

   关于join 的还可以参考我的另外一篇文章:https://blog.csdn.net/star1210644725/article/details/104862793 

 

 # #   场景一,方案二:CountDownLatch

  先来解读一下CountDownLatch这个类名字的意义。CountDown代表计数递减,Latch是“门闩”的意思。也有人把它称为“屏障”。而CountDownLatch这个类的作用也很贴合这个名字的意义,假设某个线程在执行任务之前,需要等待其它线程完成一些前置任务,必须等所有的前置任务都完成,才能开始执行本线程的任务。

CountDownLatch的方法也很简单,如下:

// 构造方法:
public CountDownLatch(int count)

public void await() // 等待
public boolean await(long timeout, TimeUnit unit) // 超时等待
public void countDown() // count - 1
public long getCount() // 获取当前还有多少count

  

我们知道,玩游戏的时候,在游戏真正开始之前,一般会等待一些前置任务完成,比如“加载地图数据”,“加载人物模型”,“加载背景音乐”等等。只有当所有的东西都加载完成后,玩家才能真正进入游戏。下面我们就来模拟一下这个demo。

public class CountDownLatchDemo {
    // 定义前置任务线程
    static class PreTaskThread implements Runnable {

        private String task;
        private CountDownLatch countDownLatch;

        public PreTaskThread(String task, CountDownLatch countDownLatch) {
            this.task = task;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.println(task + " - 任务完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 假设有三个模块需要加载
        CountDownLatch countDownLatch = new CountDownLatch(3);

        // 主任务
        new Thread(() -> {
            try {
                System.out.println("等待数据加载...");
                System.out.println(String.format("还有%d个前置任务", countDownLatch.getCount()));
                countDownLatch.await();
                System.out.println("数据加载完成,正式开始游戏!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 前置任务
        new Thread(new PreTaskThread("加载地图数据", countDownLatch)).start();
        new Thread(new PreTaskThread("加载人物模型", countDownLatch)).start();
        new Thread(new PreTaskThread("加载背景音乐", countDownLatch)).start();
    }
}

输出:

等待数据加载...
还有3个前置任务
加载人物模型 - 任务完成
加载背景音乐 - 任务完成
加载地图数据 - 任务完成
数据加载完成,正式开始游戏!

 

 # #   场景一,方案三:对象锁

利对象的 wait方法,和notify方法实现相互的通信 ,来看一下例子

/**
 * @author angus
 * @create 2020-04-18 12:38
 */
public class ObjThread {
	public static void main(String[] args) throws InterruptedException {
		Object obj = new Object();
		Thread thread = new Thread(()->{
			synchronized (obj){
				System.out.println("开始:线程A");
				try {
					obj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("线程A:我醒了");
			}

		});
		thread.start();

		Thread thread2 = new Thread(()->{
			synchronized (obj){
				System.out.println("开始:线程B");
				obj.notify();

			}

		});
		thread2.start();

		//System.out.println("main线程,唤醒线程A");

	}


}

  运行结果是:

开始:线程A
开始:线程B
线程A:我醒了

 

# #  场景二:为了合理解决资源竞争的问题

  比方说,商场就剩了最后一份韭菜,王阿姨特别想要。你也特别想要。

  我们经常会遇到这样的情况,线程A想要改一个数据,线程B也要改一个数据,这个数据恰好是同一个。比方说扣票的场景。线程A想要扣一张票,线程B也想扣一张票。而事实上恰好只剩下最后一张了,只能有一个人扣。否则都扣的话。就变成 -1,不符合实际情况,以为到时候得有一个人没座位。

  # # # 场景二,方案一,使用锁

  在我们的程序中,解决竞争问题,最常用的就是锁。锁也有很多种,其中一个重量级的锁就是 synchronized, 这个是使用到了对象锁。同时只有 一个线程能够拿到这个对象锁,这个锁一旦被线程拿到,其他的线程再想访问,就必须要等待了。

  看个例子:

public class ObjectLock {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

    除了synchronized 这个重量级锁,还有其他的锁。这里不做过多的介绍。只要明白,锁也是线程之间通信的一种方式。并且这个场景往往是,一块资源同时只能被一个占用的情况。

  # # # 场景二,方案二,使用信号量 Semaphore

  其中资源竞争问题,往往有很多多个资源的问题。比方说,停车场有两百个车位,那么同时可以停下两百辆车。而信号量则是一个比较适合的解决方案。  

 JDK提供了一个类似于“信号量”功能的类Semaphore。但本文不是要介绍这个类,而是介绍一种基于volatile关键字的自己实现的信号量通信。

Semaphore往往用于资源有限的场景中,去限制线程的数量。举个例子,我想限制同时只能有3个线程在工作:

public class SemaphoreDemo {
    static class MyThread implements Runnable {

        private int value;
        private Semaphore semaphore;

        public MyThread(int value, Semaphore semaphore) {
            this.value = value;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire(); // 获取permit
                System.out.println(String.format("当前线程是%d, 还剩%d个资源,还有%d个线程在等待",
                        value, semaphore.availablePermits(), semaphore.getQueueLength()));
                // 睡眠随机时间,打乱释放顺序
                Random random =new Random();
                Thread.sleep(random.nextInt(1000));
                semaphore.release(); // 释放permit
                System.out.println(String.format("线程%d释放了资源", value));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(new MyThread(i, semaphore)).start();
        }
    }
}

输出:

当前线程是1, 还剩2个资源,还有0个线程在等待
当前线程是0, 还剩1个资源,还有0个线程在等待
当前线程是6, 还剩0个资源,还有0个线程在等待
线程6释放了资源
当前线程是2, 还剩0个资源,还有6个线程在等待
线程2释放了资源
当前线程是4, 还剩0个资源,还有5个线程在等待
线程0释放了资源
当前线程是7, 还剩0个资源,还有4个线程在等待
线程1释放了资源
当前线程是8, 还剩0个资源,还有3个线程在等待
线程7释放了资源
当前线程是5, 还剩0个资源,还有2个线程在等待
线程4释放了资源
当前线程是3, 还剩0个资源,还有1个线程在等待
线程8释放了资源
当前线程是9, 还剩0个资源,还有0个线程在等待
线程9释放了资源
线程5释放了资源
线程3释放了资源

可以看到,在这次运行中,最开始是1, 0, 6这三个线程获得了资源,而其它线程进入了等待队列。然后当某个线程释放资源后,就会有等待队列中的线程获得资源。

当然,Semaphore默认的acquire方法是会让线程进入等待队列,且会抛出中断异常。但它还有一些方法可以忽略中断或不进入阻塞队列:

// 忽略中断
public void acquireUninterruptibly()
public void acquireUninterruptibly(int permits)

// 不进入等待队列,底层使用CAS
public boolean tryAcquire
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException
public boolean tryAcquire(long timeout, TimeUnit unit)

  

# # 场景三:传递消息 

  比方说,你想告诉王阿姨,她很美。王阿姨也想告诉你,你很帅。

  再举一个不恰当的例子。王阿姨要告诉你一个信息,今天她家里就她自己,你可以来跟她一起看电影了。

  注意逻辑,A线程把消息告诉了B线程,B线程才能接着根据这个消息去做其他的事。或者说,只有B线程只有拿到了A线程的消息以后才能接着去做其他事。

 

  # # # 场景三 方案一 :Exchanger类

  这个类是已经实现好了的,具体看怎么用:Exchanger类用于两个线程交换数据。它支持泛型,也就是说你可以在两个线程之间传送任何数据。先来一个案例看看如何使用,比如两个线程之间想要传送字符串:

public class ExchangerDemo {
    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                System.out.println("这是线程A,得到了另一个线程的数据:"
                        + exchanger.exchange("这是来自线程A的数据"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        System.out.println("这个时候线程A是阻塞的,在等待线程B的数据");
        Thread.sleep(1000);

        new Thread(() -> {
            try {
                System.out.println("这是线程B,得到了另一个线程的数据:"
                        + exchanger.exchange("这是来自线程B的数据"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

   请注意看,线程A 调用了 exchanger.exchange 以后,线程A 接着就进入了阻塞状态。只有另外一个线程B 或者 C 同样调用一下这个方法,A才会接着执行,同时,两个线程会彼此交换信息。线程A传给线程B 一份信息,然后进入阻塞状态,然后B接受了消息,并返回了一个消息。A收到了以后,A继续执行,B也执行。

 # # # 场景三 方案二:管道

为了能和王阿姨偷偷的通信,还可以有独特的方式。比方说和王阿姨约定好,把写好的信放在了公园的擦长凳下边。每次。彼此都去长等下拿信。

 

管道是基于“管道流”的通信方式。JDK提供了PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

这里的示例代码使用的是基于字符的:

public class Pipe {
    static class ReaderThread implements Runnable {
        private PipedReader reader;

        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {

        private PipedWriter writer;

        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println("this is writer");
            int receive = 0;
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader); // 这里注意一定要连接,才能通信

        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

// 输出:
this is reader
this is writer
test

我们通过线程的构造函数,传入了PipedWritePipedReader对象。可以简单分析一下这个示例代码的执行流程:

  1. 线程ReaderThread开始执行,
  2. 线程ReaderThread使用管道reader.read()进入”阻塞“,
  3. 线程WriterThread开始执行,
  4. 线程WriterThread用writer.write("test")往管道写入字符串,
  5. 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
  6. 线程ReaderThread接受到管道输出的字符串并打印,
  7. 线程ReaderThread执行完毕。

管道通信的应用场景:

这个很好理解。使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

 

 # # # 场景三 方案三:ThreadLocal

  严格意义上来说:这种也不叫线程之间通信。 比方说 隔壁王叔叔想要瞒着王阿姨存点私房钱,王阿姨也想存放一点私房钱,王叔叔要用作和我喝酒,王阿姨好拿私房钱用来和我一起看电影。

  这样的场景,就是我们往往会遇到不同的线程,在一个变量里边存放的是自己特有的值。比方说A在这个变量里边存放的是 A,B线程存放的是B,A和B存放在通一个地方,A看到的是A,B看到的是B,A不能看到B,B不能看到是A。

 

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。这里不详细介绍它的原理,而是只是介绍它的使用,以后有独立章节来介绍ThreadLocal类的原理。

有些朋友称ThreadLocal为线程本地变量线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。

ThreadLocal类最常用的就是set方法和get方法。示例代码:

public class ThreadLocalDemo {
    static class ThreadA implements Runnable {
        private ThreadLocal<String> threadLocal;

        public ThreadA(ThreadLocal<String> threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("A");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadA输出:" + threadLocal.get());
        }

        static class ThreadB implements Runnable {
            private ThreadLocal<String> threadLocal;

            public ThreadB(ThreadLocal<String> threadLocal) {
                this.threadLocal = threadLocal;
            }

            @Override
            public void run() {
                threadLocal.set("B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("ThreadB输出:" + threadLocal.get());
            }
        }

        public static void main(String[] args) {
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            new Thread(new ThreadA(threadLocal)).start();
            new Thread(new ThreadB(threadLocal)).start();
        }
    }
}

// 输出:
ThreadA输出:A
ThreadB输出:B

可以看到,虽然两个线程使用的同一个ThreadLocal实例(通过构造方法传入),但是它们各自可以存取自己当前线程的一个值。

那ThreadLocal有什么作用呢?如果只是单纯的想要线程隔离,在每个线程中声明一个私有变量就好了呀,为什么要使用ThreadLocal?

如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。

最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

 

  文中的一些案例我没有自己敲,引用的别的地方:http://concurrent.redspider.group/article/01/5.html

 

  

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值