文章目錄
1.併發與並行
●併發:指兩個或多個事件在同一時間段內發生。(交替執行)
●並行:指兩個或多個事件在同一時刻發生。(同時發生)
即,併發就是你洗完澡後再聽歌,並行就是你一邊洗澡一邊聽歌。
2.進程與線程
(線程<進程)
●進程:程序的執行過程。(可在任務管理器查看)
●線程:進程中的一個執行單元。
一個程序運行後至少有一個進程,一個進程中可以包含多個線程。
舉例:Word的使用。
每次打開一個Word就相當於啓動了一個進程,
在這個進程上又有許多其他程序在執行(例如:拼寫檢查,自動更正等),這些就是一個個線程。
如果Word關閉了,這些線程就會全部消失。但是如果這些線程消失了,Word不一定會消失。
線程一定得依附於進程才能夠存在。
“同時”執行是線程給人的感覺,在線程之間實際上是輪換執行。
3.線程調度
(1)分時調度:
所有線程輪流使用CPU的使用權,平均分配每個線程佔用CPU的時間。
(2)搶佔式調度:
優先讓優先級高的線程使用CPU,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性)。java使用的爲搶佔式調度。
●設置線程的優先級:
打開任務管理器>選擇希望設置優先級的進程>右鍵>轉到詳細信息>設置優先級
4.多線程的實現:
在Java中要想實現多線程的程序,必須依靠一個線程的主體類,即主線程(執行主方法(main)的線程)。然後此類繼承Thread
類或實現Runnable
接口。
1)繼承Thread類
java.lang.Thread
是操作線程的類,任何類只需要繼承Thread
類就可以成爲一個線程的主類。
Thread類下的兩個重要方法:
run()
和start()
方法。
線程執行體:run()。(線程需要完成的任務)
線程的起點:start()。(線程的啓動,啓動後執行的方法體是run()方法定義的代碼)
程序的起點:main()。
//1.創建一個Thread類的子類
public class MyThread extends Thread{
//2.重寫Thread類中的run方法,設置線程任務
@Override
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("run:"+i);
}
}
}
public class doMain {
public static void main(String[] args) {
//3.創建Thread類的子類對象
MyThread mt = new MyThread();
//4.調用Thread類中的start(),開啓新的線程,執行run()
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:"+i);
}
}
}
隨機性打印結果的原因:
多線程內存圖解:
Thread類的常用方法:
getName() | 取得線程名字 |
---|---|
setName() | 設置線程名字 |
currentThread() | 取得線程名字 |
sleep(long millitime) | 使當前正在執行的程序以指定的毫秒數暫定 |
Thread.currentThread().getName() 和 this.getName()的區別
2)實現Runnable接口:
爲了避免單繼承侷限的問題,我們可以使用Runnable
接口來實現多線程。
要啓動多線程,就一定需要通過Thread
類中的start()
方法,但是Runnable接口中沒有提供可以被繼承的start()方法
。這時就需要借住Thread
類中提供的有參構造方法:
public Thread(Runnable target) | 此方法可以接收一個Runnable接口對象 |
---|
//MyThread是實現了Runnable接口的子類
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
new Thread(mt).start();
new Thread(mt2).start();
3)實現Runnable接口的好處:
①避免單繼承侷限;
②降低程序的耦合性,方便解耦。即把設置線程任務(實現類中重寫run())和開啓新線程(Thread類對象調用start())進行了分離。
4)多線程的兩種實現方式及區別:
●它們的實現都需要一個線程的主類,都必須在子類中覆寫run()
方法,都必須調用Thread
類中的start()
方法來開啓線程。
●Thread
類是Runnable
接口的子類,而使用Runnable接口可以避免單繼承侷限,方便解耦,並且可以更加方便地實現數據共享的概念。
public class Thread extends Object implements Runnable
●它們的結構:
Runnable接口 | Thread類 |
---|---|
class MyThread implements Runnable{} | class MyThread extends Thread{} |
new Thread(mt).start(); | mt.start(); |
5)使用匿名內部類實現多線程的創建:
ublic class doMain {
public static void main(String[] args) {
//1.線程的父類是Thread
new Thread(){
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+">--"+"a");
}
}
}.start();
//2.線程的接口Runnable
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"b");
}
}
}).start();
}
}
5.線程的操作狀態:
要想實現多線程,必須在主線程中創建新的線程對象。任何線程一般具有以下五種狀態:
- 創建狀態
新線程對象處於新建狀態時 - 就緒狀態
新建線程對象後,調用該線程的start()方法就可以啓動線程。當線程啓動時,線程進入就緒狀態。此時,線程將進入線程隊列排隊,等待CPU服務,這表明它已經具備了運行條件。 - 運行狀態
當就緒的線程被調度並獲得CPU資源時,便進入運行狀態。此時,自動調用該線程對象的run()方法,run()方法定義了線程的操作和功能 - 堵塞狀態
在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,將讓出 CPU 並臨時中止自己的執行,進入阻塞狀態。在可執行狀態下,如果調用sleep()、suspend()、wait()等方法,線程都將進入堵塞狀態。堵塞時,線程不能進入排隊隊列,只有當引起堵塞的原因被消除後,線程纔可以進入就緒狀態。 - 終止狀態
線程調用stop()或run()方法執行結束後,就處於終止狀態。處於終止狀態的線程不具有繼續運行的功能。
6.線程的休眠與優先級:
1)線程的休眠:
是讓程序執行速度變慢一些。在Thread類中線程休眠操作方法爲:
public static void sleep(long millis) throws InterruptedException
設置的休眠單位是毫秒(ms)。
2)線程的優先級:
對高優先級,使用優先調度的搶佔式策略。哪個線程的優先級高,哪個線程就有可能被執行。
- MAX_PRIORITY : 10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
線程優先級操作方法:
setPriority(int p)
:設置線程的優先級getPriority()
:取得線程優先級
7.線程安全問題(同步與死鎖):
先來解釋不同步遇到的問題:
如果分成三個窗口賣100張票,假如不同步的話,就有可能出現三個窗口賣重票、錯票的情況。(多個線程操作同一資源可能出現的情況,因爲前面的線程還沒完成操作,其它線程也進來操作車票。(搶佔))
實現三個窗口來賣票的程序:
//實現賣票程序
public class RunnableImpl implements Runnable{
//定義一個多線程共享的票源
private int ticket = 100;
//設置線程任務:賣票
@Override
public void run() {
//先判斷票是否存在
while(true){
if (ticket>0) {
//票存在,賣票
System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
ticket--;
}
}else {
break;
}
}
}
public class doMain {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
解決方法:通過同步操作來解決。
- 同步操作:一個代碼塊中的多個操作在同一時間段內只能由一個線程進行,其他線程要等待此線程完成後纔可以繼續執行。
以下有三種方式完成同步操作:
其實就是添加上圖中的“鎖”。
1)同步代碼塊:
synchronized(this){
//需要被同步操作的代碼
}
關於this的解釋:
- 在實現Runnable接口創建多線程的方式中,我們可以使用this充當所,代替手動new一個對象,因爲後面我們只創建一個線程的對象。
- 在繼承Thread類創建多線程的方式中,慎用this,考慮我們的this是不是唯一的。我們可以使用當前類來充當這個是鎖。synchronized (類名.class)
使用同步代碼塊完成同步操作:
主要有變化的在run()方法裏,doMain類不變。
//實現賣票程序
public class RunnableImpl implements Runnable{
//定義一個多線程共享的票源
private int ticket = 100;
//設置線程任務:賣票
@Override
public void run() {
while(true){
//先判斷票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,賣票
System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
ticket--;
}else {
break;
}
}
}
}
}
2)同步方法:
利用synchronized定義的方法。
//實現賣票程序
public class RunnableImpl implements Runnable{
//定義一個多線程共享的票源
private int ticket = 100;
//設置線程任務:賣票
@Override
public void run() {
while(true){
sale();
}
}
public synchronized void sale(){
//先判斷票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,賣票
System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
ticket--;
}
}
}
}
補充:
- 非靜態同步方法,鎖對象:this
- 靜態同步方法,鎖對象:RunnableImpl.class
3)Lock鎖:
java.util.concurrent.locks.Lock接口
Lock實現提供比使用synchronized方法和語句可以獲得的更廣泛的鎖定操作。
Lock接口中的方法:
void Lock() | 獲取鎖 |
---|---|
void unlock() | 釋放鎖 |
Lock接口的實現類:ReentrantLock
java.util.concurrent.locks.ReentrantLock implements Lock
使用Lock鎖完成同步操作:
三步走。1.創建ReentrantLock對象 2.獲取鎖 3.釋放鎖
//實現賣票程序
public class RunnableImpl implements Runnable{
//定義一個多線程共享的票源
private int ticket = 100;
//1.創建ReentrantLock對象
Lock lk = new ReentrantLock();
//設置線程任務:賣票
@Override
public void run() {
while(true){
//2.在可能出現線程安全的代碼前調用Lock接口中的Lock方法獲取鎖.
lk.lock();
if (ticket>0) {
try {
Thread.sleep(10);
//票存在,賣票
System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能出現線程安全的代碼後釋放鎖。
lk.unlock();
}
}
}
}
}
4)比較 synchronized 與 Lock:
- synchronized是自動釋放鎖(顯示),lock需要手動釋放和關閉鎖(隱式)。
- 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
5)總結:
加入同步後明顯比不加入同步慢許多,所以同步的代碼性能低,但是數據安全性高。
6)常見面試題分析:
-
同步和異步有什麼區別。什麼情況下使用?
如果一塊數據要在多個線程間共享,則必須進行同步存取。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,那麼就應該用異步編程,在很多情況下采用異步途徑往往更有效率。 -
abstract的method是否可以同時是static,是否可以同時是native、synchronized?
method、static、native、synchronized都不能和“abstract”同時聲明方法。 -
當一個線程進入一個對象的synchronized方法後,其他線程是否可訪問此對象的其他方法?
不能訪問,一個對象操作一個synchronized方法只能由一個線程訪問。(其他線程等待)