前言

今天的学习内容是多线程编程的基础知识,包括对线程和进程,并发和并行,多线程和多进程的基本理解,创建线程的三种方式的原理和区别,线程的生命周期,线程的属性:守护线程,线程优先级,线程的三个方法:sleep,yield,join,线程池的使用。

一.什么是线程?

1.1 进程和线程

线程是建立在进程的基础上的,因此要了解线程首先需要知道进程。

进程是指一个运行中的程序,是程序运行的实例,是正在执行的一串指令,也就是说,程序并不是直接被运行的,程序只是一些指令,数据的集合,真正在运行的时候,是被操作系统加载进入内存,以进程的形式在计算机中被CPU执行,一个程序通常会产生多个进程,可以由多个用户同时使用。每个进程都需要占用一定的系统资源,比如CPU资源,内存资源等等,操作系统会给每一个进程分配一定的内存空间,并且分配一个进程id(PID),每个进程独自占有这些资源,不能共享。因此进程是资源分配的基本单位

线程是属于进程的,线程一串正在被执行的指令,一个程序的进程往往要同时完成不同的任务,而进程就是这些一个个的任务,不同的线程共享父进程的资源,但是为了不相互干扰,每个线程也有自己私有的资源,比如独立的堆栈,程序计数器和局部变量,一个进程至少拥有一个线程(一个程序启动当然需要执行一定的指令),通常被系统启动的线程称为主线程,一个线程可以被启动,挂起,停止等等操作,因此线程是运行和调度的基本单位

1.2 并发和并行

  • 一个CPU在同一时间只能执行一个线程,因此,只有一个CPU核心时,只能是把时间“分片”,轮流执行不同的线程,从而达到同时进行的效果,实际在时间上是”不同步“的,这就是"并发"
  • 并行是多个处理器同时执行多条线程,也就是把多个线程分配给不同的处理器同时执行,在时间上是真正“同时”的

多线程和多进程都可以实现并发,但是进程拥有自己的独立资源,有独立的内存空间,多个进程间不能共享数据。

而线程虽然也有自己独立的堆栈,程序计数器和局部变量,但是多个线程共享父进程的共享变量和部分环境,因此使用多线程进行并发编程更加方便,但是也因为存在共享的资源,尤其是数据,并发时因为实际上多个线程在时间上是不同步的,因此也更容易出现问题,这就是线程安全问题

1.3 上下文切换(Context Switch)

上下文切换也就是环境切换。包括进程的上下文切换和线程的上下文切换。

CPU从一个线程切换到另外一个线程执行,需要先保存当前的线程的状态和数据,然后载入另外一个进程的状态和数据进行执行,这个过程就是上下文切换,需要消耗一定的系统资源,这便是多线程带来的额外开销。

二.创建线程的三种方式

2.1 概述

线程在Java中的抽线是Thread类,启动线程其实只需要调用Thread类对象的start方法。如下:

  Thread thread = new Thread();
  thread.start();

上面的代码已经启动了一个线程,但是因为没有给这个线程添加执行体代码,因此这个线程一开始执行就结束了。其实我打开看了一下源码,start方法其实只是做了一些检查和准备工作,而且是被当前线程执行的,新启动的线程实际执行的是Thread对象的run方法。而里面的run方法是这样的:

/**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */   
public void run() {
        if (target != null) {
            target.run();
        }
}

从注释和源代码可以看出来,如果我们像上面那样启动线程,那么它会先看看target是否存在,如果存在,那么执行target的run方法,如果为null,那么啥也不做。而且,注释提示,子类应该重写run方法。看一下target变量:

private Runnable target;
可以看到target其实是一个实现了Runnable接口的类的对象(并且其实这个target是通过Thread的有参构造方法传进来的)

因此,为线程添加执行体代码有两种基础的方式:

  • 实现Runnable接口
  • 继承Thread类

后面为了能够接收线程执行的返回值,并且能够声明抛出异常,所以又多弄了个FutureTask和Callable接口来添加执行体代码。

2.2 继承Thread类

这个感觉不用说了,把你想执行的代码写到重写的run方法中,然后用这个子类的对象来启动线程就行了

//继承Thread类
public class MyThread extend Thread{
     @Override
     public void run(){
          do something...
     }
}

然后:

MyThread myThread = new Thread();
myThread.start();

2.3 实现Runnable接口

知道了上面的原理之后,贼简单:

//实现Runnable接口
public class MyRunnableClass implements Runnable{
     public void run(){
          do something...
     }
}

然后:

//把Runnable对象传进去 
Thread thread = new Thread(new MyRunnableClass());
thread.start();

完事了。

2.4 实现Callable接口

Callable接口其实也是从Runnable接口发展来的,底层还是Runnable接口,因为Callable接口中的call方法是被FutureTask的run方法调的,而FutureTask其实RunnableFuture的实现类,而RunnableFuture又是继承自Runnable接口的。

