四、線程同步
4.1 基本概念
- 1、由于同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問沖突這個嚴(yán)重的問題。
java語言提供了專門的機制來解決這種沖突,有效避免了同一個數(shù)據(jù)對象被多個線程同時訪問。 - 2、由于我們可以通過
private關(guān)鍵字來保證數(shù)據(jù)對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是synchronized關(guān)鍵字,它包括方法:synchronized方法和synchronized塊。
4.2 相關(guān)例子
不使用同步時可能會出現(xiàn)沖突
package cn.itcast.day178.thread02;
public class SynDemo01 {
public static void main(String[] args) {
Web12306 web = new Web12306();// 真實角色
// 代理對象
Thread t1 = new Thread(web, "黃牛1");// 第二個參數(shù)是當(dāng)前線程的名字
Thread t2 = new Thread(web, "黃牛2");
Thread t3 = new Thread(web, "黃牛3");
// 啟動線程
t1.start();
t2.start();
t3.start();
}
}
class Web12306 implements Runnable {
private int num = 10;
private boolean flag = true;
public void run() {
while (flag) {
test1();
}
}
// 線程不安全
public void test1() {
if (num <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(500);// 500ms的延時
// 加入延時之后可能會造成資源沖突的問題,這就是并發(fā)問題
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
+ "張票");
}
}
說明:這個例子是模擬搶票的情況,如果不加入同步,則可能一張票同時被多個人搶到,顯示這是有問題的,下面我們看使用同步方法來解決這個問題:
// 線程安全,同步方法
public synchronized void test2() {
if (num <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(500);// 500ms的延時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
+ "張票");
}
說明:在方法中加上synchronized關(guān)鍵字就可以將此方法變成一個同步方法,當(dāng)一個運行一個線程的此方法時,如果此方法沒有運行完,則其他線程的此方法是不能執(zhí)行的。當(dāng)然我們還可以使用同步塊來達到這個目的:
// 線程安全,同步塊
public void test3() {
synchronized (this) {// 鎖定this,即鎖定當(dāng)前線程
if (num <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(500);// 500ms的延時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第"
+ num-- + "張票");
}
}
說明:這里我們將方法中所要執(zhí)行的代碼全部放在同步塊中,這樣在同步塊中的代碼沒有執(zhí)行完的時候資源是被此線程鎖定的,同時要注意:這里同步塊需要給定鎖定的線程對象,這里我們給出的是當(dāng)前線程。當(dāng)時有時候我們將要執(zhí)行的代碼全部放在同步塊中會造成效率的下降,一般我們將可能出現(xiàn)并發(fā)錯誤的代碼放在同步塊中,達到最佳的效果,下面我們看一個錯誤的例子:
// 線程不安全,同步塊,鎖定一部分,鎖定范圍不正確
public void test4() {
synchronized (this) {// 鎖定this,即鎖定當(dāng)前線程
if (num <= 0) {
this.flag = false;
return;
}
}
try {
Thread.sleep(500);// 500ms的延時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
+ "張票");
}
說明:這里可能發(fā)生并發(fā)錯誤的位置是票數(shù)量減少的代碼,這里顯然同步塊位置是有問題的,所以并不能解決并發(fā)問題。放在同步塊中的代碼不僅要正確,我們鎖定的資源對象也要正確,下面看鎖定資源對象錯誤的一個例子:
// 線程不安全,同步塊,鎖定資源不正確
public void test5() {
synchronized ((Integer) num) {// 對于基本類型需要包裝
if (num <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(500);// 500ms的延時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第"
+ num-- + "張票");
}
}
說明:資源對象一般是某個線程對象(基本類型數(shù)據(jù)需要包裝),但是這里卻不是,所以也不能解決并發(fā)問題。還有一種同步塊范圍不對的情況:
// 線程不安全,同步塊,鎖定資源不正確
public void test6() {
if (num <= 0) {
this.flag = false;
return;
}
//a b c
synchronized (this) {
try {
Thread.sleep(500);// 500ms的延時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第"
+ num-- + "張票");
}
}
說明:這里我們可以看到,多個線程可能同時出現(xiàn)在同步塊之前進行等待,那哪個線程進入同步塊中執(zhí)行呢?這顯然是不確定的,這樣就會造成沖突。上面我們講解了同步塊的兩種形式:
synchronized(引用類型)
synchronized(this)
其實同步塊還有一種形式synchronized(類.class)。先看一種設(shè)計模式:單例設(shè)計模式
package cn.itcast.day178.thread02;
//單例設(shè)計模式:確保一個類只有一個對象
public class SynDemo02 {
public static void main(String[] args) {
test2();
}
public static void test2() {
// 此時我們看到單例就沒有達到效果,我們在getnInstance方法中加入同步關(guān)鍵字
JvmThread thread1 = new JvmThread(100);
JvmThread thread2 = new JvmThread(500);
thread1.start();
thread2.start();
}
public static void test1() {
Jvm jvm1 = Jvm.getInstance();
Jvm jvm2 = Jvm.getInstance();
// 單線程中下面兩個對象是一樣的,達到了單例的效果,但是在多線程中就不一定了
System.out.println(jvm1);
System.out.println(jvm2);
}
}
class JvmThread extends Thread {
private long time;
public JvmThread() {
}
public JvmThread(long time) {
this.time = time;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "-->"
+ Jvm.getInstance(time));
}
}
// 確保一個類只有一個對象:
// 懶漢式
class Jvm {
// 1、構(gòu)造器私有化,避免外部直接創(chuàng)建對象
private Jvm() {}
// 2、聲明一個私有靜態(tài)變量
private static Jvm instance = null;
// 3、創(chuàng)建一個靜態(tài)的公共方法訪問該變量,如果變量沒有對象,創(chuàng)建該對象
public static Jvm getInstance() {
if (instance == null) {
instance = new Jvm();
}
return instance;
}
}
說明:單例設(shè)計模式就是為了確保在程序運行過程中一個類只有一個實例對象。Jvm類我們使用了基本的單例設(shè)計模式,在單線程中可以確保只有一個對象實例,但是在多線程就不一定了(test1方法)。從這個類中我們可以知道單例設(shè)計模式的基本步驟。其中加入延時是為了放大出錯的概率。從測試結(jié)果中可以看到并沒有達到單例的效果(run方法打印出來的結(jié)果不一致)。當(dāng)然解決這個問題最簡單的方式就是在getInstance方法中加入synchronized關(guān)鍵字,但是這里我們主要看使用同步塊如何解決,下面我們改進Jvm類:
class Jvm1 {
// 1、構(gòu)造器私有化,避免外部直接創(chuàng)建對象
private Jvm1() {
}
// 2、聲明一個私有靜態(tài)變量
private static Jvm1 instance = null;
// 3、創(chuàng)建一個靜態(tài)的公共方法訪問該變量,如果變量沒有對象,創(chuàng)建該對象
public static Jvm1 getInstance1(long time) {
if (instance == null) {
try {
Thread.sleep(time);// 加入延時
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm1();
}
return instance;
}
// 加入同步,我們可以直接在方法上加上synchronized關(guān)鍵字,這里我們使用同步塊,但是效率不高
// 在下面我們進行改進
public static Jvm1 getInstance2(long time) {
synchronized (Jvm.class) {// 這里我們不能使用this了,因為this還沒有創(chuàng)建出來,于是使用字節(jié)碼
if (instance == null) {
try {
Thread.sleep(time);// 加入延時
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm1();
}
return instance;
}
}
// 改進,這里比如有a,b,c三個線程,一開始對象為空,進入第一個if,然后a進入同步塊,其他線程等待
// 當(dāng)a進去之后則對象就被創(chuàng)建了,于是當(dāng)其他線程進入同步塊的時候就不需要像上面那樣等待了,直接返回已有
// 對象
public static Jvm1 getInstance3(long time) {
if (instance == null) {
synchronized (Jvm.class) {
if (instance == null) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm1();
}
}
}
return instance;
}
}
說明:首先我們是對方法getInstance改進成了getInstance2,進入了同步關(guān)鍵字,但是參數(shù)不能再是this了,因為此時對象還沒有創(chuàng)建出來。此時我們進行測試可以發(fā)現(xiàn)達到了同步的效果。但是這種實現(xiàn)的方式可能效率和之前的同步方法的效率一樣,不太高,因為此時不管對象存在不存在都需要在同步塊前面等待,我們改進為getInstance3方法,這樣如果對象存在則不需要進入同步塊中,直接拿到對象即可使用。而單例創(chuàng)建的方式有上面提到的懶漢式,還有其他方式:
package cn.itcast.day178.thread02;
/*單例創(chuàng)建的幾種方式:
* 1、懶漢式
* a、構(gòu)造器私有化
* b、聲明私有的靜態(tài)屬性
* c、對外提供訪問屬性的靜態(tài)方法,確保該對象存在
* */
public class MyJvm03 {
private static MyJvm03 instance;
private MyJvm03(){
}
public static MyJvm03 getInstance(){
if(instance == null){//為了效率
synchronized (MyJvm03.class) {
if(instance == null){//為了安全
instance = new MyJvm03();
}
}
}
return instance;
}
}
/*2、惡漢式
* a、構(gòu)造器私有化
* b、聲明私有的靜態(tài)屬性,同時創(chuàng)建該對象
* c、對外提供訪問屬性的靜態(tài)方法,確保該對象存在
* */
class MyJvm04{
private static MyJvm04 instance = new MyJvm04();
private MyJvm04(){
}
public static MyJvm04 getInstance(){
return instance;
}
}
//惡漢式提高效率的改進:類在使用的時候才讓其加載,這樣只要不調(diào)用
//getInstance方法,那么就不會加載類,這樣延緩了類加載時機
class MyJvm05{
private static class JVMholder{
private static MyJvm05 instance = new MyJvm05();
}
private MyJvm05(){
}
public static MyJvm05 getInstance(){
return JVMholder.instance;
}
}
說明:相對來說,惡漢式的效率較高一點。
五、死鎖
過多的同步容易造成死鎖,就是一份資源同時被多個線程同時調(diào)用。
package cn.itcast.day178.thread02;
//兩個線程使用的是同一份資源,可能就會造成死鎖,但是這并不絕對
//過多的同步容易造成死鎖
public class SynDemo03 {
public static void main(String[] args) {
Object goods = new Object();
Object money = new Object();
Test t1 = new Test(goods, money);
Test1 t2 = new Test1(goods, money);
Thread proxy1 = new Thread(t1);
Thread proxy2 = new Thread(t2);
proxy1.start();
proxy2.start();
}
}
class Test implements Runnable{
Object goods;
Object money;
public Test(Object goods, Object money) {
this.goods = goods;
this.money = money;
}
public void run() {
while(true){
test();
}
}
public void test(){
synchronized (goods) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money) {
}
}
System.out.println("一手給錢");
}
}
class Test1 implements Runnable{
Object goods;
Object money;
public Test1(Object goods, Object money) {
super();
this.goods = goods;
this.money = money;
}
public void run() {
while(true){
test();
}
}
public void test(){
synchronized (money) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (goods) {
}
}
System.out.println("一手交貨");
}
}
說明:此時我們發(fā)現(xiàn)不會的打印出任何內(nèi)容,因為造成了死鎖。解決死鎖的思路就是使用生產(chǎn)者消費者設(shè)計模式。
生產(chǎn)者消費者模式
1)生產(chǎn)者消費者模式也稱有限資源緩沖問題,是一個多線程同步問題的經(jīng)典案例。該問題描述了兩個共享固定大小的緩沖區(qū)的線程-即所謂的生產(chǎn)者合格消費者-在實際運行時會發(fā)生的問題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過程。與此同時,消費者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問題的關(guān)鍵就是要保證生產(chǎn)者不會在緩沖區(qū)滿時加入數(shù)據(jù),消費者也不會在緩沖區(qū)空時消耗數(shù)據(jù)。
2)要解決該問題,就必須讓生產(chǎn)者在緩沖區(qū)滿時休眠(要么干脆就放棄數(shù)據(jù)),等到下次消費者消耗緩沖區(qū)中的數(shù)據(jù)的時候,生產(chǎn)者才能被喚醒,開始往緩沖區(qū)中添加數(shù)據(jù)。同樣,也可以讓消費者在緩沖區(qū)空的時候進入休眠,等到生產(chǎn)者往緩沖區(qū)中添加數(shù)據(jù)之后,再喚醒消費者。通常常用的方法有信號燈法,管程等。如果解決方法不不夠完善,則容易出現(xiàn)死鎖的情況,出現(xiàn)死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。
這里我們介紹信號燈法,首先給出資源:
package cn.itcast.day178.thread02;
/*一個場景,一份共同的資源
* 生產(chǎn)者消費者模式,采用信號燈法
* wait會釋放鎖,而sleep則不釋放鎖
* notify和notifyAll表示喚醒
* 注意:上面說的方法必須和同步在一起使用,不然就使用不了
* */
public class Movie {
private String pic;
//信號燈,當(dāng)為true時表示生產(chǎn)者生產(chǎn),消費者等待,生產(chǎn)完成之后通知消費者消費
//當(dāng)為false的時候,生產(chǎn)者等待,消費者消費,當(dāng)消費完成之后通知生產(chǎn)者生產(chǎn)
private boolean flag = true;
public synchronized void play(String pic){
if(!flag){//生產(chǎn)者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//開始生產(chǎn)
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產(chǎn)了: " + pic);
//生產(chǎn)完畢
this.pic = pic;
//通知消費
this.notify();
//生產(chǎn)者停止
this.flag = false;
}
public synchronized void watch(){
if(flag){
//消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//開始消費
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費了: " + pic);
//消費完畢,通知生產(chǎn)
this.notify();
//消費停止
this.flag = true;
}
}
說明:資源是一個電影,那么生產(chǎn)者就是演員:
package cn.itcast.day178.thread02;
/*表演者,這里就相當(dāng)于生產(chǎn)者 */
public class Player implements Runnable{
private Movie movie;
public Player(Movie movie) {
super();
this.movie = movie;
}
public void run() {
for(int i = 0; i < 20; i++){
if(i % 2 == 0){
movie.play("左青龍");
}else{
movie.play("右白虎");
}
}
}
}
說明:再給出消費者:
package cn.itcast.day178.thread02;
public class Watcher implements Runnable{
private Movie movie;
public Watcher(Movie movie) {
super();
this.movie = movie;
}
public void run() {
for(int i = 0; i < 20; i++){
movie.watch();
}
}
}
說明:下面我們使用:
package cn.itcast.day178.thread02;
public class App {
public static void main(String[] args) {
//共同的資源
Movie m = new Movie();
//多線程
Player p = new Player(m);
Watcher w = new Watcher(m);
new Thread(p).start();
new Thread(w).start();
}
}
說明:我們在使用的時候同時開啟了生產(chǎn)者和消費者線程,在運行過程中如果資源沒有生產(chǎn)出來則消費者線程等待,資源生產(chǎn)出來之后消費者線程執(zhí)行。
六、任務(wù)調(diào)度
- 1)
Timer定時器類 - 2)
TimerTask任務(wù)類 - 3)通過
timer和timertask:(spring的任務(wù)調(diào)度就是通過它們來實現(xiàn)的) - 4)在這種實現(xiàn)方式中,
Timer類實現(xiàn)的是類似鬧鐘的功能,也就是定時或者每個一定時間觸發(fā)一次線程。其實,Timer類本身實現(xiàn)的就是一個線程,只是這個線程是用來實現(xiàn)調(diào)用其他線程的。而TimerTask類是一個抽象類,該類實現(xiàn)了Runnable接口,所以按照前面的介紹,該類具備多線程的能力。 - 5)在這種實現(xiàn)方式中,通過繼承
TimerTask使該類獲得多線程的能力,將需要多線程執(zhí)行的代碼書寫在run方法內(nèi)部,然后通過Timer類啟動線程的執(zhí)行。 - 6)在實際使用時,一個
Timer可以啟動任意多個TimerTask實現(xiàn)的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間如果需要完全獨立運行的話,最好還是一個Timer啟動一個TimerTask實現(xiàn)。
下面看一個例子:
package cn.itcast.day178.thread02;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
* 使用方法schedule指定任務(wù)
* */
public class TimerDemo01 {
public static void main(String[] args) {
Timer timer = new Timer();
//這里第一個參數(shù)表示指定一個任務(wù),第二個參數(shù)表示什么時候開始執(zhí)行,
//第三個參數(shù)表示每隔多少秒執(zhí)行一次,如果沒有第三個參數(shù)則只運行一次
timer.schedule(new TimerTask() {
//線程體
public void run() {
System.out.println("線程體....");
}
}, new Date(System.currentTimeMillis() + 1000), 200);
}
}
最后我們看一下notify和notifyAll的區(qū)別:
這里notify()和notifyAll()都是Object對象用于通知處在等待該對象的線程的方法。
-
void notify():喚醒一個正在等待該對象的線程。 -
void notifyAll():喚醒所有正在等待該對象的線程。
兩者的最大區(qū)別在于:
notifyAll使所有原來在該對象上等待被notify的線程統(tǒng)統(tǒng)退出wait的狀態(tài),變成等待該對象上的鎖,一旦該對象被解鎖,他們就會去競爭。notify他只是選擇一個wait狀態(tài)線程進行通知,并使它獲得該對象上的鎖,但不驚動其他同樣在等待被該對象notify的線程們,當(dāng)?shù)谝粋€線程運行完畢以后釋放對象上的鎖,此時如果該對象沒有再次使用notify語句,即便該對象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于沒有得到該對象的通知,繼續(xù)處在wait狀態(tài),直到這個對象發(fā)出一個notify或notifyAll,它們等待的是被notify或notifyAll,而不是鎖。