精湛细腻版-Java多线程与并发编程

精湛细腻版-Java多线程与并发编程

本次更新修订了旧文,在通俗的基础上增加了专业术语级阐述,更具系统性、专业性 --2020/6/3

后续完整内容陆续更新在本专栏…

文章目录

  • 精湛细腻版-Java多线程与并发编程
    • 线程与进程
    • 并行与并发
    • Java线程的使用
      • 方式一:直接使用Thread
      • 方式二:使用 Runnable 配合 Thread
      • Runnable与Thread的关系
    • 线程的生命周期(Java1.8)
    • Java线程的操作方法
      • **Join原理与应用**
    • 主线程与守护线程

线程与进程

进程:

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程:

  • 线程是进程的一个单元,如使用迅雷下载任务
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
  • 多线程:一个进程中有多个线程在同时运行,如迅雷下载,迅雷软件的一次运行就是一个进程,那么在迅雷中可以同时下载多个电影,这就是多线程(每一个下载都是一个线程)
  • Jvm就是多线程的,在我们运行jvm的时候后台会运行垃圾回收的线程,来清理没有被引用的对象

二者区别:

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程通信称 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换

并行与并发

单核 CPU下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。

总结为一句话就是: 微观串行,宏观并行

一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent

精湛细腻版-Java多线程与并发编程_第1张图片

多个核之间调度线程并执行为并行

Java线程的使用

方式一:直接使用Thread

创建一个类,声明为 Thread 的子类,重写 Thread 类的 run 方法,该类可以有相关属性及构造器

public class ThreadDemo extends Thread{
    private String name; //线程名字
    /*构造器*/
    public ThreadDemo(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        //run为线程执行的逻辑体,里面的定义操作就是某个线程具体要执行的东西
    }
}

创建线程实体类对象

测试类中-new出线程类对象,就可对线程进行相关操作

ThreadDemo td = new ThreadDemo("线程1");
ThreadDemo td1 = new ThreadDemo("线程2");

方式二:使用 Runnable 配合 Thread

创建一个类实现 Runnable 接口,实现 run 方法

public class ThreadDemo implements Runnable{
    private String name; //线程名字
    /*构造器*/
    public ThreadDemo(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        //run为线程执行的逻辑体,里面的定义操作就是某个线程具体要执行的东西
    }
}

测试类中-新建Thread对象,将上述类对象作为参数

 Thread t = new Thread(new ThreadDemo("线程1")) ;
 Thread t1 = new Thread(new ThreadDemo("线程2")) ;

【了解】Thread 与 Runnable 的关系

方式三:FutureTask配合Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况,这个后面说到

线程运行的原理

栈与栈帧:属于JVM的内容

线程上下文切换:因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • CPU给的时间片用完
  • Java垃圾回收
  • 被更高优先级的线程争抢
  • 线程调用了sleep、yield、wait、join、park、synchronized、lock 等方法

频繁上下文切换会影响性能

Runnable与Thread的关系

Thread的run方法

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

点进target

    /* What will be run. */
    private Runnable target;

可见,如果我们有runable对象的话,Thread实现的是Runnable的run方法

如果没有,直接执行Thread子类的run()方法

点进Runnable:

@FunctionalInterface  //注:这是Jdk8的新特性,注解这个接口为函数式编程接口
public interface Runnable {
    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * 

* The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }

最终的run就是一个抽象方法,由开发者去实现

总结:

  • 使用Thread直接实现线程,把线程和任务合并在了一起
  • 实现Runable的方式,由于实现的Runable的类可以多样,于是把线程和任务分开了
  • 用 Runnable 也更容易与线程池等高级 API 配合,更加灵活

线程的生命周期(Java1.8)

这是Java线程的六个状态
精湛细腻版-Java多线程与并发编程_第2张图片

这是Java线程各个状态之间的切换以及相关API带来的状态切换
精湛细腻版-Java多线程与并发编程_第3张图片

补充:操作系统中的线程与Java API中的线程
先来看操作系统:
精湛细腻版-Java多线程与并发编程_第4张图片

  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 运行状态:指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 阻塞状态:排队等待被调度
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

再来看Java定义的线程对应操作系统的线程
精湛细腻版-Java多线程与并发编程_第5张图片

  • NEW:线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束

Java线程的操作方法

start

此方法将启动线程,将该线程状态变为Java可运行状态

注意:

  • start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException,
  • 调用此方法后,相关线程对象将执行线程实体类中的run方法

start() 和run()

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

  • new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作
  • 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行

join

在多个线程同时运行时,对某个线程调用join方法,t.join()方法会使所有线程都暂停并等待t的执行完毕,该方法可用来实现线程同步

t1.start();
t1.join(); //需放在start后面

Join原理与应用

 private static void test1(){

     System.out.println("主线程开始...");

     //线程1秒后改r=10
     Thread t1 = new Thread(()->{
         System.out.println("线程一开始...");
         //睡眠1s
         try {
             Thread.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }

         r = 10  ;
     }) ;
     t1.start(); //启动线程

     System.out.println("r=="+r);
 }

运行上面代码分析:

主线程开始…
r==0
线程一开始…

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决方法:

用 join,加在 t1.start() 之后即可

主线程开始…
线程一开始…
r==10

setPriority

设置线程优先级,在多线程在,对各个线程分配不同的优先级,优先级高的优先执行

Thread类中提供三个级别的常量优先级供快速选择

注意:设置优先级需要在线程启动前

t1.setPriority(Thread.Max_PRIORITY); //最高优先级
ti.start() ; 

sleep

线程休眠,为了减少服务器的压力我们需要休眠
只需要在run方法中,需要休眠的地方加入

Thread.sleep(Long xxx)

  • 调用 sleep 会让当前线程从Running进入Timed Waiting状态(属于阻塞态)

  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  • 睡眠结束后的线程未必会立刻得到执行,还需等待时间片

sleep() 和 wait() 区别

  • sleep 方法没有释放锁,而 wait 方法释放了锁 。
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法通知该线程苏醒
  • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒,这个时候苏醒的线程会进入Time_Waiting状态

yield

调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

具体的实现依赖于操作系统的任务调度器

Thread.yield();

在run方法中执行

interrupt

  • 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记(isterrupted()=false);

  • 打断正常运行的线程,打断状态为ture

  • 打断park(暂停)线程,原本属于WAITING,被打断后,打断状态为true

如果打断的是正在运行的线程,则会设置打断标记,给下面方法进行判断

isInterrupted

判断当前线程是否被打断,不会清除打断标记

主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束

package c2;

public class TestJoin {
    
    public static void main(String[] args)  {

        System.out.println("主线程开始...");

        //线程1秒后改r=10
        Thread t1 = new Thread(()->{
            System.out.println("线程一开始...");
            //睡眠2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("睡眠完毕"); //不会打印,被强制结束
        },"daemon_thread") ;
        t1.setDaemon(true); //设置t1为守护线程
        t1.start(); //启动线程

        try {
            Thread.sleep(1000); //主线程睡眠1s
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("运行结束");
    }
}

结果

主线程开始…
线程一开始…
运行结束

你可能感兴趣的:(Java并发编程)