多線程
Java基礎的第五篇,也是最後一篇-多線程
1.線程的創建和啓動
通過集成Thread類創建線程類
- 定義Thread類的子類,並重寫run()方法
- 創建Tread子類的實例
- 調用start()方法啓動線程
舉個例子:
public class FirstThread extends Thread {
private int i;
public void run(){
for (;i<100;i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i=0; i<100; i++)
{
System.out.println(Thread.currentThread().getName());
if (i == 20)
{
// 創建並啓動第一個線程
new FirstThread().start();
// 創建並啓動第二個線程
new FirstThread().start();
}
}
}
}
運行結果
可以看到一共有三個線程:main Thread0 Thread1 後面兩個是新建的。main是程序執行後創建的。
實現Runnable接口創建線程類
- 定義Runnable接口的實現類,並重寫run()方法
- 創建Runnable實現類的實例
- 調用start()方法啓動線程
舉個例子:
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for (; i<100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i=0; i<100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread();
new Thread(st,"新線程1").start();
new Thread(st,"新線程2").start();
}
}
}
}
運行結果和FirstThread類似,就不詳細描述了。
這裏有一點區別:FirstThread裏面新建Thread是可以直接調用start()方法,因爲是Tread的子類,但是Runnable裏面只是線程對象的target,不能直接調用runnable.start()
使用Callable和Future創建線程
- 創建Callable接口實現類,並實現call()方法
- 創建Callable實例使用FutureTask包裝
- 使用FutureTask對象作爲Thread對象的target創建並啓動線程
- 調用FutureTask的get()方法獲得返回值
舉個例子:
public class ThirdThread implements Callable<Integer> {
@Override
public Integer call(){
int i=0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
public static void main(String[] args) {
ThirdThread rt = new ThirdThread();
FutureTask<Integer> task = new FutureTask<>(rt);
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20){
new Thread(task,"有返回值的線程").start();
}
}
try {
System.out.println("子線程返回值:" + task.get());
}catch (Exception ex){
ex.printStackTrace();
}
}
}
運行結果和前面的類似,不過最後會輸出call()方法的返回值
說完了Thread,Runnable,Callable三種創建線程的方式,我們來比較一下
採用Runnable、Callable接口的方式:
- 線程只是實現接口,還可以繼承其他類
- 多個線程可以共享一個target對象,適合多個相同線程處理同一份資源的情況
- 劣勢:需要使用Thread.currentThread()方法訪問當前進程
採用Thread的優勢正好是上面兩種方法的劣勢。
2.線程的生命週期
新建和就緒狀態
使用new關鍵字創建對象就處於新建狀態,使用start()方法之後就處於就緒狀態,至於什麼時候開始執行,要看JVM的調度。
運行和阻塞狀態
調用了sleep()方法,調用了一個阻塞式IO方法,等待某個通知…都會讓線程阻塞
相對應的就是運行狀態,這一塊知識點有點像操作系統的CPU輪換。
線程死亡
- run()或call()方法執行完成,線程正常結束
- 線程拋出未捕獲的異常
- 直接調用stop()
這三種情況都會讓線程結束
3.線程同步
線程安全問題
在這裏我們可以用一個經典的問題-銀行取錢問題,來進行講解。
- 用戶輸入賬戶密碼,系統判斷是否正確
- 用戶輸入取款金額
- 系統判斷餘額是否大於取款金額
- 大於則取款成功,小於則取款失敗
首先定義Account類,具有賬戶名和餘額兩個屬性
public class Account {
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance){
this.accountNo = accountNo;
this.balance = balance;
}
public int hashCode(){
return accountNo.hashCode();
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == Account.class){
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
然後定義一個取錢的線程類
public class DrawThread extends Thread{
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
public void run(){
if (account.getBalance() >= drawAmount){
System.out.println("取錢成功:" + drawAmount);
account.setBalance(account.getBalance() - drawAmount);
System.out.println("餘額爲:" + account.getBalance());
}else{
System.out.println(getName() + "取錢失敗,餘額不足");
}
}
}
最後還有主程序:
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("1234567",1000);
new DrawThread("甲",acct,800).start();
new DrawThread("乙",acct,800).start();
}
}
啓動兩個子線程取錢,會出現什麼結果呢?
這種結果明顯是不對的,這就是我們上面所說的線程同步問題。
之所以出現這樣的結果,是因爲run()方法不具有同步安全性,一旦程序併發修改Account對象,就很容易出現這種錯誤結果。
爲了解決這個問題,Java多線程引入了同步監視器。語法如下:
synchronized(obj)
{
// 同步代碼塊
}
我們再修改一下DrawThread的代碼:
public void run(){
// 使用account作爲同步監視器,任何進程進入以下同步代碼塊之前
// 必須先獲得對account賬戶的鎖定- 其他縣城無法獲得鎖,也就無法修改它
// 這種做法符合 加鎖-修改-釋放 的邏輯
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println("取錢成功:" + drawAmount);
account.setBalance(account.getBalance() - drawAmount);
System.out.println("餘額爲:" + account.getBalance());
} else {
System.out.println(getName() + "取錢失敗,餘額不足");
}
}
}
再次運行就能得到正確結果:
同步鎖(Lock)
Java 5開始,Java提供另一個線程同步機制-通過顯示定義同步鎖對象實現同步。
通常使用格式如下:
class x
{
public void m(){
lock.lock(); // 加鎖
try{
// 需要線程安全的代碼
}finally{
lock.unlock();
}
}
}
通過lock和unlock來顯示加鎖,釋放鎖。
除了上面所說的知識點,還有線程池,死鎖,線程通信等,由於這些知識點都屬於高級Java特性,我會在後面的進階篇再進行總結。