Home >Java >javaTutorial >Analysis of JMM High Concurrency Programming Examples in Java
JMM is the Java memory model. Because there are certain differences in memory access under different hardware manufacturers and different operating systems, various problems will occur when the same code runs on different systems. Therefore, the Java Memory Model (JMM) shields the memory access differences of various hardware and operating systems to achieve consistent concurrency effects for Java programs on various platforms.
The Java memory model stipulates that all variables are stored in main memory, including instance variables and static variables, but does not include local variables and method parameters. Each thread has its own working memory. The thread's working memory stores the variables used by the thread and a copy of the main memory. The thread's operations on variables are all performed in the working memory. Threads cannot directly read or write variables in main memory.
Different threads cannot access variables in each other's working memory. The transfer of variable values between threads needs to be completed through main memory.
The working memory of each thread is independent. Thread operation data can only be performed in the working memory and then flushed back to the main memory. This is the basic way threads work as defined by the Java memory model.
A warm reminder, some people here will misunderstand the Java memory model as the Java memory structure, and then answer the questions about heap, stack, and GC garbage collection. In the end, it is far from the question the interviewer wants to ask. In fact, when asked about the Java memory model, they generally want to ask questions related to multi-threading and Java concurrency.
This is simple. The entire Java memory model is actually built around three characteristics. They are: atomicity, visibility, and orderliness. These three characteristics can be said to be the foundation of the entire Java concurrency.
Atomicity means that an operation is indivisible and uninterruptible, and a thread will not be interfered with by other threads during execution.
The interviewer took a pen and wrote a piece of code. Can the following lines of code guarantee atomicity?
int i = 2; int j = i; i++; i = i + 1;
The first sentence is a basic type assignment operation, which must be an atomic operation.
The second sentence reads the value of i first, and then assigns it to j. This two-step operation cannot guarantee atomicity.
The third and fourth sentences are actually equivalent. First read the value of i, then 1, and finally assign it to i. It is a three-step operation and cannot guarantee atomicity.
JMM can only guarantee basic atomicity. If you want to ensure the atomicity of a code block, it provides two bytecode instructions, monitorenter and moniterexit, which are the synchronized keyword. Therefore operations between synchronized blocks are atomic.
Visibility means that when a thread modifies the value of a shared variable, other threads can immediately know that it has been modified. Java uses the volatile keyword to provide visibility. When a variable is modified volatile, the variable will be flushed to the main memory immediately after being modified. When other threads need to read the variable, they will read the new value from the main memory. Ordinary variables cannot guarantee this.
In addition to the volatile keyword, final and synchronized can also achieve visibility.
The principle of synchronized is that after execution is completed and before entering unlock, the shared variables must be synchronized to the main memory.
Final modified fields, once initialization is completed, will be visible to other threads if no object escapes (meaning that the object can be used by other threads after initialization is completed).
In Java, you can use synchronized or volatile to ensure the orderliness of operations between multiple threads. There are some differences in the implementation principles:
The volatile keyword uses memory barriers to prohibit instruction reordering to ensure orderliness.
The principle of synchronized is that after a thread is locked, it must be unlocked before other threads can re-lock, so that the code blocks wrapped in synchronized are executed serially among multiple threads.
There are 8 types of memory interaction operations:
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
我再补充一下JMM对8种内存交互操作制定的规则吧:
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有assign的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
很多并发编程都使用了volatile关键字,主要的作用包括两点:
保证线程间变量的可见性。
禁止CPU进行指令重排序。
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
volatile保证可见性的流程大概就是这个一个过程:
先说结论吧,volatile不能一定能保证线程安全。
怎么证明呢,我们看下面一段代码的运行结果就知道了:
public class VolatileTest extends Thread { private static volatile int count = 0; public static void main(String[] args) throws Exception { Vector<Thread> threads = new Vector<>(); for (int i = 0; i < 100; i++) { VolatileTest thread = new VolatileTest(); threads.add(thread); thread.start(); } //等待子线程全部完成 for (Thread thread : threads) { thread.join(); } //输出结果,正确结果应该是1000,实际却是984 System.out.println(count);//984 } @Override public void run() { for (int i = 0; i < 10; i++) { try { //休眠500毫秒 Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } count++; } } }
为什么volatile不能保证线程安全?
很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁:
private static synchronized void add() { count++; }
首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:
指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。
所以在多线程环境下,就需要禁止指令重排序。
volatile关键字禁止指令重排序有两层意思:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
下面举个例子:
private static int a;//非volatile修饰变量 private static int b;//非volatile修饰变量 private static volatile int k;//volatile修饰变量 private void hello() { a = 1; //语句1 b = 2; //语句2 k = 3; //语句3 a = 4; //语句4 b = 5; //语句5 //... }
变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。
并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。
首先要讲一下内存屏障,内存屏障可以分为以下几类:
LoadLoad barrier: For statements like Load1, LoadLoad, Load2. Before the data to be read in Load2 and subsequent read operations is accessed, it is guaranteed that the data to be read in Load1 has been read.
StoreStore barrier: For such statements Store1, StoreStore, Store2, before Store2 and subsequent write operations are executed, ensure that the write operation of Store1 is visible to other processors.
LoadStore barrier: For statements such as Load1, LoadStore, and Store2, ensure that the data to be read by Load1 is completely read before Store2 and subsequent write operations are flushed out.
StoreLoad barrier: For such statements Store1, StoreLoad, and Load2, before Load2 and all subsequent read operations are executed, it is guaranteed that the writing of Store1 is visible to all processors.
Insert a LoadLoad barrier after each volatile read operation and a LoadStore barrier after a read operation.
Insert a StoreStore barrier in front of each volatile write operation and a SotreLoad barrier in the back.
The above is the detailed content of Analysis of JMM High Concurrency Programming Examples in Java. For more information, please follow other related articles on the PHP Chinese website!