這一篇博文是【大數據技術●降龍十八掌】系列文章的其中一篇,點擊查看目錄:大數據技術●降龍十八掌
1、join() 方法
join()方法可以理解爲線程插隊。停止當前線程,先執行插入的線程,當插入的線程執行完畢後,再執行當前線程。看下面的例子:
package join;
/**
* Created by 鳴宇淳 on 2017/12/7.
*/
public class MyJoinRunner implements Runnable {
//子線程
public void run() {
for (int n = 0; n < 100; n++) {
System.out.println(Thread.currentThread().getName() + ":" + n);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyJoin {
public static void main(String[] args) throws InterruptedException {
MyJoinRunner runner=new MyJoinRunner();
Thread t1=new Thread(runner,"子線程");
t1.start();
//將子線程插隊,先執行子線程,然後再執行其他線程(主線程)
t1.join();
for (int n = 0; n < 100; n++) {
System.out.println(Thread.currentThread().getName() + ":" + n);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
輸出結果:
子線程:0
子線程:1
子線程:2
子線程:3
子線程:4
子線程:5
子線程:6
.......
.......
main:0
main:1
main:2
main:3
main:4
main:5
.......
.......
2、一個多線程會出問題的例子
假設有個場景是自動賣票,一共有5張票,新建三個線程來同時賣票,往往就會出問題。如下實例所示。
/**
* Created by 鳴宇淳 on 2017/12/8.
*/
public class SellTicketRunner implements Runnable {
private int ticket = 5; //一共有n張票
//一個子線程執行的方法
public void run() {
while (true) {
if (this.ticket > 0) {
//判斷如果票大於0,就先睡眠,以達到模擬的效果。
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票
this.ticket--;
//打印剩餘票數
System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);
} else {
break;
}
}
}
}
public class MySellTicker {
public static void main(String[] args) throws InterruptedException {
SellTicketRunner runner=new SellTicketRunner();
System.out.println("賣票開始.....");
Thread t1=new Thread(runner,"線程一");
Thread t2=new Thread(runner,"線程二");
Thread t3=new Thread(runner,"線程三");
t1.start();
t2.start();
t3.start();
System.out.println("買票結束!");
}
}
輸出爲:
線程一正在賣票;剩餘=4
線程二正在賣票;剩餘=2
線程三正在賣票;剩餘=2
線程一正在賣票;剩餘=1
線程三正在賣票;剩餘=-1
線程二正在賣票;剩餘=-1
線程一正在賣票;剩餘=-2
買票結束!
會發現,出現票超賣的情況,這是因爲在判斷餘票數量後、賣票操作前的階段,有可能有多個線程進入,然進行賣票操作。這就需要我們使用鎖和同步進行限制。
3、同步和鎖定
(1) 對象鎖
java中每個對象都有一個內置鎖。可以使用任意一個對象上的鎖,來實現線程的同步。看下面的實例,是將上面有問題的賣票代碼,添加上對象鎖,來控制線程同步,以解決超賣的問題。
package sellticket;
/**
* Created by 鳴宇淳 on 2017/12/8.
*/
public class SellTicketRunner implements Runnable {
private int ticket = 5; //一共有n張票
private String lock=""; //創建一個對象,使用這個對象上的鎖來實現線程同步
//一個子線程執行的方法
public void run() {
while (true) {
synchronized (lock) {
//將需要保護的代碼塊,鎖住,防止多個線程同時進入這個代碼塊
if (this.ticket > 0) {
//判斷如果票大於0,就先睡眠,以達到模擬的效果。
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票
this.ticket--;
//打印剩餘票數
System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);
} else {
break;
}
}
}
}
}
public class MySellTicker {
public static void main(String[] args) throws InterruptedException {
SellTicketRunner runner=new SellTicketRunner();
Thread t1=new Thread(runner,"線程一");
Thread t2=new Thread(runner,"線程二");
Thread t3=new Thread(runner,"線程三");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("買票結束!");
}
}
(2) 方法鎖
可以在方法上添加上一個synchronized關鍵字,表明這個一個同步方法,通過鎖定一個方法,同時只讓一個線程執行這個方法,
package sellticket;
/**
* Created by 鳴宇淳 on 2017/12/8.
*/
public class SellTicketRunner2 implements Runnable {
private int ticket = 5; //一共有n張票
public void run() {
while (true) {
boolean isQuit = sell();
if (!isQuit) {
break;
}
}
}
//在方法上添加一個synchronized關鍵字,表名是同步方法
//將需要保護的方法,鎖住,防止多個線程同時進入這個方法
private synchronized boolean sell() {
System.out.println(Thread.currentThread().getName() + "進入");
boolean isHav;
if (this.ticket > 0) {
isHav = true;
//判斷如果票大於0,就先睡眠,以達到模擬的效果。
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票
this.ticket--;
//打印剩餘票數
System.out.println(Thread.currentThread().getName() + "正在賣票;剩餘=" + this.ticket);
} else {
isHav = false;
}
System.out.println(Thread.currentThread().getName() + "退出\n");
return isHav;
}
}
public class MySellTicker {
public static void main(String[] args) throws InterruptedException {
SellTicketRunner2 runner=new SellTicketRunner2();
System.out.println("賣票開始.....");
Thread t1=new Thread(runner,"線程一");
Thread t2=new Thread(runner,"線程二");
Thread t3=new Thread(runner,"線程三");
t1.start();
t2.start();
t3.start();
}
}
當sell方法不加synchronized關鍵字時輸出,看上去售票過程比較亂:
賣票開始.....
線程二進入
線程一進入
線程三進入
線程三正在賣票;剩餘=3
線程一正在賣票;剩餘=3
線程二正在賣票;剩餘=3
線程一退出
線程一進入
線程三退出
線程二退出
線程二進入
線程三進入
線程二正在賣票;剩餘=2
線程二退出
線程二進入
線程三正在賣票;剩餘=2
線程三退出
線程三進入
線程一正在賣票;剩餘=2
線程一退出
線程一進入
線程一正在賣票;剩餘=1
線程一退出
線程一進入
線程一退出
線程三正在賣票;剩餘=-1
線程三退出
線程三進入
線程三退出
線程二正在賣票;剩餘=-1
線程二退出
線程二進入
線程二退出
如果加上synchronized,就能保證同時只有一個線程在執行sell方法,輸出爲:
賣票開始.....
線程一進入
線程一正在賣票;剩餘=4
線程一退出
線程一進入
線程一正在賣票;剩餘=3
線程一退出
線程一進入
線程一正在賣票;剩餘=2
線程一退出
線程一進入
線程一正在賣票;剩餘=1
線程一退出
線程一進入
線程一正在賣票;剩餘=0
線程一退出
線程一進入
線程一退出
線程二進入
線程二退出
線程三進入
線程三退出
(3) static 方法的同步
在非static方法上添加synchronized,相當於對當前類的對象this加鎖。
private synchronized void fun() throws InterruptedException {
Thread.sleep(100);
}
等同於:
private void fun() throws InterruptedException {
synchronized (this) {
Thread.sleep(100);
}
}
但是在static方法是先於類的對象而存在的,當沒有實例化對象的時候,static方法已經可以調用了,那麼在static方法上添加synchronized ,是對什麼加鎖呢?其實是對類的class對象加鎖。
public class MyStaticDemo {
private synchronized static void fun() throws InterruptedException {
Thread.sleep(100);
}
}
等同於:
public class MyStaticDemo {
private static void fun() throws InterruptedException {
synchronized (MyStaticDemo.class) {
Thread.sleep(100);
}
}
}
4.死鎖
一個死鎖的實例:
/**
* Created by 鳴宇淳 on 2017/12/11.
*/
public class DeadLockRunner implements Runnable {
String[] source;
String[] target;
public DeadLockRunner(String[] source,String[] target)
{
this.source=source;
this.target=target;
}
public void run() {
synchronized (source) {
System.out.println(Thread.currentThread().getName() + ":進入source到target拷貝");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (target) {
for (int n = 0; n < source.length; n++) {
target[n] = source[0];
}
}
System.out.println(Thread.currentThread().getName() + ":完成source到target拷貝");
}
}
}
public class DeadLock {
public static void main(String[] args) {
String[] source = new String[]{"a", "b", "c", "d"};
String[] target = new String[]{"1", "2", "3", "4"};
DeadLockRunner runner1 = new DeadLockRunner(source,target);
DeadLockRunner runner2 = new DeadLockRunner(target,source);
Thread t1=new Thread(runner1,"線程1");
Thread t2=new Thread(runner2,"線程2");
t1.start();
t2.start();
}
}
運行時可以發現輸出:
線程2:進入source到target拷貝
線程1:進入source到target拷貝
然後就卡在這裏不繼續運行了,根據輸出結果和分析代碼可得知,線程2是先執行的,線程2先鎖定source後,就sleep了,然後線程1進入後鎖定了target,也sleep了,當線程2醒來後要求target,但是被線程1鎖定了,線程1醒來要求source,但是被線程2鎖定了,兩個線程就造成了死鎖。
避免死鎖的方法:
要確定獲得鎖的順序,然後整個程序要遵守該順序,按相反的順序釋放鎖。
5. 線程等待
必須在同步環境內調用wait()、notify()、notfiyAll()方法,也就是說在在同步代碼塊或者同步方法內才能進行等待或者喚醒。wait()、notify()、notfiyAll()方法是Object的實例方法,所以每個對象上都可以有一個線程列表。
實例1:
package wait;
/**
* Created by 鳴宇淳 on 2017/12/11.
*/
public class MyWaitRunner implements Runnable {
private int total;
public MyWaitRunner(int n) {
this.total = n;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "進入執行");
synchronized (this) {
for (int i = 0; i < total; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//喚醒對象監視器上的單個線程,當前是喚醒線程1
notify();
}
}
System.out.println(Thread.currentThread().getName() + "執行完畢");
}
}
public class MyWait {
public static void main(String[] args) throws InterruptedException {
MyWaitRunner runner1=new MyWaitRunner(100);
Thread t1=new Thread(runner1,"線程1");
t1.start();
synchronized (t1)
{
System.out.println("主線程做一些事情......");
System.out.println("等待子線程完成");
//等待線程1完成
t1.wait();
System.out.println("子線程完成");
}
}
}
實例2:
下面這個例子是模擬一個場景,司機開車帶着乘客去北京遊覽故宮,乘客線程上車後,通知司機開車,然後乘客睡覺,讓司機在到達後叫醒自己,通過這個例子看一下兩個線程同步和通知功能的使用方法。
/**
* Created by 鳴宇淳 on 2017/12/12.
*/
public class Passenger implements Runnable {
//乘客線程方法
public void run() {
synchronized (ToBeiJing.lock) {
System.out.println("[乘客]已經上車");
try {
//通知司機開車
System.out.println("[乘客]通知司機開車");
ToBeiJing.driverThread.start();
System.out.println("[乘客]開始睡覺,到北京叫我");
ToBeiJing.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[乘客]醒了,開始遊覽故宮");
}
}
}
public class Driver implements Runnable {
//司機線程方法
public void run() {
for (int n = 0; n < 5; n++) {
System.out.println("[司機]正在開車");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("[司機]已經到北京了");
synchronized (ToBeiJing.lock) {
System.out.println("[司機]叫醒乘客");
//叫醒乘客
lock.notify();
}
}
}
public class ToBeiJing {
//定義一個鎖,線程依據這個鎖實現同步
public static String lock = "";
//司機線程
public static Thread driverThread;
//乘客線程
public static Thread passengerThread;
public static void main(String[] args) throws InterruptedException {
Driver driver = new Driver();
Passenger passenger = new Passenger();
driverThread = new Thread(driver, "司機線程");
passengerThread = new Thread(passenger, "乘客線程");
//乘客線程啓動
passengerThread.start();
}
}
第三個實例:
這是個經典的生產者消費者例子,有多個生產者和多個消費者同時在生產和消費,有一個倉庫,最大存儲量是固定的,所以在生產和消費的時候要收到倉庫量的限制,就需要到線程的同步和鎖。看以下代碼。
import java.util.ArrayList;
import java.util.List;
/**
* Created by 鳴宇淳 on 2017/12/12.
*/
public class Demo {
public static void main(String[] args) {
//各個線程中,要生產或者消費的個數
Integer[] producerNum = new Integer[]{10, 20, 30, 25, 40, 60};
Integer[] consumerNum = new Integer[]{20, 25, 35, 15, 50};
Godown godown = new Godown();
List<Thread> threads = new ArrayList<Thread>();
//創建生產者線程
for (Integer p : producerNum) {
Thread t = new Thread(new Producer(p, godown));
threads.add(t);
}
//創建消費者線程
for (Integer c : consumerNum) {
Thread t = new Thread(new Consumer(c, godown));
threads.add(t);
}
System.out.println("當前倉庫中數量:" + godown.currNum);
//啓動生產者和消費者線程
for (Thread t : threads) {
t.start();
}
}
}
//倉庫類
public class Godown {
//最大庫存量
public static final int MAX_SIZE = 100;
public int currNum;//當前庫存量
/*
生產方法
*/
public synchronized void produce(int addNum) throws InterruptedException {
while (addNum + currNum > MAX_SIZE) {
System.out.println("要生產" + addNum + ",倉庫空位數爲" + (MAX_SIZE - currNum) + ",暫停生產");
this.wait();
}
//可以生產
currNum = currNum + addNum;
System.out.println("生產了" + addNum + ",當前庫存量爲:" + currNum);
//喚醒
this.notifyAll();
}
public synchronized void consume(int needNum) throws InterruptedException {
while (currNum < needNum) {
System.out.println("要求消費" + needNum + "個,剩餘" + currNum + "個,不能消費,等待生產");
this.wait();
}
currNum = currNum - needNum;
System.out.println("消費了" + needNum + "個,當前庫存量爲:" + currNum);
//喚醒
notifyAll();
}
}
/**
* Created by 鳴宇淳 on 2017/12/12.
* <p>
* 生產者類
*/
public class Producer implements Runnable {
private int addNum;
private Godown godown;
public Producer(int addNum, Godown godown) {
this.addNum = addNum;
this.godown = godown;
}
public void run() {
try {
this.godown.produce(this.addNum);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Created by 鳴宇淳 on 2017/12/12.
* 消費者
*/
public class Consumer implements Runnable {
private int needNum;
private Godown godown;
public Consumer(int needNum, Godown godown) {
this.needNum = needNum;
this.godown = godown;
}
public void run() {
try {
this.godown.consume(needNum);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}