線程的相關理解就不多說了,直接進入主題:通過同步鎖實現線程安全
在多線程,爲了避免多個線程同時操作某個共享變量導致數據錯亂,採用了同步鎖機制,保證了操作的原子性,數據的一致性及線程安全。可以結合數據庫的事務操作理解。
同步鎖又可以分爲對象鎖,實例鎖,類鎖。
對象鎖,鎖住某個對象;實例鎖,鎖住某個實例,類鎖,鎖住某個類。
哈哈哈,佛了,有點抽象,不知道怎麼解釋,很尷尬。不急,可以根據下面的應用場景及代碼來理解。
假定一個應用場景,就用經典的火車票售票系統爲例:
假設有10張票,編號爲1~10,有四個售票窗口(1~3)在賣,即3個窗口共享這10張票,總共賣出的票不能超出10張,也不能有相同號的票被賣出。
未加鎖,線程不安全,代碼如下:
/**
* 對象鎖
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //類變量,多個實例共享,數據存在線程安全問題
@Override
public void run() {
sale0();
}
//無鎖,線程不安全
public static void sale0(){
while (a>0){ //有票
try{
Thread.sleep(500); //模擬網絡延遲,更能看出問題,多個線程在此處等待開始競爭
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
}catch (Exception e){
}
}
}
}
}
運行結果:
窗口3賣出10號票
窗口2賣出9號票
窗口1賣出8號票
窗口3賣出7號票
窗口2賣出7號票
窗口1賣出6號票
窗口3賣出5號票
窗口2賣出5號票
窗口1賣出4號票
窗口3賣出3號票
窗口2賣出2號票
窗口1賣出1號票
窗口3賣出-1號票
窗口2賣出0號票
可見7號票,5號票賣了兩次,只有10張票,3個窗口總共賣了14張票,數據錯亂,線程不安全。
原因分析:
當a=1時,t1,t2,t3 都跑通了while(a>0),經過0.5s的sleep後,t1率先執行了
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
此時,a=0,然後t2接着執行,a=-1,最後t3執行,即出現了超賣現象;
同理,當a=7時,線程t3率先執行了
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
輸出賣了7號票,還沒來得及將a--賦值給a,這時,線程t2來了,也執行了:
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
即,窗口2和3同時賣出了7號票。
這兩種情況都是不允許發生的,於是對程序進行了改進,加入同步鎖,保證線程安全:
/**
* 對象鎖
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //共享變量,數據存在線程安全問題
@Override
public void run() {
sale1();
}
//線程安全,但不合理
//當某線程進入sale方法,獲取到鎖後,運行synchronized中代碼塊,while()循環,直到a爲0,才運行完才釋放鎖
//這期間其他售票窗口無法進入,無法賣票,當a=0時,其他窗口才獲取到該鎖,才能執行synchronized代碼塊,
// 而此時已沒票
public static void sale1(){
synchronized (lock){ //同步代碼塊,某一時刻只允許一個線程進來,運行完釋放鎖
while (a>0){
try{
Thread.sleep(500); //模擬買票網絡延遲
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
}catch (Exception e){
}
}
System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
}
}
}
}
運行結果如下:
窗口1賣出10號票
窗口1賣出9號票
窗口1賣出8號票
窗口1賣出7號票
窗口1賣出6號票
窗口1賣出5號票
窗口1賣出4號票
窗口1賣出3號票
窗口1賣出2號票
窗口1賣出1號票
窗口1票已賣完。。。
窗口3票已賣完。。。
窗口2票已賣完。。。
多次運行後發現,雖然沒有出現同號票,或多出的票,但是10張票都是隻由一個窗口(假設1)賣完,其他窗口無票可賣。
導致的情況是,若窗口1票賣不完,而其他窗口想買買不到,造成了資源不能合理充分利用。
雖然加了sync同步鎖,某個時刻只能有一個線程執行sync中的代碼塊,保證了變量a操作的原子性,數據的一致性。
但是由於while()循環,只要a>0有票,持有鎖的線程就會一直執行,不釋放鎖,則其他線程只能等待,無法進行售票。
直到該線程執行完sync中代碼塊,才釋放鎖,其他線程才能搶到鎖,但a已經爲0,已無票可售。雖然保證了線程安全,
但是不能滿足現實業務需求,我們需要的是在有票的情況下,各個窗口都有機會搶到票賣,既不能出現重複票,也不能
超賣。改進如下:
package com.knowsight.test.Thread.synclock;
/**
* 對象鎖
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //共享變量,數據存在線程安全問題
@Override
public void run() {
//sale1();
sale2();
//sale0();
}
//無鎖,線程不安全
public static void sale0(){
while (a>0){
try{
Thread.sleep(500); //模擬網絡延遲,更能看出問題,多個線程在此處等待開始競爭
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
}catch (Exception e){
}
}
}
//線程安全,但不合理
//當某線程進入sale方法,獲取到鎖後,運行synchronized中代碼塊,while()循環,直到a爲0,才運行完才釋放鎖
//這期間其他售票窗口無法進入,無法賣票,當a=0時,其他窗口才獲取到該鎖,才能執行synchronized代碼塊,
// 而此時已沒票
public static void sale1(){
synchronized (lock){ //同步代碼塊,某一時刻只允許一個線程進來,運行完釋放鎖
while (a>0){
try{
Thread.sleep(500); //模擬買票網絡延遲
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
}catch (Exception e){
}
}
System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
}
}
//線程安全,較爲合理
public static void sale2(){
while (a>0){ //有票,無鎖,多個線程(售票窗口)可以同時進入
synchronized (lock){ //同步代碼塊,只能一個線程進入,運行完釋放鎖
if(a>0){ //因爲當a=1時,可能會有多個線程跑通了a>0,在sync外等待,但只能給其中一個,需要再次判斷是否有票
try{
Thread.sleep(500); //模擬買票網絡延遲
System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
}catch (Exception e){
}
}else {
System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
}
}
}
}
}
}
代碼較簡單,也有註釋,就不多嘮叨。
關於多線程,個人感覺比較抽象,每個人都有各自的理解。以上是個人以火車票售票系統作爲應用場景,及代碼的具體實現,來理解多線程,及線程安全,同步鎖等,如有不妥之處,望多多指教!