一、多线程之间的通信(Java版本)
1、多线程概念介绍
多线程概念
-
在我们的程序层面来说,
多线程
通常是在每个进程
中执行的,相应的附和我们常说的线程与进程
之间的关系。线程与进程的关系:线程可以说是进程的儿子,一个进程可以有多个线程
。但是对于线程来说,只属于一个进程。再说说进程,每个进程的有一个主线程
作为入口,也有自己的唯一标识PID
,它的PID也就是这个主线程的线程ID
。 -
对于我们的计算机硬件来说,线程是
进程
中的一部分,也是进程的的实际运作单位,它也是操作系统中的最小运算调度单位。多线程可以提高CPU的处理速度。当然除了单核CPU,因为单核心CPU同一时间只能处理一个线程。在多线程环境下,对于单核CP来说,并不能提高响应速度,而且还会因为频繁切换线程上下文导致性能降低。多核心CPU具有同时并行执行线程的能力,因此我们需要注意使用环境。线程数超出核心数时也会引起线程切换,并且操作系统对我们线程切换是随机的。
2、线程之间如何通信
引入
- 对于我们Java语言来说,多线程编程也是它的特性之一。我们需要利用多线程操作同一
共享资源
,从而实现一些特殊任务。上面说了,多线程在进行切换时CPU随机调度
的,假如我们直接运行多个线程操作共享资源的话,势必会引起一些不可控错误因素。 - 接下来,我们就需要让这些不可控变为可控 !这个时候就引出了本文的重点线程通信。线程通信就是
为了解决多线程对同一共享变量的争夺
。
Java 线程通信的方式
- 共享内存机制
- 比如说Java的volatile关键字就是基于内存屏障解决变量的可见性,从而实现其他线程访问共享变量都是必须从主存中获取(对应其他线程对变量的更新也得及时的刷新到主存)。
- synchronized 关键字基于对象锁这种方式实现线程互斥,可以通知对方有其他的线程正在执行这部分代码。
- 消息传递模式
- wait() 和 notify()/notifyAll() 等待通知方式实现线程的阻塞就绪状态之间的转换。
- park、unpark
- join() 阻塞【底层也是依赖wait实现】。
- interrupt()打断阻塞状态。
- 管道输入/输出。
3、线程通信方法详细介绍
主要介绍wait/notify,也有ReentrantLock的Condition条件变量的await/signal,LockSupport的park/unpark方法,也能实现线程之间的通信。主要是阻塞/唤醒通信模式。
首先说明这种方法一般都是作用于调用方法的所在线程。比如在主线程执行wait方法,就是将主线程阻塞了。
wait/notify机制
- wait()、notify方法在Java中是Object提供给我们的。又因为所有的类都默认隐式继承了Object类,进而我们的每一个对象都具有wait和notify。
- wait方法含义:一个线程一旦调用了任意对象obj.wait()方法,它就释放了所持有的监视器对象(obj)上的锁,并转为非运行状态(阻塞)。
- notify方法含义:一个线程若执行obj.notify方法,则随机唤醒obj对象上监视器(操作系统也称为管程)monitor的阻塞队列waitset中一个线程。
- wait和notify方法的使用同时必须配合synchronized关键字使用。同时也需要成对出现。就是说wait和notify必须得在同步代码块内部使用,大致原因就是需要保证同时只有一个线程可以去执行wait,使该线程阻塞。
await/signal
- 要想使用await/signal首先是需要借用Condition条件变量,要想获取Condition条件变量,就必须通过ReentrantLock锁获取。
- ReentrantLock和Synchronized类似,都是可重入锁,并且大多都是当做重量级锁使用。
- 区别:ReentrantLock是API层面实现的,我们可以根据自己随意调用定制,但是Synchronized是JVM底层实现,我们无需关心他上锁解锁的流程。
- await/signal使用时需要配合ReentrantLock锁对象的lock和unlock方法加锁解锁。就像wait/notify在synchronized在同步代码块中使用一样。他们都需要保证当前线程是唯一执行这段逻辑的线程。防止出现多线程造成的线程安全问题。
park/unpark
二、线程通信过程中需要注意的问题
1、唤醒丢失
如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。
- 唤醒丢失主要是在我们使用wait 和 notify的过程中的时序问题。比如说我们线程二在执行某个对象notify的时候,线程一还没有执行该对象的wait方法。那么这次的唤醒就会丢失,我们就不能让线程二得notify方法起作用,自然而然线程一就不会被唤醒。
- 举个例子吧,这就好比我们平常在宿舍每天都会有叫醒服务,但是这次 因为一些原因(通宵···)我一整晚都没有睡觉,而且当第二天早上的叫醒服务来的 时候也是醒着的。那么叫醒服务就会以为你已经醒来了,就会视而不见。没想到吧,叫醒服务刚走我就躺下来睡着了,所以我错过了这次叫醒服务。就能好好的睡亿觉了。这看起来没有什么大问题,但是你仔细想想若是每个睡着的人都需要被叫醒服务才能醒过来,外加上只有一次叫醒服务的机会。那么你就可以沉睡万年了,开心不。
- 哈哈哈···
- 这在程序中也是一样 的,如果错过notify那么就会一直wait。
- 所以我们必须预防这种问题,比如说每隔一段时间去唤醒,也就是隔两分钟就去叫醒睡着的人。但是这种缺点就是太累了,对于程序来说是消耗性能和内存。实现也简单就是写入while循环体中,不停地尝试即可。
- 我们也可以使用一个标志位完美的实现。初始化设置
flag=FALSE
表示还没wait,在wait之前将设置flag=TRUE
,在notify之后设置flag=FALSE
。每次notify唤醒之前都判断flag=true
是否已经wait,在wait中判断flag=false
是否已经notify。
核心代码演示
- 首先使用线程池创建线程一使自己进入阻塞态,然后再调用LOCK1的notify方法唤醒线程一
// 线程一使用LOCK1对象调用wait方法阻塞自己
executor.execute(new ThreadTest("线程一",LOCK1,LOCK2));
synchronized (LOCK1) {
System.out.println("main执行notify方法让线程一醒过来");
LOCK1.notify();
}
-
但是他很有可能醒不来,因为主线程调用LOCK1对象的notify方法,可能主线程已经执行完了,上面线程还没创建完成,也就是没有进入wait状态。就醒不来了。
-
解决方式:使用信号量标志进行判断是否已经进入wait
synchronized (LOCK1) { while (true) { if (FLAG.getFlag()) { System.out.println("m