線程P4 | 線程安全問題及解決方法

何為線程安全?

要談及何為線程安全,總得說來,我們可以用一句話來概況:

如果在多線程環境下代碼運行結果和我們預期是相符的,即和單線程環境下的運行結果相同,那么我們就稱這個程序是線程安全的,反之則不安全,即和預期不符;

為了大家更好地理解這句話,大家可以看一下下面這個例子

public class Demo21 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++) {count++;}});Thread t2 = new Thread(() -> {for(int i = 0; i < 5000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

對于上述代碼,我們預期的結果應該是輸出10000,因為count經過了10000次的累加,然而事實上結果是....?

7019...??這樣一個令人摸不著頭腦的數字,甚至每次運行的結果都不一樣,這是為什么呢?

原因分析:

實際上,count++的操作是分為三步的

  1. load從內存中讀取數據到cpu的寄存器
  2. add把寄存器中的值+1
  3. save把寄存器中的值寫回內存中

而由于線程是隨機調度的,所以有可能t1剛執行到第1一步,cpu資源就被調度走了,那么count值就不會和預想結果一樣,也可能t1和t2同時執行第一步,那么它們讀取到的數據都是count = 0,而事實上count應該執行的操作是 + 2,因為這樣隨機調度的不確定性,就使得這樣的代碼是線程不安全的!!

線程不安全的原因

1) 根本原因

線程不安全的根本原因就是線程的隨機調度,這樣的隨機帶來了很多不確定性,使得線程的執行順序是不確定的~~

2) 多個線程修改同一個變量

通過例子我們可以發現,t1和t2都在針對count這一個變量進行修改的操作,這樣的操作就會引起線程安全問題,為了解決這樣的問題,我們就會引入"鎖"這樣的概念,具體的解釋我們會在解決安全問題篇講述~~

3) 修改操作不是原子的

何為原子的?

原子的即原子性的操作,即這個操作是不可再分的。

我們上文提到了,雖然我們肉眼看起來count++這個操作就是對count進行了一個加法操作,但事實上,count++這個操作是包含了三部分的,所以這個操作并不是一個原子性的操作~~因此引發了線程安全問題

4) 內存可見性問題

在講述這個原因之前,我們要先引入另外一個例子

import java.util.Scanner;public class Demo22 {public static int flg = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flg == 0) {}System.out.println("t1線程結束");});Thread t2 = new Thread(() -> {System.out.println("請輸入flg的值:");Scanner scan = new Scanner(System.in);flg = scan.nextInt();});t1.start();t2.start();}
}

上述代碼我們想實現的結果是,用戶輸入一個非0的數字后t1線程結束,然而真正的運行結果卻無法結束t1線程

原因分析:

造成這樣結果的原因就是因為t2修改了這個變量內存,但t1內卻沒有接收到這個變量的變化,這樣的問題我們就稱為 "內存可見性" 問題,造成這樣的問題主要有以下兩個要點:

  1. JVM識別到load加載的flg的值幾百萬次都一樣[while循環的執行速度是很快的,可能一秒幾百萬次]
  2. load操作的花費開銷是很大的,遠遠超過了其它操作

因此在很多次的執行之后,JVM就會覺得,反之每次結果都一樣,那還有什么執行的必要嗎??因此JVM就自動地優化了代碼,將load操作變成了直接使用寄存器中之前"緩存"的值,而非每次去內存中重新獲取,大大降低了花費,因此就造成了即使后面修改了flg的值也無法被t2感知到的結果

5) 指令重排序問題

指令重排序實際上也是編譯器優化代碼的一種方式,保證邏輯不變的前提下,調整原有代碼的執行順序,提高程序的效率,自然,指令順序都發生了改變,安全也無法保證

在描述解決這些問題之前,我們先來講一下"鎖"的概念

線程加鎖

加鎖的關鍵字

形如上圖的由synchronized關鍵字修飾的代碼塊就是相當于對一個Object對象加鎖,當兩個線程競爭同一把鎖的時候,就會引發阻塞,一個進程拿到鎖之后,另外一個進程就會因為拿不到鎖而陷入阻塞狀態,這樣就不會因為隨機的變化而造成線程安全問題

我們舉個例子來理解一下~~

比如你想要追求你的crush~~她有對象的時候,就相當于她加上鎖了,按理來說,你就不能再追求她了,但是如果她分手了,又回歸了單身狀態,那就是鎖解除啦,然后你就可以追求她了,你們在一起之后,就相當于你競爭到了這把鎖,那其它人就不能追求你的crush啦,除非你倆又分手了,她又回歸了單身狀態,那么別人就可以又來競爭這把鎖,此時"男朋友"這個身份就是那把鎖~~~

加鎖例子

我們來完善一下開頭的例子,看下我們加上鎖之后的結果

