多線程與高併發學習筆記(一)

在編程領域,我們要想有所提高,那麼需要跨越的一座大山就是多線程與高併發。這是我多線程與高併發學習系列文章的第一篇,我會從最基本的什麼是線程講起,希望更多的人能夠從這些文章中有所收穫爲你學習多線程與高併發助力,用最少的時間得到最大的成果。

線程

基本概念

說到線程就得從程序說起,程序是躺在磁盤中的應用代碼,進程是運行中的程序,而線程是進程的不同執行路徑,用專業一點的說法就是進程是資源分配的基本單位,而線程是調度執行的基本單位。

線程的創建與使用

在這裏插入圖片描述
線程的實現方式
1.繼承Thread 重寫run()方法
2.實現Runnable接口,重寫run()方法
3.線程池

線程的幾種狀態
new runnable terminated waiting timeWaiting blocked

常用法法
Thread.sleep(time) 線程進入睡眠 睡眠時間結束後繼續運行
Thread.yield() 放棄當前的執行機會進入就緒隊列 等待下次運行
t1.join() 在當前線程中調用其他線程的join方法 這樣會讓當前線程等待其他線程執行完畢後再執行當前線程,調用自己的join無效

synchronized的使用(可保證原子性和有序性)(鎖的是對象)

1.兩種使用方式 第一 用於方法上,當關鍵字修飾方法即可 第二用synchronized(){ } 修飾的代碼塊,二者沒有很大區別,只是同步的代碼塊大小不同而已。

2.synchronized加鎖的對象可以是任意的,一般來說方法前加關鍵字修飾時所對象是this。靜態方法或代碼塊爲class對象。

public class T {

	private static int count = 10;
	
	public synchronized static void m() { //這裏等同於synchronized(FineCoarseLock.class)
		count--;
		System.out.println(Thread.currentThread().getName() + " count = " + count);
	}
	
	public static void mm() {
		synchronized(T.class) { //考慮一下這裏寫synchronized(this)是否可以?
			count --;
		}
	}
}

3.synchronized加鎖的過程中有鎖升級的過程
JDK早期是底層實現是重量級鎖 後來經過改造加鎖的過程爲 偏向鎖 -> 自旋鎖(有個自旋次數) -> 重量級鎖。

4.synchronized鎖是可重入鎖 。
5.鎖定方法和非鎖定方法可同時執行。
6.方法內發生異常時會釋放鎖。
7.什麼時候用自旋鎖 什麼時候用重量級鎖?
鎖的代碼執行時間短,線程比較少的時候用自旋
鎖的代碼執行時間長 線程比較多的時候用重量級鎖
8.應避免鎖對象發生變化(對象屬性變了不影響,引用發生變化就會有影響)

*注:銀行問題上存款取款都得加synchronized *

/**
 * 面試題:模擬銀行賬戶
 * 對業務寫方法加鎖
 * 對業務讀方法不加鎖
 * 這樣行不行?
 *
 * 容易產生髒讀問題(dirtyRead)
 */

import java.util.concurrent.TimeUnit;

public class Account {
	String name;
	double balance;
	
	public synchronized void set(String name, double balance) {
		this.name = name;

		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		this.balance = balance;
	}
	
	public /*synchronized*/ double getBalance(String name) {
		return this.balance;
	}
	
	
	public static void main(String[] args) {
		Account a = new Account();
		new Thread(()->a.set("zhangsan", 100.0)).start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println(a.getBalance("zhangsan"));
		
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println(a.getBalance("zhangsan"));
	}
}

一般來說有兩大類 樂觀鎖 悲觀鎖。

偏向鎖:
樂觀鎖,加鎖只是標記下當前線程號
自旋鎖:
樂觀鎖 ,不釋放CPU,等待
重量級鎖:
悲觀鎖,需要OS來協助。

volatile(保證可見性和禁止指令重排序)

可見性
一個線程的修改對其他線程可見。線程之間具有隔離性,多線程參與訪問同一個對象時,可能會有問題,而用volatile修飾的對象,當一個線程對該對象做了修改後,其他線程也可見。
指令重排序
用volatile修飾的變量,在操作時禁止指令重排序 如用volatile修飾的對象在創建時指令不能重排序
注:創建對象時有3步 1.分配內存空間 2.初始化 3.給引用賦值 3條指令在執行過程中可能會進行指令重排序
例如DCL單例問題(double check lock)
指令重排序使得引用的值過早的給賦值,使得對象還未初始化就使得引用有值,因此會使得其他線程獲取到未初始化的對象,影響使用。

volatile不能保證原子性,不能代替synchronized

CAS(無鎖優化)

compare and set ,自旋鎖、樂觀鎖,底層使用原子操作(CPU指令級操作)。JUC中的AutomicXX等原子類的底層實現都是利用的CAS。
CAS是一種無鎖算法,CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。

一個線程去修改某個值的過程可簡單描述爲這樣:參數1 當前線程現在讀取到的真實內存值(新讀取的值) 參數2 當前線程在加載程序進入線程空間時獲取到的值(舊值)參數3 要賦的值 參數1和參數2比較 如果相等修改 否則失敗

計數增長問題

在大多數的程序中都會涉及到計數問題,而爲了確保數字的準確性,我們有如下幾種方式來確保程序的健壯性。
1.synchronized
2.AutomicXXX原子類
3.LongAdder類 JUC中的一個類,底層採用分段鎖

 static LongAdder count = new LongAdder();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[500];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new Thread(()-> {
                        for(int k=0; k<100000; k++) count.increment();
                    });
        }
     }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章