執行緒同步
當多個執行緒訪問一個物件時,有可能會發生汙讀,即讀取到未及時更新的資料,這個時候就需要執行緒同步。
執行緒同步:
即當有一個執行緒在對記憶體進行操作時,其他執行緒都不可以對這個記憶體地址進行操作,直到該執行緒完成操作, 其他執行緒才能對該記憶體地址進行操作,而其他執行緒又處於等待狀態,實現執行緒同步的方法有很多,臨界區物件就是其中一種。
在一般情況下,建立一個執行緒是不能提高程式的執行效率的,所以要建立多個執行緒。但是多個執行緒同時執行的時候可能呼叫執行緒函式,在多個執行緒同時對同一個記憶體地址進行寫入,由於CPU時間排程上的問題,寫入資料會被多次的覆蓋,所以就要使執行緒同步。
同步就是協同步調,按預定的先後次序進行執行。如:你說完,我再說。
“同”字從字面上容易理解為一起動作
其實不是,“同”字應是指協同、協助、互相配合。
如程序、執行緒同步,可理解為程序或執行緒A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B執行;B依言執行,再將結果給A;A再繼續操作。
所謂同步,就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不返回,同時其它執行緒也不能呼叫這個方法。按照這個定義,其實絕大多數函式都是同步呼叫(例如sin, isdigit等)。但是一般而言,我們在說同步、非同步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。例如Window API函式SendMessage。該函式傳送一個訊息給某個視窗,在對方處理完訊息之前,這個函式不返回。當對方處理完畢以後,該函式才把訊息處理函式所返回的LRESULT值返回給呼叫者。
在多執行緒程式設計裡面,一些敏感資料不允許被多個執行緒同時訪問,此時就使用同步訪問技術,保證資料在任何時刻,最多有一個執行緒訪問,以保證資料的完整性。
由於同一程序的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可能存在以下問題:
- 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起;
- 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引
起效能問題; - 如果一個優先順序高的執行緒等待- -個優先順序低的執行緒釋放鎖會導致優先順序倒
置,引起效能問題.
舉個例子,一個售票口有10張票,當100個人同時去買時,每個人都獲取到了有100張票的資料,所以每個人買了一張,導致最後剩下-90張票,執行緒不同步就會導致這種結果。
synchronized
synchronized是Java中的關鍵字,是一種同步鎖。它修飾的物件有以下幾種:
1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;
2. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件;
3. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;
4. 修改一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。
我們寫一個例子,使用執行緒不安全的List來看看效果
public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
可以看到,迴圈1000次,只存進去998個,重複執行,這個大小還會變化,所以是執行緒不安全的。
可以使用synchronized把list加鎖,就能保證每次都能插入進去。
public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
這樣就能夠保證執行緒安全。
也可以使用JUC(java.util.concurrent
)包下的執行緒安全的列表CopyOnWriteArrayList,程式碼如下
import java.util.concurrent.CopyOnWriteArrayList;
public class MyThread{
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
使用CopyOnWriteArrayList就可以不需要synchronized關鍵字實現執行緒安全
檢視原始碼可以發現,CopyOnWriteArrayList實現了List<E>介面
然後再add方法中使用了synchronized來加鎖,和我們上面的操作方法一致
//CopyOnWriteArrayList中的add()方法
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
死鎖
所謂死鎖,是指多個程序在執行過程中因爭奪資源而造成的一種僵局,當程序處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。
死鎖的條件
- 互斥條件
- 請求和保持
- 不可搶佔
- 迴圈等待
只要破壞後三個條件之一就可以避免死鎖,可以使用銀行家演算法等方法。
Lock鎖
- 從JDK 5.0開始,Java提供了更強大的執行緒同步機制一通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。
- Lock鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開
始訪問共享資源之前應先獲得Lock物件 - ReentrantLock類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。
先寫一個不使用鎖的例子
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread);
thread1.start();
thread2.start();
thread3.start();
}
public static int tickets = 10;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
}
}
}
執行後發現順序完全是亂的
使用ReentrantLock(可重入鎖)來把相關程式碼加鎖,即可實現按順序呼叫
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread);
thread1.start();
thread2.start();
thread3.start();
}
public static int tickets = 10;
final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
這樣也可以實現執行緒同步。
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖) synchronized是隱式鎖,出了
作用域自動釋放 - Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
- 使用Lock鎖, JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件
性(提供更多的子類)。 - 優先使用順序:
- Lock >同步程式碼塊(已經進入了方法體,分配了相應資源) >同步方法(在方
法體之外)
- Lock >同步程式碼塊(已經進入了方法體,分配了相應資源) >同步方法(在方
執行緒通訊
生產者和消費者問題
- 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費。
- 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止。
- 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止。
Java提供的執行緒通訊方法
方法名 | 作用 |
---|---|
wait() | 表示執行緒一直等待,直到其他執行緒通知,與sleep不同,會釋放鎖 |
wait(long timeout) | 指定等待的毫秒數 |
notify() | 喚醒一個處於等待狀態的執行緒 |
notifyAll() | 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先排程 |
均是0bject類的方法都,只能在同步方法或者同步程式碼塊中使用,否則會丟擲llegalMonitorStateException
- 對於生產者,沒有生產產品之前,要通知消費者等待.而生產了產品之後,又需要馬_上通知消費者消費
- 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費
- 在生產者消費者問題中,僅有synchronized是不夠的
- synchronized 可阻止併發更新同- -個共享資源,實現了同步
- synchronized 不能用來實現不同執行緒之間的訊息傳遞(通訊)
解決方式一:管程
首先定義一個生產者類
//生產者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
//生產
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生產第" + i + "個");
container.push(new Product(i));
}
}
}
生產者不斷往緩衝區新增產品,然後定義一個消費者類
//消費者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
//消費
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消費第" + container.pop().id + "個");
try {
Thread.sleep(500);
} catch (InterruptedException ignored) { }
}
}
}
消費者不斷在緩衝區去除產品,這裡新增一個sleep來模擬真實效果
最後定義緩衝區
//緩衝區
class SynContainer {
//容器大小
Product[] products = new Product[10];
//計數器
int count = 0;
//生產者放入產品
public synchronized void push(Product product) {
//如果滿了,通知消費者,生產者等待,否則放入產品
if (count == products.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[count++] = product;
this.notifyAll();
}
//消費者消費產品
public synchronized Product pop() {
if (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
return products[--count];
}
}
緩衝區的兩個方法都是使用synchronized修飾,保證能夠執行完整,然後根據容器大小來判斷是否讓生產者以及消費者執行緒等待
當容器中沒有產品時,通知消費者等待,生產者執行緒開始,當產品滿時,通知生產者等待,消費者執行緒開始。
最後補上產品類
//產品
class Product {
//產品編號
int id;
public Product(int id) {
this.id = id;
}
}
解決方式二:訊號量
類定義和上面類似,只不過在產品類中添加了一個訊號量來區分是否有產品,不需要一個緩衝區
//生產者
class Producer extends Thread {
Product product;
public Producer(Product product) {
this.product = product;
}
//生產
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.push("產品" + i);
}
}
}
//消費者
class Consumer extends Thread {
Product product;
public Consumer(Product product) {
this.product = product;
}
//消費
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.pop();
}
}
}
//產品
class Product {
String product;
boolean flag = true;
//生產
public synchronized void push(String product) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("生產了" + product);
//通知消費
this.notifyAll();
this.product = product;
this.flag = !this.flag;
}
//消費
public synchronized void pop() {
if (flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("消費了" + this.product);
//通知生產者
this.notifyAll();
this.flag = !this.flag;
}
}
這樣也可以解決生產者和消費者問題
執行緒池
背景
經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。
優點
- 提高響應速度(減少了建立新執行緒的時間)
- 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
- 便於執行緒管理
引數說明
- corePoolSize: 核心池的大小
- maximumPoolSize:最大執行緒數
- keepAliveTime: 執行緒沒有任務時最多保持多長時間後會終止
JDK 5.0起提供了執行緒池相關API: ExecutorService和Executors
ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
- void execute(Runnable command) :執行任務/命令,沒有返回值,-般用來執行Runnable
- <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一-般 又來執行
Callable - void shutdown() :關閉連線池
Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池
程式碼演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//建立執行緒池
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//關閉連線
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
這樣就可以實現通過執行緒池來管理執行緒
總結
- 執行緒就是獨立的執行路徑;
- 在程式執行時,即使沒有自己建立執行緒,後臺也會有多個執行緒,如主執行緒,gc執行緒;
- main()稱之為主執行緒,為系統的入口,用於執行整個程式;
- 在一個程序中,如果開闢了多個執行緒,執行緒的執行由排程器安排排程,排程器是與
- 作業系統緊密相關的,先後順序是不能認為的干預的。
- 對同一份資源操作時,會存在資源搶奪的問題,需要加入併發控制;
- 執行緒會帶來額外的開銷,如cpu排程時間,併發控制開銷。
- 每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致
Java多執行緒(上)https://www.cnblogs.com/chaofanq/p/15024558.html