In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-03-31 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)05/31 Report--
This article mainly explains "how to solve the thread safety problem of Java multithreading". Interested friends may wish to take a look. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn how to solve the thread safety problem of Java multithreading.
1. Thread Safety Overview 1.1 what are thread safety issues
First of all, we need to understand that the scheduling of threads in the operating system is preemptive or random, which causes the execution order of threads to be uncertain, and the different execution order of some codes does not affect the results of the program, but there are also some codes that change the execution order and the rewritten results will be affected, which causes the program to appear bug The code that causes the program to appear bug when multi-thread concurrency is called thread-unsafe code, which is the thread safety problem.
Next, we will introduce a typical example of a thread safety problem, the integer self-increment problem.
1.2 A program with thread safety problems
One day, the teacher assigned such a problem: use two threads to increment the variable count 100000 times, each thread undertakes 50, 000 self-incrementing tasks, the initial value of the variable count is 0.
This question is very simple, and we can figure out the final result by mouth. The answer is 100000.
Xiao Ming did things very quickly and quickly wrote the following code:
Class Counter {private int count; public void increase () {+ + this.count;} public int getCount () {return this.count;}} public class Main11 {private static final int CNT = 50000; private static final Counter counter = new Counter (); public static void main (String [] args) throws InterruptedException {Thread thread1 = new Thread (()-> {for (int I = 0; I)
< CNT; i++) { counter.increase(); } }); Thread thread2 = new Thread(() ->{for (int j = 0; j)
< CNT; j++) { counter.increase(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); }} 按理来说,结果应该是10万,我们来看看运行结果: 运行的结果比10万要小,你可以试着运行该程序你会发现每次运行的结果都不一样,但绝大部分情况,结果都会比预期的值要小,下面我们就来分析分析为什么会这样。 2.线程加锁与线程不安全的原因2.1案例分析 上面我们使用多线程运行了一个程序,将一个变量值为0的变量自增10万次,但是最终实际结果比我们预期结果要小,原因就是线程调度的顺序是随机的,造成线程间自增的指令集交叉,导致运行时出现两次自增但值只自增一次的情况,所以得到的结果会偏小。 我们知道一次自增操作可以包含以下几条指令: 将内存中变量的值加载到寄存器,不妨将该操作记为load。 在寄存器中执行自增操作,不妨将该操作记为add。 将寄存器的值保存至内存中,不妨将该操作记为save。 我们来画一条时间轴,来总结一下常见的几种情况: 情况1: 线程间指令集,无交叉,运行结果与预期相同,图中寄存器A表示线程1所用的寄存器,寄存器B表示线程2所用的寄存器,后续情况同理。 情况2: 线程间指令集存在交叉,运行结果低于预期结果。 情况3: 线程间指令集完全交叉,实际结果低于预期。 根据上面我们所列举的情况,发现线程运行时没有交叉指令的时候运行结果是正常的,但是一旦有了交叉会导致自增操作的结果会少1,综上可以得到一个结论,那就是由于自增操作不是原子性的,多个线程并发执行时很可能会导致执行的指令交叉,导致线程安全问题。 那如何解决上述线程不安全的问题呢?当然有,那就是对对象加锁。 2.2线程加锁2.2.1什么是加锁 为了解决由于"抢占式执行"所导致的线程安全问题,我们可以对操作的对象进行加锁,当一个线程拿到该对象的锁后,会将该对象锁起来,其他线程如果需要执行该对象的任务时,需要等待该线程运行完该对象的任务后才能执行。 举个例子,假设要你去银行的ATM机存钱或者取款,每台ATM机一般都在一间单独的小房子里面,这个小房子有一扇门一把锁,你进去使用ATM机时,门会自动的锁上,这个时候如果有人要来取款,那它得等你使用完并出来它才能进去使用ATM,那么这里的"你"相当于线程,ATM相当于一个对象,小房子相当于一把锁,其他的人相当于其他的线程。 在java中最常用的加锁操作就是使用synchronized关键字进行加锁。 2.2.2如何加锁 synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。 线程进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当于 解锁。 java中的加锁操作可以使用synchronized关键字来实现,它的常见使用方式如下: 方式1: 使用synchronized关键字修饰普通方法,这样会使方法所在的对象加上一把锁。 例如,就以上面自增的程序为例,尝试使用synchronized关键字进行加锁,如下我对increase方法进行了加锁,实际上是对某个对象加锁,此锁的对象就是this,本质上加锁操作就是修改this对象头的标记位。 class Counter { private int count; synchronized public void increase() { ++this.count; } public int getCount() { return this.count; }} 多线程自增的main方法如下,后面会以相同的栗子介绍synchronized的其他用法,后面就不在列出这段代码了。 public class Main11 { private static final int CNT = 50000; private static final Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() ->{for (int I = 0; I)
< CNT; i++) { counter.increase(); } }); Thread thread2 = new Thread(() ->{for (int j = 0; j)
< CNT; j++) { counter.increase(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); }} 看看运行结果: 方式2: 使用synchronized关键字对代码段进行加锁,但是需要显式指定加锁的对象。 例如: class Counter { private int count; public void increase() { synchronized (this){ ++this.count; } } public int getCount() { return this.count; }} 运行结果:Method 3: modify the static method with the synchronized keyword, which is equivalent to locking the class object of the current class.
Class Counter {private static int count; synchronized public static void increase () {+ + count;} public int getCount () {return this.count;}}
Running result:
These are the common uses. For thread locking (thread holding lock), if two threads take the lock of an object at the same time, there will be lock contention, and there will be no lock contention when two threads take locks of two different objects at the same time.
For the keyword synchronized, it means synchronization in English, but synchronization has many meanings in computers. For example, in multithreading, synchronization means "mutually exclusive", while in IO or network programming, synchronization means "async" and has nothing to do with multithreading.
The working process of synchronized:
Get mutex lock
Copy the latest copy of the variable from main memory to working memory
Execute the code
Refresh the value of the changed shared variable to the main memory
Release mutex unlock
The synchronized synchronization block is reentrant to the same thread, and there is no problem of locking itself, that is, deadlock, which will be introduced in the following article on deadlock.
To sum up, synchronized keyword locking has the following properties: mutual exclusion, refresh memory, and reentrancy.
The synchronized keyword is also equivalent to a monitor lock monitor lock. If you use the wait method (a thread waiting method, described later) without locking, an illegal monitor exception will be thrown. The reason for this exception is that it is not locked.
2.2.3 re-analysis of the case
After locking the added code, let's analyze why thread safety has been added, listing the code first:
Class Counter {private int count; synchronized public void increase () {+ + this.count;} public int getCount () {return this.count;}} public class Main11 {private static final int CNT = 50000; private static final Counter counter = new Counter (); public static void main (String [] args) throws InterruptedException {Thread thread1 = new Thread (()-> {for (int I = 0; I)
< CNT; i++) { counter.increase(); } }); Thread thread2 = new Thread(() ->{for (int j = 0; j)
< CNT; j++) { counter.increase(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); }} 多线程并发执行时,上一次就分析过没有指令集交叉就不会出现问题,因此这里我们只讨论指令交叉后,加锁操作是如何保证线程安全的,不妨记加锁为lock,解锁为unlock,两个线程运行过程如下: 线程1首先拿到目标对象的锁,对对象进行加锁,处于lock状态,当线程2来执行自增操作时会发生阻塞,直到线程1的自增操作完毕,处于unlock状态,线程2才会就绪取执行线程2的自增操作。 加锁后线程就是串行执行,与单线程其实没有很大的区别,那多线程是不是没有用了呢?但是对方法加锁后,线程运行该方法才会加锁,运行完该方法就会自动解锁,况且大部分操作并发执行是不会造成线程安全的,只有少部分的修改操作才会有可能导致线程安全问题,因此整体上多线程运行效率还是比单线程高得多。 2.3线程不安全的原因 首先,线程不安全根源是线程间的调度充满随机性,导致原有的逻辑被改变,造成线程不安全,这个问题无法解决,无可奈何。 多个线程针对同一资源进行写(修改)操作,并且针对资源的修改操作不是原子性的,可能会导致线程不安全问题,类似于数据库的事务。 由于编译器的优化,内存可见性无法保证,就是当线程频繁地对同一个变量进行读操作时,会直接从寄存器上读值,不会从内存上读值,这样内存的值修改时,线程就感知不到该变量已经修改,会导致线程安全问题(这是编译器优化的结果,现代的编译器都有类似的优化不止于Java),因为相比于寄存器,从内容中读取数据的效率要小的多,所以编译器会尽可能地在逻辑不变的情况下对代码进行优化,单线程情况下是不会翻车的,但是多线程就不一定了,比如下面一段代码: import java.util.Scanner;public class Main12 { private static int isQuit; public static void main(String[] args) { Thread thread = new Thread(() ->{while (isQuit = = 0) {} System.out.println ("Thread thread finished!");}); thread.start (); Scanner sc = new Scanner (System.in); System.out.println ("Please enter the value of isQuit, not 0 thread thread stops execution!"); isQuit = sc.nextInt () System.out.println ("mainthread execution completed!");}}
Running result:
We can know from the run results that after entering isQuit, the thread thread does not stop, which is the compiler optimization causes the thread not to be aware of memory visibility, resulting in thread safety.
We can use the volatile keyword to ensure memory visibility.
We can use the volatile keyword to modify isQuit to ensure memory visibility.
Import java.util.Scanner;public class Main12 {volatile private static int isQuit; public static void main (String [] args) {Thread thread = new Thread (()-> {while (isQuit = = 0) {} System.out.println ("Thread thread finished!");}); thread.start (); Scanner sc = new Scanner (System.in) System.out.println ("Please enter the value of isQuit, not 0 thread thread stops execution!"); isQuit = sc.nextInt (); System.out.println ("mainthread execution completed!");}}
Running result:
The difference between the synchronized and volatile keywords:
The synchronized keyword can guarantee atomicity, but whether it can guarantee memory visibility depends on the situation (not the chestnut above), while the volatile keyword only ensures memory visibility, not atomicity.
To ensure memory visibility is to prevent the compiler from making such optimizations.
Import java.util.Scanner;public class Main12 {private static int isQuit; / / Lock object private static final Object lock = new Object () Public static void main (String [] args) {Thread thread = new Thread (()-> {synchronized (lock) {while (isQuit = 0) {} System.out.println ("Thread thread finished!");}}); thread.start () Scanner sc = new Scanner (System.in); System.out.println ("Please enter the value of isQuit, not 0 thread thread stops execution!"); isQuit = sc.nextInt (); System.out.println ("mainthread execution completed!");}}
Running result:
Compiler optimization not only leads to problems that memory visibility is not aware of, but also instruction reordering can also lead to thread safety problems. Instruction reordering is also one of the compiler optimizations, that is, the compiler will intelligently (keep the original logic unchanged) to adjust the order of code execution, so as to improve the efficiency of program execution. Single-threading is fine, but multi-threading may turn over.
3. Standard classes for thread safety
Many of the Java standard libraries are thread-unsafe. These classes may involve multithreading to modify shared data without any locking measures. For example, ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder.
But some are thread-safe, using locking mechanisms to control, such as Vector (deprecated), HashTable (deprecated), ConcurrentHashMap (recommended), StringBuffer.
Others are not locked, but do not involve "modification" and are still thread-safe, such as String.
In thread safety problems, you may also encounter the JMM model. To add here, JMM is actually re-encapsulating the registers, cache and memory in the operating system, in which registers and caches in JMM are called working memory, and memory is called main memory.
The cache is divided into the first-level cache L1, the second-level cache L2 and the third-level cache L3. The space from L1 to L3 is getting larger and larger, the maximum is smaller than the memory space, the minimum is larger than the register space, and the access speed is getting slower and slower. The slowest access speed is faster than memory access, and the fastest is not as fast as register access.
Thread wait methods provided by the 4.Object class
In addition to the methods in the Thread class that can implement thread waiting, such as join,sleep, related thread waiting methods are also provided in the Object class.
The ordinal method indicates that 1public final void wait () throws InterruptedException releases the lock and causes the thread to enter the WAITING state 2public final native void wait (long timeout) throws InterruptedException; compared to method 1, the maximum waiting time 3public final void wait (long timeout, int nanos) throws InterruptedException is more accurate than method 2, and the maximum waiting time precision is higher 4public final native void notify (); wake up a thread in WAITING state and add a lock, using 5public final native void notifyAll () with wait method Wake up all threads in the WAITING state and add locks (which are likely to cause lock contention), using the wait method
When the synchronized keyword is described above, an illegal monitoring exception will be generated if the thread is not locked. Let's verify it:
Public class TestDemo12 {public static void main (String [] args) throws InterruptedException {Thread thread = new Thread (()-> {try {Thread.sleep (5000);} catch (InterruptedException e) {e.printStackTrace ();} System.out.println ("execution complete!") ;}); thread.start (); System.out.println (before wait); thread.wait (); System.out.println (after wait);}}
Take a look at the running results:
Sure enough, an IllegalMonitorStateException was thrown, because the execution step of the wait method is: first release the lock, and then make the thread wait. You don't have a lock now, so how do you release the lock? So this exception is thrown, but it is harmless to execute notify.
The wait method is often used in conjunction with the notify method, the former can release the lock so that the thread can wait, and the latter can acquire the lock so that the thread can continue to execute. The flow chart of this combination is as follows:
Now that there are two tasks executed by two threads, assuming that thread 2 executes before thread 1, write a multithreaded program to make task 1 complete before task 2, where thread 1 executes task 1 and thread 2 executes task 2.
This requirement can be implemented using wait/notify.
Class Task {public void task (int I) {System.out.println ("Task" + I + "complete!") ;} public class WiteNotify {/ / Lock object private static final Object lock = new Object (); public static void main (String [] args) throws InterruptedException {Thread thread1 = new Thread (()-> {synchronized (lock) {Task task1 = new Task (); task1.task (1)) / / notify thread 2 that the task of thread 1 completes System.out.println ("before notify"); lock.notify (); System.out.println ("after notify");}}) Thread thread2 = new Thread (()-> {synchronized (lock) {Task task2 = new Task (); / wait for task 1 of thread 1 to finish executing System.out.println ("before wait"); try {lock.wait () } catch (InterruptedException e) {e.printStackTrace ();} task2.task (2); System.out.println ("after wait");}}); thread2.start (); Thread.sleep (10); thread1.start ();}}
Running result:
At this point, I believe that everyone on the "Java multithreading thread safety problem how to solve" have a deeper understanding, might as well to the actual operation of it! Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!
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.