1.多線程
1.1.多線程介紹
學習多線程以前,咱們先要了解幾個關於多線程有關的概念。java
進程:正在運行的程序。確切的來講,當一個程序進入內存運行,即變成一個進程,進程是處於運行過程當中的程序,而且具備必定獨立功能,進程是系統進行資源分配和調度的一個獨立單位。進程是正在運行的程序,進程負責給程序分配內存空間,而每個進程都是由程序代碼組成的,這些代碼在進程中執行的流程就是線程。編程
線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序。安全
簡而言之:一個程序運行後至少有一個進程,一個進程中能夠包含多個線程,但至少有一個線程。什麼是多線程呢?即就是一個程序中有多個線程在同時執行。服務器
1.2.多線程運行原理
大部分操做系統都支持多進程併發運行,如今的操做系統幾乎都支持同時運行多個任務。好比:如今咱們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。感受這些軟件好像在同時運行着。多線程
其實這些軟件在某一時刻,只會運行一個進程。這是爲何呢?這是因爲CPU(中央處理器)在作着高速的切換而致使的。對於CPU而言,它在某個時間點上,只能執行一個程序,即就是說只能運行一個進程,CPU不斷地在這些進程之間切換。只是咱們本身感受不到。爲何咱們會感受不到呢?這是由於CPU的執行速度相對咱們的感受實在太快了,雖然CPU在多個進程之間輪換執行,但咱們本身感到好像多個進程在同時執行。併發
多線程真的能提升效率嗎?其實並非這樣的,由於咱們知道,CPU會在多個進程之間作着切換,若是咱們開啓的程序過多,CPU切換到每個進程的時間也會變長,咱們也會感受機器運行變慢。因此合理的使用多線程能夠提升效率,可是大量使用,並不能給咱們帶來效率上的提升。dom
1.3.主線程
回想咱們之前學習中寫過的代碼,當咱們在dos命令行中輸入java空格類名回車後,啓動JVM,而且加載對應的class文件。虛擬機並會從main方法開始執行咱們的程序代碼,一直把main方法的代碼執行結束。若是在執行過程遇到循環時間比較長的代碼,那麼在循環以後的其餘代碼是不會被執行的。以下代碼演示:jvm
class Demo
{
String name;
Demo(String name)
{
this.name = name;
}
void show()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
d.show();
d2.show();
System.out.println("Hello World!");
}
}
複製代碼
若在上述代碼中show方法中的循環執行次數不少,這時書寫在d.show();下面的代碼是不會執行的,而且在dos窗口會看到不停的輸出name=小強,i=值,這樣的語句。爲何會這樣呢?編輯器
緣由是:jvm啓動後,必然有一個執行路徑(線程)從main方法開始的。一直執行到main方法結束。這個線程在java中稱之爲主線程。當主線程在這個程序中執行時,若是遇到了循環而致使程序在指定位置停留時間過長,沒法執行下面的程序。ide
可不能夠實現一個主線程負責執行其中一個循環,由另外一個線程負責其餘代碼的執行。實現多部分代碼同時執行。這就是多線程技術能夠解決的問題。
1.4.如何建立線程
1.4.1.建立線程方式一:繼承Thread類
該如何建立線程呢?經過API中的英文Thread的搜索,查到Thread類。經過閱讀Thread類中的描述。建立新執行線程有兩種方法。一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。接下來能夠分配並啓動該子類的實例。
建立線程的步驟:
1. 定義一個類繼承Thread。
2. 重寫run方法。
3. 建立子類對象,就是建立線程對象。
4. 調用start方法,開啓線程並讓線程執行,同時還會告訴jvm去調用run方法。
class Demo extends Thread //繼承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//複寫其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//建立兩個線程任務
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
//d.run(); 這裏仍然是主線程在調用run方法,並無開啓兩個線程
//d2.run();
d2.start();//開啓一個線程
d.run();//主線程在調用run方法
}
}
複製代碼
打印部分結果:因爲多線程操做,輸出數據會有所不一樣
name=旺財,i=1
name=小強,i=1
name=旺財,i=2
name=小強,i=2
name=小強,i=3
name=旺財,i=3
name=旺財,i=4
name=旺財,i=5
name=旺財,i=6
name=旺財,i=7
..........
思考:線程對象調用 run方法和調用start方法區別?
線程對象調用run方法不開啓線程。僅是對象調用方法。線程對象調用start開啓線程,並讓jvm調用run方法在開啓的線程中執行。
1.4.2.繼承Thread類原理
爲何要繼承Thread類,並調用其的start方法才能開啓線程呢?
繼承Thread類:由於Thread類描述線程事物,具有線程應該有功能。
那爲何不直接建立Thread類的對象呢?
1 Thread t1 = new Thread();
2 t1.start();//這樣作沒有錯,可是該start調用的是Thread類中的run方法,而這個run方法沒有作什麼事情,更重要的是這個run方法中並無定義咱們須要讓線程執行的代碼。
複製代碼
建立線程的目的是什麼?
是爲了創建單獨的執行路徑,讓多部分代碼實現同時執行。也就是說線程建立並執行須要給定的代碼(線程的任務)。對於以前所講的主線程,它的任務定義在main函數中。自定義線程須要執行的任務都定義在run方法中。Thread類中的run方法內部的任務並非咱們所須要,只有重寫這個run方法,既然Thread類已經定義了線程任務的位置,只要在位置中定義任務代碼便可。因此進行了重寫run方法動做。
1.5.多線程的內存圖解
多線程執行時,到底在內存中是如何運行的呢?
以上個程序爲例,進行圖解說明:
多線程執行時,在棧內存中,其實每個執行線程都有一片本身所屬的棧內存空間。進行方法的壓棧和彈棧。
當執行線程的任務結束了,線程自動在棧內存中釋放了。可是當全部的執行線程都結束了,那麼進程就結束了。
1.6.獲取線程名稱
開啓的線程都會有本身的獨立運行棧內存,那麼這些運行的線程的名字是什麼呢?該如何獲取呢?既然是線程的名字,按照面向對象的特色,是哪一個對象的屬性和誰的功能,那麼咱們就去找那個對象就能夠了。查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,咱們就能夠試試。
Thread.currentThread()獲取當前線程對象
Thread.currentThread().getName();獲取當前線程對象的名稱
class Demo extends Thread //繼承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//複寫其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//建立兩個線程任務
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
d2.start();//開啓一個線程
d.run();//主線程在調用run方法
}
}
複製代碼
原來主線程的名稱: main
自定義的線程: Thread-0 線程多個時,數字順延。Thread-1......
進行多線程編程時不要忘記了Java程序運行時從主線程開始,main方法的方法體就是主線程的線程執行體。
1.7.建立線程的第二種方式
掌握瞭如何建立線程對象,以及開啓線程後,記得在查閱API時,還說了有第二種開啓線程的方式,那麼第二種是什麼呢?
1.7.1.實現Runnable接口
繼續查看API發現,建立線程的另外一種方法是聲明實現 Runnable 接口的類。該類而後實現 run 方法。而後能夠分配該類的實例,在建立 Thread 時做爲一個參數來傳遞並啓動。
怎麼還要實現Runnable接口,Runable是啥玩意呢?繼續API搜索。
查看Runnable接口說明文檔:Runnable 接口應該由那些打算經過某一線程執行其實例的類來實現。類必須定義一個稱爲 run 的無參數方法。
總結:
建立線程的第二種方式:實現Runnable接口。
一、定義類實現Runnable接口。
二、覆蓋接口中的run方法。。
三、建立Thread類的對象
四、將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。
五、調用Thread類的start方法開啓線程。
代碼演示:
class Demo implements Runnable
{
private String name;
Demo(String name)
{
this.name = name;
}
//覆蓋了接口Runnable中的run方法。
public void run()
{
for(int i=1; i<=20; i++)
{ System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
//建立Runnable子類的對象。注意它並非線程對象。
Demo d = new Demo("Demo");
//建立Thread類的對象,將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
//將線程啓動。
t1.start();
t2.start();
System.out.println(Thread.currentThread().getName()+"----->");
System.out.println("Hello World!");
}
}
複製代碼
輸出結果:
1.7.2.實現Runnable的原理
爲何須要定一個類去實現Runnable接口呢?繼承Thread類和實現Runnable接口有啥區別呢?
實現Runnable接口,避免了繼承Thread類的單繼承侷限性。覆蓋Runnable接口中的run方法,將線程任務代碼定義到run方法中。建立Thread類的對象,只有建立Thread類的對象才能夠建立線程。線程任務已被封裝到Runnable接口的run方法中,而這個run方法所屬於Runnable接口的子類對象,因此將這個子類對象做爲參數傳遞給Thread的構造函數,這樣,線程對象建立時就能夠明確要運行的線程的任務。
1.7.3.實現Runnable的好處
第二種方式實現Runnable接口避免了單繼承的侷限性,因此較爲經常使用。實現Runnable接口的方式,更加的符合面向對象,線程分爲兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一塊兒。一旦建立Thread類的子類對象,既是線程對象,有又有線程任務。實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。
1.8.線程狀態圖
查閱API關於IllegalThreadStateException這個異常說明信息發現,這個異常的描述信息爲:指示線程沒有處於請求操做所要求的適當狀態時拋出的異常。這裏面說適當的狀態,啥意思呢?難道是說線程還有狀態嗎?
一、新建(new):線程對象被建立後就進入了新建狀態。如:Thread thread = new Thread();
二、就緒狀態(Runnable):也被稱爲「可執行狀態」。線程對象被建立後,其餘線程調用了該對象的start()方法,從而啓動該線程。如:thread.start(); 處於就緒狀態的線程隨時可能被CPU調度執行。
三、運行狀態(Running):線程獲取CPU權限進行執行。須要注意的是,線程只能從就緒狀態進入到運行狀態。
四、阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權限,暫時中止運行。直到線程進入就緒狀態,纔有機會進入運行狀態。阻塞的三種狀況:
等待阻塞:經過調用線程的wait()方法,讓線程等待某工做的完成。
同步阻塞:線程在獲取synchronized同步鎖失敗(由於鎖被其餘線程佔用),它會進入同步阻塞狀態。
其餘阻塞:經過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或超時、或者I/O處理完畢時,線程從新轉入就緒狀態。
五、死亡狀態(Dead):線程執行完了或因異常退出了run()方法,該線程結束生命週期。
1.8.1.sleep,wait,yield,join的區別
sleep()方法
在指定時間內讓當前正在執行的線程暫停執行,但不會釋放「鎖標誌」。不推薦使用。 sleep()使當前線程進入阻塞狀態,在指定時間內不會執行。
wait()方法
在其餘線程調用對象的notify或notifyAll方法前,致使當前線程等待。線程會釋放掉它所佔有的「鎖標誌」,從而使別的線程有機會搶佔該鎖。 當前線程必須擁有當前對象鎖。若是當前線程不是此鎖的擁有者,會拋出IllegalMonitorStateException異常。 喚醒當前對象鎖的等待線程使用notify或notifyAll方法,也必須擁有相同的對象鎖,不然也會拋出IllegalMonitorStateException異常。 waite()和notify()必須在synchronized函數或synchronized block中進行調用。若是在non-synchronized函數或non-synchronized block中進行調用,雖然能編譯經過,但在運行時會發生IllegalMonitorStateException的異常。
yield方法
暫停當前正在執行的線程對象。 yield()只是使當前線程從新回到可執行狀態,因此執行yield()的線程有可能在進入到可執行狀態後立刻又被執行。 yield()只能使同優先級或更高優先級的線程有執行的機會。 調用yield方法並不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態,它只須要等待從新獲取CPU執行時間,這一點是和sleep方法不同的。
join方法
等待該線程終止。 等待調用join方法的線程結束,再繼續執行。如:t.join();//主要用於等待t線程運行結束,若無此句,main則會執行完畢,致使結果不可預測
1.9.線程的安全問題
帶女友看電影,須要買票,電影院要賣票,模擬電影院的買票操做
假設咱們想要的電影是 「功夫熊貓3」,本次電影的座位共100個(本廠電影只能賣100張票)
模擬電影院的售票窗口,實現多個窗口同時賣 「功夫熊貓3」這場電影票(多個窗口一塊兒賣這100張票)
須要窗口:採用線程對象
須要票:Runnable接口子類來模擬
public class ThreadDemo {
public static void main(String[] args) {
//建立票對象
Ticket ticket = new Ticket();
//建立3個窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模擬賣票
while(true){
//t1,t2,t3
if (ticket > 0) {
//模擬選坐的操做
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
複製代碼
總結:上面程序出新了問題
票出現了重複的票
錯誤的票 0
1.10.同步的鎖
同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {
可能會產生線程安全問題的代碼
}
複製代碼
同步代碼塊中的鎖對象能夠是任意的對象,多個線程對象使用的是同一個鎖對象
把Ticket.java進行了代碼修改
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義所對象
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步代碼塊
synchronized (lock){
if (ticket > 0) {
//模擬選坐的操做
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
複製代碼
當使用了同步代碼塊後,上述的線程的安全問題,解決了。
同步方法:在方法聲明上加上synchronized
public synchronized void method(){
可能會產生線程安全問題的代碼
}
複製代碼
同步方法中的鎖對象是 this
//同步方法,鎖對象this
public synchronized void method(){
//this.name = name;
if (ticket > 0) {
//模擬選坐的操做
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
複製代碼
靜態同步方法: 在方法聲明上加上synchronized
public static synchronized void method(){
可能會產生線程安全問題的代碼
}
複製代碼
1.11.死鎖
同步鎖的另外一個弊端:當線程任務中出現了多個同步(多個鎖)時,若是同步中嵌套了其餘的同步。這時容易引起一種現象:死鎖。這種狀況能避免就避免掉。
synchronzied(A鎖){
synchronized(B鎖){
}
}
複製代碼
/*
* 定義鎖對象
*/
public class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
/*
* 線程任務類
*/
public class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定線程要執行的任務代碼
@Override
public void run() {
while(true){
if (x%2 ==0) {
//狀況一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if大口吃肉");
}
}
} else {
//狀況二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else大口吃肉");
}
}
}
x++;
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//建立線程任務類對象
ThreadTask task = new ThreadTask();
//建立兩個線程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//啓動線程
t1.start();
t2.start();
}
}
複製代碼
1.12.Lock接口
查閱API,發現Lock接口,比同步更厲害,有更多操做;
lock():獲取鎖
unlock():釋放鎖;
提供了一個更加面對對象的鎖,在該鎖中提供了更多的顯示的鎖操做。使用Lock接口,以及其中的lock()方法和unlock()方法替代同步。
以下代碼演示:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//建立Lock鎖對象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模擬賣票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模擬選坐的操做
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
ck.unlock();
//}
}
}
}
複製代碼
1.13.線程的匿名內部類的使用
方式1
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
方式2
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...Y...." + x);
}
}
};
new Thread(r).start();
複製代碼
2.多線程文件上傳
實現服務器端能夠同時接收多個客戶端上傳的文件。 咱們要修改服務器端代碼
/*
* 文件上傳 服務器端
*
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1,建立服務器,等待客戶端鏈接
ServerSocket serverSocket = new ServerSocket(6666);
//實現多個客戶端鏈接服務器的操做
while(true){
final Socket clientSocket = serverSocket.accept();
//啓動線程,完成與當前客戶端的數據交互過程
new Thread(){
public void run() {
try{
//顯示哪一個客戶端Socket鏈接上了服務器
InetAddress ipObject = clientSocket.getInetAddress();//獲得IP地址對象
String ip = ipObject.getHostAddress(); //獲得IP地址字符串
System.out.println("小樣,抓到你了,鏈接我!!" + "IP:" + ip);
//7,獲取Socket的輸入流
InputStream in = clientSocket.getInputStream();
//8,建立目的地的字節輸出流 D:\\upload\\192.168.74.58(1).jpg
BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\\upload\\"+ip+"("+System.currentTimeMillis()+").jpg"));
//9,把Socket輸入流中的數據,寫入目的地的字節輸出流中
byte[] buffer = new byte[1024];
int len = -1;
while((len = in.read(buffer)) != -1){
//寫入目的地的字節輸出流中
fileOut.write(buffer, 0, len);
}
//-----------------反饋信息---------------------
//10,獲取Socket的輸出流, 做用:寫反饋信息給客戶端
OutputStream out = clientSocket.getOutputStream();
//11,寫反饋信息給客戶端
out.write("圖片上傳成功".getBytes());
out.close();
fileOut.close();
in.close();
clientSocket.close();
} catch(IOException e){
e.printStackTrace();
}
};
}.start();
}
//serverSocket.close();
}
}
複製代碼
3.總結
建立線程的方式
方式1,繼承Thread線程類
步驟
1, 自定義類繼承Thread類
2, 在自定義類中重寫Thread類的run方法
3, 建立自定義類對象(線程對象)
4, 調用start方法,啓動線程,經過JVM,調用線程中的run方法
方式2,實現Runnable接口
步驟
1, 建立線程任務類 實現Runnable接口
2, 在線程任務類中 重寫接口中的run方法
3, 建立線程任務類對象
4, 建立線程對象,把線程任務類對象做爲Thread類構造方法的參數使用
5, 調用start方法,啓動線程,經過JVM,調用線程任務類中的run方法
同步鎖
多個線程想保證線程安全,必需要使用同一個鎖對象
A.同步代碼塊
synchronized (鎖對象){
可能產生線程安全問題的代碼
}
複製代碼
同步代碼塊的鎖對象能夠是任意的對象
B.同步方法
public synchronized void method()
可能產生線程安全問題的代碼
}
複製代碼
同步方法中的鎖對象是 this
C.靜態同步方法
public synchronized static void method() 可能產生線程安全問題的代碼 }複製代碼靜態同步方法中的鎖對象是 類名.class--------------------- 做者:weixin_34179968 來源:CSDN 原文:https://blog.csdn.net/weixin_34179968/article/details/88125116 版權聲明:本文爲博主原創文章,轉載請附上博文連接!