21.3 共享受限资源
21.3.3 原子性和易变性
1. 关于多线程的内存模型是怎样的?
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
下图就是和上面逻辑一样的一个图,左边相当于指令顺序。read and load 从主存复制变量到当前工作内存,use and assign 执行代码,改变共享变量值 ,store and write 用工作内存数据刷新主存相关内容,其中use and assign 可以多次出现。但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
那么这里面提到的关于那个指令在前那个在后只需要掌握以下规则就可以了,SR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。happens- before的定义很微妙,后文会具体说明happens-before为什么要这么定义。
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6,线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
所以这就是为什么volatile只具有可视性而不具有原子性,可视性就是说在其他线程拿到该变量是在这个变量最新一次修改之后的值,所以这种修改应该也是原子性的修改才最好.
2. volatile和synchronized的区别?
我们首先通过一个例子
int i1;
int geti1() {
return i1;
}
volatile int i2;
int geti2() {
return i2;
}
int i3;
synchronized int geti3() {
return i3;
}
geti1()在当前线程中立即获取在i1变量中的值。线程可以获得变量的本地拷贝,而所获得的变量的值并不一定与其他线程所获得的值相同。特别是,如果其他的线程修改了i1的值,那么当前线程获得的i1的值可能与修改后的值有所差别。实际上,Java有一种主内存的机制,使用一个主内存来保存变量当前的正确的值。线程将变量的值拷贝到自己独立的内存中,而这些线程的内存拷贝可能与主内存中的值不同。所以实际当中可能发生这样的情况,在主内存中i1的值为1,线程1和线程2都更改了i1,但是却没把更新的值传回给主内存或其他线程中,那么可能在线程1中i1的值为2,线程2中i1的值却为3。
另一方面,geti2()可以有效的从主内存中获取i2的值。一个volatile类型的变量不允许线程从主内存中将变量的值拷贝到自己的存储空间。因此,一个声明为volatile类型的变量将在所有的线程中同步的获得数据,不论你在任何线程中更改了变量,其他的线程将立即得到同样的结果。由于线程存取或更改自己的数据拷贝有更高的效率,所以volatile类型变量在性能上有所消耗。
那么如果volatile变量已经可以使数据在线程间同步,那么synchronizes用来干什么呢?两者有两方面的不同。首先,synchronized获取和释放由监听器控制的锁,如果两个线程都使用一个监听器(即相同对象锁),那么监听器可以强制在一个时刻只有一个线程能处理代码块,这是最一般的同步。另外,synchronized还能使内存同步。在实际当中,synchronized使得所有的线程内存与主内存相同步。所以geti3()的执行过程如下:
1. 线程从监听器获取对象的锁。(这里假设监听器非锁,否则线程只有等到监听器解锁才能获取对象锁)
2. 线程内存更新所有的变量,也就是说他将读取主内存中的变量使自己的变量保证有效。(JVM会使用一个“脏”标志来最优化过程,使得仅仅具有“脏”标志变量被更新。详细的情况查询JAVA规范的17.9)
3. 代码块被执行(在这个例子中,设置返回值为刚刚从主内存重置的i3当前的值。)
4. 任何变量的变更将被写回到主内存中。但是这个例子中geti3()没有什么变化。
5. 线程释放对象的锁给监听器。
所以volatile只能在线程内存和主内存之间同步一个变量的值,而synchronized则同步在线程内存和主内存之间的所有变量的值,并且通过锁住和释放监听器来实现。显然,synchronized在性能上将比volatile更加有所消耗。
Java递增操作不是原子性的。
_两者区别:(主要结合内存模型来理解具体是用volatile可以可以满足了)_
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
21.4 终结任务
21.4.3 中断
1. 当线程A运行的时候执行线程B中断,线程B该如何相应中断?
interrupt()是一种相对温和的中断方法,不同于stop那么暴力,stop是直接杀死线程并释放所有资源。
每个线程都有一个与之相关联的 Boolean 属性(有点类似于有个cancle来标志是否要中断),用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况之一。如果那个线程在执行一个低级可中断阻塞方法,例如Thread.sleep()、 Thread.join() 或 Object.wait(),那么它将取消阻塞并抛出 InterruptedException。否则, interrupt()只是设置线程的中断状态。 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过Thread.isInterrupted() 来读取,并且可以在执行了Thread.interrupted() 的操作中将中断状态将被清除。
2. 如何中断一个没有线程,该线程的任务是没有阻塞等指令的
当一个线程被执行中断也只是改变了他的状态,真正是否要中断需要他自己来执行,然后当线程执行之后当执行到一个可能发生阻塞的命令时,该线程就主动抛出了InterruptedException,并且结束该线程,那么一些清理工作就可以在catch中做了。如果任务里面没有阻塞等命令,那么需要任务自己去判断通过interrupted()方法,这个方法就是去查询中断状态,并返回状态,同时返回之后就清除了中断状态了。但问题的关键就是如果在这样一个线程中是循环任务,虽然中断了跳出循环但是这个线程却没有结束,我们该怎么办?
一种更优雅的方式则是:抛出InterruptedException异常,例如下面的代码
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try{
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("should be stopped and exit");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("this line cannot be executed. cause thread throws exception");//这行语句不会被执行!!!
}catch(InterruptedException e){
System.out.println("catch interrupted exception");
e.printStackTrace();
}
}
}
我们虽然抛出了异常来结束线程,但是这样的方式是不好的,也就是说你得让更高层的代码知道这个线程被中断了,而这里面只是让异常在内部消化了,但是我又不能说在run方法中后面throws,毕竟这是一个实现了Runnable接口的方法。所以最好的办法就是来调用 interrupt() 以 “重新中断” 当前线程。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try{
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("should be stopped and exit");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("this line cannot be executed. cause thread throws exception");
}catch(InterruptedException e){
/**这样处理不好
* System.out.println("catch interrupted exception");
* e.printStackTrace();
*/
Thread.currentThread().interrupt();//这样处理比较好
}
}
}
这样,就由 生吞异常 变成了 将 异常事件 进一步扩散了
3. 有哪些程序在线程执行的任务中是不能够被中断的?,如果要中断他们该怎么办?
I/O和在synchronized块上的等待是不可中断的;
对于I/O操作一般是要先关闭低层资源才能中断线程;
对于synchronized块以一种不可中断的方式被阻塞,但是使用RcentrantLock加锁,这种阻塞是可以中断的。
21.5 线程间的协作
21.5.1 wait()与notifyAll()
1. 为什么wait、notify等要在同步代码块中?
首先我们要理解在Java中每个对象都可以作为一个监视器,这个监视器的结构有三个部分,分别是一个锁,一个线程入口队列,一个线程等待队列。如果说线程需要同步去执行某个对象的方法,那么首先你需要去获得这个方法的对象锁,然后该线程进入到入口队列中,其他线程在进入到这个方法时因为没有拿到锁会作为阻塞状态。
只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。为什么这么说呢,当调用wait方法时,这个线程会释放该对象的锁,并进入到这个对象的等待队列,那么你想当调用wait方法之前你得先获得对象锁才能谈得上释放吧,同理notify也是如此,你得先获得锁然后才能够调用方法来唤醒等待队列上的线程(自身可能不会唤醒,因为只唤醒等待队列上面的),自己会释放锁。
这就是为什么wait(),notify()和notifyAll()方法必须在同步控制方法或者同步控制块中被调用,而为什么用对象(锁)来调用,也是因为由对象来执行如何在队列中加入或者移除线程。
21.5.2 notify()与notifyAll()
- 示例程序
class Blocker {
synchronized void waitingCall() {
try {
while(!Thread.interrupted()) {
wait();
System.out.print(Thread.currentThread() + " ");
}
} catch(InterruptedException e) {
// OK to exit this way
}
}
synchronized void prod() { notify(); }
synchronized void prodAll() { notifyAll(); }
}
class Task implements Runnable {
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); }
}
class Task2 implements Runnable {
// A separate Blocker object:
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); }
}
public class NotifyVsNotifyAll {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new Task());
exec.execute(new Task2());
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
boolean prod = true;
public void run() {
if(prod) {
System.out.print("
notify() ");
Task.blocker.prod();
prod = false;
} else {
System.out.print("
notifyAll() ");
Task.blocker.prodAll();
prod = true;
}
}
}, 400, 400); // Run every .4 second
TimeUnit.SECONDS.sleep(5); // Run for a while...
timer.cancel();
System.out.println("
Timer canceled");
TimeUnit.MILLISECONDS.sleep(500);
System.out.print("Task2.blocker.prodAll() ");
Task2.blocker.prodAll();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("
Shutting down");
exec.shutdownNow(); // Interrupt all tasks
}
}
- 结果
1. 锁队列和等待队列区别?
先区分一个概念,如果一个线程调用了某个对象上的wait方法,那么他会进入到该对象的等待队列中,并且会将锁释放,而其他线程如果要进入到这个同步区域中,是待在这个对象的锁队列(入口队列)中,需要通过优先级等来竞争对象锁进入到同步区域中的。但是这两个队列都是由对象来管理的。
2. notify和notifyAll区别?
以上面的程序为例,当5个线程执行之后,开始只有一个线程获得对象锁进入到waitingCall中,而在这个过程中其他四个线程都进入到这个锁对象的锁队列中等待锁。而获得锁的线程执行了(主动执行)wait之后会进入到该锁对象的等待队列中,并释放锁,那4个线程就会来抢锁来执行,但是那个执行了wait的线程是不会参与抢锁的过程,他只能够通过notify或者notifyAll来进入到锁队列中才能开始主动抢锁。
重复上面的流程后,5个线程全部都进入到对象锁的等待队列中。
当第一次执行notify的时候只会选择一个等待队列中的线程来唤醒,然后该线程注意并不是重新开始执行run,因为内存中是会保存该线程之前的一些状态等参数,所以该线程是继续执行也就是执行了print那一步,并输出了该线程的名字。然后while循环又执行了wait,再一次加入到了等待队列中了。
当第二次执行的是notifyAll的时候,会将等待队列中的所有线程全部唤醒,这些线程全部进入到了对象的入口队列中了,开始竞争对象锁了,那么接下来又和上面第一段说的流程是一样的了。
根据面向对象的特点来理解就是这个线程如果如何进入队列,进入什么队列属于行为,这个是某个对象的行为,而这个对象想来想去只能是锁对象,而谁可以作为锁,所有对象都可以当锁,所以wait、notify、notifyAll是Object的一部分,由锁来操作这个流程,这也就很好理解为什么在task2中虽然用notifyAll却只打印出一个线程,因为调用notifyAll的那个锁对象的等待队列中只存了这个线程。
上面说到的有些是在同步代码块中的前提下说明的,wait和notify等是一定得在同步代码块中,而线程执行的过程不一定要在同步代码中(只是为了保证互斥访问),所以在同步代码中上面的流程就是那样的。
3. wait、sleep和yield方法区别?
wait()和sleep()的关键的区别在于,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。
sleep的对象是线程而wait的对象是Object,所以执行sleep是线程调用的,而且是Thread,因为sleep是静态方法,它暂停的是当前线程。同时如果在同步代码块中调用了sleep之后是不会释放对象锁的,只能够等同步代码执行完了才能释放。如果其他的线程中断了一个休眠的线程,sleep方法会抛出Interrupted Exception。
回到yield()方法上来,与wait()和sleep()方法有一些区别,它仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能够获得CPU资源。谁能获得CPU完全取决于调度器,在有些情况下调用yield方法的线程甚至会再次得到CPU资源。所以,依赖于yield方法是不可靠的,它只能尽力而为,而且我们也尽量是不会使用yeild的。
所以,根据你的需求,如果你需要暂定你的线程一段特定的时间就使用sleep()方法,如果你想要实现线程间通信就使用wait()方法。