JAVA多線程線程安全性基礎(chǔ)
線程安全性
一個對象是否需要是線程安全的,取決于它是否被多個線程訪問,而不取決于對象要實現(xiàn)的功能
什么是線程安全的代碼
核心:對 共享的 和 可變的 狀態(tài)的訪問進(jìn)行管理。防止對數(shù)據(jù)發(fā)生不受控的并發(fā)訪問。
何為對象的狀態(tài)?
狀態(tài)是指存儲在對象的狀態(tài)變量(例如實例或靜態(tài)域)中的數(shù)據(jù)。還可能包括 其他依賴對象 的域。
eg:某個HashMap的狀態(tài)不僅存儲在HashMap對象本身,還存儲在許多Map.Entry對象中。

總而言之,在對象的狀態(tài)中包含了任何可能影響其外部可見行為的數(shù)據(jù)。
何為共享的?
共享的 是指變量可同時被多個線程訪問
何為可變的?
可變的 是指變量的值在其生命周期內(nèi)可以發(fā)生變化。試想,如果一個共享變量的值在其生命周期內(nèi)不會發(fā)生變化,那么在多個
線程訪問它的時候,就不會出現(xiàn)數(shù)據(jù)不一致的現(xiàn)象,自然就不存在線程安全性問題了。
什么是線程安全性
當(dāng)多個線程訪問某個類時,不管運(yùn)行時環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,達(dá)到預(yù)期的效果,那么就稱這個類是線程安全的。
如下啟動10個線程,每個線程對inc執(zhí)行1000次遞增,并添加一個計時線程,預(yù)期效果應(yīng)為10000,而實際輸出值為6880,是一個小于10000的值,并未達(dá)到預(yù)期效果,因此INS類不是線程安全的,整個程序也不是線程安全的。原因是遞增操作不是原子操作,并且沒有適當(dāng)?shù)耐綑C(jī)制
package hgh0808;
public class Test {
public static void main(String[] args){
for(int i = 0;i < 10;i++){
Thread th = new Thread(new CThread());
th.start();
}
TimeThread tt = new TimeThread();
tt.start();
try{
Thread.sleep(21000);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(INS.inc);
}
}
---------------------------------------------------------------------
package hgh0808;
import java.util.concurrent.atomic.*;
public class TimeThread extends Thread{
@Override
public void run(){
int count = 1;
for(int i = 0;i < 20;i++){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(count++);
}
}
}
---------------------------------------------------------------------
package hgh0808;
public class CThread implements Runnable{
@Override
public void run(){
for(int j = 0;j < 1000;j++){
INS.increase();
}
}
}
---------------------------------------------------------------------
package hgh0808;
public class INS{
public static volatile int inc = 0;
public static void increase(){
inc++;
}
}
=====================================================================
執(zhí)行結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880
通過synchronized加鎖機(jī)制,對INS類實現(xiàn)同步,如下得到了正確的運(yùn)行結(jié)果,很容易可以看出,主調(diào)代碼中并沒有任何額外的同步或協(xié)同,此時的INS類是線程安全的,整個程序也是線程安全的
package hgh0808;
public class INS{
public static volatile int inc = 0;
public static void increase(){
synchronized (INS.class){
inc++;
}
}
}
執(zhí)行結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
如何編寫線程安全的代碼
------------------------------------------------------------------------------------------------
如果當(dāng)多個線程訪問同一個可變的狀態(tài)變量時沒有使用合適的同步,那么程序就會出現(xiàn)錯誤,像上文中進(jìn)行同步之前的代碼
有三種方式可以修復(fù)這個問題:
*不在線程之間共享該狀態(tài)變量
*將狀態(tài)變量修改為不可變的變量
*在訪問狀態(tài)變量時使用同步
前兩種方法是針對 共享 和 不變 這兩個屬性(見上文)解決問題,在有些情境下會違背程序設(shè)計的初衷(比如上文中INS類中的inc變量不可能不變,且在多核處理器的環(huán)境下為了提高程序性能,就需要多個線程同時處理,這樣變量就必然要被多個線程共享)。
基于此,我們針對第三種方式------ 在訪問狀態(tài)變量時使用同步 展開討論
在討論第三種方式之前,我們先介紹幾個簡單的概念
原子性 :一個操作序列的所有操作要么不間斷地全部被執(zhí)行,要么一個也沒有執(zhí)行
競態(tài)條件 :當(dāng)某個計算的正確性取決于多個線程的的交替執(zhí)行時序時,就會發(fā)生競態(tài)條件。通俗的說,就是某個程序結(jié)果的正確性取決于運(yùn)氣時,就會發(fā)生競態(tài)條件。(競態(tài)條件并不總是會產(chǎn)生錯誤,還需要某種不恰當(dāng)?shù)膱?zhí)行時序)
常見的競態(tài)條件類型:
*檢查–執(zhí)行(例如延遲初始化)
*讀取–修改–寫入(例如自增++操作)
針對以上兩種常見的競態(tài)條件類型,我們分別給出例子
延遲初始化(檢查--執(zhí)行)
--------------------------------------------------------------------
package hgh0808;
import java.util.ArrayList;
public class Test1 {
public ArrayList<Ball> list;
public ArrayList<Ball> getInstance(){
if(list == null){
list = new ArrayList<Ball>();
}
return list;
}
}
class Ball{
}
大概邏輯是先判斷l(xiāng)ist是否為空,若為空,創(chuàng)建一個新的ArrayList對象,若不為空,則直接使用已存在的ArrayList對象,這樣可以保證在整個項目中l(wèi)ist始終指向同一個對象。這在單線程環(huán)境中是完全沒有問題的,但是如果在多線程環(huán)境中,list還未實例化時,A線程和B線程同時執(zhí)行if語句,A和B線程都會認(rèn)為list為null,A和B線程都會執(zhí)行實例化語句,造成混亂。
自增++操作(讀取--修改--寫入) ------------------------------------------------------------------------ 參考上文中為改進(jìn)之前的代碼(對INS類中inc的自增)
以上兩個例子告訴我們,必須添加適當(dāng)?shù)耐讲呗?,保證復(fù)合操作的原子性,防止競態(tài)條件的出現(xiàn)
策略一:使用原子變量類,在java.util.concurrent.atomic包中包含了一些原子變量類

