您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java并發編程之介紹線程安全基礎的示例,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
線程安全基礎
1.線程安全問題
2.賬戶取款案例
3.同步代碼塊synchronized
synchronized的理解
java中有三大變量的線程安全問題
在實例方法上使用synchronized
總結
面試題
4.死鎖
5.開發中應該怎么解決線程安全問題
6.守護線程
7.定時器
8.實現線程的第三種方式:實現Callable接口
9.Object類中的wait和notify方法
10.生產者和消費者
1.線程安全問題
2.1、為什么這個是重點?
??以后在開發中,我們的項目都是運行在服務器當中,而服務器已經將線程的定義,線程對象的創建,線程的啟動等,都已經實現完了。這些代碼我們都不需要編寫。
??最重要的是:你要知道,你編寫的程序需要放到一個多線程的環境下運行,你更需要關注的是這些數據在多線程并發的環境下是否是安全的。(重點:*****)
2.2、什么時候數據在多線程并發的環境下會存在安全問題呢?
??三個條件:
??條件1:多線程并發。
??條件2:有共享數據。
??條件3:共享數據有修改的行為。
??滿足以上3個條件之后,就會存在線程安全問題。
2.3、怎么解決線程安全問題呢?
當多線程并發的環境下,有共享數據,并且這個數據還會被修改,此時就存在線程安全問題,怎么解決這個問題?
??線程排隊執行。(不能并發)。
??用排隊執行解決線程安全問題。
??這種機制被稱為:線程同步機制。
??專業術語叫做:線程同步,實際上就是線程不能并發了,線程必須排隊執行。
??怎么解決線程安全問題呀?
??使用“線程同步機制”。
??線程同步就是線程排隊了,線程排隊了就會犧牲一部分效率,沒辦法,數據安全第一位,只有數據安全了,我們才可以談效率。數據不安全,沒有效率的事兒。
2.4、說到線程同步這塊,涉及到這兩個專業術語:
異步編程模型:
??線程t1和線程t2,各自執行各自的,t1不管t2,t2不管t1,
??誰也不需要等誰,這種編程模型叫做:異步編程模型。
??其實就是:多線程并發(效率較高。)
同步編程模型:
??線程t1和線程t2,在線程t1執行的時候,必須等待t2線程執行結束,或者說在t2線程執行的時候,必須等待t1線程執行結束,兩個線程之間發生了等待關系,這就是同步編程模型。效率較低。線程排隊執行。
異步就是并發。同步就是排隊。
2.賬戶取款案例
Account類
package ThreadSafe;public class Account { //賬號 private String actno; //余額 private double balance; public Account(String actno, double balance) { super(); this.actno = actno; this.balance = balance; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } //取款的方法 public void withdraw(double money){ //t1和t2并發執行這個方法(t1和t2是兩個棧 ,兩個棧操作堆中同一個對象) //取款之前的余額 double before=this.getBalance(); //取款之后的余額 double after=before-money; //模擬一下網絡延遲,會出現問題 try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //更新余額 //思考:t1執行到這里了,但還沒有來得及執行這行代碼,t2線程進來withdraw方法了,此時一定出問題 this.setBalance(after); }}AccountThread類public class AccountThread extends Thread{ //兩個線程必須共享一個賬戶對象 private Account act; //通過構造方法傳遞過來賬戶對象 public AccountThread(Account act) { this.act = act; } @Override public void run() { //假設取款5000 double money=5000; //多線程執行這個方法 act.withdraw(money); System.out.println(Thread.currentThread().getName()+"賬戶"+act.getActno()+"取款成功"+act.getBalance()); }}
Test類
public class Test { public static void main(String[] args) { //創建賬戶對象 Account act=new Account("act-001",10000); //創建兩個線程 Thread t1=new AccountThread(act); Thread t2=new AccountThread(act); t1.setName("t1"); t2.setName("t2"); //啟動兩個線程執行 t1.start(); t2.start(); }}
3.同步代碼塊synchronized
synchronized的理解
//以下這幾行代碼必須是線程排隊的,不能并發
//一個線程把這里的代碼全部執行結束后,另一個線程才能進來
/*
線程同步機制的語法是
synchronized(){
//線程同步代碼塊。
}
synchronized后面小括號中傳的這個數據是相當關鍵的,
這個數據必須是多線程共享的數據,才能達到多線程排隊。
()中寫什么?
那要看你想讓哪些線程同步
假設t1、t2、t3、t4、t5有5個線程
你只希望t1 t2 t3排隊,t4 t5不需要排隊,怎么辦
你一定要在()中寫一個t1 t2 t3共享的對象,而這個對象對于t4 t5來說不是共享的
這里的共享對象是賬戶對象
賬戶對象是共享的,this是賬戶對象
有時不一定是this,這里只要是多線程共享的那個對象就行
synchronized(this){ double before=this.getBalance(); double after=before-money; try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.setBalance(after); }
在java語言中,任何對象都有一把鎖,其實這把鎖就是一個標記,(只是把它叫做鎖)
100個對象,100個鎖,1個對象1把鎖。
以上代碼的執行原理是什么呢?
1.假設t1和t2線程并發,開始執行以上代碼的時候,肯定有一個先一個后,
2.假設t1先執行了,遇到了synchronized,這個時候自動找后面共享對象的對象鎖,找到之后,并占有這把鎖,然后執行同步代碼塊中的程序,在程序執行過程中一直都占有這把鎖,直到同步代碼塊執行結束,這把鎖才會釋放。
3.假設t1已經占有這把鎖,此時t2也遇到synchronized關鍵字,也會去占有后面共享對象的這把鎖,結果這把鎖被t1占有,t2只能在同步代碼塊外邊等待t1的結束,直到t1把同步代碼塊執行結束了,t1會歸還這把鎖,此時t2終于等到這把鎖,然后t2占有這把鎖之后,進入同步代碼塊執行程序。
這樣就達到了線程排隊執行
這里需要注意的是:這個共享對象一定要選好了,這個共享對象一定是你需要排隊執行的這些線程對象所共享的。
//對象
Object obj=new Object(); //實例變量(Account對象是多線程共享的,Account對象中的實例變量obj也是共享的)
synchronized(obj){}
括號里邊只要是共享對象就行。
Object obj2=new Object(); //局部變量
synchronized(obj2){}
這樣寫就不安全了,因為obj2是局部變量,不是共享對象。
synchronized(“abc”){}
這樣寫時可以的。存在字符串常量池中
寫"abc"的話所有線程都會同步
而如果是寫synchronized(this){}的話,我們創建了一個新的對象act2可不用共享對象。
所以最好是寫synchronized(this){},比如你要取款,要讓其他取別的賬戶的人也要等嗎?不應該,只有同時對你這1個賬戶取款的時候,需要等待,別人取錢的時候,需要從其他賬戶中取錢,就不需要等待。
java中有三大變量的線程安全問題
實例變量,在堆中
靜態變量,在方法區
局部變量,在棧中
以上三大變量
局部變量永遠不會存在線程安全問題
因為局部變量不共享(一個線程一個棧)
局部變量在棧中,所以局部變量永遠都不會共享
實例變量在堆中,堆只有1個。
靜態變量在方法區中,方法區只有1個。
堆和方法區都是多線程共享的,所以可能存在線程安全問題。
局部變量+常量:不會有線程安全問題。
成員變量:可能會有線程安全問題。
同步代碼塊越小,效率越高。
//多線程執行這個方法//synchronized(this)//這里的this是AccountThread對象,這個對象不共享。synchronized(act){ act.withdraw(money); System.out.println(Thread.currentThread().getName()+"賬戶"+act.getActno()+"取款成功,余額:"+act.getBalance());}
在實例方法上使用synchronized
在實例方法上可以使用synchronized嗎?可以的。
synchronized出現在實例方法上,一定鎖的是this
沒得挑,只能是this,不能是其他的對象了
所以這種方式不靈活。
另外還有一個缺點:synchronized出現在實例方法上,表示整個方法體都需要同步
可能會擴大同步的范圍,導致程序的執行效率降低。所以這種方式不常用。
synchronized使用在實例方法上有什么優點?
代碼寫的少了,節儉了。
如果共享的對象是this,并且需要同步的代碼是整個方法體,建議使用這種方式。
StringBuffer就是在每個方法上加了synchronized關鍵字
使用局部變量的話,最好使用StringBuilder
因為局部變量不存在線程安全問題,選擇StringBuilder,StringBuffer效率比較低。
ArrayList是非線程安全的。
Vector是線程安全的。
HashMap HashSet是非線程安全的。
Hashtable是線程安全的。
總結
synchronized有三種寫法:
??第一種:同步代碼塊
????靈活
??????synchronized(線程共享對象){
????????同步代碼塊;
??????}
??第二種:在實例方法上使用synchronized
??????表示共享對象一定是this
??????并且同步代碼塊是整個方法體。
??第三種:在靜態方法上使用synchronized
??????表示找類鎖。
??????類鎖永遠只有1把。
??????就算創建了100個對象,那類鎖也只有一把。
對象鎖:1個對象1把鎖,100個對象100把鎖。
類鎖:100個對象,也可能只是1把類鎖。
面試題
//面試題:doother方法的執行需不需要等待dosome方法的結束。
//不需要,因為doother方法沒有synchronized
public class exam01 { public static void main(String[] args) { MyClass mc=new MyClass(); Thread t1=new MyThread(mc); Thread t2=new MyThread(mc); t1.setName("t1"); t2.setName("t2"); t1.start(); try { Thread.sleep(1000); //這個睡眠的作用是:為了保證t1線程先執行 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } t2.start(); }}class MyThread extends Thread{ private MyClass mc; public MyThread(MyClass mc) { super(); this.mc = mc; } public void run(){ if(Thread.currentThread().getName().equals("t1")){ mc.dosome(); } if(Thread.currentThread().getName().equals("t2")){ mc.doOther(); } } }class MyClass{ public synchronized void dosome(){ System.out.println("doSome begin"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("doSome end"); } public void doOther(){ System.out.println("doOther begin"); System.out.println("doOther end"); }}
當在doother上面加了synchronized呢
//面試題:doother方法的執行需不需要等待dosome方法的結束。
//需要,因為doother方法沒有synchronized
public synchronized void doOther(){ System.out.println("doOther begin"); System.out.println("doOther end"); }
//面試題:doother方法的執行需不需要等待dosome方法的結束。
//不用排隊,誰也不用管誰
MyClass mc1=new MyClass();MyClass mc2=new MyClass();Thread t1=new MyThread(mc1);Thread t2=new MyThread(mc2);
//面試題:doother方法的執行需不需要等待dosome方法的結束。
//需要,因為靜態方法是類鎖,類鎖不管創建了幾個對象,類鎖只有一把
MyClass mc1=new MyClass();MyClass mc2=new MyClass();Thread t1=new MyThread(mc1);Thread t2=new MyThread(mc2);public synchronized static void dosome(){ System.out.println("doSome begin"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("doSome end"); } public synchronized static void doOther(){ System.out.println("doOther begin"); System.out.println("doOther end"); }
這種鎖叫排他鎖:
4.死鎖
synchronized在開發中最好不要嵌套使用,一不小心就可能導致死鎖。
死鎖代碼要會寫。
一般面試官要求你會寫
只有會寫的,才會在以后的開發中注意這個事兒
因為死鎖很難調試。
public class DeadLock { public static void main(String[] args) { Object o1=new Object(); Object o2=new Object(); //t1線程和t2線程共享o1,o2 Thread t1=new MyThread1(o1,o2); Thread t2=new MyThread2(o1,o2); t1.start(); t2.start(); }}class MyThread1 extends Thread{ Object o1; Object o2; public MyThread1(Object o1,Object o2){ this.o1=o1; this.o2=o2; } @Override public void run() { synchronized (o1) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (o2) { } } }}class MyThread2 extends Thread{ Object o1; Object o2; public MyThread2(Object o1,Object o2){ this.o1=o1; this.o2=o2; } @Override public void run() { synchronized (o2) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (o1) { } } }}
5.開發中應該怎么解決線程安全問題
聊一聊,我們以后開發中應該怎么解決線程安全問題?
??是一上來就選擇線程同步嗎?synchronized
??不是,synchronized會讓程序的執行效率降低,用戶體驗不好。
??系統的用戶吞吐量降低。用戶體驗差。在不得已的情況下再選擇
??線程同步機制。
??第一種方案:盡量使用局部變量代替“實例變量和靜態變量”。
??第二種方案:如果必須是實例變量,那么可以考慮創建多個對象,這樣實例變量的內存就不共享了。(一個線程對應1個對象,100個線程對應100個對象,對象不共享,就沒有數據安全問題了。)
??第三種方案:如果不能使用局部變量,對象也不能創建多個,這個時候就只能選擇synchronized了。線程同步機制。
線程這塊還有那些內容呢?列舉一下
7.1、守護線程
7.2、定時器
7.3、實現線程的第三種方式:FutureTask方式,實現Callable接口。(JDK8新特性。)
7.4、關于Object類中的wait和notify方法。(生產者和消費者模式!)
6.守護線程
守護線程
java語言中線程分為兩大類:
??一類是:用戶線程
??一類是:守護線程(后臺線程)
其中具有代表性的就是:垃圾回收線程(守護線程)。
??守護線程的特點:
??一般守護線程是一個死循環,所有的用戶線程只要結束,
??守護線程自動結束。
??注意:主線程main方法是一個用戶線程。
??守護線程用在什么地方呢?
??每天00:00的時候系統數據自動備份。
??這個需要使用到定時器,并且我們可以將定時器設置為守護線程。
??一直在那里看著,沒到00:00的時候就備份一次。所有的用戶線程如果結束了,守護線程自動退出,沒有必要進行數據備份了。
package testThread;/* 實現守護線程 * */ public class ThreadTest13 { public static void main(String[] args) { // TODO Auto-generated method stub Thread t=new BakDataThread(); t.setName("備份數據的線程"); //啟動之前,將線程設置為守護線程 t.setDaemon(true); t.start(); //主線程:主線程是用戶線程 for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+"---->"+i); try { Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception } } }}class BakDataThread extends Thread{ @Override public void run() { int i=0; //即使是死循環,但由于該線程是守護者,當用戶線程結束,守護線程自動終止 while(true){ System.out.println(Thread.currentThread().getName()+"---->"+(++i)); try { Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception } } }}
7.定時器
定時器的作用:
??間隔特定的時間,執行特定的程序。
??每周要進行銀行賬戶的總賬操作。
??每天要進行數據的備份操作。
??在實際的開發中,每隔多久執行一段特定的程序,這種需求是很常見的,
那么在java中其實可以采用多種方式實現:
??可以使用sleep方法,睡眠,設置睡眠時間,沒到這個時間點醒來,執行任務。這種方式是最原始的定時器。(比較low)
??在java的類庫中已經寫好了一個定時器:java.util.Timer,可以直接拿來用。不過,這種方式在目前的開發中也很少用,因為現在有很多高級框架都是支持定時任務的。
??在實際的開發中,目前使用較多的是Spring框架中提供的SpringTask框架,這個框架只要進行簡單的配置,就可以完成定時器的任務。
8.實現線程的第三種方式:實現Callable接口
實現線程的第三種方式:實現Callable接口。(JDK8新特性。)
這種方式實現的線程可以獲取線程的返回值。
之前講解的那兩種方式是無法獲取線程返回值的,因為run方法返回void。
思考:
系統委派一個線程去執行一個任務,該線程執行完任務之后,可能會有一個執行結果,我們怎么能拿到這個執行結果呢?
使用第三種方式:實現Callable接口方式。
public class ThreadTest14 { public static void main(String[] args) throws Exception, ExecutionException { //第一步:創建一個未來任務類對象 //參數非常重要,需要給一個callable接口的實現類對象 FutureTask task=new FutureTask(new Callable(){ @Override //call方法相當于是run方法,只不過這個有返回值,線程執行一個任務,執行之后可能會有一個執行結果。 public Object call() throws Exception { System.out.println("call method begin"); Thread.sleep(1000); System.out.println("call method begin"); int a=100; int b=200; return a+b; //自動裝箱 } }); //創建線程對象 Thread t=new Thread(task); //啟動線程 t.start(); //這里是main方法,這是在主線程中 //在線程中,怎么獲取t線程的執行結果 //get方法的執行會導致當前線程阻塞 Object obj=task.get(); System.out.println("線程執行結果"+obj); //main方法這里的程序要想執行必須等待get()方法的結束 //而get方法可能需要很久。因為get()方法是為了拿另一個線程的執行結果。 //另一個線程的執行是需要時間的 System.out.println("hello,world"); }}
這種方式的優點:可以獲取到線程的執行結果
這種方式的缺點:效率比較低,在獲取t線程執行結果的時候,當前線程受阻塞,效率較低。
9.Object類中的wait和notify方法
(生產者和消費者模式!)
??第一:wait和notify方法不是線程對象的方法,是java中任何一個java對象都有的方法,因為這兩個方式是Object類中自帶的。
??wait方法和notify方法不是通過線程對象調用,
??不是這樣的:t.wait(),也不是這樣的:t.notify()…不對。
??第二:wait()方法作用?
????Object o = new Object();
????o.wait();
??表示:
??讓正在o對象上活動的線程進入等待狀態,無期限等待,直到被喚醒為止。
??o.wait();方法的調用,會讓“當前線程(正在o對象上活動的線程)”進入等待狀態。
??第三:notify()方法作用?
????Object o = new Object();
????o.notify();
??表示:
??喚醒正在o對象上等待的線程。
??還有一個notifyAll()方法:
??這個方法是喚醒o對象上處于等待的所有線程。
10.生產者和消費者
1.使用wait方法和notify方法實現生產者和消費者模式
2.什么是生產者和消費者模式?
生產線程負責生產,消費線程負責消費
生產線程和消費線程要達到均衡
這是一種特殊的業務需求,在這種特殊的情況下需要使用wait方法和notify方法
3.wait和notify方法不是線程對象的方法,是普通java對象都有的方法
4.wait方法和notify方法是建立在線程同步的基礎之上。因為多線程要同時操作一個倉庫,有線程安全問題
5.wait方法作用:o.wait()讓正在o對象上活動的線程t進入等待狀態,并且釋放掉t線程之前占有的o對象的鎖
6.notify方法的作用:o.notify()讓正在o對象上等待的線程喚醒,只是5通知,不會釋放o對象上之前占有的鎖
7.模擬這樣一個需求:
倉庫我們采用list集合
list集合中假設只能存儲1個元素
1個元素就表示倉庫滿了
如果list集合中的元素個數是0,就表示倉庫空了。
保證list集合中永遠都是最多存儲1個元素
必須做到這種效果,生產1個消費1個。
public class ThreadTest15 { public static void main(String[] args) { //創建一個倉庫獨享,共享的 List list=new ArrayList(); //創建兩個線程對象 //生產者線程 Thread t1=new Thread(new Producer(list)); //消費者線程 Thread t2=new Thread(new Consumer(list)); t1.setName("生產者線程"); t2.setName("消費者線程"); t1.start(); t2.start(); }}//生產線程class Producer implements Runnable{ //倉庫 private List list; public Producer(List list) { this.list = list; } public void run() { //一直生產 while(true){ //給倉庫對象list加鎖 synchronized (list) { if(list.size()>0){ //大于0說明倉庫中已經有1個元素了 //當前線程進入等待狀態,并且釋放list集合的鎖 try { list.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //程序能夠執行到這里說明倉庫是空的,可以生產 Object obj =new Object(); list.add(obj); System.out.println(Thread.currentThread().getName()+"---->"+obj); //喚醒消費者進行消費 list.notifyAll(); } } }}//消費線程class Consumer implements Runnable{ //倉庫 private List list; public Consumer(List list) { this.list = list; } public void run() { //一直消費 while(true){ //給倉庫對象list加鎖 synchronized (list) { if(list.size()==0){ //倉里已經空了 try { //倉庫已經空了 //消費者線程等待,并釋放掉list集合鎖 list.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } Object obj=list.remove(0); System.out.println(Thread.currentThread().getName()+"---->"+obj); //喚醒生產者生產 list.notifyAll(); } }}
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Java并發編程之介紹線程安全基礎的示例”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。