多執行緒的基本概念與執行緒安全問題
多執行緒:
基本概念
-
程式:是一個可執行的檔案.
-
程序:是一個正在執行的程式.在記憶體中開闢了一塊兒空間
-
執行緒:負責程式的執行,可以看做程式執行的一條通道或者一個執行單元.所以我們通常將程序的工作理解成執行緒的工作.
-
程序中可不可以沒有執行緒?
- 必須有執行緒,至少有一個,當有一個執行緒存在的時候,我們稱為單執行緒,這個唯一的執行緒就是主執行緒(main執行緒)
-
當有一個以上的執行緒存在的時候,我們稱為多執行緒.
-
多執行緒存在的意義:為了實現同一時間做多件事情.
-
任務區:我們將執行緒完成工作的方法稱為任務區
- 每一個執行緒都有自己的任務區.
-
JVM是多執行緒嗎?
- 一定是多執行緒
- 至少有兩個
- 主執行緒的任務區:main方法
- 垃圾回收執行緒的任務區:finalize()方法
public class Demo7 {
public static void main(String[] args) {//一個main執行緒
new Test();
/*
* 手動執行gc方法,執行垃圾回收器,觸發垃圾回收機制.
* 工作原理:執行gc方法,觸發垃圾回收機制,執行垃圾回收執行緒,呼叫finalize()方法
*
* 多個執行緒是搶cpu的關係,cpu有隨機性
*/
System.gc();//兩個:主執行緒和垃圾回收執行緒
System.out.println("main");
}//執行緒是隨著任務的開始而開始結束而結束,只要任務沒有結束,執行緒就不會結束.當執行緒還在工作的時候,程序沒有辦法結束.
}
class Test{
/*
* finalize()方法應該由系統呼叫,為了模擬多執行緒的使用環境,我們將它進行了重寫
* 正常情況下,當Test類的物件被釋放的時候,這個方法就會被呼叫
*/
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
自己建立執行緒的原因
系統的執行緒無法完成我們自己的功能,我們就自己建立執行緒.系統將執行緒面向物件了,形成的類就是Thread.
-
Thread的任務區是run()方法
-
注意:如果我們想讓run方法作為任務區,不能手動去呼叫,必須通過呼叫start方法,讓run自動執行.
-
兩種建立執行緒的方式:
- 通過Thread直接建立執行緒
- 重寫的run方法,作為任務區
public class Demo8 {
// public static void main(String[] args) {//為了方便研究,暫時忽略垃圾回收執行緒.認為這裡有一個執行緒--main執行緒
// //1.通過Thread直接建立執行緒
// //建立兩個執行緒
// Thread thread1 = new Thread();
// Thread thread2 = new Thread();
//
// //通過start方法讓執行緒工作
// thread1.start();
// thread2.start();//有三個執行緒,兩個子執行緒一個主執行緒
//
// System.out.println("main");
// }
public static void main(String[] args) {//為了方便研究,暫時忽略垃圾回收執行緒.認為這裡有一個執行緒--main執行緒
//1.通過Thread的子類建立執行緒
//建立兩個執行緒
MyThread thread1 = new MyThread("bingbing");//thread-0
MyThread thread2 = new MyThread("yingying");//thread-1
//通過start方法讓執行緒工作
thread1.start();
thread2.start();//有三個執行緒,兩個子執行緒一個主執行緒
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" main i:"+i);
}
/*
* 當手動呼叫run時,run變成了普通方法,失去了任務區的功能.
* run內部對應的執行緒就是run方法被手動呼叫的位置對應的執行緒.
*/
//thread1.run();
}
}
class MyThread extends Thread{
String myname;
public MyThread(String myname) {
super();
this.myname = myname;
}
@Override
public void run() {
/*
* 重寫的run方法,作為任務區
* Thread.currentThread():獲取的當前執行緒
* Thread.currentThread().getName():獲取的是當前執行緒的名字,系統給的名字.
*/
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+myname+" i:"+i);
}
}
}
執行緒的兩種建立方式:
- 建立執行緒的第一種方式:通過建立Thread類的子類—讓run留在了執行緒了內部,造成任務與執行緒的繫結,操作不方便
- 建立執行緒的第二種方式:讓執行緒與任務分離—將run從執行緒中獨立出來.好處:操作更方便,那個執行緒想工作,我就把任務交給誰
例項:實現四個售票員售票
- 分析:建立4個執行緒–模擬四個售票員
- 任務:只需要一個
- 資料:只需要一個
建立執行緒的第一種方式
public class Demo2 {
public static void main(String[] args) {
//建立執行緒物件
Seller seller1 = new Seller();
Seller seller2 = new Seller();
Seller seller3 = new Seller();
Seller seller4 = new Seller();
//開啟執行緒
seller1.start();
seller2.start();
seller3.start();
seller4.start();
}
}
class Seller extends Thread{
static int num = 40;//為了實現num的共享
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" i:"+" "+ (--num));
}
}
}
建立執行緒的第二種方式
public class Demo2 {
public static void main(String[] args) {
//建立任務物件
Ticket ticket = new Ticket();
//建立執行緒並關聯同一個任務
//如果我們建立了自己獨立的任務類,執行緒會優先呼叫我們手動傳入執行緒的任務類物件的run方法,不會再去呼叫Thread類的run方法
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
Thread thread4 = new Thread(ticket);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
//建立任務類
class Ticket implements Runnable {
//因為Ticket物件被四個執行緒共享,所以num作為屬性也被共享了
int num = 40;
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" i:"+" "+ (--num));
}
}
}
執行緒安全問題:
-
分析:4個執行緒共用了一個數據,出現了-1,-2,-3等錯誤的資料
-
具體分析:
- 共用了一個數據
- 共享語句有多條,一個執行緒使用cpu,沒有使用完,cpu被搶走,當再次搶到cpu的時候,直接執行後面的語句,造成了錯誤的發生.
-
解決:
- 在程式碼中使用同步程式碼塊兒(同步鎖)
- 解釋:在某一段任務中,同一時間只允許一個執行緒執行任務,其他的執行緒即使搶到了cpu,也無法進入當前的任務區間,只有噹噹前的執行緒將任務執行完後,其他的執行緒才能有資格進入
-
同步程式碼塊兒的構成:
synchronized(鎖(物件)){
同步的程式碼
}
-
對作為鎖的物件的要求:
- 必須是物件
- 必須保證被多個執行緒共享
-
可以充當鎖的:
- 一個普通的物件
- 當前物件的引用–this
- 類的位元組碼檔案
-
同步程式碼塊兒的特點:
- 可以保證執行緒的安全
- 由於每次都要進行判斷處理,所以降低了執行效率
-
總結:什麼時候使用同步程式碼塊兒
- 多個執行緒共享一個數據
- 至少有兩個執行緒
//第二種:執行緒與任務分離
public class Demo3 {
public static void main(String[] args) {
//建立任務物件
Ticket1 ticket = new Ticket1();
//建立執行緒物件並關聯同一個任務
//如果我們建立了自己獨立的任務類,執行緒會優先呼叫我們手動傳入執行緒的任務類物件的run方法,不會再去呼叫Thread預設的run方法
Thread seller1 = new Thread(ticket);
Thread seller2 = new Thread(ticket);
Thread seller3 = new Thread(ticket);
Thread seller4 = new Thread(ticket);
//開啟執行緒
seller1.start();
seller2.start();
seller3.start();
seller4.start();
}
}
//建立任務類
class Ticket1 implements Runnable{
//因為Ticket物件被四個執行緒共享,所以num作為屬性也被共享了
int num = 20;
boolean flag = false;
//讓object充當鎖
//作為鎖要滿足兩個條件:1.必須是物件 2.必須供所有的執行緒共享.
//可以作為鎖的有:1.任意一個例項物件 2.this 3.位元組碼檔案物件
Object object = new Object();
public void run() {
while (!flag) {
synchronized (object) {//同步程式碼塊兒--讓執行緒之間互斥
//製造一個延遲,相當於讓當前執行run的執行緒休息一會兒(臨時讓出cpu)
try {
Thread.sleep(100);//100是時間,單位是毫秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (num >0) {
System.out.println(Thread.currentThread().getName()+" "+ --num);
}else {
flag = true;
}
}
}
}
}
例項:兩個人向同一個賬戶裡面存錢
-
一人存三次,每次存100
-
注意:
- 當在一個類中同時存在多個synchronized修飾的程式碼塊兒或函式時,要想安全,就必須讓他們後面的物件一致。因為只有同一把鎖才能安全。
- 同步函式的鎖:this
- 靜態同步函式在進記憶體的時候不會建立物件,但是存在其所屬類的位元組碼檔案物件,屬於class型別的物件,所以靜態同步函式的鎖是其所屬類的位元組碼檔案物件
理解synchronized關鍵字
-
synchronized關鍵字的作用域有二種:
- 是某個物件例項內,synchronized aMethod(){}可以防止多個執行緒同時訪問這個物件 的synchronized方法(如果一個物件有多個synchronized方法,只要一個執行緒訪問了其中的一個synchronized方法,其它執行緒不能同時訪問這個物件中任何一個synchronized方法)。這時,不同的物件例項的synchronized方法是不相干擾的。也就是說,其它執行緒照樣可以同時訪問相同類的另一個物件例項中的synchronized方法;
- 是某個類的範圍,synchronized static aStaticMethod{}防止多個執行緒同時訪問 這個類中的synchronized static 方法。它可以對類的所有物件例項起作用。
-
除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是:synchronized(this){/區塊/},它的作用域是當前物件;
-
synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronizedf(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法.
public class Demo4 {
public static void main(String[] args) {
//1.建立任務類物件
CunQian cunQian = new CunQian();
//2.建立執行緒並繫結任務
Thread thread1 = new Thread(cunQian);
Thread thread2 = new Thread(cunQian);
//3.開啟執行緒
thread1.start();
thread2.start();
}
}
class Bank{
int sum;//放的是當前賬戶的錢
//使用同步程式碼塊兒
// public void addMoney(int money){
// synchronized (this) {
// sum+=money;
// System.out.println(sum);
// }
// }
//使用同步函式
//非靜態的同步函式
//相當於預設在synchronized後面跟著this充當鎖
// public synchronized void addMoney(int money){
// sum+=money;
// System.out.println(sum);
// }
//靜態的同步函式
//相當於預設在synchronized後面跟著當前的類的位元組碼檔案充當鎖----Bank.class
public synchronized static void addMoney(int money){
}
}
//建立任務類
class CunQian implements Runnable{
Bank bank = new Bank();
public void run() {
for(int i=0;i<3;i++){
bank.addMoney(100);
}
}
}
單例模式與執行緒
public class Demo5 {
}
//懶漢式
class SingleInstance1{
private static SingleInstance1 singleInstance = null;
private SingleInstance1(){
}
//因為同步程式碼塊兒的效率高於同步函式,所以儘量使用同步程式碼塊兒
public static SingleInstance1 getInstance() {
if (singleInstance == null) {//目的:儘量減少執行緒安全程式碼的判斷次數,提高效率
synchronized (SingleInstance1.class) {
if (singleInstance == null) {
singleInstance = new SingleInstance1();
}
}
}
return singleInstance;
}
}
//餓漢式
class SingleInstance{
private final static SingleInstance singleInstance = new SingleInstance();
private SingleInstance(){
}
public static SingleInstance getInstance() {
return singleInstance;
}
}
class Test implements Runnable{
@Override
public void run() {
SingleInstance1 singleInstance1 = SingleInstance1.getInstance();
}
}
Thread物件作為引數
public class Demo6 {
public static void main(String[] args) {
Thread thread1 = new Thread();
//1.這裡是可以的,這裡將thread1當做了任務類物件,執行的時候呼叫的是thread1內部的run方法
Thread thread2 = new Thread(thread1);
thread2.start();
//2.建立Thread類的匿名子類物件充當執行緒類
new Thread(){
public void run() {
System.out.println("haha");
};
}.start();
}
}
執行緒的停止
如何讓他的任務結束
-
通過一個標識結束執行緒
-
通過呼叫stop方法結束執行緒----有固有的安全問題,已經過時,不建議再使用
-
呼叫interrupt()方法結束執行緒
原理:執行緒可以呼叫wait()方法,讓當前的執行緒處於鈍化的狀態(會立刻釋放cpu,並且處於無法搶cpu的狀態,但是當前的執行緒並沒有死亡) 注意點:wait方法必須在同步狀態下使用. 呼叫interrupt方法就是將處於wait狀態的執行緒停止.
通過一個標識結束執行緒
public class Demo7 {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
thread.start();
//讓主執行緒睡一會兒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int i=0;
while (true) {
if (++i == 10) {
test1.flag = false;//當主執行緒執行到某個階段的時候,讓flag值變成false,控制while迴圈的結束,從而控制子執行緒的結束
break;//目的:為了讓主執行緒結束
}
}
}
}
class Test1 implements Runnable{
boolean flag = true;
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName()+" "+"我們很happy");
}
}
}
呼叫interrupt()方法結束執行緒
public class Demo7 {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread = new Thread(test1);
thread.start();
//讓主執行緒睡一會兒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int i=0;
while (true) {
if (++i == 10) {
thread.interrupt();//當呼叫這個方法的時候,會觸發wait方法的interruptedException異常,我們就可以在捕獲異常的
//時候將flag值變成false,從而結束迴圈,結束任務,結束執行緒
break;//目的:為了讓主執行緒結束
}
}
}
}
class Test1 implements Runnable{
boolean flag = true;
public synchronized void run() {
while (flag) {
try {
this.wait();
} catch (InterruptedException e) {
flag = false;
System.out.println("InterruptedException");
}//wait方法由鎖物件來呼叫
System.out.println(Thread.currentThread().getName()+" "+"我們很happy");
}
}
}