上节我们讲了线程的由来、CPU由单核到多核、进程与线程的关系以及多线程的状态,我们还通过示例查看线程状态《并发编程技术一》
通过上节线程状态图可以看到线程终止有两种情况。我们采购程序控制线程中断方法.
调用thread.stop方法(),JDK已经废弃此方法,我们还可以采用如下两种方法
- 使用Thread.interrupt();
- 通过volatile修饰的 boolean 变量
示例代码
public class InterruptDemo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println(i);
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//设置interrupt标识为true
System.out.println(thread.isInterrupted());
}
}
使用volatile修饰变量,若有需要请关注微信公众号"零售云技术"
线程安全性考虑哪些问题
可见性、原子性、有序性
高速缓存
由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了。
缓存一致性
在多处理情况下,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。
除此之外,为了使得处理器内部的运算单元能尽可能被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
Java内存模型
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不同架构下的物理机拥有不一样的内存模型,Java虚拟机也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)
如何解决原子性、可见性、有序性问题
volatile/synchronized/final/j.u.c
原子性
synchronized(monitorenter/monitorexit)
可见性
volatile/synchronized /final
有序性
volatile/ synchronized
主内存和本地内存
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面讲的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示。
基于保守策略的JMM内存屏障插入策略
1.在每个volatile写操作的前面插入一个StoreStore屏障
2.在每个volatile写操作的后面插入一个SotreLoad屏障
3.在每个volatile读操作的后面插入一个LoadLoad屏障
4.在每个volatile读操作的后面插入一个LoadStore屏障
上图的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了
因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存
x86处理器仅仅会对写-读操作做重排序
因此会省略掉读-读、读-写和写-写操作做重排序的内存屏障
在x86中,JMM仅需在volatile后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义
这意味着在x86处理器中,volatile写的开销比volatile读的大,因为StoreLoad屏障开销比较大
为什么volatile保证可见性和有序性?
有序性:
使用volatile可以禁止指令重排优化,就可以保证代码程序会严格按照代码的先后顺序执行。
volatile是通过 内存屏障 来禁止指令重排的
可见性:
java中 volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用前都从主内存刷新。
java中的内存屏障
用于控制特定条件下的重排序和内存可见性问题。Java编译器也会内存屏障的规则禁止重排序
volatile只能保证可见性和有序性,无法保证原子性
为什么volatile不能保证原子性
因为他不是锁,他没做任务可以保证原子性的处理。当然就不能保证原子性了。
锁的获取过程
自旋锁:
循环获取锁
偏向锁:
1. 锁不公能不存在竞争,并且都是由同一个线程获得
2.当一个线程访问同步块时,它会在对象头中的栈针里面的锁记录里面,去存储这个锁的偏向线程Id。也就是存储到mark word中(偏向锁的线程id)
3.如果对象头存在标识,则标识获取锁成功,若是则使用CAS把对象指向线程
当线程2访问时,则无法获取锁就会升级轻量级锁
轻量级锁:
判断当前对象 是否是无锁状态,是则分配空间并复制Mark word到栈,在通过CAS修改Mark word,若修改成功,则将Mark word 替换为轻量级锁(指向栈的指针为00)
如果修改失败,则线程1获得锁,线程2自旋获取锁,自旋锁获取失败,就会锁澎涨,修改为重量级锁(指向重量级的指针10)
重量级锁
在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;
它可以把任意一个非NULL的对象当作锁。
作用于方法时,锁住的是对象的实例(this);
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
留下两个问题
1.wait或者notify为什么要先 获取锁?
2.wait和sleep有什么区别?
-------------
欢迎关注微信公众号“零售云技术” ,文章持续更新