java多執行緒(1):執行緒的建立和多執行緒的安全問題
前言
java多執行緒多用於服務端的高併發程式設計,本文就java執行緒的建立和多執行緒安全問題進行討論。
正文
一,建立java執行緒
建立java執行緒有2種方式,一種是繼承自Thread類,另一種是實現Runnable介面。由於java只支援單繼承,所以很多時候繼承也是一種很寶貴的資源,我們多采用繼承Runnable介面的方式。下面來看一下這兩種方式。
1,繼承Thread,其中包括關鍵的4步
package com.jimmy.basic;
class MyThread extends Thread{ // 1,繼承Thread
public void run() { // 2,重寫run()方法
for (int i = 0; i < 10; i++)
{
System.out.println(Thread.currentThread().getName());
}
}
}
public class ExtendsThread {
public static void main(String[] args) {
MyThread myThread1 = new MyThread(); // 3,建立執行緒例項
MyThread myThread2 = new MyThread();
myThread2.start(); // 4,start()方法啟動執行緒
myThread1.start();
}
}
多執行緒執行的程式碼都寫在run()方法體裡面。上面程式碼run方法中表示迴圈輸出10次執行緒的名字。測試程式碼中建立2個執行緒並啟動,那麼這兩個執行緒交替執行各自run方法中的程式碼,共產生20條輸出記錄。上面這段程式碼的輸出如下:
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread -0
Thread-1
Thread-1
Thread-1
2,實現Runnable介面
package com.jimmy.basic;
class MyThread2 implements Runnable{ // 1,類實現Runnable介面
@Override
public void run() { // 2,實現run方法
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
public class ImplementsRunnable {
public static void main(String[] args) {
MyThread2 mt = new MyThread2(); // 3,例項化介面Runnable子類物件
Thread thread1 = new Thread(mt);// 4,將Runnable子類物件傳遞給Thread類的建構函式
Thread thread2 = new Thread(mt);
thread1.start();// 5,開啟執行緒
thread2.start();
}
}
實現介面是我們推薦的建立執行緒的方法。Runnable介面中只有一個run方法,我們在建立Thread執行緒物件時,將實現了Runnable介面的子類物件傳遞給Thread的建構函式:Thread(Runnable target)。此時再使用start()方法開啟執行緒時,就會執行Runnable介面的子類中的run方法。我們看下Thread的原始碼
//Thread類的部分原始碼
class Thread implements Runnable {
private Runnable target; // 持有Runnable型別變數
public Thread(Runnable target) { // 建構函式,構造過程藉助於init函式
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
//這裡略去了函式其他傳來的引數的操作
this.target = target;
}
public synchronized void start() {
//這裡略去了其他初始化步驟
run();
}
@Override
public void run() {
if (target != null) { // 如果有Runnable介面子類物件傳進來,就執行其run方法。不然什麼都不做。
target.run();
}
}
}
截取了Thread類原始碼中的一部分。可以看出,Thread類中持有一個Runnable介面型別變數,並提供該介面變數的建構函式,雖然其構造過程放到了init()方法中了,一樣的。重點是Thread類的run方法會判斷建立執行緒的時候是否傳入了Runnable子類物件,如果有,就執行Runnable子類物件的run()方法。
所以Runnable介面在建立執行緒時,跟前面直接繼承Thread類不同。要先例項化Runnable子類物件,然後在建立Thread類時,將其作為引數傳遞給Thread類的建構函式。其執行結果跟前面類似,20條記錄交替執行。
二,多執行緒的安全問題
我們看到,前面的程式碼中,每個執行緒在各自的棧記憶體中交替執行,互不影響。之所以互不影響,是因為run方法中程式碼沒有操作執行緒共享的變數。一旦各個執行緒都要操作共享變數,那麼就可能會出現執行緒安全問題。下面來看一個小例子,這個例子中4個執行緒操作同一個共享變數。我們來看一下會出現什麼問題,以及怎麼解決。
package com.jimmy.basic;
class SellTickets implements Runnable {
private int tickets = 10; // 共享變數
@Override
public void run() { // 實現run方法
sell(); // 呼叫sell方法
}
public void sell(){
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "..." + tickets);
tickets--;
}
}
}
public class TicketsSharedVariable {
public static void main(String[] args) {
SellTickets sellTickets = new SellTickets(); // 例項化介面物件
Thread thread1 = new Thread(sellTickets); // 只有傳入Runnable子類物件的Thread才能共享變數
Thread thread2 = new Thread(sellTickets);
Thread thread3 = new Thread(sellTickets);
Thread thread4 = new Thread(sellTickets);
thread4.start(); // 啟動執行緒
thread3.start();
thread2.start();
thread1.start();
}
}
注意,tickets變數定義在Runnable介面子類中,並不是我們說它是共享變數,它就是共享變數。而是將Runnable子類物件傳遞給Thread的建構函式,傳遞後的執行緒才能共享這個tickets變數。
像下面這樣就不會是共享變數,而是各個執行緒的私有變數。
Thread thread1 = new SellTickets2(); // 不傳參而建立的執行緒,每一個都有自己的變數
Thread thread2 = new SellTickets2();
Thread thread3 = new SellTickets2();
Thread thread4 = new SellTickets2();
當執行緒操作共享變數時,問題就出現了。下面是上面程式碼的輸出。
Thread-3...10
Thread-0...10
Thread-2...10
Thread-1...10
Thread-2...7
Thread-0...8
Thread-3...9
Thread-0...4
Thread-2...5
Thread-1...6
Thread-2...1
Thread-0...2
Thread-3...3
從輸出上來看,很明顯出現了執行緒安全的問題,這樣的操作顯然是不正確的。究其原因,是各個執行緒在進行sell方法操作時,搶佔了執行順序。我們希望一個執行緒在操作變數的時候,不會被其他執行緒干擾。也就是說,如果一個執行緒在執行sell方法的時候具有原子性,也就是不能有其他執行緒再來執行sell方法。
java保證操作的原子性很簡單,就是synchronized關鍵字。該關鍵字既可以用來修飾程式碼塊,也可以用來修飾函式。synchronized可以理解為加鎖,為程式碼塊加鎖,為函式加鎖。既然是加鎖,那麼鎖怎麼來表示呢?“鎖”也是物件,在程式碼塊上使用要顯示加鎖,如下:
Object obj = new Object();
public void sell(){
synchronized (obj) { // 鎖物件可以是任意物件
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "..." + tickets);
tickets--;
}
}
}
}
上面就是同步程式碼塊的使用,將需要同步的程式碼放進同步程式碼塊,就可以實現執行緒同步。既然是對需要同步的程式碼進行封裝,就可以將synchronized用在函式上,用法如下:
public synchronized void sell() { // synchronized修飾函式,使用的是this鎖物件。
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "..." + tickets);
tickets--;
}
}
}
一般都會使用函式來封裝同步程式碼,再用synchronized來修飾函式,實現執行緒同步。注:static靜態函式使用的是“類名.class”鎖物件。
最後說一下同步程式碼塊和同步函式的區別。函式使用固定的“this”鎖,而程式碼塊的鎖物件可以任意,如果執行緒任務只需要一個同步時可用同步函式,如果需要多個同步時,必須使用不同的鎖來區分。
總結
執行緒的安全需要同步來實現。