In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article mainly introduces "how to understand the volatile keyword in Java". In the daily operation, I believe many people have doubts about how to understand the volatile keyword in Java. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful for you to answer the doubt of "how to understand the volatile keyword in Java"! Next, please follow the editor to study!
CPU cache caching model
All the operations in the computer are performed by CPU, and the execution of CPU instructions involves data reading and writing operations, but CPU can only access data in memory, and the speed of memory is far from equal to that of CPU, so there is a cache model, that is, a cache layer is added between CPU and memory. Generally speaking, the modern CPU cache layer is divided into three levels, which are called L1 cache, L2 cache and L3 cache. The brief figure is as follows:
L1 cache: the access speed of the third-tier cache is the fastest, but the capacity is the smallest. In addition, the L1 cache is divided into data cache (L1dforce data initials) and instruction cache (L1iinstruction initials).
L2 cache: slower than L1, but larger than L1. In modern multicore CPU, L2 is generally monopolized by a single core.
L3 cache: the third-tier cache is the slowest, but the largest. In modern CPU, L3 is also designed for multi-core sharing, such as the design of zen3 architecture.
Cache appears to solve the problem that it is inefficient for CPU to access memory directly. When CPU performs operations, it copies a copy of the data needed from the main memory to the cache. Because the access speed of the cache is faster than the memory, you only need to read the cache and update the results to the cache during the calculation, and then refresh the results to the main memory at the end of the operation. This greatly improves the computing efficiency. The overall interaction diagram is briefly as follows:
Cache consistency issu
Although the emergence of caching has greatly improved the throughput capacity, it also introduces a new problem, that is, cache inconsistency. For example, in the simplest iPrep + operation, you need to copy a copy of the memory data to the cache. CPU reads the cache values and updates them, writes to the cache first, and then flushes the new ones in the cache to memory after the operation. The specific process is as follows:
Read I from memory to cache
CPU reads values from cache I
Add 1 to I
Write the result back to the cache
Then refresh the data to the main memory
There is no problem with such inotify + operation in single thread, but in multithreading, because each thread has its own working memory (also called local memory, which is the thread's own cache), the variable I has a copy in the local memory of multiple threads. If two threads perform iThread + operation:
Suppose the two threads are An and B, and assume that the initial value of I is 0
Thread A reads the value of I from memory and puts it in the cache, where the value of I is 0, and the value of thread B is the same, and the value put into the cache is also 0.
When two threads perform self-increment operation at the same time, the value of I in the cache of threads An and B is 1.
Two threads write I to main memory, which is equivalent to I being assigned to 1 twice
The end result is that the value of I is 1
This is a typical cache inconsistency problem, and the mainstream solutions are:
Bus locking
Cache consistency protocol
Bus locking
This is a pessimistic way of implementation. Specifically, the lock instruction is issued by the processor to lock the bus. When the bus receives the instruction, it will block the requests of other processors until the processor occupying the lock completes the operation. The characteristic is that only one processor that grabs the bus lock runs, but this method is inefficient. Once one processor acquires the lock, other processors can only block waiting, which will affect the performance of multi-core processors.
Cache consistency protocol
The figure is as follows:
The most famous cache consistency protocol is the MESI protocol, where MESI ensures that copies of shared variables used in each cache are consistent. The general idea is that when CPU manipulates data in the cache, if you find that the variable is a shared variable, do the following:
Read: do no other processing, just read the data in the cache into the register
Write: send a signal to other CPU to set the cache line of this variable to invalid state (Invalid). When other CPU reads the variable, it needs to be retrieved from main memory again.
Specifically, MESI specifies that cache lines use four status markers:
M:Modified, modified
E:Exclusive, exclusive
S:Shared, shared
I:Invalid, invalid
The detailed implementation of MESI is beyond the scope of this article, and for more information, you can refer to here or here.
JMM
After looking at the CPU cache, let's take a look at the JMM, that is, the Java memory model, which specifies how the JVM works with the computer's main memory, and also determines when the writing of one thread to the shared variable is visible to other threads. JMM defines the abstract relationship between thread and main memory, as follows:
Shared variables are stored in main memory and can be accessed by each thread
Each thread has private working memory, or local memory.
Working memory stores only the thread's copy of the shared variable
The thread cannot directly manipulate the main memory and can only write to the main memory after operating the working memory first.
Working memory, like the JMM memory model, is an abstract concept, but it does not exist. It covers cache, register, compile-time optimization and hardware.
The brief picture is as follows:
Similar to MESI, if one thread modifies the shared variable and flushes it to the main memory, other threads find that the cache is invalid when they read the working memory, and it will read from the main memory to the working memory again.
The following figure shows the relationship between JVM and computer hardware allocation:
Three characteristics of concurrent programming
I've read most of the articles and haven't reached volatile yet? Don't worry, let's take a look at three important features in concurrent programming, which are of great help to the correct understanding of volatile.
Atomicity
Atomicity is in one or more operations:
Or all operations are performed and will not be interrupted by any factors.
Or all operations are not performed.
A typical example is a transfer between two people, such as 1000 yuan from A to B. then this includes two basic operations:
A's account deducts 1000 yuan
B's account increased by 1000 yuan.
These two operations are either successful or failed, that is, there can be no deduction of 1000 from the An account but no change in the amount of the B account, nor can there be a 1000 increase in the amount of the An account.
It is important to note that the combination of two atomic operations is not necessarily atomic, such as iTunes +. In essence, iTunes + involves three operations:
Get i
ITunes 1
Set i
All three operations are atomic, but when combined (iTunes +), they are not atomic.
Visibility
Another important feature is visibility, which means that if one thread makes changes to the shared variable, the other thread can see the latest value immediately.
A simple example is as follows:
Public class Main {private int x = 0; private static final int MAX = 1000000; public static void main (String [] args) throws InterruptedException {Main m = new Main (); Thread thread0 = new Thread (()-> {while (m.x)
< MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{while (m.x)
< MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); }} 线程thread1会一直运行,因为thread1把x读入工作内存后,会一直判断工作内存中的值,由于thread0改变的是thread0工作内存的值,并没有对thread1可见,因此永远也不会输出finish,使用jstack也可以看到结果: 有序性 有序性是指代码在执行过程中的先后顺序,由于JVM的优化,导致了代码的编写顺序未必是代码的运行顺序,比如下面的四条语句: int x = 10;int y = 0;x++;y = 20; 有可能y=20在x++前执行,这就是指令重排序。一般来说,处理器为了提高程序的效率,可能会对输入的代码指令做一定的优化,不会严格按照编写顺序去执行代码,但可以保证最终运算结果是编码时的期望结果,当然,重排序也有一定的规则,需要严格遵守指令之间的数据依赖关系,并不是可以任意重排序,比如: int x = 10;int y = 0;x++;y = x+1; y=x+1就不能先优于x++执行。 在单线程下重排序不会导致预期值的改变,但在多线程下,如果有序性得不到保证,那么将可能出现很大的问题: private boolean initialized = false;private Context context;public Context load(){ if(!initialized){ context = loadContext(); initialized = true; } return context;} 如果发生了重排序,initialized=true排序到了context=loadContext()的前面,假设两个线程A、B同时访问,且loadContext()需要一定耗时,那么: 线程A通过判断后,先设置布尔变量的值为true,再进行loadContext()操作 线程B中由于布尔变量被设置为true,会直接返回一个未加载完成的context volatile 好了终于到了volatile了,前面说了这么多,目的就是为了能彻底理解和明白volatile。这部分分为四个小节: volatile的语义 如何保证有序性以及可见性 实现原理 使用场景 与synchronized区别 先来介绍一下volatile的语义。 语义 被volatile修饰的实例变量或者类变量具有两层语义: 保证了不同线程之间对共享变量操作时的可见性 禁止对指令进行重排序操作 如何保证可见性以及有序性 先说结论: volatile能保证可见性 volatile能保证有序性 volatile不能保证原子性 下面分别进行介绍。 可见性 Java中保证可见性有如下方式: volatile:当一个变量被volatile修饰时,对共享资源的读操作会直接在主内存中进行(准确来说也会读取到工作内存中,但是如果其他线程进行了修改就必须从主内存重新读取),写操作是先修改工作内存,但是修改结束后立即刷新到主内存中 synchronized:synchronized一样能保证可见性,能够保证同一时刻只有一个线程获取到锁,然后执行同步方法,并且确保锁释放之前,变量的修改被刷新到主内存中 使用显式锁Lock:Lock的lock方法能保证同一时刻只有一个线程能够获取到锁然后执行同步方法,并且确保锁释放之前能够将对变量的修改刷新到主内存中 具体来说,可以看一下之前的例子: public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{while (m.x)
< MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{while (m.x)
< MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); }} 上面说过这段代码会不断运行,一直没有输出,就是因为修改后的x对线程thread1不可见,如果在x的定义中加上了volatile,就不会出现没有输出的情况了,因为此时对x的修改是线程thread1可见的。有序性 JMM中允许编译期和处理器对指令进行重排序,在多线程的情况下有可能会出现问题,为此,Java同样提供了三种机制去保证有序性: volatile synchronized 显式锁Lock 另外,关于有序性不得不提的就是Happens-before原则。Happends-before原则说的就是如果两个操作的执行次序无法从该原则推导出来,那么就无法保证有序性,JVM或处理器可以任意重排序。这么做的目的是为了尽可能提高程序的并行度,具体规则如下: 程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生与编写在前面的操作之后 锁定规则:如果一个锁处于锁定状态,则unlock操作要先行发生于对同一个锁的lock操作 volatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作 传递规则:如果操作A先于操作B,操作B先于操作C,那么操作A先于操作C 线程启动规则:Thread对象的start()方法先行发生于对该线程的任何动作 线程中断规则:对线程执行interrupt()方法肯定要优于捕获到中断信号,换句话说,如果收到了中断信号,那么在此之前必定调用了interrupt() 线程终结规则:线程中所有操作都要先行发生于线程的终止检测,也就是逻辑单元的执行肯定要发生于线程终止之前 对象终结规则:一个对象初始化的完成先行发生于finalize()之前 对于volatile,会直接禁止对指令重排,但是对于volatile前后无依赖关系的指令可以随意重排,比如: int x = 0;int y = 1;//private volatile int z;z = 20;x++;y--; 在z=20之前,先定义x或先定义y并没有要求,只需要在执行z=20的时候,可以保证x=0,y=1即可,同理,x++或y--具体先执行哪一个并没有要求,只需要保证两者执行在z=20之后即可。 原子性 在Java中,所有对基本数据类型变量的读取赋值操作都是原子性的,对引用类型的变量读取和赋值也是原子性的,但是: 将一个变量赋值给另一个变量的操作不是原子性的,因为涉及到了一个变量的读取以及一个变量的写入,两个原子性操作结合在一起就不是原子性操作 多个原子性操作在一起就不是原子性操作,比如i++ JMM只保证基本读取和赋值的原子性操作,其他的均不保证,如果需要具备原子性,那么可以使用synchronized或Lock,或者JUC包下的原子操作类 也就是说,volatile并不能保证原子性,例子如下: public class Main { private volatile int x = 0; private static final CountDownLatch latch = new CountDownLatch(10); public void inc() { ++x; } public static void main(String[] args) throws InterruptedException { Main m = new Main(); IntStream.range(0, 10).forEach(i ->{new Thread (()-> {for (int j = 0; j)
< 1000; j++) { m.inc(); } latch.countDown(); }).start(); }); latch.await(); System.out.println(m.x); }} 最后输出的x的值会少于10000,而且每次运行的结果也并不相同,至于原因,可以从两个线程A、B开始分析,图示如下: 0-t1:线程A将x读入工作内存,此时x=0 t1-t2:线程A时间片完,CPU调度线程B,线程B将x读入工作内存,此时x=0 t2-t3:线程B对工作内存中的x进行自增操作,并更新到工作内存中 t3-t4:线程B时间片完,CPU调度线程A,同理线程A对工作内存中的x自增 t4-t5:线程A将工作内存中的值写回主内存,此时主内存中的值为x=1 t5以后:线程A时间片完,CPU调度线程B,线程B也将自己的工作内存写回主内存,再次将主内存中的x赋值为1 也就是说,多线程操作的话,会出现两次自增但是实际上只进行一次数值修改的操作。想要x的值变为10000也很简单,加上synchronized即可: new Thread(() ->{synchronized (m) {for (int j = 0; j < 1000; jacks +) {m.inc ();}} latch.countDown ();}) .start (); implementation principle
As you already know, volatile guarantees orderliness and visibility, so how does it work?
The answer is a lock; prefix, which actually acts as a memory barrier that provides the following guarantees for instruction execution:
Make sure that the code behind the instruction is not placed in front of the memory barrier when the instruction is reordered
Make sure that the code in front of it is not placed behind the memory barrier when the instruction is reordered
Ensure that all the previous code is executed when the memory barrier modified instruction is executed
Force value modifications in thread working memory to be flushed to main memory
If it is a write operation, it will invalidate the cached data in the working memory of other threads.
Working with scen
A typical usage scenario is to use a switch to close a thread. An example is as follows:
Public class ThreadTest extends Thread {private volatile boolean started = true; @ Override public void run () {while (started) {}} public void shutdown () {this.started = false;}}
If the Boolean variable is not modified by volatile, it is likely that the new Boolean value will not be refreshed into main memory, causing the thread to not end.
The difference from synchronized
The difference in use: volatile can only be used to modify instance variables or class variables, but not to modify methods, method parameters, local variables, and so on. Another variable that can be modified is null. However, synchronized cannot be used to modify variables, only methods or statement blocks can be modified, and monitor objects cannot be null.
Guarantee of atomicity: volatile cannot guarantee atomicity, but synchronized can guarantee
Guarantee of visibility: both volatile and synchronized can ensure visibility, but synchronized is guaranteed by the JVM instruction monitor enter/monitor exit. During monitor exit, all shared resources are flushed into the main memory, while volatile is implemented through the lock; machine instruction, forcing other threads to work memory invalidation, which needs to be loaded into the main memory.
Guarantee of ordering: volatile can prohibit JVM and processors from reordering it, while synchronized guarantees ordering through program serialization, and instruction rearrangement occurs in the code in the synchronized code block.
Other differences: volatile does not block threads, but synchronized does
At this point, the study on "how to understand the volatile keyword in Java" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.