Java 多线程(一、线程概念、创建和启动)

283 阅读5分钟

一、线程的概念

进程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的内存空间,即进程空间或虚空间。进程不依赖线程而独立存在,一个进程中可以启动多个线程。进程是系统进行资源分配和调度的基本单位。

线程

线程是指进程中一个执行流程,一个进程可以有多个线程。一个线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程中其他线程共享分配给该进程的所有资源。

线程是CPU调度和分配的基本单位,它是比进程更小的能够独立运行的基本单位。线程有自己的堆栈、程序计数器和局部变量,多个线程共享内存、文件句柄和其他每个进程应有度状态。

在Java中,每次程序运行至少启动两个线程:主线程和垃圾回收线程

二、线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程

流程图

image.png

状态说明

  • 新建状态

    使用new关键字和Thread类或者其子类建立一个线程对象后,该线程对象就处于 新建状态。它将保持这个状态直到程序start()这个线程。

  • 就绪状态

    当线程对象调用类start()方法之后,该线程就进入了就绪状态。就绪状态的线程处于就绪队列中,等待JVM线程调度器的调度。

  • 运行状态

    如果就绪状态的线程获取到CPU资源,就可以执行run()方法,此时线程就处于运行状态。处于运行的状态的线程最复杂,它可以变成阻塞状态、死亡状态和就绪状态。

  • 阻塞状态

    如果一个线程执行了sleep(睡眠)suspend(挂起)等方法,失去了所占有的资源后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或者获得设备资源后可以重新进入就绪状态。 可以分为三种:

    • 等待阻塞:运行状态的线程调用wait()方法,使线程进入等待阻塞状态;
    • 同步阻塞:线程获取同步锁失败(因为同步锁被其他线程占用);
    • 其他阻塞:通过调用线程的sleep()join()发出I/O请求时,线程就会进入阻塞状态。当sleep()状态超时、join()等待看出终止或超时,或者I/O处理完毕,线程重新转入就绪状态
  • 死亡状态

    一个运行状态的线程完成任务或者其他终止条件发生时,线程就会进入死亡状态。

三、线程的创建和启动

Java提供三种创建线程的方式

通过继承Thread类本身

Thread类API说明

代码示例

public class MyThread extends Thread {

    @Override
    public void run() {
        super.run();
        //doSomething
        System.out.println(getName());
    }
}
    
    public static void main(String[] args) {
        //合适的地方启动线程
        new MyThread().start();
    }

说明

Thread类的实例代表着一个线程。启动线程的唯一的方法就是通过Threadstart()start是个native方法,它将启动一个线程,并执行Threadrun方法。

通过实现Runnable接口

代码示例

public class MyThread implements Runnable{

    @Override
    public void run() {
        //doSomething
        System.out.println("run");
    }
}
    public static void main(String[] args) {
        //合适的地方启动线程
        new Thread(new MyThread()).start();
    }

说明

这个方式和上面方式本质上是一样的。Thread本身也是一个实现了Runnable接口的类,区别仅是使用场景的不同。我们看一下Thread类的run方法默认实现:

public
class Thread implements Runnable {
    
    /* What will be run. */
    private Runnable target;
    //省略其他代码
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

} 

上面的target即是我们传入的Runnable实现类示例,执行Threadrun方法即执行传入的Runnable对象的run方法。实际上的线程对象仍然是Thread实例。

通过Callable和Future创建线程

代码示例

public class MyCallable implements Callable<Boolean> {

    private final Object mValue;

    public MyCallable(Object value) {
        this.mValue = value;
    }

    @Override
    public Boolean call() throws Exception {
        boolean isString = mValue instanceof String;
        if (isString){
            Thread.sleep(1000);
        }
        System.out.println("call方法执行结束,value"+mValue);
        return isString;
    }
}
       //合适到地方启动线程
       FutureTask<Boolean> f1 = new FutureTask<>(new MyCallable("1"));
        new Thread(f1).start();
        FutureTask<Boolean> f2 = new FutureTask<>(new MyCallable(2));
        new Thread(f2).start();
        System.out.println(f1.get());
        System.out.println(f2.get());

打印结果:

call方法执行结束,value2
call方法执行结束,value1
true
false

说明

  • FutureTask类实现了Runnable接口,所以最终还是通过Thread来创建和启动线程,FutureTask还是作为target被传入Thread实例中。

  • FutureTask内部包装了Callable实例,执行run方法时会执行Callablecall方法,并将返回值保存到FutureTask实例的成员变量中。

  • FutureTaskget()方法调用,返回Callablecall方法的返回值,调用该返回将会导致线程(方法调用的线程)阻塞,必须等到子线程结束后才会得到返回值。

  • FutureTaskget(long timeout, TimeUnit unit)方法调用,返回Callablecall方法的返回值。该方法调用最多阻塞线程timeoutunit指定的时长;如果超过指定时长call方法还没有执行完成,则会抛出TimeoutException异常。

其他FutrueTask类方法说明可查看:FutrueTask类API文档

总结

  1. 虽然从形式上有三种创建线程的方式,但是本质上最后还是要通过Thread类来创建线程,三种方法的区别仅上run() 方法内实际执行线程逻辑的执行体的区别;不管使用哪种方式,最终均需要调用Therad类的start()方法,使线程进入就绪状态。

  2. 实际开发中我们一般使用实现Runnable接口的方式(或者方式三)来实现多线程编程。有如下好处:(1).避免继承的局限;(2).接口实现便于资源共享(不是说方式一不能实现资源共享,仅仅是接口实现方便一点,可以通过不同的线程对象传入同一的Runnalbe接口实例的方式共享资源)。

参考

Java线程详解

Java 多线程编程