首頁  >  文章  >  Java  >  零基礎了解java線程的實現,線程的常用方法大全

零基礎了解java線程的實現,線程的常用方法大全

php是最好的语言
php是最好的语言原創
2018-07-26 14:42:391266瀏覽

執行緒和行程有什麼差別? 答:一個進程是一個獨立的運作環境,它可以被視為一個程式或一個應用。而執行緒是在行程中執行的一個任務。線程是進程的子集,一個進程可以有很多線程,每個線程並行執行不同的任務。不同的行程使用不同的記憶體空間,而所有的執行緒共享一片相同的記憶體空間。

一、執行緒與進程的差異:

1)執行緒:在進程中負責程式執行的執行單元
執行緒本身依賴程式進行執行
執行緒是程序中的順序控制流,只能使用分配給程式的資源和環境

2) 行程:執行中的程式
一個行程至少包含一個執行緒

3 )單執行緒:程式中只存在一個線程,實際上主方法就是一個主線程

4 )多線程:在一個程式中執行多個任務
目的是更好地使用CPU資源

#二、執行緒的實作

1.繼承Thread類別:在java.lang套件中定義, 繼承Thread類別必須重寫run()方法

class MyThread extends Thread{
    private static int num = 0;    public MyThread(){
        num++;
    }

    @Override    public void run() {
        System.out.println("主动创建的第"+num+"个线程");
    }
}

創建好了自己的線程類別之後,就可以建立線程物件了,然後透過start()方法去啟動線程。請注意,不是呼叫run()方法啟動線程,run方法中只是定義需要執行的任務,如果呼叫run方法,即相當於在主執行緒中執行run方法,跟普通的方法呼叫沒有任何區別,此時並不會建立一個新的執行緒來執行定義的任務。

public class Test {
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
    }
}class MyThread extends Thread{
    private static int num = 0;    public MyThread(){
        num++;
    }
    @Override    public void run() {
        System.out.println("主动创建的第"+num+"个线程");
    }
}

在上面程式碼中,透過呼叫start()方法,就會建立一個新的執行緒了。為了分辨start()方法呼叫和run()方法呼叫的區別,請看下面一個範例:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}class MyThread extends Thread{
    private String name;    public MyThread(String name){        this.name = name;
    }

    @Override    public void run() {
        System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
    }
}

執行結果:
零基礎了解java線程的實現,線程的常用方法大全從輸出結果可以得到以下結論:

1)thread1和thread2的線程ID不同,thread2和主線程ID相同,表示透過run方法呼叫並不會創建新的線程,而是在主線程中直接運行run方法,跟普通的方法呼叫沒有任何區別;

2)雖然thread1的start方法調用在thread2的run方法前面調用,但是先輸出的是thread2的run方法調用的相關信息,說明新線程創建的過程不會阻塞主線程的後續執行。

2.實作Runnable介面
在Java中建立執行緒除了繼承Thread類別之外,還可以透過實作Runnable介面來實現類似的功能。實作Runnable介面必須重寫其run方法。
下面是一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override    public void run() {
        System.out.println("子线程ID:"+Thread.currentThread().getId());
    }
}

Runnable的中文意思是“任務”,顧名思義,透過實作Runnable接口,我們定義了一個子任務,然後將子任務交由Thread去執行。注意,這種方式必須將Runnable作為Thread類別的參數,然後透過Thread的start方法來建立一個新執行緒來執行該子任務。如果呼叫Runnable的run方法的話,是不會建立新執行緒的,這根普通的方法呼叫沒有任何差別。

事實上,檢視Thread類別的實作原始碼會發現Thread類別是實作了Runnable介面的。

在Java中,這2種方式都可以用來建立執行緒去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類別的話,可能比實作Runnable介面看起來更簡潔,但是由於Java只允許單繼承,所以如果自訂類別需要繼承其他類,則只能選擇實作Runnable介面。

3.使用ExecutorService、Callable、Future實作有回傳結果的多執行緒

##多執行緒後續會學到,這裡暫時先知道有這種方法即可。

ExecutorService、Callable、Future這個物件其實都是屬於Executor框架中的功能類別。想詳細了解Executor框架的可以訪問http://www.javaeye.com/topic/366591 ,這裡面對該框架做了很詳細的解釋。回傳結果的線程是在JDK1.5中引入的新特徵,確實很實用,有了這種特徵我就不需要再為了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。

可傳回值的任務必須實作Callable接口,類似的,無回傳值的任務必須Runnable介面。執行Callable任務後,可以取得一個Future的對象,在該對像上調用get就可以獲取到Callable任務返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。下面提供了一個完整的有回傳結果的多執行緒測試例子,在JDK1.5下驗證過沒問題可以直接使用。程式碼如下:

