五分钟带你入门线程

在这里插入图片描述

先欣赏个美景~~

前言

我们在上一篇【学习并发先看这一介绍篇】大致汇总了整个多线程并发大概需要如何学习,这一篇开始真正的去开肝

我们从上一篇了解了线程和进程的一些区别,来回顾下

进程和线程的区别:

  • 进程是操作系统资源分配基本单位,线程是操作系统资源分配最小单位

  • 一个进程可以包含多个线程,一个进程中的多个线程可以共同完成

  • 进程有自己的独立运行空间,程序切换进程开销大;线程可以看做轻量级进程,每个线程有自己的运行栈和PC程序计数器,线程切换开销小

  • 一个进程崩溃后,在保护模式下不会对其它进程产生影响,但是一个线程崩溃会导致整个进程崩溃掉,多进程比多线程健壮

为什么要使用多线程:

  • 更多的处理器核心:处理器核心数量越来越多,以及超线程技术的运用,为了更好的利用CPU

  • 更快的响应时间:对于一些复杂的代码,可以并发进行计算和合并,缩短响应时间

  • 更好的编程模型:Java为多线程提供了良好的编程模型,使开发人员更专注于问题的解决

接下来按照线程状态、创建方法以及基本操作这些来简单介绍线程

在这里插入图片描述

多线程并发多么重要这个应该不用我多说了吧,懂的都懂,总之湿兄给大家分享的都是干货

线程状态

先给大家肝个图:
在这里插入图片描述

不废话,直接解释
在这里插入图片描述

慢点,别打,么么,不愿意看图片的马上解释

线程在运行周期有6种不同状态:

  • New:初始状态,线程刚刚被创建还未调用start()方法

  • RUNNABLE:运行,线程就绪和运行中都称为运行状态,即调用start之后还未获得CPU执行权和正在执行

  • BLOCKED:阻塞状态,线程阻塞于锁,即等待进入Synchronized块或者方法,还未获取到锁的状态

  • WAITING:等待状态,像wait()、join()等方法使线程进入等待状态,CPU不会分配执行时间,需要显示的被其它线程唤醒

  • TIMED_WAITING:超时等待,和等待类似,只不过不会无期限的等待其他线程唤醒,而是达到一定时间自动唤醒

  • nTERMINATED:终止状态,线程执行完毕,不可复生,在终止的线程上再次调用start()方法,会抛出java.lang.IllegalThreadStateException异常

线程有两个基本类型:

  • 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理

  • 系统级线程:由操作系统内核进行管理,操作系统内核提供相应的系统调用和应用程序接口API,使用户程序可以创建、执行以及撤销线程

等待队列:

线程进入阻塞状态之前必须要先获得对象的锁,在锁的范围内,调用wait方法会立即将锁释放,进入等待队列。当其它线程调用notify()方法时,会随机在等待队列中唤醒一个线程进入同步队列,等待获得CPU的时间片(执行权)

如果执行notifyAll()方法,会唤醒所有该锁对应的等待队列的线程进入同步队列,这些都抢夺CPU的执行权

并行和并发:

  • 并行是不同实体上,同一时刻发生的两个或者多个事件

  • 并发指的是在同一实体上,同一时间间隔内发生的两个或者多个事件

可以说,并行针对进程,并发针对线程~

线程创建

理解了上面的线程的概念和状态等基础之后,接下来我们看Java是如何实现线程的~

创建新线程的多个方法:

  • 继承Thread,重写run方法

  • 实现Runnable接口,重写run方法

  • 通过实现Callable接口,实现call()方法,结合FutureTask创建线程

  • 通过线程池创建,主要使用ExecutorService、Executors等

接下来我们来分析这几种创建方法:

1、继承Thread,重写run方法:

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 500; x++) {
            System.out.println(x);
        }
    }
}

测试一下:

  public static void main(String[] args) {
    
    // 创建两个线程对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
  }

看测试结果(两个线程会交替执行,但是最终会输出全部):

在这里插入图片描述

2、实现Runnable接口,重写run方法:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int x = 0; x < 500; x++) {
            System.out.println(x);
        }
    }

}

测试一下(需要new这个实现接口的对象,传入Thread对象):

public static void main(String[] args) {
        // 创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();

        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);

        t1.start();
        t2.start();
    }

测试结果和上面一样,不多说了

3、通过实现Callable接口,实现call()方法,结合FutureTask创建线程:

  • 实现Callable接口,重写call方法,可以指定返回值的类型

  • 创建FutureTask,传入实现Callable接口的实例对象

  • 新建线程对象传入FutureTask对象,start启动即可

public class MyCallable implements Callable<String>{

  @Override
  public String call() throws Exception {

    for (int i = 0; i < 200; i++) {
              System.out.println(i);
          }
          return "MyCallable";
  }
}

测试一下:

FutureTask<String> task = new FutureTask<String>(new MyCallable());
    
Thread t1 = new Thread(task);
t1.start();
    
 //获得返回值
String result = task.get();
System.out.println(result);

看结果(返回Callable,成功):

在这里插入图片描述

4、通过线程池创建,主要使用ExecutorService、Executors等:

  • 创建ExecutorService对象,根据具体需求创建合适的线程池

  • 用executorService.submit()方法,传入Runnable实例对象.(也可以传入Callable对象,Callable结合submit()这种方式可以获取线程执行完返回的结果)

  • 使用executorService.shutdown()方法销毁线程池

 //newFixedThreadPool: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待.
 ExecutorService executorService = Executors.newFixedThreadPool(5);