说白了,FutureTask的对象其实也就是target对象,只是它在run方法中调用了Callable接口实现类的对象中的call方法,所以最终执行的是call方法,但是为什么要加一个FutureTask对象做中介?目的就是在中介FutureTask的run方法中实现接收返回值,接收call方法抛出的异常等等扩展的功能,也就是说,Callable通过FutureTask的辅助,实现了Runnable接口的增强。

也正因为有了一层中介,Callable的对象要使用FutureTask再包装一层再传给Thread对象。

因为时间有限,我就不自己写示例代码了,直接摘了菜鸟教程的一段代码,需要注意的是,call方法重写时的返回值类型,需要通过泛型传递给FutureTask对象,比如这段代码中的Integer类型返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

2.5 三种方法的比较

  • 直接继承Thread类的方式编写时最简单,但是因为是继承方式,不可再继承其他类。
  • 实现Runnable接口或者Callable接口的方式相对复杂,但是还可以继承其他类。

三.线程的生命周期

3.1 概述

线程并不是一创建就开始执行,也不是一直在执行。线程在生命周期的不同时期具有不同状态。

3.2 新建

新建状态就是刚刚创建(刚刚被new出来),还没有被执行,也还没有”等待执行“。

3.3 就绪

线程创建之后,调用start方法,就进入”等待执行“的状态了,就是”就绪“,也就是可以执行了,但是还不是开始执行。什么时候才开始,取决于JVM线程调度器的调度,相当于开始"排队"等待被执行了。

3.4 运行

也就是线程中的代码被执行的过程,但是这个状态不是一直保持,会有中断。

3.5 阻塞

阻塞也就是运行状态中断了,停止执行,并且释放出所占有的资源,进入的等待状态,其实不会被执行,也不是“等待执行”,它需要再次进入就绪状态,才能被接着执行。

线程可以主动进入阻塞状态,比如调用sleep方法,也可以被动进入,比如由于阻塞式I/O的方法没有返回,该线程被阻塞。

3.6 死亡

顾名思义,就是线程结束了。包括:

  • 执行体代码执行完成,正常结束
  • 抛出异常,异常结束
  • 调用stop方法停止运行

死亡的线程无法再次启动。

四.线程的属性

4.1 概述

线程拥有一些属性,包括:守护线程,线程优先级,线程组和处理未捕获异常的处理器。用来指示JVM如何对待这个线程。

4.2 守护线程(daemon属性)

守护线程其实就是后台线程,也叫“精灵线程”,把一个线程设置为守护线程是为了让该线程为其他线程服务,并且不要阻碍JVM的关闭

普通线程在停止前JVM不会自动关闭,而普通线程全部死亡时,JVM就会自动关闭,而不会理会是否还有守护线程在运行。

具体的设置方法为:setDaemon(boolean isDaemon);

注意:这个方法必须在start方法之前调用,否则会报错IllegalThreadStateException

4.3 线程优先级(priority属性)

线程优先级越高,被执行的机会越多。

线程优先级的范围是1·10,Thread类中有三个静态常量:

  • MAX_PRIORITY = 10
  • MIN_PRIORITY =1
  • NORM_PRIORITY = 5

PS:后面两个属性太啰嗦了,不想写了,用的时候再说

五.线程的方法

5.1 概述

Thread类提供了一些方法来方便我们对类进行控制。

5.2 sleep方法

让类停止运行一段时间(进入阻塞状态),参数为停止的毫秒数

5.3 yield方法

这个方法是用来线程让步,不会阻塞该线程,会进入就绪状态让JVM线程调度器重新调度,也就是让优先级高的线程执行,如果该线程刚好就是优先级最高的,那么继续执行。

5.4 join方法

让当前线程等待被调用了jion方法的线程执行完毕,再继续执行。

六.使用线程池

关于线程池的知识,这篇博客:深入理解 Java 线程池:ThreadPoolExecutor 将得很明白了,我还是等做了项目实践之后,再来写心得吧,这一块暂且留空。

七.总结

之前训练营和考核时期,没有系统地学过多线程编程,基本都是遇到问题再去查,现查现用,今天比较完整地学习了多线程的基础知识,还专门去看了源码一探究竟,感觉对线程的理解比以前更加深刻了,但是多线程的知识远远不止这些,接下来的打算是明天学习线程安全和同步机制的知识,后天去学更深入的同步器和一些Java并发集合类,多线程的实践任务已经发下来了,主要还是要多在项目中实践。

小结今日收获:

  • 进程是资源分配的基本单位,线程是执行和调度的基本单位
  • 并发是轮流执行,并行是真正的同步执行
  • 多线程和多进程都可以实现并发,但是多线程更容易编程,线程切换开销小,弊端是存在线程安全问题
  • 启动线程的实质都是执行run方法中的执行体
  • 线程的生命周期包括:创建,就绪,运行,阻塞,死亡
  • JVM的关闭不会理会守护线程的状态
  • 三个线程方法:sleep,yield,join

参考资料

Last modification:March 7th, 2020 at 10:30 am
本文作者黄钰朝
文章标题Java多线程编程入门笔记
本文链接https://hellochaos.cn/archives/2020/03/15.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
如果觉得我的文章对你有用,请随意赞赏