/**
* 有返回值的线程 
*/ @SuppressWarnings("unchecked")  
public class Test {  public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序开始运行----");  
   Date date1 = new Date();  

   int taskSize = 5;  
   // 创建一个线程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 创建多个有返回值的任务  
   List list = new ArrayList();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 执行任务并获取Future对象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 关闭线程池  
   pool.shutdown();  

   // 获取所有并发任务的运行结果  
   for (Future f : list) {  
    // 从Future对象上获取任务的返回值,并输出到控制台  
    System.out.println(">>>" + f.get().toString());  
   }  

   Date date2 = new Date();  
   System.out.println("----程序结束运行----,程序运行时间【" 
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  

class MyCallable implements Callable {  
private String taskNum;  

MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  

public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任务启动");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任务终止");  
   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
}
}

程式碼說明:

上述程式碼中Executors類,提供了一系列工廠方法用於創先線程池,返回的線程池都實作了ExecutorService介面。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目執行緒的執行緒池。

public static ExecutorService newCachedThreadPool()
建立一個可快取的執行緒池,呼叫execute 將重複使用先前建構的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒未使用的執行緒。

public static ExecutorService newSingleThreadExecutor()
建立一個單執行緒化的Executor。

public static Sc​​heduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用於替代Timer類別。

ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,回傳Future。如果Executor後台執行緒池還沒有完成Callable的計算,這呼叫傳回Future物件的get()方法,會阻塞直到計算完成。

三、執行緒的狀態

在正式學習Thread類別中的具體方法之前,我們先來了解一下執行緒有哪些狀態,這個將會有助於後面對Thread類別中的方法的理解。

1)建立(new)狀態: 準備好了一個多執行緒的物件
2)就緒(runnable)狀態: 呼叫了start()方法, 等待CPU進行調度
3)運行(running)狀態: 執行run()方法
4)阻塞(blocked)狀態: 暫時停止執行, 可能將資源交給其它執行緒使用
5)終止(dead)狀態: 執行緒銷毀
當需要新起一個執行緒來執行某個子任務時,就建立了一個執行緒。但是在執行緒建立之後,不會立即進入就緒狀態,因為執行緒的運作需要一些條件(例如記憶體資源,在前面的JVM記憶體區域劃分一篇部落格文章中知道程式計數器、Java棧、本機方法棧都是執行緒私有的,所以需要為執行緒分配一定的記憶體空間),只有執行緒運行所需的所有條件都滿足了,才進入就緒狀態。

當執行緒進入就緒狀態後,不代表立刻就能取得CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入運作狀態。

執行緒在運行狀態過程中,可能有多個原因導致當前執行緒不繼續運行下去,例如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步區塊給予阻塞,此時就對應多個狀態:time waiting(睡眠或等待一定的事件)、waiting(等待被喚醒)、blocked(阻塞)。

當因為突然中斷或子任務執行完畢,執行緒就會被消亡。

下面這副圖描述了線程從創建到消亡之間的狀態:
零基礎了解java線程的實現,線程的常用方法大全在有些教程上將blocked、waiting、time waiting統稱為阻塞狀態,這個也是可以的,只不過這裡我想將執行緒的狀態和Java中的方法呼叫連結起來,所以將waiting和time waiting兩個狀態分開。

註:sleep和wait的區別:

sleep是Thread類別的方法,wait是Object類別中定義的方法.
Thread.sleep不會導致鎖定行為的改變, 如果當前線程是擁有鎖的, 那麼Thread.sleep不會讓線程釋放鎖.
Thread.sleep和Object.wait都會暫停當前的線程. OS會將執行時間分配給其它線程. 區別是, 呼叫wait後, 需要別的線程執行notify/notifyAll才能夠重新獲得CPU執行時間.
上下文切換

對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉去運行另外一個線程,這個叫做線程上下文切換(對於進程也是類似)。

由於可能目前執行緒的任務並沒有執行完畢,所以在切換時需要保存執行緒的運行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態運行。舉個簡單的例子:例如一個線程A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停線程A,轉去執行線程B,當再次切換回來執行線程A的時候,我們不希望線程A又從檔案的開頭來讀取。

因此需要記錄執行緒A的運作狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前線程已經執行到哪條指令了,所以需要記錄程序計數器的值,另外比如說線程正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。

說簡單點的:對於執行緒的上下文切換其實就是 儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行。

虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

四、线程的常用方法

零基礎了解java線程的實現,線程的常用方法大全零基礎了解java線程的實現,線程的常用方法大全1.静态方法

