多執行緒三(多執行緒資料安全問題與三種解決方式)
最近在做與下載相關的APK的時候,需要用到多執行緒的一些知識,之前用的不是很多很深入,所以現在重新翻出來學習並且記錄一下,這部分內容目前準備三個階段完成;第一部分是一些基本概念與多執行緒幾種常見的實現方式;第二部分是執行緒相關的一些方法以及使用過程中的一些注意事項;由於學習的調整,內容安排上優點變化,所以第三部分先簡單說一下多執行緒中資料安全問題與相應的解決方法。
1,多執行緒資料安全問題引入
多執行緒可以提高程式的使用率,也可以提高cpu的使用使用率,同時也會帶來一些弊端,比如:
- 資料安全問題
今天我們就先說一下多執行緒會產生什麼樣的資料安全問題;在講之前,我們需要明確:
- cpu執行的操作是原子性的,是不可拆分的
- 這裡說的資料安全針對的資料是多個執行緒共享的資料,所以我們會使用在第一部分中說的方式2來實現多執行緒
由此,我們可以總結出會造成多執行緒資料安全的必要條件:
- 多執行緒環境
- 多個執行緒操作共享資料
- 操作共享資料的語句不是原子性的(多條)
2,程式碼演示
先舉一個搶錢的小案例,就是一共有1000元錢,現在開三個執行緒,分別是張三,李四,王五來搶這1000元錢;首先需要一個儲存資料的類:Data.java,其程式碼如下:
/** * @author why * @date 2018年3月4日 * @description: */ package com.why.test; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Data { public static int zsMoney = 0; public static int lsMoney = 0; public static int wwMoney = 0; public static int zsHitNum = 0; public static int lsHitNum = 0; public static int wwHitNum = 0; public static Object lockObject1 = new Data(); public static Object lockObject2 = new Data(); public static Lock lock=new ReentrantLock(); }
裡面有很多元素,目前不會全用到,在後面會用到;除了資料類之外,我們還需要一個實現Runnable介面的功能類,實現搶錢的功能:MoneyRunnable.java,其程式碼如下:
/** * @author why * @date 2018年2月25日 * @description: */ package com.why.test; public class MoneyRunnable implements Runnable { private int sumMoney = 1000; @Override public void run() { while(true){ if (sumMoney > 0) { /** * sumMoney = sumMoney - 1; * 放在前面就不會有問題 */ //sumMoney = sumMoney - 1; System.out.println(Thread.currentThread().getName() + "獲得一元錢"); if (Thread.currentThread().getName().equals("張三")) { Data.zsMoney++; } else if (Thread.currentThread().getName().equals("李四")) { Data.lsMoney++; } else { Data.wwMoney++; } /** * sumMoney = sumMoney - 1; * 放在後面就會出現資料安全問題(執行緒安全問題) */ sumMoney = sumMoney - 1; } else { System.out.println("錢搶完了:"); System.out.println("張三獲得了:" + Data.zsMoney); System.out.println("李四獲得了:" + Data.lsMoney); System.out.println("王五獲得了:" + Data.wwMoney); System.out.println("他們一共獲得了:"+(Data.zsMoney+Data.wwMoney+Data.lsMoney)); try { //防止資料刷的過快,休眠一段時間 Thread.sleep(4000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
這些準備完了,就可以開始編寫測試類了:ThreadTestDemo.java,其程式碼如下:
/**
* @author why
* @date 2018年2月25日
* @description:
*/
package com.why.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ThreadTestDemo {
public static void main(String[] args) {
/**
* 執行緒資料安全問題出現條件:
*
* (1)多執行緒環境
* (2)多個執行緒操作共享資料
* (3)每一個執行緒共享資料的語句有多條
*
* 解決方法:
* (1)同步程式碼塊
* (2)同步方法
* (3)Lock物件鎖
*/
/**
* 搶錢案例
*/
MoneyRunnable my1 = new MoneyRunnable();
// MoneyRunnableImp my1 = new MoneyRunnableImp();
Thread t1 = new Thread(my1);
Thread t2 = new Thread(my1);
Thread t3 = new Thread(my1);
t1.setName("張三");
t2.setName("李四");
t3.setName("王五");
t1.start();
t2.start();
t3.start();
/**
* 賣票案例
*/
// TicketRunnable tr=new TicketRunnable();
// TicketRunnableImp tr=new TicketRunnableImp();
// Thread t4=new Thread(tr);
// Thread t5=new Thread(tr);
// Thread t6=new Thread(tr);
// t4.setName("視窗1");
// t5.setName("視窗2");
// t6.setName("視窗3");
// t4.start();
// t5.start();
// t6.start();
/**
* 打小明案例
*/
// HitPeopleRunnable hr = new HitPeopleRunnable();
// HitPeopleRunnableImp hr = new HitPeopleRunnableImp();
// Thread t7 = new Thread(hr);
// Thread t8 = new Thread(hr);
// Thread t9 = new Thread(hr);
// t1.setName("張三");
// t2.setName("李四");
// t3.setName("王五");
// t1.start();
// t2.start();
// t3.start();
}
}
執行測試類,有下面執行結果:
我們發現一共1000元,他們三人卻搶到了1002元,為什麼會出現這樣的問題了?而且這裡你可以多測試幾次,他們最多也只能搶到1002元,主要只因為這裡面的
sumMoney = sumMoney - 1
不是一個原子性的操作,所以很可能在執行了,sumMoney-1之後,賦值操作之前,另個執行緒進來了,所以,就會多執行一次搶錢動作,所以,你也可以開n個執行緒,最多他們可以搶到(1000+n-1)元,當然n<=1000;
3,解決方法
既然遇到上面的問題,那麼怎麼解決了;解決的方式主要有下面三種:
- 同步程式碼塊
- 同步方法
- Lock物件(本身是一個介面,使用其子類)
就針對上面的案列,我們只用第一種方式來解決一下,這裡我們把MoneyRunnable.java換成MoneyRunnableImp.java:其程式碼如下:
/**
* @author why
* @date 2018年3月5日
* @description:
*/
package com.why.test;
public class MoneyRunnableImp implements Runnable {
private int sumMoney = 1000;
@Override
public void run() {
while (true) {
/**
* 同步程式碼塊實現資料安全:
*
* 這裡面的this就是一把鎖,使用這個類建立的執行緒使用同一把鎖
*
*/
synchronized (this) {
if (sumMoney > 0) {
/**
* sumMoney = sumMoney - 1; 放在前面就不會有問題
*/
// sumMoney = sumMoney - 1;
System.out.println(Thread.currentThread().getName() + "獲得一元錢");
if (Thread.currentThread().getName().equals("張三")) {
Data.zsMoney++;
} else if (Thread.currentThread().getName().equals("李四")) {
Data.lsMoney++;
} else {
Data.wwMoney++;
}
/**
* sumMoney = sumMoney - 1; 放在後面就會出現資料安全問題(執行緒安全問題)
*/
sumMoney = sumMoney - 1;
} else {
System.out.println("錢分完了:");
System.out.println("張三獲得了:" + Data.zsMoney);
System.out.println("李四獲得了:" + Data.lsMoney);
System.out.println("王五獲得了:" + Data.wwMoney);
System.out.println("他們一共獲得了:" + (Data.zsMoney + Data.wwMoney + Data.lsMoney));
try {
// 防止資料刷的過快,休眠一段時間
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
然後把測試ThreadTestDemo.java中的下面兩行註釋對調一下:
MoneyRunnable my1 = new MoneyRunnable();
// MoneyRunnableImp my1 = new MoneyRunnableImp();
再次執行,結果如下:
這個時候發現數據正常了,原理也很簡單,就是在進入同步程式碼塊之前,必須要拿到this這個鎖,當時當其他執行緒正常執行時,即便丟失cpu執行權,也不釋放this這個鎖,所以,其他執行緒無法執行,必須等待該執行緒執行完同步程式碼塊,把鎖釋放了,其他的執行緒才可以拿著這個鎖進入同步程式碼塊。
4,擴充套件部分
前面說了有三種方式可以解決這個問題,那麼其他兩種是什麼了,為了加強練習,我又重新寫了兩個小案例,下面分別給出解決前程式碼和解決後的程式碼:
打小明案例:
解決前程式碼:
/**
* @author why
* @date 2018年3月5日
* @description:
*/
package com.why.test;
public class HitPeopleRunnable implements Runnable {
private int sumHit = 1000;
@Override
public void run() {
while (true) {
if (sumHit > 0) {
/**
* sumHit = sumHit - 1; 放在前面就不會有問題
*/
// sumMoney = sumMoney - 1;
System.out.println(Thread.currentThread().getName() + "打了小明一拳");
if (Thread.currentThread().getName().equals("張三")) {
Data.zsHitNum++;
} else if (Thread.currentThread().getName().equals("李四")) {
Data.lsHitNum++;
} else {
Data.wwHitNum++;
}
/**
* sumHit = sumHit - 1; 放在後面就會出現資料安全問題(執行緒安全問題)
*/
sumHit = sumHit - 1;
} else {
System.out.println("\n小明被打死了:");
System.out.println("張三打了小明:" + Data.zsHitNum + "拳");
System.out.println("李四打了小明:" + Data.lsHitNum + "拳");
System.out.println("王五打了小明:" + Data.wwHitNum + "拳");
System.out.println("他們一共打了小明:" + (Data.zsHitNum + Data.lsHitNum + Data.wwHitNum)+"拳");
try {
// 防止資料刷的過快,休眠一段時間
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
解決後代碼:
/**
* @author why
* @date 2018年3月5日
* @description:
*/
package com.why.test;
public class HitPeopleRunnableImp implements Runnable {
private int sumHit = 1000;
@Override
public void run() {
while (true) {
// 這裡加鎖
Data.lock.lock();
if (sumHit > 0) {
/**
* sumHit = sumHit - 1; 放在前面就不會有問題
*/
// sumMoney = sumMoney - 1;
System.out.println(Thread.currentThread().getName() + "打了小明一拳");
if (Thread.currentThread().getName().equals("張三")) {
Data.zsHitNum++;
} else if (Thread.currentThread().getName().equals("李四")) {
Data.lsHitNum++;
} else {
Data.wwHitNum++;
}
/**
* sumHit = sumHit - 1; 放在後面就會出現資料安全問題(執行緒安全問題)
*/
sumHit = sumHit - 1;
} else {
System.out.println("\n小明被打死了:");
System.out.println("張三打了小明:" + Data.zsHitNum + "拳");
System.out.println("李四打了小明:" + Data.lsHitNum + "拳");
System.out.println("王五打了小明:" + Data.wwHitNum + "拳");
System.out.println("他們一共打了小明:" + (Data.zsHitNum + Data.lsHitNum + Data.wwHitNum) + "拳");
try {
// 防止資料刷的過快,休眠一段時間
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Data.lock.unlock();
}
}
}
賣票案例:
解決前程式碼:
/**
* @author why
* @date 2018年3月5日
* @description:
*/
package com.why.test;
public class TicketRunnable implements Runnable {
private int number = 100;
private int count =0;
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
if (number > 0) {
System.out.println( "當前賣了"+(++count)+"張票");
System.out.println(Thread.currentThread().getName() + "出售了第:" + number + "張票");
number=number-1;
}
}
}
}
解決後代碼:
/**
* @author why
* @date 2018年3月5日
* @description:
*/
package com.why.test;
public class TicketRunnableImp implements Runnable {
private int number = 100;
private int count =0;
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
/**
* 同步方法實現資料安全:
*
* 這裡面的this就是一把鎖,使用這個類建立的執行緒使用同一把鎖
*
*/
sysoInfo();
}
}
//同步方法實現,關鍵字synchronized也可以放在許可權修飾符前面
private synchronized void sysoInfo() {
if (number > 0) {
System.out.println( "當前賣了"+(++count)+"張票");
System.out.println(Thread.currentThread().getName() + "出售了第:" + number + "張票");
number=number-1;
}
}
}
總結:測試程式碼還是ThreadTestDemo.java,只需要把想測試的案例註釋放開即可,測試結果這裡就不在給出。把所有的java類放在一個專案中,因為後面的案列需要使用到DATA這個資料類。後面通過Lock實現的是在jdk1.5之後才有,不過相信各位jdk肯定大於這個版本啦。同步可以改進資料安全問題,但與此同時付出的代價就是效率降低,這是一個由來已久的話題了。下一章我們會介紹互鎖與相應的解決辦法,就會使用到這部分和第二部分的知識了。