Java-執行緒詳解
程序和執行緒:
-
程序:每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換會有較大的開銷,一個程序包含1–n個執行緒。(程序是資源(CPU,記憶體)分配的基本單位,是靜態的概念)
-
執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。(執行緒是程式執行的最小單位,真正幹活的)
程序中至少會有一個執行緒
並行:是指同時發生了,程式支援併發而已,任務同時發生了;
併發:是同時進行,同時執行某些任務
在單核CPU情況下,並不是真正的多執行緒,只不過是CPU在多個執行緒間進行快速切換,使用者就認為執行緒在同時執行而已;
執行緒建立的三種方式
在java中要想實現多執行緒,有三種手段,一種是繼承Thread類,另外一種是實現Runable介面,還有一種是實現Callable介面,並與Future、執行緒池結合使用。推薦使用實現Runable介面
1、自定義類繼承Thread,重寫run方法
public class MyThreadA extends Thread {
@Override
public void run() {
// 執行緒的執行邏輯
for (int i = 0; i < 20; i++) {
System.out.println ("執行緒A:" + i);
}
}
}
測試類
public class ThreadDemo {
public static void main(String[] args) {
System.out.println("main 執行緒開始了");
//建立執行緒物件
Thread threadA = new MyThreadA();
//start 只是OS排程器 執行緒準備好了
threadA.start();
//threadA.run();
System.out.println("main 執行緒結束了");
}
}
注意:start()方法呼叫後並不是立即執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),什麼時候執行是由作業系統決定的。
main方法其實也是一個執行緒。在java中所有的執行緒都是同時啟動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。
2、實現Runnable() 介面
- 匿名內部類
- lambda表示式
public class Test {
public static void main(String[] args) {
//匿名內部類
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒A:" + i);
}
}
});
//lambda表示式
Thread threadB = new Thread(() -> {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒B:" + i);
}
});
threadA.start();
threadB.start();
}
}
實現Runnable介面比繼承Thread類所具有的優勢:
- 避免了java中單繼承的限制
- 適合多個相同的程式程式碼的執行緒去處理同一個資源
- 增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立
- 執行緒池只能放入實現Runable或callable類執行緒,不能直接放入繼承Thread的類
3、實現Callable介面
- 匿名內部類
- lambda表示式
執行緒執行完之後如果需要得到執行緒的返回結果,需要使用到執行緒間的通訊,就要實現Callable介面
public class Demo {
public static void main(String[] args) {
//lambda
FutureTask futureTask1 = new FutureTask(() -> {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒A:" + i);
}
return 1;
});
//匿名內部類
FutureTask futureTask2 = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒B:"+i);
}
return 100;
}
});
Thread threadA = new Thread(futureTask1);
Thread threadB = new Thread(futureTask2);
threadA.start();
threadB.start();
try {
//get() 方法是阻塞時的方法,必須等執行緒執行完了才會得到返回值
Object o1 = futureTask1.get();
System.out.println("執行緒A的返回值:"+o1);
Object o2 = futureTask2.get();
System.out.println("執行緒B的返回值:"+o2);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("執行緒A 執行完了");
System.out.println("執行緒B 執行完了");
}
}
執行緒的常用方法
/**
* @author Zjy
* @date 2021/1/7 10:58
* 建立執行緒的第二種方式:
* 1、實現Runnable() 介面
* 1.1 匿名內部類
* 1.2 lambda表示式
*/
public class Demo {
public static void main(String[] args) {
//匿名內部類
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒A:" + i);
}
}
}, "ThreadA");
//lambda表示式
Thread threadB = new Thread(() -> {
for (int i = 0; i < 20; i++) {
System.out.println("執行緒B:" + i);
}
}, "ThreadB");
System.out.println("執行緒A的狀態:" + threadA.getState());
threadA.start();
System.out.println("執行緒A的狀態:" + threadA.getState());
threadB.start();
try {
//當前執行緒休眠1s
Thread.sleep(1000L);
TimeUnit.SECONDS.sleep(1L);
//返回一個當前執行緒
Thread thread = Thread.currentThread();
//獲取執行緒名
String name = thread.getName();
//獲取執行緒id
long id = thread.getId();
System.out.println("執行緒id:" + id);
//當前執行緒執行結束了,其他執行緒才能繼續執行
//thread.join();
//讓出CPU時間片,給其他執行緒進行排程
// Thread.yield();
//獲取執行緒的狀態
System.out.println("執行緒A的狀態:" + threadA.getState());
// 優先順序
int priority = threadA.getPriority();
System.out.println("執行緒A的優先順序:" + priority);//優先順序預設都是5
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Java執行緒有優先順序,優先順序高的執行緒會獲得較多的執行機會。
Java執行緒的優先順序用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:
static int MAX_PRIORITY 執行緒可以具有的最高優先順序,取值為10。
static int MIN_PRIORITY 執行緒可以具有的最低優先順序,取值為1。
static int NORM_PRIORITY 分配給執行緒的預設優先順序,取值為5。
Thread類的setPriority()和getPriority()方法分別用來設定和獲取執行緒的優先順序。(一般情況下,不會手動設定優先順序)
每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。
執行緒池
頻繁建立執行緒和銷燬執行緒會消耗系統資源;使用執行緒池可以節省系統資源
建立執行緒池:
- newFixedThreadPool():生成一個固定數量執行緒的執行緒池
- newCachedThreadPool():建立一個可快取的執行緒池
Executors.newCachedThreadPool()
如果執行緒池的大小超過了處理任務所需要的執行緒, 那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。
//newFixedThreadPool生成一個固定數量執行緒的執行緒池
//ExecutorService pool = Executors.newFixedThreadPool(10);
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(i);
}
System.out.println("當前執行緒的id:" + Thread.currentThread().getId());
}
});
pool.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
return 1;
});
//把執行緒池關閉
pool.shutdown();
執行緒安全問題
在多執行緒情況下,同時操作主存中某個資料的時候,出現數據不一致情況
建立一個BankCount類
public class BankCount {
private double balance;
public double getBalance() {
return balance;
}
/**
* 存錢
* balance = balance + money;
* 一共經歷三步:
* 1、取出balance的值
* 2、相加
* 3、賦值操作
*/
public void saveMoney(double money) {
balance += money;
}
/**
* 取錢
*/
public void drawMoney(double money) {
balance -= money;
}
}
建立一個測試類
public class SafeTest {
public static void main(String[] args) {
BankCount bankCount = new BankCount();
Thread threadA = new Thread(()->{
for (int i = 0; i < 20000; i++) {
bankCount.saveMoney(10);
}
System.out.println("threadA結束了");
});
Thread threadB = new Thread(()->{
for (int i = 0; i < 20000; i++) {
bankCount.drawMoney(10);
}
System.out.println("threadB結束了");
});
threadA.start();
threadB.start();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("餘額:"+bankCount.getBalance());
}
}
執行結果:
* 存錢 * balance = balance + money; * 一共經歷三步: * 1、取出balance的值 * 2、相加 * 3、賦值操作
以上三步必須進行原子性操作,否則當多個執行緒同時操作一個數據時,會出現執行緒安全問題,導致資料不一致。那我們怎麼解決的?這就需要用到執行緒同步:
執行緒同步
同步:一個任務的執行需要等待另一個任務的結束
非同步:任務可以同時進行
1、synchronized關鍵字
synchronized關鍵字修飾方法
public synchronized void saveMoney(double money) {
balance += money;
}
public synchronized void drawMoney(double money) {
balance -= money;
}
結果:
synchronized關鍵字修飾程式碼塊
public void saveMoney(double money) {
synchronized(this){
balance += money;
}
}
public void drawMoney(double money) {
synchronized(this){
balance -= money;
}
}
結果:
無論synchronized關鍵字加在方法上還是物件上,它取得的鎖都是物件,而不是把一段程式碼或函式當作鎖
在靜態方法中的使用 synchronized
public synchronized static void method1(){
}
//synchronized在靜態方法中使用需要的是類物件(class)的物件鎖
public static void method2(){
synchronized (BankCount.class){
}
}
2、使用ReentrantLock()鎖
- 使用ReentrantLock鎖上鎖後,一定要釋放鎖
public class BankCount {
private double balance;
private ReentrantLock lock = new ReentrantLock();
public double getBalance() {
return balance;
}
public void saveMoney(double money) {
//上鎖
lock.lock();
try {
balance += money;
} catch (Exception e) {
} finally {
//釋放鎖,一定要執行
lock.unlock();
}
}
public void drawMoney(double money) {
lock.lock();
try {
balance -= money;
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
jdk中獨佔鎖的實現除了使用關鍵字synchronized外,還可以使用ReentrantLock。雖然在效能上ReentrantLock和synchronized沒有什麼區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合複雜的併發場景。
- 1.ReentrantLock和synchronized都是獨佔鎖,只允許執行緒互斥的訪問臨界區。但是實現上兩者不同:synchronized加鎖解鎖的過程是隱式的,使用者不用手動操作,優點是操作簡單,但顯得不夠靈活。一般併發場景使用synchronized的就夠了;ReentrantLock需要手動加鎖和解鎖,且解鎖的操作儘量要放在finally程式碼塊中,保證執行緒正確釋放鎖。ReentrantLock操作較為複雜,但是因為可以手動控制加鎖和解鎖過程,在複雜的併發場景中能派上用場。
- 2.ReentrantLock和synchronized都是可重入的。synchronized因為可重入因此可以放在被遞迴執行的方法上,且不用擔心執行緒最後能否正確釋放鎖;而ReentrantLock在重入時要卻確保重複獲取鎖的次數必須和重複釋放鎖的次數一樣,否則可能導致其他執行緒無法獲得該鎖。
區別:
- ReentrantLock可以實現公平鎖。公平鎖是指當鎖可用時,在鎖上等待時間最長的執行緒將獲得鎖的使用權。而非公平鎖則隨機分配這種使用權。和synchronized一樣,預設的ReentrantLock實現是非公平鎖,因為相比公平鎖,非公平鎖效能更好。當然公平鎖能防止飢餓,某些情況下也很有用。
- ReentrantLock可響應中斷:當使用synchronized實現鎖時,阻塞在鎖上的執行緒除非獲得鎖否則將一直等待下去,也就是說這種無限等待獲取鎖的行為無法被中斷。而ReentrantLock給我們提供了一個可以響應中斷的獲取鎖的方法lockInterruptibly()。該方法可以用來解決死鎖問題。
公平鎖和非公平鎖該如何選擇:
大部分情況下我們使用非公平鎖,因為其效能比公平鎖好很多。但是公平鎖能夠避免執行緒飢餓,某些情況下也很有用
死鎖
多個執行緒間出現互相等待對方釋放鎖的情況,就是死鎖
解決:巢狀上鎖的時候,上鎖的順序保持一致
synchronized 死鎖
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
new Thread(()->{
synchronized (o1){
System.out.println("執行緒A獲取到1號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
synchronized (o2){
System.out.println("執行緒B獲取到2號鎖");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
synchronized (o2){
System.out.println("執行緒B獲取到2號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
synchronized (o1){
System.out.println("執行緒B獲取到1號鎖");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
結果:
上鎖順序保持一致,可以避免死鎖情況的發生
ReentrantLock()死鎖
public class DeadLockTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
new Thread(()->{
lock1.lock();
System.out.println("執行緒A獲取到1號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
lock2.lock();
System.out.println("執行緒A獲取到2號鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.unlock();
lock1.unlock();
}).start();
new Thread(()->{
lock2.lock();
System.out.println("執行緒B獲取到2號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
lock1.lock();
System.out.println("執行緒B獲取到1號鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.unlock();
lock2.unlock();
}).start();
}
}
結果:
使用tryLock
new Thread(()->{
lock1.tryLock();
System.out.println("執行緒A獲取到1號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
lock2.tryLock();
System.out.println("執行緒A獲取到2號鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.unlock();
lock1.unlock();
}).start();
new Thread(()->{
lock2.tryLock();
System.out.println("執行緒B獲取到2號鎖");
try {
TimeUnit.SECONDS.sleep(1L);
lock1.tryLock();
System.out.println("執行緒B獲取到1號鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.unlock();
lock2.unlock();
}).start();
結果:
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
執行緒安全的類
- Vector
- CopyOnWriteArrayList
- AtomicInteger
public class SafetyDemo {
public static void main(String[] args) {
//ArrayList<Integer> integers = new ArrayList<>();//執行緒不安全
//Vector<Integer> integers = new Vector<>();//執行緒安全
//CopyOnWriteArrayList也是執行緒安全的類
CopyOnWriteArrayList<Integer> integers = new CopyOnWriteArrayList<>();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
integers.add(i);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
integers.add(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(integers.size());
}
}
public class Demo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
test();
}
public static void test() {
Thread threadA = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
count.incrementAndGet();//遞增
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
count.decrementAndGet();//遞減
}
});
threadA.start();
threadB.start();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count的值是:" + count);
}
}
結果:
總結
1、執行緒同步的目的是為了保護多個執行緒訪問一個資源時對資源的破壞。
2、執行緒同步方法是通過鎖來實現的,每個物件都有且僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他非同步方法
3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不干預。一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖。
4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的執行緒無法訪問競爭資源。
6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。
7、死鎖是執行緒間相互等待鎖造成的,在實際中發生的概率非常的小。但是,一旦程式發生死鎖,程式將死掉。