多线程学习总结---volatile以及wait()、notify()介绍
1.什么时Java内存模型?
再讲这个关键字之前,我们先介绍一下Java内存模型(JMM,Java Memory Model).
JMM规定了jvm内存分为主内存和工作内存 ,
(1) 主内存存放程序中所有的类实例、静态数据等变量,是多个线程共享的;
(2) 工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量,是每个线程私有的其他线程不能访问。每个线程对变量的操作都是以先从主内存将其拷贝到工作内存再对其进行操作的方式进行,多个线程之间不能直接互相传递数据通信,只能通过共享变量来进行。
简单来说,如上图所显示的:线程1,2,3共享同一个主内存的共享变量X。而每个线程在想操作变量X的时候,都需要先从主内存拉取数据到工作内存中.然后再有线程去操作工作内存的变量X的副本数据。最后执行完数据操作后,在推到主内存中更新主内存中。
这是聪明的小伙伴就会发现,如果多个线程同时操作同一个主内存共享的数据资源的时候,当其中有一个线程操作完修改了主内存的变量,但是其他线程的工作内存中仍然使用的还是修改前的变量,这是就会出现问题了。
2.主内存与工作内存的数据交互
(1) JLS一共定义了8种操作来完成主内存与线程工作内存的数据交互:
lock:把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写
unlock:解锁一个主内存变量
read:把一个主内存变量值读入到线程的工作内存
load:把read到变量值保存到线程工作内存中作为变量副本
use:线程执行期间,把工作内存中的变量值传给字节码执行引擎
assign:字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量
store:把工作内存中的变量值传送到主内存
write:把store传送进来的变量值写入主内存的变量中
使用标准的操作再来重现一下上方的2个线程之间的交互流程则是这样的:
线程1从主内存read一个值为0的变量x到工作内存
使用load把变量x保存到工作内存作为变量副本
将变量副本x使用use传递给字节码执行引擎进行x++操作
字节码执行引擎操作完毕后使用assign将结果赋值给变量副本
使用store把变量副本传送到主内存
使用write把store传送的数据写到主内存
线程2从主内存read到x,然后load–>use–>assign–>store–>write
(2)使用这8种操作也有一些规则:
read 和 load必须以组合的方式出现,不允许一个变量从主内存读取了但工作内存不接受情况出现
store和write必须以组合的方式出现,不允许从工作内存发起了存储操作但主内存不接受的情况出现
工作内存的变量如果没有经过 assign 操作,不允许将此变量同步到主内存中
在 use 操作之前,必须经过 load 操作
在 store 操作之前,必须经过 assign 操作
unlock 操作只能作用于被 lock 操作锁定的变量
一个变量被执行了多少次 lock 操作就要执行多少次 unlock 才能解锁
一个变量只能在同一时刻被一条线程进行 lock 操作
执行 lock 操作后,工作内存的变量的值会被清空,需要重新执行 load 或 assign 操作初始化变量的值
对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中
(3)多线程中的原子性、可见性、有序性
volatile与可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
volatile与有序性
有序性即程序执行的顺序按照代码的先后顺序执行。由于代码执行时,出现的指令重排.可能导致后面的代码会优先于前面的执行.
volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save
volatile与原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
volatile是不能保证原子性的。
Java语言提供了一种稍微同步机制,即volatile变量,用来确保将变量的更新操作通知其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作仪器重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile类型变量是总会返回最新写入的值。
在访问volatile变量是不会执行加锁操作,因此也就不会重新执行线程阻塞,volatile变量是一种比synchronized关键字轻量级的同步机制。
总的来说,当一个变量被volatile修饰后,不但具有可见性,而且还禁止指令重排。volatile的读性能消耗与普通变量几乎相同,但是写操作就慢一些,因为它要保证本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
3.多线程-wait()、notify()、yield()、sleep()、join()、interrupt()解释
在Object.java中,定义了wait(),notify()和notifyAll()等接口。
wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;
yield():让当前线程,放弃CPU分配的资源.从运行状态退到到准备状态;
sleep():让当前线程休眠,即当前线程会从“远程状态”进入到“休眠(阻塞)状态”;
join():如果A线程在B线程上调用A.join(),则B线程会进入阻塞状态,等待A线程执行完.
interrupt():作用是中断本线程。本线程中断自己是被允许的;其他线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清理并且会收到一个InterruptedException异常。
下面通过一个简单的例子来理解一下:
问题:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
解析:上面是一个非常经典的多线程使用的问题,多线程在执行时是无序的,如何保证线程再按我们想要的方式顺序执行呢?如何才能保证金在A线程执行完一次后,只能由B线程执行一次,B线程执行完一次后,C线程执行一次,最后在循环到A,依次往后交替进行。
思路:结合之前学习的synchronized,ABC三个线程都会竞争打印的资源,所以添加同步锁以解决资源争抢问题,也保证了一次只有一个线程在执行打印动作。然后就是执行顺序问题了,这个就需要结合wait()和notify()了,利用wait()会让当前线程释放它所持有的锁的特性。具体实现,直接先看代码吧!
代码:
/**
* 问题:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,
* 要求线程同时运行,交替打印10次ABC
* @作者:onereader
* @日期:2018年9月5日
*/
public class MyThreadDemo4 implements Runnable{
private String name;
private Object prev;
private Object self;
public MyThreadDemo4(String name,Object prev,Object self) {
this.name=name;
this.prev=prev;
this.self=self;
}
@Override
public void run() {
for(int i=10;i>0;i--) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThreadDemo4 t1 = new MyThreadDemo4("A", c, a);
MyThreadDemo4 t2 = new MyThreadDemo4("B", a, b);
MyThreadDemo4 t3 = new MyThreadDemo4("C", b, c);
new Thread(t1).start();
Thread.sleep(100);
new Thread(t2).start();
Thread.sleep(100);
new Thread(t3).start();
}
}
看了上面的代码,相信大部分也明白了吧。执行中,第一步就是使用synchronized锁前一个对象,第二步锁当前对象。这样就保证其他两个线程都会进入阻塞并等待锁资源释放。第三步,看到prev.wait();意思是前一个当前线程阻塞,并让当前线程释放所有锁资源,这样下一个线程就可以获取锁资源执行了。
简单描述一下,先是线程A启动,带有对象c和a。执行打印完A后,会调用a.notify()释放a.wait()。然后执行了c.wait()使当前线程A阻塞等待c.notify()。然后执行线程B,带有对象a,b。打印完B后,调用b.notify()释放b.wait()。同时执行a.wait()阻塞当前线程,并等待a.notify()释放。最后执行线程C,同样打印C后,会执行一段代码c.notify(),然后b.wait()阻塞当前线程。这时,线程C执行的c.notify()会让阻塞的线程A从阻塞状态恢复到准备状态,依次往后就有了ABC交替打印的结果了。