currentThread()方法

currentThread()方法可以返回代码段正在被哪个线程调用的信息。

public class Run1{    public static void main(String[] args){                 
    System.out.println(Thread.currentThread().getName());
    }
}

2.sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

sleep方法有两个重载版本:

sleep(long millis)     //参数为毫秒
sleep(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:

public class Test {    private int i = 10;    private Object object = new Object();    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 

    class MyThread extends Thread{
        @Override        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);                try {
                    System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {                    // TODO: handle exception
                }
                System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}

零基礎了解java線程的實現,線程的常用方法大全从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

3.yield()方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

public class MyThread  extends Thread{
    @Override
    public void run() {        long beginTime=System.currentTimeMillis();        int count=0;        for (int i=0;i<50000000;i++){
            count=count+(i+1);            //Thread.yield();
        }        long endTime=System.currentTimeMillis();
        System.out.println("用时:"+(endTime-beginTime)+" 毫秒!");
    }
}public class Run {
    public static void main(String[] args) {
        MyThread t= new MyThread();
        t.start();
    }
}

执行结果:
用时:3 毫秒!

如果将 //Thread.yield();的注释去掉,执行结果如下:
用时:16080 毫秒!

对象方法

4.start()方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

5.run()方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

5.getId()
getId()的作用是取得线程的唯一标识
代码:

public class Test {    public static void main(String[] args) {
        Thread t= Thread.currentThread();
        System.out.println(t.getName()+" "+t.getId());
    }
}

输出:
main 1

6.isAlive()方法
方法isAlive()的功能是判断当前线程是否处于活动状态
代码:

public class MyThread  extends Thread{
    @Override
    public void run() {
        System.out.println("run="+this.isAlive());
    }
}public class RunTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        System.out.println("end =="+myThread.isAlive());
    }
}

程序运行结果:

begin ==falserun=trueend ==false

方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
有个需要注意的地方
System.out.println(“end ==”+myThread.isAlive());

虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        Thread.sleep(1000);
        System.out.println("end =="+myThread.isAlive());
    }

则上述代码运行的结果输出为false,因为mythread对象已经在1秒之内执行完毕。

7.join()方法
在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

public class Thread4 extends Thread{
    public Thread4(String name) {        super(name);
    }    public void run() {        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "  " + i);
        }
    }    public static void main(String[] args) throws InterruptedException {        // 启动子进程
        new Thread4("new thread").start();        for (int i = 0; i < 10; i++) {            if (i == 5) {
                Thread4 th = new Thread4("joined thread");
                th.start();
                th.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

执行结果:

main  0main  1main  2main  3main  4new thread  0new thread  1new thread  2new thread  3new thread  4joined thread  0joined thread  1joined thread  2joined thread  3joined thread  4main  5main  6main  7main  8main  9

由上可以看出main主线程等待joined thread线程先执行完了才结束的。如果把th.join()这行注释掉,运行结果如下:

main  0main  1main  2main  3main  4main  5main  6main  7main  8main  9new thread  0new thread  1new thread  2new thread  3new thread  4joined thread  0joined thread  1joined thread  2joined thread  3joined thread  4

8.其他方法
getName和setName
用来得到或者设置线程名称。

getPriority和setPriority
用来获取和设置线程优先级。

setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:
零基礎了解java線程的實現,線程的常用方法大全

五、停止线程

停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。
停止一个线程可以使用Thread.stop()方法,但最好不用它。该方法是不安全的,已被弃用。
在Java中有以下3种方法可以终止正在运行的线程:

1.使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
2.使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
3.暂停线程

interrupt()方法

六、线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {            throw new IllegalArgumentException();
        }        if((g = getThreadGroup()) != null) {            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:

public final static int MIN_PRIORITY = 1;public final static int NORM_PRIORITY = 5;public final static int MAX_PRIORITY = 10;

1.线程优先级特性:

1)继承性
比如A线程启动B线程,则B线程的优先级与A是一样的。
2)规则性
高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
3)随机性
优先级较高的线程不一定每一次都先执行完。

七、守护线程

在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

八、同步与死锁

1.同步代码块
在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块
同步代码块格式

synchronized(同步物件){
需要同步的程式碼區塊;
}
2.同步方法
除了程式碼區塊可以同步,方法也是可以同步的
方法同步格式
1
synchronized void 方法名稱(){}

##相關文章:


Java -- 多執行緒

##Java 實例- 執行緒掛起

相關影片:

Java多執行緒與並發庫高階應用程式影片教學

#

以上是零基礎了解java線程的實現,線程的常用方法大全的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn