一、併發編程基礎篇
分爲兩塊
- 1.x線程安全基礎知識、synchronized、volatile關鍵字的實際場景使用
1、併發編程線程安全問題。
- 理解線程安全👇
線程安全概念:當多個線程訪問某-一個類 (對象或方法)時,這個類始終都能表現出 正確的行爲,那麼這個類(對象或方法)就是線程安全的。
synchronized:可以在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或" 臨界區"
- 多個線程多個鎖。
多個線程多個鎖:多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後,
執行synchronized方法體的內容。
當多個線程訪問myThread的run方法時,以排隊的方式進行處理(這裏排對是按 照CPU分配的先後順序而定的)
,
一個線程想要執行synchronized修飾的方法裏的代碼,首先是嘗試獲得鎖,如果拿到鎖,執行synchronized代碼體內容;拿不到鎖, 這個線程就會不斷的嘗試獲得這把鎖,直到拿到爲止,而且是多個線程同時去競爭這 把鎖。( 也就是會有鎖競爭的問題)。
關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當做鎖,
所以示例代碼中那個線程先執行synchronized關鍵字的方法,那個線程就持有該方法
所屬對象的鎖(Lock),兩個對象,線程獲得的就是兩個不同的鎖,他們互不影響。
有一-種情況則是相同的鎖,即在靜態方法上加synchronized關鍵字,表示鎖 定.class類,類一級別的鎖(獨佔.class類)。
2、上乾貨 🐂🍺
package multithreading_demo;
import java.util.concurrent.atomic.AtomicInteger;
public class MultiThread {
//不要設置靜態變量,因爲靜態變量只能初始話一次
//想實現原子性就可以加 static靜態修飾
private static int num = 0;
//一個原子性的計數器
//private AtomicInteger num;
//加一個線程鎖 鎖的是當前的對象,誰調用鎖誰。
//static 如果在方法上加 static就是 當前類鎖了,
//無論實例化多少個對象都是當前同一把鎖。那麼就會先搶到鎖的先執行,
public static synchronized void printNum(String tag) {
try {
if (tag == "a") {
num= 100;
System.out.println("tag 100 aaaa");
//開兩個線程,當走到這裏讓這個線程 休眠1秒 那麼就永遠是 b線程 先執行完。
//如果不休眠。那就就搶線程。就不可預知誰先執行完。
new Thread().sleep(1000);
}else {
num=200;
System.out.println("tag 200 bbbb");
}
System.out.println(tag+"------thread -------"+num);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//開啓兩個線程 兩個線程是並行執行黨的,所以會出現搶線程的問題
MultiThread m=new MultiThread();
MultiThread m2=new MultiThread();
Thread t=new Thread(new Runnable() {
@Override
public void run() {
m.printNum("a");
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t.start();
t2.start();
}
}
3、線程安全
線程安全概念:當多個線程訪問某一個類(對象或方法)時,這個類始終都能表現出 正確的行爲,那麼這個類(對象或方法)就是線程安全的。
- 實現線程安全的手段有很多種。synchronized
- synchronized JDK 1.7之前的不是特別好,但1.6版本以上已經做的非常好了。
synchronized:可以在任意對象及方法上加鎖,
而加鎖的這段代碼稱爲"互斥區"
或"臨界區"
比如一個 方法加上 synchronized之後,有兩個線程過來訪問,那隻能是第一個線程走完之後,第二個線程在走。
- 示例總結:
當多個線程訪問myThread的run方法時,以排隊的方式進行處理(這裏排對是按
照CPU分配的先後順序而定的),-一個線程想要執行synchronized修飾的方法裏的
代碼,首先是嘗試獲得鎖,如果拿到鎖,執行synchronized代碼體內容;拿不到鎖,
這個線程就會不斷的嘗試獲得這把鎖,直到拿到爲止,而且是多個線程同時去競爭這 把鎖。( 也就是會有鎖競爭的問題)。
4、上TM乾貨,真🐂🍺
線程安全代碼。
package multithreading_demo;
/**
* 線程安全概念:當多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的。
* synchronized: 可以在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或”臨界區"
*
* @author alienware
*/
public class MyThread extends Thread {
private int count = 5;
/**
* synchronized 如果不加鎖,就是並行線程的執行,
* 不加鎖:線程不安全
* 加鎖:線程安全
**/
// 重寫run方法
public synchronized void run() {
count--;
// currentThread 當前線程
System.out.println(this.currentThread().getName() + "----count ---" + count);
}
/**
* 分析:當多個線程訪問MyThread的run方法時,以排隊的方式進行處理(這裏排對是按照CPU分配的先後順序而定的), 一個線程想要執
* 行synchronized修飾的方法裏的代碼: 1: 嘗試獲得鎖
* 2:如果拿到鎖,執行synchronized代碼體內容:拿不到鎖,這個線程就會不斷的嘗試獲得這把鎖,直到拿到爲止, 而且是多個線程同時去競爭這把鎖。(
* 也就是會有鎖競爭的問題)
**/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 把myThread放到 五個線程裏面
Thread t1 = new Thread(myThread, "t1");
Thread t2 = new Thread(myThread, "t2");
Thread t3 = new Thread(myThread, "t3");
Thread t4 = new Thread(myThread, "t4");
Thread t5 = new Thread(myThread, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
/**
* 結果 不是並行的,誰先獲得鎖,誰先執行方法synchronized的內容。
* 但 搶鎖的過程是並行的。
* 排隊的方式進行執行的。
* t1----count ---4
* t5----count ---3
* t4----count ---2
* t3----count ---1
* t2----count ---0
*/
}
}
當然後多個線程搶一把鎖,那麼cpu就是燃的特別高,服務器就會出現卡頓之類的,後面會做出優化怎麼解決 多線程的壓力
。
1、對象鎖的同步和異步
- 同步
- 同步 :可以理解爲 分佈式mysql 主服務器寫,從服務器高併發的去讀,讀到就有數據,讀不到就沒有數據,這屬於主從同步
- 分爲sync 同步操作 async 異步同步操作
異步同步操作理解:有兩個數據庫,A主服務器 B從服務器
- A服務器支持 讀支持寫`
B服務器只支持 讀
n個 Thread去併發的從服務器中讀操作,不會影響數據的問題的,因爲這些進程沒有任何的DML操作 ,那麼就可以理解爲數據是靜態的。
- `那對於DML寫操作,那麼兩個線程同時操作同一條數據的時候,
- 如果兩個線程同時操作一條數據的時候,就會產生鎖表鎖行的概念。
那這樣的話,就產生另外一種同步,就是雙主雙從了,雙主都可以進行讀寫。
mysql 同步機制,比如一個Thread去執行一個 insert操作,會記錄一條日誌,從服務器會存儲這條日誌,那麼從服務器會讀取這條日誌,解析爲一條sql,在內部在執行一次insert操作來進行同步。
分爲兩種情況,
1.同步刷盤
同步就是 主服務器存儲完,從服務器讀取日誌也同步完之後纔算完成。
2.異步刷盤
就是主服務器,存儲完之後記錄過log日誌就直接向外發出 over存儲結束。
這種情況有可能會主服務器掛掉,log日誌無法恢復會產生數據丟失,但是很極端的情況,一般來講mysql做 雙主多從服務了。
mysql中的分表分庫和同步是兩個不同的東西。
2、加鎖的對象訪問不同的方法 看👆圖
package multithreading_demo;
public class MyObject {
//這個可以看作同步的方法
public synchronized void printA() {
System.out.println(Thread.currentThread().getName() + "--print---A");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//這個可以看作異步的 其他操作完全可以進行互不干涉
public void printB() {
System.out.println(Thread.currentThread().getName() + "--print---B");
}
//開了兩個線程,雖然是鎖的對象是同一個但是,第一個方法需要搶鎖,
//第二個方法並沒有加鎖,所以兩個線程互不干涉
public static void main(String[] args) {
MyObject ob = new MyObject();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ob.printA();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ob.printB();
}
});
t1.start();
t2.start();
}
}
注意看代碼中的註釋
3、髒讀
對於對象的同步和異步的方法,我們在設計自己的程序的時候,一定要考慮問題的整體,
不然就會出現數據不一致的錯誤, 很經典的錯誤就是髒讀(dirtyread)
如果關乎到業務的話, 最好都加上同步鎖。保證整體業務的安全完整性
髒讀乾貨 代碼
package multithreading_demo;
public class DirtyReading {
private String userName = "zhang";
private String passWord = "123";
// 加上鎖之後 可以保證其他線程都不能訪問這個 方法的
// 就可以保證這個 方法是安全的
public synchronized void setValue(String userName, String passWord) {
this.userName = userName;
try {
System.out.println("set Value userName =" + this.userName + "passWord =" + this.passWord);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.passWord = passWord;
}
/**
* 如果加上 synchroized 那麼設置完值之後的結果爲
* set Value userName =wangpassWord =123
* set Value userName =wangpassWord =123456
*/
public void getValue() {
System.out.println("set Value userName =" + this.userName + "passWord =" + this.passWord);
}
public static void main(String[] args) {
DirtyReading dr = new DirtyReading();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// DirtyReading dr = new DirtyReading();
dr.setValue("wang", "123456");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
// 他們兩個方法的synchronized 鎖的都不是同一個對象
// 那麼 他們不是同一把鎖,永遠獲取不到另一把鎖的賦的值。
// 如果是同一個對象那麼 他們訪問帶 synchronized就會得到賦的值
// DirtyReading dr2 = new DirtyReading();
@Override
public void run() {
dr.getValue();
}
});
t2.start();
/**
* 結果
* set Value userName =wangpassWord =123
* set Value userName =wangpassWord=123
* 可以看出 t1線程 設置的password並沒有設置到 因爲他們倆個線程之間沒有關係。沒有所以並行執行,而設置值的時候 休眠了1秒鐘 所以沒設置上。造成髒讀問題
* 所以遇到要求一致性比較強的系統 ,最好都加上 synchronized
*/
}
}
*******************千萬注意代碼中細節**
- 如果針對某種要求一致性業務,或者安全的,要加鎖 都加鎖,要不加鎖都不加鎖
- 但針對讀寫業務可以設置讀寫分離鎖
- 讀的話可以併發讀,因爲數據是不變的。可以當作是靜態的。隨便讀
- 到今爲止的設置鎖上面會在效率上大大降低,後面會講解jdk中鎖的優化。從而達到效率上的保證。
示例總結
在我們對一個對象的方法加鎖的時候,需要考慮業務的整體性,即爲
setValue/getValue方法同時加鎖synchronized同步關鍵字,保證業務(service) 的原子性,不然會出現業務錯誤(也從側面保證業務的一致性)。
4、synchroized其他概念
synchronized鎖重入: 關鍵字synchronized擁有鎖重入的功能,也就是在使synchronized時,當一個線程得到了一個對象的鎖後,再次請求此對象時是可以再次得到該對象的鎖。
出現異常,鎖自動釋放:
說明:對於web應用程序,異常釋放鎖的情況,如果不及時處理,很可能對你的應用程序業務邏輯產生嚴重的錯誤,
比如你現在執行-一個隊列任務,很多對象都去在 等待第-一個對象正確執行完畢再去釋放鎖,但是第-一個對象由於異常的出現,導致業務邏輯沒有正常執行完畢,
就釋放了鎖,那麼可想而知後續的對象執行的都是錯誤的邏輯。
所以這一點一定要引起注意,在編寫代碼的時候,一定要考慮周全。
1.可以自己寫一寫 方法內多次嵌套方法 加synchroized 測試 也是線程安全的
2.子父類也可以實現線程安全
5、volatile關鍵字的概念
- volatile 關鍵字的作用:可以使變量在多個線程間可見。
重要★★★★★
分析一下JDK的內核 怎麼工作的 |
---|
每一個線程 start的時候都會有一個工作區域 |
類中方法上的變量 全局變量 共享主內存區 ,而每個線程啓動的時候會把主內存區的變量拷貝一份過來。就是run方法內需要用哪些變量就去主內存區取 拿過來,線程執行的時候就使用這些固定的值了當線程解鎖時(也就是跑完了)纔會把該線程的變量寫入到共享主內存中 |
這樣就能保證線程效率高,線程中有一塊獨立的內存,就每次執行的時候不用頻繁的到外面的主內存區加載這個值並取過來。 |
- 一個線程可以執行的操作有使用(use)、賦值(assign) 、裝載(load)、存儲 (store)、鎖定(lock) 解鎖(unlock)。
- 而主內存可以執行的操作有讀(read) 、寫(write) 、鎖定(lock) 、解鎖(unlock) , 每個操作都是原子的。
重要★★★★★
volatile的作用就是強制線程到主內存(共享內存)裏去讀取變量,而不去線程工作內存區裏去讀取,從而實現了多個線程間的變量可見。也就是滿足線程安全的可見性。
volatile 使用代碼 👇
package volatile_demo;
//多個線程的使用
public class Volatile_Thread extends Thread {
// 爲什麼加 volatile詳情看 👇的文字敘述
// volatile 只針對頻繁的使用此變量。只使用一次沒有效果,
private volatile boolean isRunning = true;
private volatile String name = "123";
// 假設多個線程來操作這個值的時候 加上鎖既安全效率又高。
// 前提是一個對象 多個對象是永遠無法干涉到另一個對象中的值的
// 設置鎖之後 誰先搶到 鎖誰先執行 賦值完畢後釋放鎖 下一個線程搶到鎖接着執行
private synchronized void setRunning(boolean isRunning, String name) {
this.isRunning = isRunning;
this.name = name;
}
public void run() {
// 這裏必須 寫爲 = 或 == 去比較值。
// 如果直接寫 isRunning 就會去加載共享的主內存區加載這個值。
// 相當於動態監控變量的變化了
while (this.isRunning == true) {
/**
* System 避免使用浪費資源 能不用就不用 而且加這個之後 不加volatile
* 讀一段時間 其他線程 進行主內存區的變量進行修改就因爲這個系統輸出語句
* 系統插過來一腳就會重新到主內存區去重新加載一次變量
* 那麼就會產生不同線程也會去主內存區加載變量
* 大概是 干擾了線程的狀態
**/
System.out.println("進入 while " + "----name= " + name);
}
System.out.println("--線程停止");
}
public static void main(String[] args) throws InterruptedException {
// 開啓Volatile_Thread 線程
Volatile_Thread vt = new Volatile_Thread();
vt.start();
// 上來就讓 main線程 睡TM兩秒
Thread.sleep(2000);
vt.setRunning(true, "456");
System.out.println("se----true---完畢");
// 再開一個線程
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
vt.setRunning(false, "789");
System.out.println("set----false---完畢");
}
});
// 一上來又讓這個狗線程睡 6秒
// sleep必須要放在 start前 不然就會先跑run方法 在休眠。
t3.sleep(6000);
t3.start();
}
}
注意代碼中註釋 及細節
volatile 原子性問題
volatile 代碼
package volatile_demo;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileNoAtomic extends Thread {
//volatile 並不能保證數據的完整性
//private static volatile int count;
//原子類 能保證結果會加到 10000 中間的結果不能保證 單個JVM考慮使用這個
private static AtomicInteger count = new AtomicInteger(0) ;
//synchronized 加了之後就能保證數據的 原子性 執行完纔會 輸出 並不是執行到一半其他的線程就直接搶這輸出了
private static void addCount() {
for(int i=0;i<1000;i++){
//count++ ;
//count.incrementAndGet(); // ++ 累加
//這種add 只能保證自身原子性 並不能保證 addCount這個方法是原子性 每次都會打印固定的值
count.addAndGet(1); //指定累加 步長
}
System.out.println (count.get()) ;
}
public void run() {
addCount();
}
public static void main (String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for(int i=0;i<10;i++){
arr[i] = new VolatileNoAtomic() ;
}
for(int i=0;i<10;i++){
//十個線程同時運行
arr[i].start();
}
}
}
- 2.x線程之間通信wait、notify、 ThreadLocal 、單例和多線程。
6、線程與線程之間的通信
notify和wait
代碼
package volatile_demo;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
public class List_Add2
{
private static ArrayList list = new ArrayList<>();
public void addList() {
list.add("元素");
}
public int getSize() {
return list.size();
}
public static void main(String[] args) throws InterruptedException {
final List_Add2 la = new List_Add2();
final Object lock = new Object();
// 用這個玩意 就不用設置鎖了 synchronized 兩個線程是並行執行的 兩邊互不影響
final CountDownLatch countDownLatch = new CountDownLatch(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) { // 可以把這個鎖加到for循環裏面 細粒度一點
for (int i = 0; i < 10; i++) {
la.addList();
System.out.println(Thread.currentThread().getName() + "添加元素");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (la.getSize() == 5) {
// 這裏是元素的個數 並不是索引 不要搞錯
System.out.println(la.getSize() + "---發出通知 獲取鎖");
// 發出通知
// countDownLatch.countDown();
lock.notify();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("進入 " + Thread.currentThread().getName() + "run 方法");
if (la.getSize() != 5) {
try {
// 只要 !=5 就釋放鎖
// 剛好 ==5 這裏面並沒有獲取到鎖 而是另一個 線程 調用lock.notify();強行獲取鎖
// 而這個線程又只能處於等待狀態
// 釋放鎖 等於等待狀態 等另一個線程執行結束釋放鎖 這個線程在獲取鎖
System.out.println("釋放鎖 處於等待狀態");
lock.wait();
// 接到countDownLatch.countDown(); 通知 直接往下執行
// countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//------------------------------------------------------------------------
System.out.println(Thread.currentThread().getName() + "--線程結束");
throw new RuntimeException();
}
}
});
// 先開啓 t2線程 誰先開啓誰先執行
t2.start();
// 然後讓 t1線程先睡3秒 然後再開啓
t1.sleep(3000);
t1.start();
// t1 先開啓 t2先開啓
// Thread-0添加元素 進入 Thread-1run 方法
// Thread-0添加元素 釋放鎖 處於等待狀態
// Thread-0添加元素 Thread-0添加元素
// Thread-0添加元素 Thread-0添加元素
// Thread-0添加元素 Thread-0添加元素
// 5---發出通知 獲取鎖 Thread-0添加元素
// Thread-0添加元素 Thread-0添加元素
// Thread-0添加元素 5---發出通知 獲取鎖
// Thread-0添加元素 Thread-0添加元素
// Thread-0添加元素 Thread-0添加元素
// Thread-0添加元素 Thread-0添加元素
// 進入 Thread-1run 方法 Thread-0添加元素
// 釋放鎖 處於等待狀態 Thread-1--線程結束
}
}
- 細節藏在TM代碼裏
7、 notify和wait 模仿Queue
- 模擬BlockingQueue
put方法圖解
take方法圖解
Queue代碼 TM乾貨 🐂🍺
package myqueue_demo;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class MyQueue
{
// 1.需要一個容器
private LinkedList<Object> list = new LinkedList();
// 2.需要一個計數器 聲明一個原子性的計數器
private AtomicInteger count = new AtomicInteger(0);
// 3.需要上限和和下限
private final int minSize = 0;
private final int maxSize;
// 4.構造方法
public MyQueue(int size) {
this.maxSize = size;
}
// 5.構造一個對象用於加鎖
private final Object lock = new Object();
// put (anobject) :把anobject加到BlockingQueue裏,
// 如果BlockQueue沒有空間,則調用此方法的線程被阻晰,直到BlockingQueue裏面有空間再繼續.
public void put(Object obj, MyQueue mq) throws InterruptedException {
synchronized (lock) {
// 容器最大長度是5 計數器小於 5纔可以獲取鎖
if (count.get() < this.maxSize) {
lock.notify();
}
// 如果==(容器最大長度)則容器已滿 無法put入值 處於阻塞
while (count.get() == this.maxSize) {
lock.wait();
}
}
// 加入元素
list.add(obj);
// 計數器累加
count.incrementAndGet();
System.out.println("新的元素加入------" + obj);
}
public Object take() throws InterruptedException {
Object ret = null;
synchronized (lock) {
// 如果等於最小值 則裏面沒有元素 則釋放鎖
while (count.get() == this.minSize) {
// 不走循環則不會釋放鎖
lock.wait();
}
// 走出循環說明另一個 說明裏面有元素
lock.notify();
}
// 1.移除第一個元素 有點類似於消費者概念了
ret = list.removeFirst();
// 2.計數器遞減
count.decrementAndGet();
return ret;
}
public int getSize() {
// 獲取計數器(等同容器的長度)的值
return this.count.get();
}
public static void main(String[] args) throws InterruptedException {
// 先執行main線程 指定最大長度
final MyQueue mq = new MyQueue(5);
// 調用put方法 容器有空間則不會阻塞
mq.put("a", mq);
mq.put("b", mq);
mq.put("c", mq);
mq.put("d", mq);
mq.put("e", mq);
System.out.println("當前容器的長度是" + mq.getSize());
// 在執行t1線程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 調用這個方法 則這個線程也跟着阻塞
mq.put("f", mq);
mq.put("g", mq);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 給線程設置名字
}, "t1");
// 容器已滿 肯定put不進去咯 就成了阻塞狀態 兩個線程並行的每次的結果都不一樣
t1.start();
// 在執行t2線程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Object o1;
try {
//調用這個方法 則這個線程也跟着阻塞
o1 = mq.take();
System.out.println("移除的元素是--" + o1);
Object o2 = mq.take();
System.out.println("移除的元素是--" + o2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2");
TimeUnit.SECONDS.sleep(2);
t2.start();
}
}
鎖的優化
- 鎖的粒度越小越好,原先100行加一把鎖,如果可以50行那更好。
- 那麼鎖的性能會更好。加鎖訪問必須得獲取鎖,而有些東西不牽扯到寫的話,就不建議加鎖性能不好,不加鎖十個線程可以併發訪問,加鎖則得排隊訪問。
8、ThreadLocal的概念
class Thread_test{
public static ThreadLocal th=new ThreadLocal () ;
}
如果只創建一個對象 並且在兩個線程中去,使用改變量無法使用,
這個ThreadLocal 只能在一個線程中使用,開兩個線程則另個線程拿不到,