public class Demo21 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {      //共同競爭Locker這把鎖count++;}}});Thread t2 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

可以看到,此時結果就是10000了

提示

當對同一個線程多次加同一把鎖的時候,是無效的,只會算一把鎖,因為鎖有可重入性~~

解決線程安全問題

對于問題1)

這個問題是無法解決的,這是系統的底層邏輯

對于問題2)、 3)

想要解決問題2和問題3,就是要對操作加鎖,詳情請參看鎖篇章~~

對于問題4)、5)

想要解決這兩個問題,我們要引入另一個關鍵字 volatile

volatile可以強制關閉JVM的代碼優化機制,確保每次循環都要重新從內存中讀取數據,雖然這增加了開銷,但可以增加代碼的準確性~~

同樣的例子,我們對flg加上volatile關鍵字

import java.util.Scanner;public class Demo22 {public static volatile int flg = 0;        //加上volatilepublic static void main(String[] args) {Thread t1 = new Thread(() -> {while (flg == 0) {}System.out.println("t1線程結束");});Thread t2 = new Thread(() -> {System.out.println("請輸入flg的值:");Scanner scan = new Scanner(System.in);flg = scan.nextInt();});t1.start();t2.start();Object locker = new Object();synchronized (locker) {}}}

運行則可以發現,t1此時就可以正常結束了~~


?? 覺得博主寫的有幫助的話,請點個贊 b( ̄▽ ̄)d ,謝謝~~~ ??

?? 你的喜歡是我更新的最大動力~~~ ??

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/93332.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/93332.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/93332.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

水印消失術!JavaAI深度學習去水印技術深度剖析

一、飛算JavaAI平臺概述1.1 飛算JavaAI定位與技術特色 飛算JavaAI是國內領先的智能化Java開發平臺&#xff0c;通過AI技術賦能軟件開發全流程&#xff0c;特別針對小程序、Web應用等輕量級開發場景提供*零基礎編程→高質量交**的一站式解決方案。其核心優勢體現在&#xff1a; …

醋酸釓:醫學影像與科技創新中的重要角色

醋酸釓是一種由釓元素和醋酸根離子組成的化合物。釓是稀土金屬之一&#xff0c;常常用于醫學影像、核磁共振成像&#xff08;MRI&#xff09;以及某些工業應用。醋酸釓作為釓的鹽之一&#xff0c;具有許多獨特的性質&#xff0c;尤其在醫學和科學研究領域表現突出。一、醋酸釓的…

插入排序專欄

插入排序&#xff08;Insertion Sort&#xff09;是一種簡單直觀的排序算法&#xff0c;其思想源于我們日常生活中整理撲克牌的方式。本文將詳細解析插入排序的工作原理&#xff0c;通過 Java 實現代碼進行分析&#xff0c;深入探討其時間復雜度的計算過程&#xff0c;并闡述其…

高效Unicode字符表示:一種創新的詞表構建策略分析

在自然語言處理中&#xff0c;處理多語言和特殊字符的表示始終是一項挑戰。本文將分析一種創新的詞表構建策略&#xff0c;該策略通過數學優化和雙token機制&#xff0c;在保持詞表緊湊的同時實現了對Unicode字符的全面覆蓋。 詞表構建的核心邏輯 該策略包含四個關鍵步驟&#…

python與物聯網基礎知識

軟件準備&#xff1a;軟件&#xff1a;thonny-4.0.1-windows-portable(win10,11系統64位)驅動&#xff1a;CP210x_Windows_Drivers固件&#xff1a;esp8266-1m-20220618-v1.19.1.bin物料準備&#xff1a;面包板、開發板、電源線一、安裝與調試&#xff1a;1.在軟件文件中找到th…

SVN提交服務器拒絕訪問的問題

SVN提交服務器拒絕訪問的問題 介紹 分析 1.服務器的SVN沒有開啟 2.服務器的網絡端口除了問題沒有開放端口 3.客戶端的SVN配置除了問題刷新一下數據 4.客戶端的SVN重裝 找原因 1.初步以為是**防火墻**的問題 2.網絡運營商的問題 總結 介紹 SVN相信大家都用過,今天反饋一個比較…

【Linux】庫制作與原理

前言 本篇博客我們來認識下庫方面的知識 &#x1f493; 個人主頁&#xff1a;zkf ? 文章專欄&#xff1a;Linux 若有問題 評論區見&#x1f4dd; &#x1f389;歡迎大家點贊&#x1f44d;收藏?文章 目錄 1.什么是庫 2.靜態庫 2.1靜態庫的生成 2.2靜態庫的使用 3.動態庫 …

Android ADB 常用指令全解析

ADB&#xff08;Android Debug Bridge&#xff09;是 Android 開發和測試不可或缺的調試工具&#xff0c;它建立了電腦與 Android 設備之間的通信橋梁&#xff0c;通過命令行指令可實現對設備的全方位控制。掌握 ADB 指令能大幅提升開發效率&#xff0c;解決各類調試難題。本文…

使用 Rust 創建 32 位 DLL 的完整指南

使用 Rust 創建 32 位 DLL 的完整指南 在 Rust 中創建 32 位 DLL 需要特定的工具鏈配置和編譯選項。以下是詳細步驟和最佳實踐&#xff1a; 環境準備 1. 安裝 Rust 工具鏈 # 安裝 Rust curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh# 安裝 32 位目標 rustu…

算法基礎 第3章 數據結構

1.單調棧 1.什么是單調棧 單調棧&#xff0c;即具有單調性的棧。 實現 #include <iostream> #include <stack> using namespace std; const int N 3e6 10; int a[N], n; void test1() {stack<int> st; // 維護?個單調遞增的棧for(int i 1; i < n; i…

[機器學習]08-基于邏輯回歸模型的鳶尾花數據集分類

使用sklearn的LogisticRegression多分類模型程序代碼&#xff1a;import numpy as np from sklearn.linear_model import LogisticRegression import matplotlib.pyplot as plt import matplotlib as mpl from sklearn import datasets from sklearn import preprocessing impo…

【STM32入門教程】stm32簡介

一、STM32簡介二、ARM三、stm32f103c8t6四、命名規則五、系統結構六、引腳定義七、啟動配置一般情況下&#xff0c;都是在flash開始程序&#xff0c;而啟動程序也可以進行配置在其他地方啟動程序&#xff0c;通過配置boot0和boot1來進行配置八、最小系統電路

SAE J2716多協議網關的硬件架構與實時協議轉換機制解析

本文解析符合SAE J2716標準的工業級協議轉換設備技術架構&#xff0c;通過拆解其四路雙向SENT通道與多總線&#xff08;CANFD/Ethernet/USB&#xff09;的實時交互機制、MicroSD獨立日志系統設計及模擬量動態映射方案&#xff0c;為汽車電子與工業通信開發者提供可復用的技術參…

VS2022+QT5.15.2+OCCT7.9.1的開發環境搭建流程

以下是VS2022 QT5.15.2 OCCT7.9.1開發環境搭建的完整流程&#xff1a; 一、安裝Visual Studio 2022 下載安裝程序 訪問VS官網下載Community版安裝組件 選擇"使用C的桌面開發"工作負載勾選&#xff1a; MSVC v143 - VS 2022 C x64/x86生成工具Windows 10 SDK (建議…

數據庫訪問模式詳解

數據庫訪問模式詳解數據庫訪問模式是軟件架構中數據訪問層&#xff08;Data Access Layer&#xff09;設計的核心&#xff0c;它定義了應用程序如何與數據庫進行交互的策略和方法。選擇合適的訪問模式對于系統的性能、可維護性、可擴展性、事務一致性和開發效率至關重要。不同的…

BGE向量算法

一、是什么 什么是BGE向量算法&#xff1f;先說說網上的概念吧。本文不講解太深的算法知識&#xff0c;主要講解如何用&#xff01; BGE&#xff08;BAAI General Embedding&#xff09;是北京智源研究院開源的“通用語義向量模型”。一句話&#xff1a;把中文或英文句子變成…

AI數據倉庫的核心優勢解析

內容概要本文旨在全面解析AI數據倉庫的核心優勢&#xff0c;為讀者提供清晰的框架。文章首先從基礎定義出發&#xff0c;探討其如何高效整合多源數據&#xff0c;并支持人工智能與機器學習應用。隨后&#xff0c;將詳細闡述處理TB級數據的能力&#xff0c;包括兼容結構化和非結…

具身智能Scaling Law缺失:機器人界的“摩爾定律“何時誕生?

8月9日&#xff0c;在世界機器人大會的演講臺上&#xff0c;宇樹科技創始人王興興談論到目前機器人運動控制領域存在的RL Scaling Law問題&#xff0c;他認為現在的機器人在學習一項新的技能時&#xff0c;往往都是需要從頭開始研究以及教學。而在未來更加希望的是能夠在原有的…

【跨越 6G 安全、防御與智能協作:從APT檢測到多模態通信再到AI代理語言革命】

跨越 6G 安全、防御與智能協作&#xff1a;從APT檢測到多模態通信再到AI代理語言革命引言單篇總結**2. Integrated Multimodal Sensing and Communication: Challenges, Technologies, and Architectures****3. Why do AI agents communicate in human language?**引言 在邁向…

微前端-解決MicroApp微前端內存泄露問題

前言 之前使用京東微前端框架MicroApp集成10個微前端的頁面到AngularJs的后臺管理系統中&#xff0c;每個微前端做成一個菜單&#xff0c;一共10個&#xff0c;每次打開都是一個新的微前端&#xff0c;但是發現打開的微前端越多&#xff0c;容易造成內存泄露&#xff0c;下面講…