死磕Java系列之多线程创建和线程状态

当我们提到单线程的时候,会思考几个问题:
(1)线程是什么?人们通常将线程和进程进行比较,那么,线程和进程有哪些相似和不同呢?
(2)如何创建一个线程呢?创建线程有哪些方法呢?
(3)当我们定义好一个线程以后,那么,线程有哪些状态呢?
这篇文章将会从这三个方面介绍线程的基础知识。

(一)线程是什么? 

       在计算机程序中,一个程序能够执行多个任务,通常将每一个任务称之为线程,可以运行一个以上线程的程序称之为多线程程序。线程和进程的区别在于每个进程有自己的一整套变量,而线程则是资源共享。

1. 什么是进程呢?
        进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程。
        进程 指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,表示资源   分配的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。进程是系统中的并发执行的单位,  比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。 
2. 什么是线程呢? 

      线程是系统中最小的执行单元;同一进程中可以有多个线程;线程共享进程的资源。
       线程是进程中执行运算的最小单位,即执行处理机调度的基本单位。现代操作系统调度的最小单元是线程, 也叫轻量级进程(Light Weight Process) , 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 


      例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。

    Java编写程序都在Java虚拟机(JVM)中运行,在JVM的内部,程序的多任务是通过线程来实现的。每用java命令启动一个java应用程序,就会启动一个JVM进程。在同一个JVM进程中,有且只有一个进程,就是它自己。在这个JVM环境中,所有程序代码的运行都是以线程来运行。

 3.线程和进程的比较

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

        进程中包含有线程,在实际生活中,如果我们将吃饭看成一个进程的话,在整个吃饭的过程中,盛饭,夹菜等行为可以看成是一个线程,线程是计算机系统执行进程中的一个基本单位。日常生活中吃饭时夹菜,当我们开始吃饭的时候大脑给出命令,这段时间用来吃饭,相当于执行吃饭的进程,吃饭过程中有很多任务,盛饭,备筷子,等等,这些活动可以看成吃饭的每一个小任物。线程可以看成执行进程中的打怪升级,这些小任务就相当于执行进程中的线程,线程是进程完成目标的一个个小任物,完成这些小任物就相当于完成进程 。

(二)如何创建一个线程?

  在java中要想实现多线程,有三种手段,

        1)继承Thread类创建线程

        2)实现Runnable接口创建线程

       3)使用Callable和Future创建线程

  • 继承Thread类  

        通过继承Thread类来创建并启动多线程的一般步骤如下

        1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。

       2】创建Thread子类的实例,也就是创建了线程对象

       3】启动线程,即调用线程的start()方法

代码举例如下:

package VideoTest;
/*
 * 定义一个类继承Thread,并且重写run()方法,在方法中加入要实现的程序
 * 然后在另一个类中实例化,并调用Start()方法
 */
 
// ThreadTest.java 源码
//MyThread继承于Thread,它是自定义个线程。每个MyThread都会卖出10张票
class MyThread extends Thread{  
    private int ticket=10;  
    public void run(){
        for(int i=0;i<20;i++){ 
            if(this.ticket>0){
                System.out.println(this.getName()+" 卖票:ticket"+this.ticket--);
            }
        }
    } 
};
//主线程main创建并启动3个MyThread子线程。每个子线程都各自卖出了10张票。
public class Test01 {  
    public static void main(String[] args) {  
        // 启动3个线程t1,t2,t3;每个线程各卖10张票!
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        MyThread t3=new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }  
}

运行结果:

说明:

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用 MyThread的三个对象的start方法, 整个应用就在多线程下运行。

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的,具体的执行会根据线程的优先级和所抢到的时间片来决定。

  • 实现Runnable接口创建线程

通过实现Runnable接口创建并启动线程一般步骤如下:

1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体

2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

3】通过调用线程对象的start()方法来启动线程

package VideoTest;
 
class Thread2 implements Runnable{ //实现Runnable接口 
    private String name;  
  
    public Thread2(String name) {  
        this.name=name;  
    }  
  
    @Override  
    public void run() {  //线程的方法体
          for (int i = 0; i < 5; i++) {  
                System.out.println(name + "运行  :  " + i);  
                try {  
                    Thread.sleep((int) Math.random() * 10); //线程运行停止随机数乘以十秒的时间 
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
          
    }  
      
}  
public class Test01 {  
  
    public static void main(String[] args) {  
    	
        new Thread(new Thread2("C")).start();  //实例化线程类
        new Thread(new Thread2("D")).start();  
    
    }  
  
} 

运行结果如下:

说明:

Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

  •  使用Callable和Future创建线程

      1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable                  对象)。

