一、關於Java多線程中的一些概念
1.1 線程基本概念
從JDK1.5開始,Java提供了3中方式來創建、啟動多線程:
方式一(不推薦)、通過繼承Thread類來創建線程類,重寫run()方法作為線程執行體;
方式二、實現Runnable接口來創建線程類,重寫run()方法作為線程執行體;
方式三、實現Callable接口來創建線程類,重寫run()方法作為線程執行體;
不同的是,其中方式一的效果最差,是因為
1、線程類繼承了Thread類,無法再繼承其他父類;
2、因為每條線程都是一個Thread子類的實例,因此多個線程之間共享數據比較麻煩。
二、一些關於synchronized關鍵字的簡單Demo
2.1、多個線程單個鎖
當多個線程訪問myThread的run方法時,以排隊的方式進行處理(這里排對是按照CPU分配的先后順序而定的),一個線程想要執行synchronized修飾的方法里的代碼首先會去嘗試獲得鎖,如果拿到鎖,執行synchronized代碼體內容;
如果拿不到鎖,這個線程就會不斷的嘗試獲得這把鎖,直到拿到為止,而且是多個線程同時去競爭這把鎖。(也就是會有鎖競爭的問題)
package com.sun.multithread.sync; public class MyThread extends Thread { private int count = 5; // synchronized加鎖 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(); } }
2.2、多個線程多把鎖
關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當成鎖,所以代碼中哪個線程先執行synchronized關鍵字的方法,哪個線程就持有該方法所屬對象的鎖。但是在靜態(static)方法上加synchronized關鍵字,表示鎖定class類,類級別的鎖
package com.sun.multithread.sync; public class MultiThread { private static int num = 0; /** * 在靜態(static)方法上加synchronized關鍵字,表示鎖定class類,類級別的鎖
* * 關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當成鎖, * 所以代碼中哪個線程先執行synchronized關鍵字的方法,哪個線程就持有該方法所屬對象的鎖 * * @param tag 參數 */ public static synchronized void printNum(String tag){ try { if(tag.equals("a")){ num = 100; System.out.println("tag a, set num over!"); Thread.sleep(1000); } else { num = 200; System.out.println("tag b, set num over!"); } System.out.println("tag " + tag + ", num = " + num); } catch (InterruptedException e) { e.printStackTrace(); } } 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() { m1.printNum("b"); } }); t1.start(); t2.start(); } }
2.3 銀行取錢的多線程例子
1、創建一個銀行賬戶,並在里面寫好取錢的方法,其中使用synchronized關鍵字修飾多線程操作的方法
package com.sun.multithread.sync.bankdemo; /** * 銀行賬戶類 * * @author ietree */ public class Account { /** * 賬號 */ private String accountNo; /** * 余額 */ private double balance; public Account() { } /** * 帶參構造函數 * * @param accountNo 賬戶 * @param balance 余額 */ public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } // 訪問該賬戶的余額,使用synchronized修飾符將它變成同步方法 public synchronized double getBalance() { return balance; } /** * 取錢的方法 * * @param drawAmount 取錢金額 */ public synchronized void draw(double drawAmount) { // 如果余額大於等於用戶取的錢,則取款成功 if (balance >= drawAmount) { // 取款成功 System.out.println(Thread.currentThread().getName() + "取錢成功!用戶取出" + drawAmount + "元"); // 修改余額 balance -= drawAmount; System.out.println("\t余額為: " + balance); } else { System.out.println(Thread.currentThread().getName() + "取錢失敗!您的余額不足"); } } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } // 重寫hashCode()方法 public int hashCode() { return accountNo.hashCode(); } // 重寫equals()方法 public boolean equals(Object obj) { if (this == obj) { return true; } if (obj != null && obj.getClass() == Account.class) { Account target = (Account) obj; return target.accountNo.equals(accountNo); } return false; } }
2、創建多個線程同時操作一個賬戶
package com.sun.multithread.sync.bankdemo; class DrawThread extends Thread { /** * 模擬用戶賬戶 */ private Account account; /** * 當前取錢線程所希望取的錢數 */ private double drawAmount; public DrawThread(String name, Account account, double drawAmount){ super(name); this.account = account; this.drawAmount = drawAmount; } // 當多條線程修改同一個共享數據時,將涉及數據安全問題 public void run(){ account.draw(drawAmount); } } public class DrawTest { public static void main(String[] args) { // 創建一個賬戶 Account acct = new Account("1234567", 1000); // 模擬兩個線程對同一賬戶取錢 new DrawThread("路人甲", acct, 800).start(); new DrawThread("路人乙", acct, 800).start(); } }
2.4 業務整體需要使用完整的synchronized,保持業務的原子性
package com.ietree.multithread.sync; /** * 業務整體需要使用完整的synchronized,保持業務的原子性 * * @author ietree */ public class DirtyRead { private String username = "Jack"; private String password = "123"; public synchronized void setValue(String username, String password) { this.username = username; try { // 睡眠2秒 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } this.password = password; System.out.println("setValue最終結果:username = " + username + ", password = " + password); } // synchronized public void getValue() { System.out.println("getValue方法得到:username = " + this.username + ", password = " + this.password); } public static void main(String[] args) throws Exception { final DirtyRead dr = new DirtyRead(); Thread t1 = new Thread(new Runnable() { @Override public void run() { dr.setValue("Dylan", "456"); } }); t1.start(); Thread.sleep(1000); dr.getValue(); } }
程序輸出:
getValue方法得到:username = Dylan, password = 123
setValue最終結果:username = Dylan, password = 456
這里出現了臟讀現象,應該要保持業務的原子性,修改如下:
package com.ietree.multithread.sync; /** * 業務整體需要使用完整的synchronized,保持業務的原子性 * * @author ietree */ public class DirtyRead { private String username = "Jack"; private String password = "123"; public synchronized void setValue(String username, String password) { this.username = username; try { // 睡眠2秒 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } this.password = password; System.out.println("setValue最終結果:username = " + username + ", password = " + password); } // synchronized public synchronized void getValue() { System.out.println("getValue方法得到:username = " + this.username + ", password = " + this.password); } public static void main(String[] args) throws Exception { final DirtyRead dr = new DirtyRead(); Thread t1 = new Thread(new Runnable() { @Override public void run() { dr.setValue("Dylan", "456"); } }); t1.start(); Thread.sleep(1000); dr.getValue(); } }
程序輸出:
setValue最終結果:username = Dylan, password = 456
getValue方法得到:username = Dylan, password = 456
當在set和get方法上同時使用了synchronized能確保業務原子性,不會出現臟讀現象。
2.5 synchronized的重入
demo1:
package com.ietree.multithread.sync; public class SyncDemo1 { 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 SyncDemo1 sd = new SyncDemo1(); Thread t1 = new Thread(new Runnable() { @Override public void run() { sd.method1(); } }); t1.start(); } }
demo2:
package com.ietree.multithread.sync; public class SyncDemo2 { static class Parent { public int i = 10; public synchronized void operationSup() { try { i--; System.out.println("Main print i = " + i); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } static class Sub extends Parent { public synchronized void operationSub() { try { while (i > 0) { i--; System.out.println("Sub print i = " + i); Thread.sleep(100); this.operationSup(); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { Sub sub = new Sub(); sub.operationSub(); } }); t1.start(); } }
2.6 synchronized的Exception
package com.ietree.multithread.sync; public class SyncException { private int i = 0; public synchronized void operation() { while (true) { try { i++; Thread.sleep(100); System.out.println(Thread.currentThread().getName() + " , i = " + i); if (i == 20) { throw new RuntimeException(); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { final SyncException se = new SyncException(); Thread t1 = new Thread(new Runnable() { @Override public void run() { se.operation(); } }, "t1"); t1.start(); } }
2.7 鎖對象的改變問題
demo1:
package com.ietree.multithread.sync; public class ChangeLock { private String lock = "lock"; private void method() { synchronized (lock) { try { System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始"); lock = "change lock"; Thread.sleep(2000); System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { final ChangeLock changeLock = new ChangeLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { changeLock.method(); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { changeLock.method(); } }, "t2"); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } }
程序輸出:
當前線程 : t1開始
當前線程 : t2開始
當前線程 : t1結束
當前線程 : t2結束
demo2:
package com.ietree.multithread.sync; public class ChangeLock { private String lock = "lock"; private void method() { synchronized (lock) { try { System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始"); // lock = "change lock"; Thread.sleep(2000); System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { final ChangeLock changeLock = new ChangeLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { changeLock.method(); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { changeLock.method(); } }, "t2"); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } }
程序輸出:
當前線程 : t1開始
當前線程 : t1結束
當前線程 : t2開始
當前線程 : t2結束
兩個程序相比差異在於是否含有 lock = "change lock";這個改變了鎖對象,所以輸出有差異。
2.8 死鎖問題,在設計程序時就應該避免雙方相互持有對方的鎖的情況
package com.ietree.multithread.sync; public class DeadLock implements Runnable { private String tag; private static Object lock1 = new Object(); private static Object lock2 = new Object(); public void setTag(String tag) { this.tag = tag; } @Override public void run() { if (tag.equals("a")) { synchronized (lock1) { try { System.out.println("當前線程 : " + Thread.currentThread().getName() + " 進入lock1執行"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("當前線程 : " + Thread.currentThread().getName() + " 進入lock2執行"); } } } if (tag.equals("b")) { synchronized (lock2) { try { System.out.println("當前線程 : " + Thread.currentThread().getName() + " 進入lock2執行"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("當前線程 : " + Thread.currentThread().getName() + " 進入lock1執行"); } } } } public static void main(String[] args) { DeadLock d1 = new DeadLock(); d1.setTag("a"); DeadLock d2 = new DeadLock(); d2.setTag("b"); Thread t1 = new Thread(d1, "t1"); Thread t2 = new Thread(d2, "t2"); t1.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } }
程序輸出:
當前線程 : t1 進入lock1執行
當前線程 : t2 進入lock2執行
程序一直等待獲取鎖的狀態,造成死鎖問題。
2.9 同一對象屬性的修改不會影響鎖的情況
package com.ietree.multithread.sync; 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 changeAttributte(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 modifyLock = new ModifyLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { modifyLock.changeAttributte("Jack", 18); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { modifyLock.changeAttributte("Rose", 20); } }, "t2"); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } }
程序輸出:
當前線程 : t1 開始 當前線程 : t1 修改對象內容為: Jack, 18 當前線程 : t1 結束 當前線程 : t2 開始 當前線程 : t2 修改對象內容為: Rose, 20 當前線程 : t2 結束
2.10 使用synchronized代碼塊加鎖,比較靈活
package com.ietree.multithread.sync; 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 objLock = new ObjectLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { objLock.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { objLock.method2(); } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { objLock.method3(); } }); t1.start(); t2.start(); t3.start(); } }
程序輸出:
do method2.. do method3.. do method1..
以上語句是同時輸出的,因為他們拿到的鎖都是不同的鎖,所以互不影響。
2.11 使用synchronized代碼塊減小鎖的粒度,提高性能
package com.ietree.multithread.sync; public class Optimize { public void doLongTimeTask() { try { System.out.println("當前線程開始:" + Thread.currentThread().getName() + ", 正在執行一個較長時間的業務操作,其內容不需要同步"); Thread.sleep(2000); synchronized (this) { System.out.println("當前線程:" + Thread.currentThread().getName() + ", 執行同步代碼塊,對其同步變量進行操作"); Thread.sleep(1000); } System.out.println("當前線程結束:" + Thread.currentThread().getName() + ", 執行完畢"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { final Optimize otz = new Optimize(); Thread t1 = new Thread(new Runnable() { @Override public void run() { otz.doLongTimeTask(); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { otz.doLongTimeTask(); } }, "t2"); t1.start(); t2.start(); } }
程序輸出:
當前線程開始:t2, 正在執行一個較長時間的業務操作,其內容不需要同步
當前線程開始:t1, 正在執行一個較長時間的業務操作,其內容不需要同步
當前線程:t1, 執行同步代碼塊,對其同步變量進行操作
當前線程結束:t1, 執行完畢
當前線程:t2, 執行同步代碼塊,對其同步變量進行操作
當前線程結束:t2, 執行完畢
2.12 避免使用字符串常量鎖
package com.ietree.multithread.sync; public class StringLock { public void method() { // new String("字符串常量") synchronized ("字符串常量") { try { while (true) { System.out.println("當前線程 : " + Thread.currentThread().getName() + "開始"); Thread.sleep(1000); System.out.println("當前線程 : " + Thread.currentThread().getName() + "結束"); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { final StringLock stringLock = new StringLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { stringLock.method(); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { stringLock.method(); } }, "t2"); t1.start(); t2.start(); } }
程序輸出:
當前線程 : t1開始
當前線程 : t1結束
當前線程 : t1開始
當前線程 : t1結束
當前線程 : t1開始
當前線程 : t1結束
當前線程 : t1開始
......