同步的方法:
一、同步方法
二、同步代碼塊
synchronized(object){
}
package com.xhj.thread;
/**
* 線程同步的運用
*
* @author XIEHEJUN
*
*/
public class SynchronizedThread {
class Bank {
private int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法實現
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代碼塊實現
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "賬戶余額為:" + bank.getAccount());
}
}
}
/**
* 建立線程,調用內部類
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("線程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("線程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
}
示例加講解
同步是多線程中的重要概念。同步的使用可以保證在多線程運行的環境中,程序不會產生設計之外的錯誤結果。同步的實現方式有兩種,同步方法和同步塊,這兩種方式都要用到synchronized關鍵字。
同步方法:給一個方法增加synchronized修飾符之后就可以使它成為同步方法,這個方法可以是靜態方法和非靜態方法,但是不能是抽象類的抽象方法,也不能是接口中的接口方法。下面代碼是一個同步方法的示例:
public synchronized void aMethod() {
// do something
}
public static synchronized void anotherMethod() {
// do something
}
線程在執行同步方法時是具有排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的所有同步方法都被鎖定了,在此期間,其他任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程執行完它所調用的同步方法並從中退出,從而導致它釋放了該對象的同步鎖之后。在一個對象被某個線程鎖定之后,其他線程是可以訪問這個對象的所有非同步方法的。
同步塊:同步塊是通過鎖定一個指定的對象,來對同步塊中包含的代碼進行同步;而同步方法是對這個方法塊里的代碼進行同步,而這種情況下鎖定的對象就是同步方法所屬的主體對象自身。如果這個方法是靜態同步方法呢?那么線程鎖定的就不是這個類的對象了,也不是這個類自身,而是這個類對應的java.lang.Class類型的對象。同步方法和同步塊之間的相互制約只限於同一個對象之間,所以靜態同步方法只受它所屬類的其它靜態同步方法的制約,而跟這個類的實例(對象)沒有關系。
如果一個對象既有同步方法,又有同步塊,那么當其中任意一個同步方法或者同步塊被某個線程執行時,這個對象就被鎖定了,其他線程無法在此時訪問這個對象的同步方法,也不能執行同步塊。
synchronized 關鍵字用於保護共享數據。請大家注意“共享數據”,你一定要分清哪些數據是共享數據,請看下面的例子:
public class ThreadTest implements Runnable{
public synchronized void run(){
for(int i=0;i<10;i++) {
System.out.print(" " + i);
}
}
public static void main(String[] args) {
Runnable r1 = new ThreadTest(); //也可寫成ThreadTest r1 = new ThreadTest();
Runnable r2 = new ThreadTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}}
在這個程序中,run()雖然被加上了synchronized 關鍵字,但保護的不是共享數據。因為這個程序中的t1,t2 是兩個對象(r1,r2)的線程。而不同的對象的數據是不同的,r1,r2 有各自的run()方法,所以輸出結果無法預知。
synchronized的目的是使同一個對象的多個線程,在某個時刻只有其中的一個線程可以訪問這個對象的synchronized 數據。每個對象都有一個“鎖標志”,當這個對象的一個線程訪問這個對象的某個synchronized 數據時,這個對象的所有被synchronized 修飾的數據將被上鎖(因為“鎖標志”被當前線程拿走了),只有當前線程訪問完它要訪問的synchronized 數據時,當前線程才會釋放“鎖標志”,這樣同一個對象的其它線程才有機會訪問synchronized 數據。
示例3:
public class ThreadTest implements Runnable{
public synchronized void run(){
for(int i=0;i<10;i++){
System.out.print(" " + i);
}
}
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}}
如果你運行1000 次這個程序,它的輸出結果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。因為這里的synchronized 保護的是共享數據。t1,t2 是同一個對象(r)的兩個線程,當其中的一個線程(例如:t1)開始執行run()方法時,由於run()受synchronized保護,所以同一個對象的其他線程(t2)無法訪問synchronized 方法(run 方法)。只有當t1執行完后t2 才有機會執行。
示例4:
public class ThreadTest implements Runnable{
public void run(){
synchronized(this){
for(int i=0;i<10;i++){
System.out.print(" " + i);
}
}
}
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
這個程序與示例3 的運行結果一樣。在可能的情況下,應該把保護范圍縮到最小,可以用示例4 的形式,this 代表“這個對象”。沒有必要把整個run()保護起來,run()中的代碼只有一個for循環,所以只要保護for 循環就可以了。
示例5:
public class ThreadTest implements Runnable{
public void run(){
for(int k=0;k<5;k++){
System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);
}
synchronized(this){
for(int k=0;k<5;k++) {
System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);
}} }
public static void main(String[] args){
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1_name");
Thread t2 = new Thread(r,"t2_name");
t1.start();
t2.start();
} }
運行結果:
t1_name : for loop : 0
t1_name : for loop : 1
t1_name : for loop : 2
t2_name : for loop : 0
t1_name : for loop : 3
t2_name : for loop : 1
t1_name : for loop : 4
t2_name : for loop : 2
t1_name : synchronized for loop : 0
t2_name : for loop : 3
t1_name : synchronized for loop : 1
t2_name : for loop : 4
t1_name : synchronized for loop : 2
t1_name : synchronized for loop : 3
t1_name : synchronized for loop : 4
t2_name : synchronized for loop : 0
t2_name : synchronized for loop : 1
t2_name : synchronized for loop : 2
t2_name : synchronized for loop : 3
t2_name : synchronized for loop : 4
第一個for 循環沒有受synchronized 保護。對於第一個for 循環,t1,t2 可以同時訪問。運行結果表明t1 執行到了k=2 時,t2 開始執行了。t1 首先執行完了第一個for 循環,此時t2還沒有執行完第一個for 循環(t2 剛執行到k=2)。t1 開始執行第二個for 循環,當t1的第二個for 循環執行到k=1 時,t2 的第一個for 循環執行完了。t2 想開始執行第二個for 循環,但由於t1 首先執行了第二個for 循環,這個對象的鎖標志自然在t1 手中(synchronized 方法的執行權也就落到了t1 手中),在t1 沒執行完第二個for 循環的時候,它是不會釋放鎖標志的。所以t2 必須等到t1 執行完第二個for 循環后,它才可以執行第二個for 循環。
=====================================
三、wait與notify
wait():使一個線程處於等待狀態,並且釋放所持有的對象的lock。
sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException異常。
notify():喚醒一個處於等待狀態的線程,注意的是在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且不是按優先級。
Allnotity():喚醒所有處入等待狀態的線程,注意並不是給所有喚醒線程一個對象的鎖,而是讓它們競爭。
詳細見:wait、notify、notifyAll的使用方法
四、使用特殊域變量(volatile)實現線程同步
//只給出要修改的代碼,其余代碼與上同
class Bank {
//需要同步的變量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//這里不再需要synchronized
public void save(int money) {
account += money;
}
}
五、使用重入鎖實現線程同步
ReentrantLock() : 創建一個ReentrantLock實例
lock() : 獲得鎖
unlock() : 釋放鎖
//只給出要修改的代碼,其余代碼與上同
class Bank {
private int account = 100;
//需要聲明這個鎖
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//這里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
六、使用局部變量實現線程同步
ThreadLocal() : 創建一個線程本地變量
get() : 返回此線程局部變量的當前線程副本中的值
initialValue() : 返回此線程局部變量的當前線程的"初始值"
set(T value) : 將此線程局部變量的當前線程副本中的值設置為value
//只改Bank類,其余代碼與上同
public class Bank{
//使用ThreadLocal類管理共享變量account
private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
}
七、使用阻塞隊列實現線程同步
前面5種同步方式都是在底層實現的線程同步,但是我們在實際開發當中,應當盡量遠離底層結構。 使用javaSE5.0版本中新增的java.util.concurrent包將有助於簡化開發。 本小節主要是使用LinkedBlockingQueue<E>來實現線程的同步 LinkedBlockingQueue<E>是一個基於已連接節點的,范圍任意的blocking queue。 隊列是先進先出的順序(FIFO),關於隊列以后會詳細講解~LinkedBlockingQueue 類常用方法 LinkedBlockingQueue() : 創建一個容量為Integer.MAX_VALUE的LinkedBlockingQueue put(E e) : 在隊尾添加一個元素,如果隊列滿則阻塞 size() : 返回隊列中的元素個數 take() : 移除並返回隊頭元素,如果隊列空則阻塞代碼實例: 實現商家生產商品和買賣商品的同步
package com.xhj.thread; import java.util.Random; import java.util.concurrent.LinkedBlockingQueue; /** * 用阻塞隊列實現線程同步 LinkedBlockingQueue的使用 * 實現商家生產商品和買賣商品的同步 * @author XIEHEJUN * */
public class BlockingSynchronizedThread { /** * 定義一個阻塞隊列用來存儲生產出來的商品 */
private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); /** * 定義生產商品個數 */
private static final int size = 10; /** * 定義啟動線程的標志,為0時,啟動生產商品的線程;為1時,啟動消費商品的線程 */
private int flag = 0; private class LinkBlockThread implements Runnable { @Override public void run() { int new_flag = flag++; System.out.println("啟動線程 " + new_flag); if (new_flag == 0) { for (int i = 0; i < size; i++) { int b = new Random().nextInt(255); System.out.println("生產商品:" + b + "號"); try { queue.put(b); } catch (InterruptedException e) { // TODO Auto-generated catch block
e.printStackTrace(); } System.out.println("倉庫中還有商品:" + queue.size() + "個"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block
e.printStackTrace(); } } } else { for (int i = 0; i < size / 2; i++) { try { int n = queue.take(); System.out.println("消費者買去了" + n + "號商品"); } catch (InterruptedException e) { // TODO Auto-generated catch block
e.printStackTrace(); } System.out.println("倉庫中還有商品:" + queue.size() + "個"); try { Thread.sleep(100); } catch (Exception e) { // TODO: handle exception
} } } } } public static void main(String[] args) { BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); LinkBlockThread lbt = bst.new LinkBlockThread(); Thread thread1 = new Thread(lbt); Thread thread2 = new Thread(lbt); thread1.start(); thread2.start(); } }
注:BlockingQueue<E>定義了阻塞隊列的常用方法,尤其是三種添加元素的方法,我們要多加注意,當隊列滿時:
add()方法會拋出異常
offer()方法返回false
put()方法會阻塞
7.使用原子變量實現線程同步
需要使用線程同步的根本原因在於對普通變量的操作不是原子的。
那么什么是原子操作呢?原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作即-這幾種行為要么同時完成,要么都不完成。在java的util.concurrent.atomic包中提供了創建了原子類型變量的工具類,使用該類可以簡化線程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在應用程序中(如以原子方式增加的計數器),但不能用於替換Integer;可擴展Number,允許那些處理機遇數字類的工具和實用工具進行統一訪問。
AtomicInteger類常用方法:
AtomicInteger(int initialValue) : 創建具有給定初始值的新的
AtomicIntegeraddAddGet(int dalta) : 以原子方式將給定值與當前值相加
get() : 獲取當前值
代碼實例:
只改Bank類,其余代碼與上面第一個例子同
class Bank {
private AtomicInteger account = new AtomicInteger(100);
public AtomicInteger getAccount() {
return account;
}
public void save(int money) {
account.addAndGet(money);
}
}
補充--原子操作主要有:
對於引用變量和大多數原始變量(long和double除外)的讀寫操作;
對於所有使用volatile修飾的變量(包括long和double)的讀寫操作。