一:并发编程的难点
1:原子性问题
- 操作系统做任务切换,可以发生在任何一条CPU指令执行完成后;
- CPU能保证的原子操作是指令级别的,而不是高级语言的操作符;
n++不是原子操作的,而是3条指令
2:可见性问题
- 可见性是指一个线程对一个变量进行修改,另外一个线程可以看的到
- 可见性问题是由CPU的缓存导致的,多核CPU均有各自的缓存,这些缓存要与内存进行同步。(其实就是多线程环境下,一个线程对一个变量的改变了,而另一个线程没看到,那么的话还是按照原来的变量的值进行计算的话,那么就会出错)。
3:有序性问题
- 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序;
- 重排序不会影响单线程的执行结果,但是在并发情况下,可能会出现诡异的BUG。
二:并发编程
1:并发编程的目标
解决多核多线程下,造成的 缓存不一致问题,指令重排问题,处理器优化问题
- 在cpu和主存之间添加缓存,在多线程下会存在缓存一致性问题(可见性问题)
- 处理器内部为了使运算单元尽可能的被充分利用,处理器可能会对输入的代码进行乱序处理。这就是处理器优化。(原子性问题)
- 很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。(指令重排问题)
2:并发编程的内存模型
- 为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。
- 通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
3:这个内存模型是什么
- JMM是Java Memory Model的缩写,Java线程之间的通信由JMM控制,即JMM决定一个线程对共享变量的写入何时对另一个线程可见。
- JMM定义了线程和主内存之间的抽象关系,通过控制主内存与每个本地内存(抽象概念)之间的交互,JMM为Java程序员提供了内存可见性的保证。
- JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
4:源代码和指令间的重排序
为了提高性能,编译器和处理器常常会对指令做重排序。重排序有3种类型,其中后2种都是处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
- 1.编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 2.指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 3.内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
5:解决CPU带来的重排序
- CPU内存屏障:
- 1.LoadLoad:禁止读和读的重排序;
- 2.StoreStore:禁止写和写的重排序,
- 3.LoadStore:禁止读和写的重排序,
- 4.StoreLoad:禁止写和读的重排序。
- Java内存屏障:
public final class Unsafef{
public native void loadFence();//LoadLoad LoadStore
public native void storeFence();//StoreStore LoadStore
public native void fullFence();//loadFence()+storeFence()+StoreLoad
}
6:解决编译器带来的重排序
(1):如何解决
JMM使用nappens-before规则来阐述操作之间的内存可见性,以及什么时候不能重排序。在JMM中如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在nappens-.before:关系。换个角度来说,如果A happens-before B,则意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。其中,前4条规则与程序员密切相关。
- 1.程序顺序规则:一个线程中的每个操作,happens-before于(对…可见)该线程中的任意后续操作,
- 2.volatile?变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读,
- 3.synchronized规则:对一个锁的解锁,happens-before于随后对这个锁的加锁,
- 4.传递性:若A happens-.before B,且B happens-before C,则A happens-before C,
- 5.start()规则:若线程A执行Thread.start(0,则线程A的start()操作nappens-before于线程B中的任意操作,
- 6.join规则:若线程A执行ThreadB.join0并成功返回,那么线程B中的任意操作happens-.before于线程A从ThreadB.join0的成功返回。
(2):关键字vlatile
- 4.1 volatile的基本特性
- 可见性:对一个volatile变量的读,总是能看到对这个volatile变量最后的写入:
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具有原子性。
- 4.2 volatilet的内存语义
- 写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存
- 读内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。