Java 實現線程安全的三種方式

一個程序在運行起來的時候會轉換成進程,一般含有多個線程。java

  一般狀況下,一個進程中的比較耗時的操做(如長循環、文件上傳下載、網絡資源獲取等),每每會採用多線程來解決。spring

好比顯示生活中,銀行取錢問題、火車票多個售票窗口的問題,一般會涉及到併發的問題,從而須要多線程的技術。apache

  當進程中有多個併發線程進入一個重要數據的代碼塊時,在修改數據的過程當中,頗有可能引起線程安全問題,從而形成數據異常。例如,正常邏輯下,同一個編號的火車票只能售出一次,卻因爲線程安全問題而被屢次售出,從而引發實際業務異常。安全

 

如今咱們就以售票問題來演示線程安全的問題網絡

1, 在不對多線程數據進行保護的狀況下會引起的情況多線程

複製代碼
public class ThreadUnSecurity {
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{

        @Override
        public void run() {
            // 未加同步時產生髒數據
            while(tickets > 0) {
                
                System.out.println(Thread.currentThread().getName()+"--->售出第:  "+tickets+" 票");
                tickets--;
                
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
            
            if (tickets <= 0) {
                
                System.out.println(Thread.currentThread().getName()+"--->售票結束!");
            }
        }
    }
    
    
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadUnSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1號窗口");
        Thread thread2 = new Thread(sell, "2號窗口");
        Thread thread3 = new Thread(sell, "3號窗口");
        Thread thread4 = new Thread(sell, "4號窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
        
    }
    

}
複製代碼

上述代碼運行的結果:併發

複製代碼
1號窗口--->售出第:  10 票
3號窗口--->售出第:  10 票
2號窗口--->售出第:  10 票
4號窗口--->售出第:  10 票
2號窗口--->售出第:  6 票
1號窗口--->售出第:  5 票
3號窗口--->售出第:  4 票
4號窗口--->售出第:  3 票
2號窗口--->售出第:  2 票
4號窗口--->售出第:  1 票
1號窗口--->售出第:  1 票
3號窗口--->售票結束!
2號窗口--->售票結束!
1號窗口--->售票結束!
4號窗口--->售票結束!
複製代碼

咱們能夠看出同一張票在不對票數進行保護時會出現同一張票會被出售屢次!因爲線程調度中的不肯定性,讀者在演示上述代碼時,出現的運行結果會有不一樣。ide

 

第一種實現線程安全的方式性能

  同步代碼塊this

複製代碼
package com.bpan.spring.beans.thread;

import com.sun.org.apache.regexp.internal.recompile;

public class ThreadSynchronizedSecurity {
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{

        @Override
        public void run() {
            // 同步代碼塊
            while(tickets > 0) {
                
                synchronized (this) {
                    
//                    System.out.println(this.getClass().getName().toString());
                    
                    if (tickets <= 0) {
                        
                        return;
                    }
                    
                    System.out.println(Thread.currentThread().getName()+"--->售出第:  "+tickets+" 票");
                    tickets--;
                    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                if (tickets <= 0) {
                    
                    System.out.println(Thread.currentThread().getName()+"--->售票結束!");
                }
            }
        }
    }
    
    
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1號窗口");
        Thread thread2 = new Thread(sell, "2號窗口");
        Thread thread3 = new Thread(sell, "3號窗口");
        Thread thread4 = new Thread(sell, "4號窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
        
    }
    

}
複製代碼

輸出結果讀者可自行調試,不會出現同一張票被出售屢次的狀況。

 

第二種 方式

  同步方法

複製代碼
package com.bpan.spring.beans.thread;

public class ThreadSynchroniazedMethodSecurity {
    
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{

        @Override
        public void run() {
            //同步方法
            while (tickets > 0) {
                
                synMethod();
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                
                if (tickets<=0) {
                    
                    System.out.println(Thread.currentThread().getName()+"--->售票結束");
                }
                
            }
            
            
        }
        
        synchronized void synMethod() {
            
            synchronized (this) {
                if (tickets <=0) {
                    
                    return;
                }
                
                System.out.println(Thread.currentThread().getName()+"---->售出第 "+tickets+" 票 ");
                tickets-- ;
            }
            
        }
        
    }
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadSynchroniazedMethodSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1號窗口");
        Thread thread2 = new Thread(sell, "2號窗口");
        Thread thread3 = new Thread(sell, "3號窗口");
        Thread thread4 = new Thread(sell, "4號窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
    }

}
複製代碼

讀者可自行調試上述代碼的運行結果

 

第三種 方式