       2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

        3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

       4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test01 {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }

        System.out.println("主线程for循环执行完毕..");
        
        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

使用Callable和Future创建线程是线程池中的知识点,在线程池中会更加详细的介绍。

  • 三种创建线程方法的比较

Thread 和 Runnable 的相同点:都是“多线程的实现方式”。
Thread 和 Runnable 的不同点
Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
通常,建议通过“Runnable”实现多线程!

无论是哪种方法开辟多线程,都请不要忘记,除了开辟和运行新线程,本身还存在一个正在执行的线程(两个例子中都是Main 线程)

无论是哪种方法,如果要使用新线程执行方法体,都需要使用start 方法来被动的运行 run,而不能直接运行run方法


(三)线程有哪些状态?

     当我们创建一个线程之后,需要考虑线程的状态,即线程的生命周期,线程的生命周期包括线程的创建,准备运行,运行状态。阻塞状态和死亡状态,即线程的创建,准备运行,运行,死亡,在运行的过程中可能会涉及到阻塞的状态。

(1)   线程在出生后(被new出来以后)即为新建状态,此时jvm会为其分配内存、初始化字段,仅此。然后进入就绪状态(执行Start方法)开始排队,紧接着当cpu开始执行(抢占到资源)该线程,线程进入运行状态,接着由于某种原因需要等待(如睡眠,等待需要调用的资源(如被其它资源占用,或者开启需要准备时间))而被迫进入阻塞状态。待等待结束后(睡眠时间到后、等待资源被释放)线程会再次进入就绪状态,等待着cpu的再次垂青。于此同时如果运行状态中的线程由于cpu调度的问题,而失去运行权利后也会被变为就绪状态,等待cpu的再次降临。当运行中的线程执行完线程体中的任务后、或者由于某种异常而挂起后,该线程就会进入死亡状态

  •  新建状态:    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  •   阻塞状态:   如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
  • 死亡状态:线程执行完了、因异常退出了run()方法或者直接调用该线程的stop()方法(容易导致死锁,现在已经不推荐使用),该线程结束生命周期。

(2)一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。  java中各个线程的运行是抢占式的:cpu一般不会为一个线程从出生一直服务到老,各个线程总是争抢的希望得到cpu的“青睐”。当某个线程发生阻塞时,那么cpu就会被其他线程迅速抢占。而当前阻塞的线程只能变为就绪状态,等待cpu下次的“垂青”。这里有句老话挺符合的:机会总是留给有准备的人的(当前处于就绪状态的),如果你还没准备好(阻塞),机会就转瞬即逝了(转向其他线程了)。

阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

名词解释:

       主线程:JVM调用程序main()所产生的线程。

       当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。

      后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。

      前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、                            幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设                           置 一个线程是否为后台线程。

常见问题备注:

(1)线程启动后,并不会立刻运行,具体运行时间要看虚拟机的调度(前文中有讲)

(2)线程一旦死亡就不能再重新启动就绪(使用Start方法),如果强制启动会抛出异常。

(3)想判断当前的线程是否已经被启动并且还未死亡(就绪、运行、阻塞),可以使用isAlive()方法判断

(4)启动线程进入就绪状态请务必使用Start()方法启动,而非Run方法,原因在创建多线程时也曾经讲过。

(5)线程一旦启动,大家都是独立的处理,并不会因为父线程(创建并启动子线程的线程)出现某种状态,而影响到自身(如父线程死亡后,子线程仍然会继续运行)

(6)线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字不确定。

(7)线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。

(8)获取当前线程的对象的方法是:Thread.currentThread();

(9)每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。

(10)当线程目标run()方法结束时该线程完成。

(11)一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。

(12)线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。

众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。

(13)尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。

(14)尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

线程类的一些常用方法: 

  sleep(): 强迫一个线程睡眠N毫秒。 
  isAlive(): 判断一个线程是否存活。 
  join(): 等待线程终止。 
  activeCount(): 程序中活跃的线程数。 
  enumerate(): 枚举程序中的线程。 
    currentThread(): 得到当前线程。 
  isDaemon(): 一个线程是否为守护线程。 
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
  setName(): 为线程设置一个名称。 
  wait(): 强迫一个线程等待。 
  notify(): 通知一个线程继续运行。 
  setPriority(): 设置一个线程的优先级。

猜你喜欢

转载自blog.csdn.net/weixin_41792162/article/details/86349107