社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
涉及到并发,我们通常会想到两方面,一方面是使用多线程实现程序并发,另一方面是使用锁防止程序并发。不管是哪一方面都涉及到两大核心,三大性质。
两大核心分别是Java内存模型、Happen-Before原则。三大性质分别是原子性、可见性、有序性。
在并发编程中主要需要解决两个问题:一、线程之间如何通信;二、线程之间如何完成同步。通信是指线程之间以何种机制来交换信息,主要有两种方式:共享内存和消息传递。而Java内存模型就是共享内存的并发模型,也可以说Java内存模型是共性内存方式的基础,线程之间主要通过读-写共享变量来完成隐式通信。
Java内存模型,即JMM(Java Memory Model),规定了所有的共享变量都存储在主内存(Main Memory)中,每个线程拥有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的主内存中变量的的拷贝副本,线程改变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接操作主内存中的变量。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的
Java内存模型包括主内存和工作内存。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。
在并发编程中,我们通常需要考虑线程间如何通信。线程之间的通信机制有两种:共享内存和消息传递。
而JMM的目标正是共享内存通信方式的基础,JMM决定一个线程对共享变量的写入何时对另一个线程可见,所以说JMM控制了变量在主内存和工作内存读写方式,也就控制了共享数据的公共状态,保证了共享数据的安全。
上边提到了隐式通信,何为隐式通信?线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。
java内存模型为了提高性能,提供并发性,会运行编译器和处理区常常会对指令进行重排序,从而减少等待,提高性能。一般重排序可以分为如下三种:
如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是单例双重锁问题。针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。
重排序确实可以提升性能,但是有时候会造成实际结果有我们所期望的结果不符,所以,JMM又提供了happers-before。而happens-before所做的事就是,每一条规则对应一个或多个编译器和处理器重排序规则,对于程序员而言,happens-before相当于对于重排序规则以及具体实现做了封装,方便程序员使用,程序员只需要理解happens-before规则就好,不需要去理解复杂的重排序相关的事儿。
happens-before指定两个操作之间的执行顺序。具体定义为
第一条是对程序员而言,方便程序员使用,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!实际并不是真正的不可重排序。
第二条是编译器和处理器重排序而言的约束原则。只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。所以说,第一条只是方便程序员使用时方便,程序员关系的只是结果,只要结果不变,是否真的被重排序并不重要。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!