  Lock鎖機制, 經過建立Lock對象,採用lock()加鎖,unlock()解鎖,來保護指定的代碼塊

複製代碼
package com.bpan.spring.beans.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLockSecurity {
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{
        
        Lock lock = new ReentrantLock();

        @Override
        public void run() {
            // Lock鎖機制
            while(tickets > 0) {
                
                try {
                    lock.lock();
                    
                    if (tickets <= 0) {
                        
                        return;
                    }
                        
                    System.out.println(Thread.currentThread().getName()+"--->售出第:  "+tickets+" 票");
                    tickets--;
                } catch (Exception e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }finally {
                    
                    lock.unlock();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
                
            if (tickets <= 0) {
                
                System.out.println(Thread.currentThread().getName()+"--->售票結束!");
            }
            
        }
    }
    
    
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadLockSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1號窗口");
        Thread thread2 = new Thread(sell, "2號窗口");
        Thread thread3 = new Thread(sell, "3號窗口");
        Thread thread4 = new Thread(sell, "4號窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
        
    }
    

}
複製代碼

 

 最後總結:

  因爲synchronized是在JVM層面實現的,所以系統能夠監控鎖的釋放與否;而ReentrantLock是使用代碼實現的,系統沒法自動釋放鎖,須要在代碼中的finally子句中顯式釋放鎖lock.unlock()。

  另外,在併發量比較小的狀況下,使用synchronized是個不錯的選擇;可是在併發量比較高的狀況下,其性能降低會很嚴重,此時ReentrantLock是個不錯的方案。

 

 補充:  

  在使用synchronized 代碼塊時,能夠與wait()、notify()、nitifyAll()一塊兒使用,從而進一步實現線程的通訊。
其中,wait()方法會釋放佔有的對象鎖,當前線程進入等待池,釋放cpu,而其餘正在等待的線程便可搶佔此鎖,得到鎖的線程便可運行程序;線程的sleep()方法則表示,當前線程會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖,也就是說,在休眠期間,其餘線程依然沒法進入被同步保護的代碼內部,當前線程休眠結束時,會從新得到cpu執行權,從而執行被同步保護的代碼。
wait()和sleep()最大的不一樣在於wait()會釋放對象鎖,而sleep()不會釋放對象鎖。

  notify()方法會喚醒由於調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。調用notify()後,當前線程並不會當即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼所有執行完畢,纔會釋放對象鎖。JVM會在等待的線程中調度一個線程去得到對象鎖,執行代碼。

  須要注意的是,wait()和notify()必須在synchronized代碼塊中調用。

  notifyAll()是喚醒全部等待的線程。

 

下面是示例代碼,

複製代碼
package com.bpan.spring.beans.thread;

public class ThreadDemo {
    
    static final Object obj = new Object();
    
    //第一個子線程
    static class ThreadA implements Runnable{

        @Override
        public void run() {
            
            
            int count = 10;
            while(count > 0) {
                
                synchronized (ThreadDemo.obj) {
                    
                    System.out.println("A-----"+count);
                    count--;
                    
                    synchronized (ThreadDemo.obj) {
                        
                        //notify()方法會喚醒由於調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。
                        //調用notify()後,當前線程並不會當即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼所有執行完畢,
                        ThreadDemo.obj.notify();
                        
                        try {
                            ThreadDemo.obj.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }
            
        }
        
    }
    
    static class ThreadB implements Runnable{
        
        
        @Override
        public void run() {
            
            int count = 10;
            
            while(count > 0) {
                
                synchronized (ThreadDemo.obj) {
                    System.out.println("B-----"+count);
                    count--;
                    
                    synchronized (ThreadDemo.obj) {
                    
                        //notify()方法會喚醒由於調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。
                        //調用notify()後,當前線程並不會當即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼所有執行完畢,
                        ThreadDemo.obj.notify();
                        
                        try {
                            ThreadDemo.obj.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    
                }
                
            }
            
        }
        
    }
    
    public static void main(String[] args) {
        
        
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
        
    }

}
相關文章
相關標籤/搜索