最近學習並發編程,並發編程肯定和多線程、線程安全有關系,那麼下面是我總結了自己學習線程安全的筆記!!!
1.線程安全的概念
多個線程訪問某一個類(對象或方法)時,這個類始終都能保持正確的行為,那麼這個類(對象或方法是線程安全的)。
2.synchronized
可以在任意對象和方法上加鎖,而加鎖的這段代碼稱為“互斥區”或“臨界區”。
a.示例
package com.wp.test; public class MyThread extends Thread { private int count = 5; public synchronized void run(){ count--; System.out.println(this.currentThread().getName()+" count="+count); } public static void main(String args[]){ MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread,"t1"); Thread t2 = new Thread(myThread,"t2"); Thread t3 = new Thread(myThread,"t3"); Thread t4 = new Thread(myThread,"t4"); Thread t5 = new Thread(myThread,"t5"); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
b.示例總結
如果run方法前沒有加關鍵字“synchronized”,那麼對於多個線程操作count變量,是不安全的。加上“synchronized”時,那麼線程是安全的;當多個線程訪問run方法時,以排隊的方式進行處理(此處的排隊是按照CPU分配的先後順序而定),一個線程想要執行這個synchronized修飾的run方法,首先要獲得鎖,如果獲取不到,便會一直等到獲取這把鎖為止,此時,如果有多個線程,那麼多個線程會競爭這把鎖,這時會有鎖競爭問題。鎖競爭問題很導致應用程序非常慢。
3.多個線程多個鎖
多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後執行鎖定的內容。此處有兩個概念:對象鎖和類鎖,示例總結將做解釋。
a.示例
package com.wp.test; /** * 關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當作鎖 * 所以代碼中哪個線程先執行synchronized對應的方法,該線程就持有該方法所屬對象的鎖(LOCK) * * 在靜態方法前加關鍵字synchronized,表示該方法的鎖是類級別的鎖。 * */ public class MultiThread { private static int num = 0; /* static **/ public static synchronized void printNum(String tag){ try{ if("a".equals(tag)){ num = 100; System.out.println("tag a!set number over!"); Thread.sleep(1000); }else{ num = 200; System.out.println("tag b! set number over!"); } System.out.println("num="+num); }catch(Exception e){ e.printStackTrace(); } } //注意觀察run方法輸出順序 public static void main(String args[]){ //兩個不同的對象 final MultiThread m1 = new MultiThread(); final MultiThread m2 = new MultiThread(); Thread t1 = new Thread(new Runnable() { @Override public void run() { m1.printNum("a"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { m2.printNum("b"); } }); t1.start(); t2.start(); } }View Code
執行結果:
1. 無static: tag a!set number over! tag b! set number over! num=200 num=100 2.有static tag a!set number over! num=100 tag b! set number over!
b.示例總結
代碼中線程取得的鎖都是對象鎖,而不是把一段代碼(或方法)當作鎖,對象鎖的特點是不同的對象對應著不同的鎖,互不影響。在靜態方法上加上關鍵字synchronized,表示鎖定的是.class類,屬於類級別的鎖。
4.對象鎖的同步和異步
a.同步synchronized
同步的概念是共享,如果多個線程共享資源,那麼就沒有必要進行同步了。
b.異步asynchronized
異步的概念是獨立,多個線程相互之間不受任何制約。
c.同步的目的就是為了線程安全,對於線程安全,需要滿足兩個特性:1).原子性(同步) 2).可見性
d.示例
package com.wp.test; /** * 對象鎖的同步和異步問題 */ public class MyObject { public synchronized void method1(){ try { System.out.println(Thread.currentThread().getName()); Thread.sleep(4000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void method2(){ System.out.println(Thread.currentThread().getName()); } public static void main(String args[]){ final MyObject mo = new MyObject(); Thread t1 = new Thread(new Runnable() { @Override public void run() { mo.method1(); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { mo.method2(); } },"t2"); t1.start(); t2.start(); } }View Code
示例總結:若t1線程先持有Object對象鎖,t2線程如果這個時候要調用對象中的同步(synchronized)方法則需要等待t1釋放鎖,才可以調用,也就說同步了;若t1線程先持有Object對象鎖,t2線程這個時候調用異步(非synchronized修飾)的方法,則會立即調用,t1和t2之間沒有影響,也就說是異步了。
4.髒讀
對於對象的同步和異步方法,我們在設計的時候一定要考慮問題的整體性,不然就會出現數據不一致的錯誤,很經典的一個錯誤就是髒讀。
a.示例
package com.wp.test; public class DirtyRead { private String uname = "sxt"; private String pwd = "123"; public synchronized void setValue(String uname,String pwd){ try { this.uname = uname; Thread.sleep(2000); this.pwd = pwd; System.out.println("setValue設置的值:uname="+uname+" pwd="+pwd); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void getValue(){ System.out.println("getValue的最終值:uname="+uname+" pwd="+pwd); } public static void main(String args[]) throws Exception{ final DirtyRead dr = new DirtyRead(); Thread t = new Thread(new Runnable() { @Override public void run() { dr.setValue("wangping", "456"); } }); t.start(); Thread.sleep(1000); dr.getValue(); } } /**getValue的最終值:uname=wangping pwd=123 setValue設置的值:uname=wangping pwd=456*/View Code
b.示例分析
pwd不一致,解決方法:getValue方法加關鍵字synchronized。加上對象鎖之後,等到鎖被釋放才可以獲得鎖。保持業務數據一致性。
c.示例總結
當我在給對象的方法加鎖的時候,一定要考慮業務的整體性,即示例中setValue和getValue方法都加synchronized鎖住,就保證了業務的原子性,保證業務不會出錯。
d.oracle關系型數據庫,一致性讀實現原理
案例描述:oracle,用戶A,9點查詢某條數據(100),9:10才可以查到所需數據。而用戶B在9:05執行了DML操作,那麼A所查的數據在數據庫已經變化(200)。那麼A在9:10查到的結果是100還是200?答案是:100。
原因:oracle數據庫有一致性讀的特性。B在update時,會將之前的數據保存到undo中做記錄。當A在9:00這一時刻查時,將這一動作保存到ITL中,當A在9:10查100數據時,會判斷ITL中對該數據的動作是否更新(是否在9:00這一刻之後發生變化),如果更新了,那麼會從100對應的undo中將之前的數據返回。所以,結果為100。
Undo的作用:提供一致性讀(Consistent Read)、回滾事務(Rollback Transaction)以及實例恢復(Instance Recovery)。
詳細解釋:http://www.educity.cn/shujuku/1121393.html
5.synchronized鎖重入
關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個對象得到對象鎖之後,不釋放鎖,還可以再次獲得該對象的鎖,稱為鎖重入。
a.示例1:方法鎖重入
package com.wp.test; public class SyncDubbo1 { public synchronized void method1(){ System.out.println("method1------------------"); method2(); } public synchronized void method2(){ System.out.println("method2------------------"); method3(); } public synchronized void method3(){ System.out.println("method3------------------"); } public static void main(String args[]){ final SyncDubbo1 s = new SyncDubbo1(); Thread t = new Thread(new Runnable() { @Override public void run() { s.method1(); } }); t.start(); } } /**結果: * method1----------------- * method2------------------ * method3------------------ */View Code
示例2:子類方法鎖重入
package com.wp.test; public class SyncDubbo2 { static class Main{ public int i = 10; public synchronized void operationSup(){ try{ i--; System.out.println("Main print i="+i); Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); } } } static class Sub extends Main{ public synchronized void operationSub(){ try{ while(i>0){ i--; System.out.println("Sub print i="+i); Thread.sleep(100); this.operationSup(); } }catch(Exception e){ e.printStackTrace(); } } } public static void main(String args[]){ Thread t = new Thread(new Runnable() { @Override public void run() { Sub s = new Sub(); s.operationSub(); } }); t.start(); } }View Code
結果:
Sub print i=9 Main print i=8 Sub print i=7 Main print i=6 Sub print i=5 Main print i=4 Sub print i=3 Main print i=2 Sub print i=1 Main print i=0
示例3:鎖中的內容出現異常
對於web應用程序,異常釋放鎖的情況,如果不特殊處理,那麼業務邏輯會出現很嚴重的錯,比如執行一個隊列任務,很對任務對象都在等待第一個對象正確執行完之後釋放鎖,但是執行第一個對象時出現了異常,導致剩余任務沒有執行,那麼業務邏輯就會出現嚴重錯誤,所以在設計代碼的時候一定要慎重考慮。
解決方式:出現異常時,捕獲異常後通過記錄異常信息到日志文件,然後剩余任務對象繼續執行,那麼整個任務執行完之後,再對出現異常的那個任務對象進行處理,從而不會影響其他任務對象的執行。
package com.wp.test; public class SyncException { private int i = 0; public synchronized void operation(){ while(true){ try{ i++; Thread.sleep(200); System.out.println(Thread.currentThread().getName()+",i="+i); if(i==3){ Integer.parseInt("a");//throw RuntimeException } }catch(Exception e){ e.printStackTrace(); System.out.println("log info i="+i); } } } public static void main(String args[]){ final SyncException se = new SyncException(); Thread t = new Thread(new Runnable() { @Override public void run() { se.operation(); } }); t.start(); } }View Code
結果:執行方法的時候,出現異常,不會釋放鎖,業務繼續執行。執行完之後,對發生的異常進行處理。比如說存儲過程:當更新某張表的數據時,出現異常,那麼將異常信息保存到日志表中,稍後進行處理,而使得該表的數據繼續更新。
Thread-0,i=1 Thread-0,i=2 Thread-0,i=3 java.lang.NumberFormatException: For input string: "a" log info i=3 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:492) at java.lang.Integer.parseInt(Integer.java:527) at com.wp.test.SyncException.operation(SyncException.java:11) at com.wp.test.SyncException$1.run(SyncException.java:24) at java.lang.Thread.run(Thread.java:745) Thread-0,i=4 Thread-0,i=5 Thread-0,i=6
6.synchronized關鍵字
使用synchronized聲明的方法在某些情況下是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待同樣長的時間才能執行,這樣的情況下可以使用synchronized代碼塊去優化代碼執行時間,通常說,減小了鎖的粒度。
Synchronized可以使用任意的Object進行加鎖,用法比較靈活。
特別注意,就是不要使用String的常量加鎖,會出現死循環問題。
鎖對象改變問題:當使用一個對象進行加鎖的時候,要注意對象本身是否發生變化(地址),如果發生變化,那麼就會釋放鎖。例如,如果是字符串的鎖,當字符串改變後就會釋放鎖。但是,對象的屬性發生變化,不會有影響的。
a.示例1:synchronized代碼塊
package com.wp.test; public class ObjectLock { public void method1(){ synchronized(this){ try { System.out.println("do method1..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void method2(){ synchronized(ObjectLock.class){ try { System.out.println("do method2..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } private Object lock = new Object(); public void method3(){ synchronized(lock){ try { System.out.println("do method3..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]){ final ObjectLock ol = new ObjectLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { ol.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { ol.method2(); } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { ol.method3(); } }); t1.start(); t2.start(); t3.start(); } }View Code
結果:
do method1... do method3... do method2...
b.示例2:字符串鎖改變
package com.wp.test; public class ChangeLock { private String lock = "lock"; public void method(){ synchronized(lock){ try { System.out.println("當前線程:"+Thread.currentThread().getName()+"開始"); lock = "lock1"; Thread.sleep(2000); System.out.println("當前線程: "+Thread.currentThread().getName()+"結束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]){ final ChangeLock cl = new ChangeLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { cl.method(); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { cl.method(); } },"t2"); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); }View Code
結果:字符串改變之後,會釋放鎖。
示例3:
package com.wp.test; public class ModifyLock { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public synchronized void changeAttribute(String name,int age){ try { System.out.println("當前線程:"+Thread.currentThread().getName()+"開始"); this.setName(name); this.setAge(age); System.out.println("當前線程:"+Thread.currentThread().getName()+"修改的內容為:" +this.getName()+","+this.getAge()); Thread.sleep(2000); System.out.println("當前線程:"+Thread.currentThread().getName()+"結束"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String args[]){ final ModifyLock ml = new ModifyLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { ml.changeAttribute("張三", 20); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { ml.changeAttribute("李四", 21); } },"t2"); t1.start(); t2.start(); } }View Code
結果分析:如果對象本身不發生變化,那麼依然同步,屬性變化,沒有影響,依然同步。
7.volatile關鍵字
當多個線程使用同一變量時,為了線程安全,通常我們會在訪問變量的地方加一把鎖,但是這樣效率比較低。但是Volatile的效率相對高點。
Volatile關鍵字的主要作用是使用變量在多個線程間可見。原理:強制線程到主內存裡去讀取變量,而不去線程工作內存區去讀,那麼當前線程使用的變量是最新的變量(不管其他線程有無修改),從而實現了多個線程間變量可見,也就是滿足了線程安全的可見性。如果不明白,下面的示例,運行一下,就明白了。
package com.wp.test; public class RunThread extends Thread{ /**volatile*/ private volatile boolean isRunning = true; public void setRunning(boolean isRunning){ this.isRunning = isRunning; } public void run(){ System.out.println("進入run方法。。"); while(isRunning == true){ //... } System.out.println("線程終止。。"); } public static void main(String args[]) throws InterruptedException{ RunThread rt = new RunThread(); rt.start(); Thread.sleep(3000); rt.setRunning(false); System.out.println("isRunning的值已經設置成了false"); Thread.sleep(1000); System.out.println(rt.isRunning); } }View Code
結果分析:isRunning不用volatile修飾時,當主線程修改isRunning的值時,線程rt的內存中的isRunning副本不會變化;isRunning用volatile修飾時,當主線程修改isRunning的值時,會強制線程rt從內存中讀取isRunning的值,那麼rt內存裡的isRunning也就發生了修改。
Volatile關鍵字雖然擁有多個線程之間的可見性,但是卻不具備同步性(也就是原子性),可以算得上一個輕量級的synchronized,性能要比synchronized強很多,不會造成阻塞(在很多開源的框架裡,比如netty的底層代碼就是大量使用volatile,可見netty性能非常不錯。)這裡需要注意,一般volatile用於只針對於多個線程可見的變量操作,並不能代替synchronized的同步功能。具體來說,volatile關鍵字只有可見性,沒有原子性。要實現原子性,建議使用atomic類的系列對象,支持原子性操作(注意atomic類只保證本身方法原子性,並不保證多次操作的原子性)。用一個示例說明:
示例1:volatile關鍵字只有可見性,沒有原子性,建議使用atomic類的系列對象
package com.wp.test; import java.util.concurrent.atomic.AtomicInteger; public class VolatileNoAtomic extends Thread { //private static volatile int count; private static AtomicInteger count = new AtomicInteger(); private static void addCount(){ for(int i=0;i<1000;i++){ //count++; count.incrementAndGet(); } System.out.println(count); } public void run(){ addCount(); } public static void main(String args[]){ VolatileNoAtomic[] arr = new VolatileNoAtomic[10]; for(int i=0;i<10;i++){ arr[i] = new VolatileNoAtomic(); } for(int i=0;i<10;i++){ arr[i].start(); } } }View Code
結果分析:當使用volatile修飾時,由於沒有原子性,因此,(線程不安全)結果達不到10000;那麼使用AtomicInteger時,具有原子性,結果正確為10000。
示例2:atomic類只保證本身方法原子性,並不保證多次操作的原子性
package com.wp.test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class AtomicUse { private static AtomicInteger count = new AtomicInteger(0); //多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性 /**synchronized*/ public synchronized int multiAdd(){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } count.addAndGet(1); count.addAndGet(2); count.addAndGet(3); count.addAndGet(4); return count.get(); } public static void main(String args[]){ final AtomicUse au = new AtomicUse(); List<Thread> ts = new ArrayList<Thread>(); for(int i=0;i<100;i++){ ts.add(new Thread(new Runnable() { @Override public void run() { System.out.println(au.multiAdd()); } })); } for(Thread t : ts){ t.start(); } } }View Code
結果分析:多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性。不加,每次加的值不是整10,加的話是整10。