package hgh0808;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class INS{
public static AtomicInteger inc = new AtomicInteger(0);
public static void increase(){
inc.incrementAndGet();
}
}
執(zhí)行結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
值得注意的是,只有一個狀態(tài)變量時,可以通過原子變量類實現(xiàn)線程安全。但是如果有多個狀態(tài)變量呢?
設(shè)想一個情景
多個線程不斷產(chǎn)生1到10000的隨機(jī)數(shù)并且發(fā)送到一個計算線程,計算線程每獲取一個數(shù)字n,就計算sinx在[0,n]上的積分并打印到控制臺上,為了提高程序性能,設(shè)計一個緩存機(jī)制,保存上次的數(shù)字n和積分結(jié)果(兩個狀態(tài)變量)。如果本次的數(shù)字和上次的數(shù)字相等,直接打印積分結(jié)果,避免重復(fù)計算。
看代碼:
package hgh0808;
import java.util.concurrent.atomic.AtomicReference;
public class Calculate extends Thread{
private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //緩存機(jī)制,原子變量類
private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //緩存機(jī)制,原子變量類
private static final double N = 100000; //將區(qū)間[0,e]分成100000份,方便定積分運(yùn)算
public void service() throws Exception{
getData();
Thread.sleep(10000); //等待MyQueue隊列中有一定數(shù)量的元素后,再開始從其中取元素
while(true){
Double e;
if(!MyQueue.myIsEmpty()){
e = MyQueue.myRemove();
}else{
return;
}
if(e.equals(lastNumber.get())){
System.out.println(lastNumber.get()+" "+lastRes.get());
}else{
Double temp = integral(e);
lastNumber.set(e);
lastRes.set(temp);
System.out.println(e+" "+temp);
}
Thread.sleep(2000);
}
}
public void getData(){ //創(chuàng)建并啟動四個獲取隨機(jī)數(shù)的線程,這四個線程交替向MyQueue隊列中添加元素
Thread1 th1 = new Thread1();
Thread2 th2 = new Thread2();
Thread3 th3 = new Thread3();
Thread4 th4 = new Thread4();
th1.start();
th2.start();
th3.start();
th4.start();
}
public Double integral(double e){ //計算定積分
double step = (e-0)/N;
double left = 0,right = step;
double sum = 0;
while(right <= e){
double mid = left+(right-left)/2;
sum+=Math.sin(mid);
left+=step;
right+=step;
}
sum*=step;
return sum;
}
}
---------------------------------------------------------------------
package hgh0808;
import java.util.LinkedList;
public class MyQueue { //由于LinkedList是線程不安全的,因此需要將其改寫為線程安全類
private static LinkedList<Double> queue = new LinkedList<>();
synchronized public static void myAdd(Double e){
queue.addLast(e);
}
synchronized public static void myClear(){
queue.clear();
}
synchronized public static int mySize(){
return queue.size();
}
synchronized public static boolean myIsEmpty(){
return queue.isEmpty();
}
synchronized public static double myRemove(){
return queue.removeFirst();
}
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread1 extends Thread{
private double data;
@Override
public void run(){
while(true){
Random random = new Random();
data = (double) (random.nextInt(10000)+1);
if(MyQueue.mySize() > 10000){ //由于從隊列中取元素的速度低于四個線程向隊列中加元素的速度,因此隊列的長度是趨于擴(kuò)張的,當(dāng)達(dá)到一定程度時,清空隊列
MyQueue.myClear();
}
MyQueue.myAdd(data);
try {
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread2 extends Thread{
private double data;
@Override
public void run(){
while(true){
Random random = new Random();
data = (double) (random.nextInt(10000)+1);
if(MyQueue.mySize() > 10000){
MyQueue.myClear();
}
MyQueue.myAdd(data);
try {
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread3 extends Thread{
private double data;
@Override
public void run(){
while(true){
Random random = new Random();
data = (double) (random.nextInt(10000)+1);
if(MyQueue.mySize() > 10000){
MyQueue.myClear();
}
MyQueue.myAdd(data);
try {
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread4 extends Thread{
private double data;
@Override
public void run(){
while(true){
Random random = new Random();
data = (double) (random.nextInt(10000)+1);
if(MyQueue.mySize() > 10000){
MyQueue.myClear();
}
MyQueue.myAdd(data);
try {
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
只看Calculate線程,不看其他線程和MyQueue中的鎖機(jī)制,本問題的焦點(diǎn)在于Calculate線程中對多個狀態(tài)變量的同步策略
存在問題:
盡管對lastNumber和lastRes的set方法的每次調(diào)用都是原子的,但仍然無法同時更新lastNumber和lastRes;如果只修改了其中一個變量,那么在這兩次修改操作之間,其它線程將發(fā)現(xiàn)不變性條件被破壞了。換句話說,就是沒有足夠的原子性
**當(dāng)在不變性條件中涉及多個變量時,各個變量間并不是彼此獨(dú)立的,而是某個變量的值會對其它變量的值產(chǎn)生約束。因此當(dāng)更新某一個變量時,需要在同一個原子操作中對其他變量同時進(jìn)行更新。
改進(jìn) ================>加鎖機(jī)制 內(nèi)置鎖 synchronized
之所以每個對象都有一個內(nèi)置鎖,只是為了免去顯式地創(chuàng)建鎖對象
synchronized修飾方法就是橫跨整個方法體的同步代碼塊
非靜態(tài)方法的鎖-----方法調(diào)用所在的對象
靜態(tài)方法的鎖-----方法所在類的class對象
public class Calculate extends Thread{
private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //緩存機(jī)制,原子變量類
private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //緩存機(jī)制,原子變量類
private static final double N = 100000; //將區(qū)間[0,e]分成100000份,方便定積分運(yùn)算
public void service() throws Exception{
getData();
Thread.sleep(10000); //等待MyQueue隊列中有一定數(shù)量的元素后,再開始從其中取元素
while(true){
Double e;
synchronized (this){ //檢查--執(zhí)行 使用synchronized同步,防止出現(xiàn)競態(tài)條件
if(!MyQueue.myIsEmpty()){
e = MyQueue.myRemove();
}else{
return;
}
}
if(e.equals(lastNumber.get())){
System.out.println(lastNumber.get()+" "+lastRes.get());
}else{
Double temp = integral(e);
synchronized (this) { //兩個狀態(tài)變量在同一個原子操作中更新
lastNumber.set(e);
lastRes.set(temp);
}
System.out.println(e+" "+temp);
}
Thread.sleep(2000);
}
}
public void getData(){ //創(chuàng)建并啟動四個獲取隨機(jī)數(shù)的線程,這四個線程交替向MyQueue隊列中添加元素
Thread1 th1 = new Thread1();
Thread2 th2 = new Thread2();
Thread3 th3 = new Thread3();
Thread4 th4 = new Thread4();
th1.start();
th2.start();
th3.start();
th4.start();
}
public Double integral(double e){ //計算定積分
double step = (e-0)/N;
double left = 0,right = step;
double sum = 0;
while(right <= e){
double mid = left+(right-left)/2;
sum+=Math.sin(mid);
left+=step;
right+=step;
}
sum*=step;
return sum;
}
}
對于包含多個變量的不變性條件中,其中涉及的所有變量都需要由同一個鎖來保護(hù)
synchronized (this) { //兩個狀態(tài)變量在同一個原子操作中更新
lastNumber.set(e);
lastRes.set(temp);
}
鎖的重入
如果某個線程試圖獲得一個已經(jīng)由它自己持有的鎖,那么這個請求就會成功,“重入”意味著獲取鎖的操作的粒度是‘線程',而不是‘調(diào)用'。
重入的一種實現(xiàn)方式 :
為每個鎖關(guān)聯(lián)一個獲取計數(shù)值和一個所有者線程。當(dāng)計數(shù)值為0時,這個鎖就被認(rèn)為是沒有被任何線程持有。當(dāng)線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,并且將獲取計數(shù)值置為1。如果同一個線程再次獲取這個鎖,計數(shù)值將遞增,當(dāng)線程退出同步代碼塊時,計數(shù)器會相應(yīng)地遞減。當(dāng)計數(shù)值為0時,這個鎖將被釋放。
如果內(nèi)置鎖不可重入,那么以下這段代碼將發(fā)生死鎖(每個doSomething方法在執(zhí)行前都會獲取Father上的內(nèi)置鎖)
----------------------------------------------------------------------
public class Father{
public synchronized void doSomething(){
}
}
public class Son extends Father{
@Override
public synchronized void doSomething(){
System.out.println("重寫");
super.doSomething();
}
}
線程安全性與性能和活躍性之間的平衡
活躍性:是否會發(fā)生死鎖饑餓等現(xiàn)象
性能:線程的并發(fā)度
不良并發(fā)的應(yīng)用程序:可同時調(diào)用的線程數(shù)量,不僅受到可用處理資源的限制,還受到應(yīng)用程序本身結(jié)構(gòu)的限制。幸運(yùn)的是,通過縮小同步代碼塊的作用范圍,可以平衡這個問題。
縮小作用范圍的原則====>當(dāng)執(zhí)行時間較長的計算或者可能無法快速完成的操作時,一定不能持有鎖?。。?/p>
總結(jié)
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SSH框架網(wǎng)上商城項目第30戰(zhàn)之項目總結(jié)(附源碼下載地址)
這篇文章主要介紹了SSH框架網(wǎng)上商城項目第30戰(zhàn)之項目總結(jié),并附源碼下載地址,感興趣的小伙伴們可以參考一下2016-06-06
單點(diǎn)登錄的概念及SpringBoot實現(xiàn)單點(diǎn)登錄的操作方法
在本文中,我們將使用Spring Boot構(gòu)建一個基本的單點(diǎn)登錄系統(tǒng),我們將介紹如何使用Spring Security和JSON Web Tokens(JWTs)來實現(xiàn)單點(diǎn)登錄功能,本文假設(shè)您已經(jīng)熟悉Spring Boot和Spring Security,感興趣的朋友一起看看吧2024-10-10
Intellij IDEA使用restclient測試的教程圖解
這篇文章主要介紹了Intellij IDEA使用restclient測試的教程圖解,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
Eclipse中查看android工程代碼出現(xiàn)"android.jar has no source attachment
這篇文章主要介紹了Eclipse中查看android工程代碼出現(xiàn)"android.jar has no source attachment"的解決方案,需要的朋友可以參考下2016-05-05
Spring Boot創(chuàng)建非可執(zhí)行jar包的實例教程
這篇文章主要介紹了Spring Boot創(chuàng)建非可執(zhí)行jar包的實例教程,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02

