JVM在运行时会将本身管理的内存区域,划分为不一样的数据区,称为运行时数据区。每一个线程都有本身私有的内存空间,以下图示:java
Java线程按照本身虚拟机栈中的方法代码一步一步的执行下去,在这一过程当中不可避免的会使用到线程共享的内存区域堆或方法区。为了防止多个线程在同一时刻访问同一个内存地址,须要互相告知本身的状态以免资源争夺。编程
线程的通讯方式主要分为三种方式:①共享内存②消息传递③管道流数组
共享内存:线程之间经过对共享内存的读-写来实现隐式通讯。Java中的具体实现是:volatile共享内存。缓存
消息传递:线程之间经过明确的发送消息来实现显示通讯。Java中的具体实现是:等待/通知机制(wait/notify),join方法。安全
管道流:管道输入/输出流。多线程
其过程是:线程A因为某些缘由,自主调用了对象o的wait方法,进入WAITING状态,释放占有的锁并等待通知。而线程B则调用对象o的notify方法或notifyall方法进行通知,线程A会收到通知,并从wait方法中返回,继续执行后面的代码。并发
能够发现,线程A和线程B就是经过对象o的wait方法和notify方法来发送消息,进行通讯。ide
wait方法和notify方法是Object类的方法,而Object类是全部类的父类,所以全部对象都实现了Object类的方法。即全部的对象都具备wait方法和notify方法。学习
方法 | 做用 | 备注 |
---|---|---|
wait | 线程调用共享对象的wait()方法后会进入WAITING状态,释放占有的对象锁并等待其余线程的通知或中断才从该方法返回。 | 该方法能够传参数,wait(long n):超时等待n毫秒,进入TIME-WAITING状态,若是在n毫秒内没有通知或中断,则自行返回 |
notify | 线程调用共享对象的notify()方法后会通知一个调用了wait方法并在此等待的线程返回。但因为在共享变量上等待的线程可能不止一个,故具体通知哪个线程是随机的。 | notifyAll()方法与notify()方法做用一致,不过notify是随机通知一个线程,而notifyAll则是通知全部在该共享变量上等待的线程 |
因为线程的等待/通知机制须要借助共享对象,因此在调用wait方法前,线程必须先得到该对象的锁,即只能在同步方法或同步块(synchronized代码块)中调用wait方法,在调用wait方法后,线程释放锁。优化
一样的notify方法在调用前也须要得到对象的锁,即也只能在同步方法或同步块中调用notify方法。如有多个线程在等待,则线程调度器会随机挑选一个线程来通知。须要注意的是,被通知的线程并不会在获得通知后就立刻从wait方法返回,而是须要等待得到对象的锁后才能从wait方法返回。而调用了notify方法的线程也并不会在调用时就立刻释放对象的锁,而是在执行完同步方法或同步块(synchronized代码块)后,才释放对象的锁。所以,被通知的线程要等调用了notify的线程释放锁后,才能从wait方法中返回。
综上所述,等待/通知机制的经典范式以下:
/** * 等待线程(调用wait方法的线程) */ synchronized(共享对象){ //同步代码块,进入条件是得到锁 while(判断条件){ //进行wait线程任务的条件不知足时进入 共享对象.wait() } 线程任务代码 } /** * 通知线程(调用notify方法的线程) */ synchronized(共享对象){ //同步代码块,进入条件是得到锁 线程任务代码 改变wait线程任务的条件 共享对象.notify() }
根据以上范式,有代码以下:
public class WaitNotify { static boolean flag = true; //等待线程继续执行往下执行的条件 static Object lock = new Object(); //上锁的对象 public static void main(String[] args) throws InterruptedException { Thread waitThread = new Thread(new WaitRunnable(),"waitThread"); //以WaitRunnable为任务类的线程 Thread notifyThread = new Thread(new NotifyRunnable(),"notifyThread"); //以NotifyRunnable为任务类的线程 waitThread.start(); //wait线程启动 Thread.sleep(2000); //主线程休眠2s notifyThread.start(); //notify线程启动 } /** * Runnable等待实现类 * synchronized关键字:能够修饰方法或者以同步块的形式来使用 */ static class WaitRunnable implements Runnable{ @Override public void run() { //对lock加锁 synchronized(lock){ //判断,若flag为true,则继续等待(wait) while(flag){ try { System.out.println( Thread.currentThread().getName()+ "---flag为true,等待 @"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) ); lock.wait(); //等待,并释放锁资源 } catch (InterruptedException e) { e.printStackTrace(); } } //若flag为false,则进行工做 System.out.println( Thread.currentThread().getName()+ "---flag为false,运行 @"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) ); } } } /** * Runnable通知实现类 */ static class NotifyRunnable implements Runnable{ @Override public void run(){ //对lock加锁 synchronized(lock){ //以NotifyRunnable为任务类的线程释放lock锁,并进行通知后,以Wait为任务类的线程才能够跳出循环 System.out.println( Thread.currentThread().getName()+ "---当前持有锁,释放 @"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) ); lock.notifyAll(); //通知全部正在等待的线程从wait返回 flag = false; try { Thread.sleep(5000); //notifyThread线程休眠5s } catch (InterruptedException e) { e.printStackTrace(); } } //再次对lock加锁,并休眠 synchronized (lock){ System.out.println( Thread.currentThread().getName()+ "---再次持有锁,休眠 @"+ new SimpleDateFormat("hh:mm:ss").format(new Date()) ); try { Thread.sleep(2000); //再次让notifyThread线程休眠2s } catch (InterruptedException e) { e.printStackTrace(); } } } } } //该代码示例来自《Java并发编程的艺术》
其结果以下:
waitThread---flag为true,等待 @01:53:51 notifyThread---当前持有锁,释放 @01:53:53 waitThread---flag为false,运行 @01:53:58 notifyThread---再次持有锁,休眠 @01:53:58
以上代码根据等待/通知的经典范式,设置一个线程是否继续往下执行的条件变量flag,以及一个共享对象lock,并使用synchronized关键字对lock上锁。
waitThread线程是等待线程,在启动时会尝试得到锁,成功则进入synchronized代码块。在synchronized代码块中,若是条件不知足(即flag为true),则waitThread线程会进入while循环,并在循环体中调用wait方法,进入WAITING状态及释放锁资源。直到有其余线程调用notify方法通知才从wait方法返回。
notifyThread线程是通知线程,在启动时也会尝试得到锁,成功则一样进入synchronized代码块。在synchronized代码块中,notifyThread线程会改变条件,使waitThread线程能够继续往下执行(即令flag为false),同时notifyThread线程也会调用notyfiAll方法,让waitThread线程收到通知。
但注意,notifyThread线程并不会在调用notyfiAll方法后就立刻释放锁,而是在执行完synchronized代码块的内容后才释放锁。咱们在notifyThread线程调用notyfiAll后,将该线程休眠5s。能够从打印结果发现,在notifyThread线程休眠的5s中,即便waitThread线程获得了通知,且继续运行的条件也已知足(flag为flase),但waitThread线程在这5s中依然没有获得执行。在notifyThread线程5s的休眠时间结束后,并从synchronized代码块退出,waitThread线程才继续执行。因此,等待线程在获得通知后,仍然须要等待通知线程释放锁,而且在尝试得到锁成功后才能真正从wait方法中返回,并继续执行。
有以下代码,
/** * @Author Feng Jian * @Date 2021/1/20 13:18 * @Version 1.0 */ public class JMMTest { private static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread My_Thread = new Thread(new Runnable() { @Override public void run() { while(run){ //... } } }, "My_Thread"); My_Thread.start(); //启动My_Thread线程 System.out.println(Thread.currentThread().getName()+"正在休眠@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run); Thread.sleep(1000); //主线程休眠1s run = false; //改变My_Thread线程运行条件,但My_Thread线程并不会停下 System.out.println(Thread.currentThread().getName()+"正在运行@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run); } }
定义了一个变量run,并以此做为My_Thread线程中while循环执行的条件。在启动My_Thread线程,并使主线程休眠1s后,改变变量run的值。其结果以下:
能够看出,即便是run的值已经改变,但My_Thread线程依然不会停下来。为何呢?这就须要了解Java的内存模型(JMM)。
咱们知道,CPU要从内存中读取出数据来进行计算,但实际上CPU并不老是直接从内存中读取数据。因为CPU和内存间(常称之为主存)的速度不匹配(CPU的速度比主存快得多),为了有效利用CPU,使用多级cache的机制,如图
所以,CPU读取数据的顺序是:寄存器-高速缓存-主存。主存中的部分数据,会先拷贝一份放到cache中,当CPU计算时,会直接从cache中读取数据,计算完毕后再将计算结果放置到cache中,最后在主存中刷新计算结果。所以每一个CPU都会拥有一份拷贝。
以上只是CPU访问内存,进行计算的基本方式。实际上,不一样的硬件,访问过程会存在不一样程度的差别。好比,不一样的计算机,CPU和主存间可能会存在三级缓存、四级缓存、五级缓存等等的状况。
为了屏蔽掉各类硬件和操做系统的内存访问差别,实现让 Java 程序在各类平台下都能达到一致的内存访问效果,定义了Java的内存模型(Java Memory Model,JMM)。
JMM 的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到主存和从主存中取出变量这样的底层细节。这里的变量指的是可以被多个线程共享的变量,它包括了实例字段、静态字段和构成数组对象的元素,方法内的局部变量和方法的参数为线程私有,不受JMM的影响。
Java的内存模型以下,
JMM定义了线程和主内存之间的关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储着主内存中的共享变量的副本。
JMM规定:将全部共享变量放到主内存中,当线程使用变量时,会把其中的变量复制到本身的本地内存,线程读写时操做的是本地内存中的变量副本。一个线程不能访问其余线程的本地内存。
本地内存其实只是一个抽象的概念,它实际上并不真实存在,其包含了缓存、写缓冲区、寄存器以及其余的硬件和编译器的优化。
在多线程环境下,因为每一个线程都有主内存中共享变量的副本,因此当线程运行时,读取的是本身本地内存中的共享变量的副本,这就产生了线程的安全问题:好比主内存中的共享变量i为1,线程A和B从主内存取出变量i,放入本身的本地内存中成为共享变量i的副本。当线程A执行时,会直接从本身的本地内存中读取副本变量i的值,进行加1计算,完成后更新本地内存中的副本i的值,再写回到主内存中,此时主内存中的i的值为2。
而若是此时线程B也须要用到变量i的值,则它并不会去主内存中读取i的值,而是直接在本身的本地内存中读取i的副本,而此时线程B的本地内存中的副本i的值依然为1,而不是通过线程A修改后的,主内存中的值2。
这也是为何在上述代码中,main线程明明已经修改了变量run的值,但My_Thread线程依然在执行while循环的缘由。如图所示,
这一样是JMM所要处理的多线程可见性的问题:当一个共享变量在多个线程的工做内存中都有副本时,若是一个线程修改了这个共享变量的副本值,那么其余线程应该可以看到这个被修改后的值。即如何保证指令不会受 cpu 缓存的影响。
回到上述的代码,如何使My_Thread线程能接收到main线程已经修改run = false
的信息?即My_Thread线程和main线程如何可以通讯。
根据Java的内存模型,这两个线程若是须要通讯,则必须经历如下两步:
①main线程把本地内存中修改过的共享变量run的值刷新到主内存中。
②My_Thread线程到主内存中去读取main线程以前已经更新过的共享变量run的值。
这意味着,两个线程的通讯必须通过主内存。Java提供volitale关键字实现这一要求。
volitale关键字能够用来修饰字段(成员变量),告知Java程序任何对该变量的访问都要从共享内存(主内存)中获取,而对它的改变都必须同步刷新回共享内存,故volitale关键字能够保证全部线程对变量访问的可见性。即对共享变量的读写都须要通过主内存,所以达到线程经过共享内存进行通讯的目的。
知道了线程之间如何经过共享内存进行通讯,咱们改写一下上述代码,使main线程修改完run = false
后,My_Thread线程中的while循环即当即中止。
实际上只须要给共享变量run加上volitale关键字便可:
private static volatile boolean run = true;
修改后的运行结果以下:
可见,在main线程修改共享变量run的值后,即刷新回主内存。而My_Thread线程读取主内存中的run发现值为false后即中止了while循环。
实际上,也可使用synchronized关键字来保证内存可见性问题,实现线程通讯。其机制是:在synchronized修饰的同步块中,若是对一个共享变量进行操做,将会清空线程本地内存中此变量的值,并在使用这个共享变量前从新在主内存中读取这个变量的值。而在同步块执行完毕,释放锁资源时,则必须先把此共享变量同步回主内存中。
因为还未学习使用到,先暂时略过。。。
以上内容为本人在学习过程当中所作的笔记。参考的书籍、文章或博客以下:
[1]方腾飞,魏鹏,程晓明. Java并发编程的艺术[M].机械工业出版社.
[2]霍陆续,薛宾田. Java并发编程之美[M].电子工业出版社.
[3]Simen郎. 拜托,线程间的通讯真的很简单.知乎.https://zhuanlan.zhihu.com/p/138689342
[4]极乐君.Java线程内存模型,线程、工做内存、主内存.知乎.https://zhuanlan.zhihu.com/p/25474331