我们可以通过submit提交Runnable、Callable类型的Task,传入实例对象到线程池,其实也可以在新建ExecutorService时传入线程工厂ThreadFactory对象,用来创建线程,这个后续会详解~

接下来我们对比下几种方法的优缺点

  • 继承Thread类,实现简单,但是由于Java是单继承,扩展性不好

  • 实现Runnable接口的方式,还可以继承其它类,如果需要访问当前线程,则必须使用Thread.currentThread()来获取当前线程对象

  • Callable接口可以实现返回值,还可以继承其他类,如果需要访问当前线程,则必须使用Thread.currentThread()来获取当前线程对象

  • 线程池方式避免频繁创建销毁线程,减少系统开销,有并发问题

在这里插入图片描述

线程的基本操作和状态

启动方法start和run(对象方法):

  • run():仅仅是封装被线程执行的代码,直接调用是普通方法

  • start():首先启动了线程,然后再由jvm去调用该线程的run()方法

JVM虚拟机启动是多线程的,因为启动的不仅仅有main线程,还伴随着垃圾回收线程,我们一般用Runnable接口来实现,这样可将并发任务和任务运行机制解耦~

sleep(类方法,休眠):
在这里插入图片描述

Java中的线程的暂停指的是java.lang.sleep方法,属于类所有。这个方法会使当前正在执行的线程暂停指定时间,如果线程持有锁,sleep在结束前不会释放锁

调用sleep进入计时等待状态,等时间到了,进入的是就绪状态,并非运行状态!

在这里插入图片描述

当main线程调用Thread.sleep(1000)之后,线程会被暂停,如果被interrupt打断,则会抛出InterruptedException异常

yield(释放CPU):

当前线程放弃CPU的执行权,也就是放弃CPU分配的执行时间片,但是不会释放资源,由运行状态转变成就绪状态,让OS再次选择线程,但是有可能再次选择本线程

作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行

实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

join(对象方法,等待):

join()方法调用,会等待该线程执行完毕之后才执行别的线程

在这里插入图片描述

举个例子说明:

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 10; x++) {
            System.out.println(x);
            try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
        }
    }
}

这个是线程的实现类,再写个main调用

  public static void main(String[] args) {
    MyThread t1 = new MyThread();
    t1.start();
    try {
      t1.join();
     } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    System.out.println("main线程结束");
  }

看看效果:

在这里插入图片描述

结果是每隔一秒打印数字,最后打印main线程结束

main中new了一个线程类对象,start启动,main线程会等待调用join方法的t1线程执行完返回之后才会继续向下执行

就是一个线程等待另一个线程执行完,才会向下继续执行

过期的suspend()、resume()、stop()方法:

大家对于电视应该很熟悉吧,如果把电视播放节目比作一个线程运作,那么节目的暂停、恢复、停止操作在线程Thread中的API对应的就是上面的三个方法

这些方法时过期的,不建议使用

在这里插入图片描述
不建议使用原因:在调用之后,线程不会释放已经占有的资源(比如锁),而是占着资源进入别的状态,容易引发死锁,导致程序工作处于不确定状态下。正是因为这些副作用,才被标为不建议使用

总而言之,就是这些方法太暴力了,不安全,可用等待/通知机制来代替

中断线程:

线程中断在之前版本是有stop方法的,但是现在被设置过时了,现在没有强制线程终止的方法了。那么现在我们如何终止线程呢?

我们使用interrupt()方法来设置标志位,请求终止线程

  • interrupt不会真正停止一个线程,它只是发了一个信号,设置了一个标志位,告诉线程,你应该结束了

  • 思想在于想线程自己来终止,我们设置标志位信号,可以根据业务终止线程

在这里插入图片描述

强调:调用interrupt并不是真正终止掉当前线程,并不会对线程的状态造成影响,仅仅是设置了一个中断标志位。这个中断标志位可以用来判断什么时候该干什么活,什么时候中断由我们自己来决定

interrupt线程中断有两个方法来辅助检查线程是否被中断:

  • interrupted():静态方法,清除标志位

  • isInterrupted():实例方法,不会清除标志位

如果阻塞线程调用了interrupt方法,会抛出异常,设置标志位为false,同时线程退出阻塞

线程通信:

  • 等待通知机制:wait()、notify()、notifyAll()方法,线程A调用对象S的wait()进入等待状态,另一个线程B调用对象S的notify()或者notifyAll()方法唤醒,线程A收到通知返回wait()方法,继续执行

  • join:也是一种通信机制,一个线程调用了thread.join()语句,即当前线程等待thread线程终止之后才从thread.join()返回,也可传入时间参数,用于超时自动返回

  • 管道输入和输出流:管道输入流/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,传输的媒介是内存,主要实现是PipedOutputStream、- PipedIutputStream、PipedReader、PipedWriter,前两种面向字节,而后两种面向字符

  • ThreadLocal线程变量:以ThreadLocal对象为键、任意对象为值得存储结构,此结构附在线程上,一个线程可根据一个ThreadLocal对象查询到在这个线程上的值

在这里插入图片描述

偷个懒,图片来源于Java并发编程的艺术

Daemon守护线程:

我们可以通过**setDaemon(true)**来把线程设置为守护线程

守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止

  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断

  • 将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现

传统的守护线程是运行在后台的一种特殊线程,它独立于控制终端,与系统同生共死~

在这里插入图片描述

絮叨叨

建议:并发有安全,使用需谨慎,点赞关注推荐三连不需要谨慎~
在这里插入图片描述
你知道的越多,你不知道的也越多。
在这里插入图片描述

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页