聊聊Java线程是个啥东西-Java多线程

Posted niceffking

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊Java线程是个啥东西-Java多线程相关的知识,希望对你有一定的参考价值。

为什么要有线程

在这个效率和质量并存的时代,首先, "并发编程" 成为 "刚需".

单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU

资源. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编

程.

就比如一个人为了节约出时间来看手机, 那么他肯定会在吃饭的时候去看或者是休息的时候去看会手机等等.

初识线程

上次我们所讲的进程, 是一个比较重量级别的的概念, 因为进程消耗的资源多, 执行的速度慢, 无论是创建一个进程还是销毁, 调度一个进程, 其成本都比较高. 多进程编程可以解决高并发的问题, 但不是一个高效的选择.

为什么进程这么"重量"? 主要是体现在 资源的分配上, 资源分配是需要花时间的, 它是一个很耗时的操作, 比如系统要给一个进程分配内存,系统就需要遍历自己的空闲内存的表(一种特定的数据结构),找到一个空间大小合适的内存来进行分配. 其二, 有很多个进程在向系统申请内存. 多进程编程的成本高就是体现在资源这块上.

相比于进程, 线程是一个轻量级别的概念, 他是如何解决这种资源分配的问题呢? 其实就是把申请资源的过程简化. 一个进程中可以包含多个线程, 多个线程每个线程都是独立可以调度的"执行流", 这些执行流之间本身就是并发的, 这些线程共用同一份进程的内存资源, 这就意味着对于线程而言, 内存资源是分配好的, 创建线程就省下了分配资源的开销.

多线程之间, 可能是在多个CPU上执行, 也可能是一个CPU核心上, 通过调度来进行运行. 操作系统在调度的时候, 其实在调度的线程, 而不是进程. 线程是操作系统 调度运行的基本单位.

一个进程中的多个线程之间, 共用同一份系统资源, 也就是 :

  1. 内存空间

  1. 文件描述符表

只有在进程启动,创建第一个线程的时候, 需要花成本去申请系统资源, 一旦第一个线程创建完毕, 后续在创建的线程将会申请更少的空间, 于是创建/销毁的效率就提高了许多.

线程和进程的区别联系

  1. 进程包含线程

  1. 进程有自己独立的内存空间和文件描述符表, 同一个进程中的多个线程之间, 共享同一份地址空间和文件描述符表

  1. 进程是操作系统资源分配的基本单位, 线程操作系统调度执行的基本单位

  1. 进程之间具有独立性, 一个进程挂了, 不会影响到另外一个进程, 同一个进程里的多个线程之间, 一个线程挂了有可能会把整个进程挂掉, 同时也会影响到其他进程\\

  1. 进程和线程都可以并发和并行

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

Java标准库提供了一个类Thread可以用来表示一个线程

Thread类

首先来介绍如何创建一个Thread对象?

创建thread的子类

class MyThread extends Thread 
    public void run() 
        System.out.println("hello world!");
    

其中Thread为MyThread类的父类, 其中run方法被重写了, Thread类中的run方法如下:

实例化(向上转型)

先创建MyThread的实例, 然后赋值给他的父类Thead向上转型, 然后使用变量名+点号+start()的形式启动线程.

class MyThread extends Thread 
    public void run() 
        System.out.println("hello world!");
    

public class threadingDemo1 
    public static void main(String[] args) 
        Thread t = new MyThread(); // 向上转型
        t.start();  // 启动线程
    

t.start() : 启动线程, 在进程中另外创建了一个流水线, 开始并发执行新的逻辑

上述代码Java创建了两个线程

  1. main方法所对应的线程(一个进程里面至少有一个线程, 这个main方法相当于是这个程序自带的一个线程), 也可以称之为主线程

  1. t.start()所创建的线程

独立执行流

java程序每一个线程都是一个独立的执行流

第一个多线程java程序

接下来的代码, 来感受java多线程的和普通程序的区别:

class MyTread extends Thread 
    @Override
    public void run() 
        while(true) 
            System.out.println("hello libo");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                throw new RuntimeException(e);
            
        
    

public class Main 
    public static void main(String[] args) throws InterruptedException 
        Thread tem = new MyTread();
        tem.start();
        while(true) 
            System.out.println("i love you libo!");
            Thread.sleep(1000);
        
    

打印结果为hello libo i love you libo!

交替打印, 因为输出窗口的打印函数, 每次只能显示一个打印信息, 所以在打印的时候, 会无序的打印两个字符串:

创建多线程程序的方法

  1. 继承Thread来创建一个线程.

首先实现一个继承了Thread类的类, 然后再重写其run方法. 随后实例化一个实例对象, 然后调用里面重写的run方法, 再使用start方法启动线程即可.

class MyTread extends Thread  // 继承了Thread类的类
    @Override // 重写run方法
    public void run() 
        System.out.println(" this is your code! ");
    

public class Main 
    public static void main(String[] args) throws InterruptedException 
        MyTread tem = new MyTread(); // 实例化
        tem.start(); // 启动线程
    

2. 实现Runable接口

class MyThread implements Runnable 
    public void run() 
        System.out.println("hello world!");
    


public class Main 
    public static void main(String[] args) 
        Thread t = new Thread(new MyThread());
        t.start();
    

结果输出 "hello world!"

3. 匿名内部类

匿名内部创建Thread的子类

public class Main 
    public static void main(String[] args) 
        Thread t = new Thread() 
            public void run() 
                System.out.println("hello world!");
            
        ;
        t.start();
    

结果通过线程调用输出"hello world!"

4.匿名创建Runnable子类对象

public class Main 
    public static void main(String[] args) 
        Thread t = new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("runnable 子类!");
            
        );
    

5. lambda表达式创建Runnable子类对象

public class Main 
    public static void main(String[] args) 
        Thread t = new Thread(() -> 
            System.out.println("使用匿名类创建 Thread 子类对象");
        );
    

使用JDK提供的工具来查看Java进程里的线程详细

  1. 在java目录下找到JDK工具包:

  1. 到bin目录下找打jconsole.exe程序

  1. 运行一个多线程进程


public class Main 
    public static void main(String[] args) 
        Thread t = new Thread(() -> 
            while(true) 
                System.out.println("xxxxx!!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    throw new RuntimeException(e);
                
            
        );
        t.start();
        while(true) 
            System.out.println("hello world!");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                throw new RuntimeException(e);
            
        
    

交替运行

4. 运行jconsole

jconsole只能分析java进程, 不能识别非java进程, 其中:

Main为我们运行的代码的进程, 其次 jconsole也有自己的进程, jconsole也是java实现的,

双击Main进入这个java进程, 可以看到这个界面:

然后进入到线程页签:

可以看到, 一个java进程里面不止一个线程, 里面还有很多其他的线程,

这个main为main方法的进程, 因为main方法里面有一个while(true)循环, 所以main方法线程一直没有结束, Thread-0同样如此,Thread-0线程是我们的Thread对象执行的线程.

这个堆栈跟踪线程中的代码执行到哪了.

其中这个Thread-0就是我们创建的第一个线程, 同理第二个线程为Thread-1, 以此类推.

Thread类及其常见方法

Thread类是Jvm用来管理线程的一个类, 换句话来说, 每个线程都有一个唯一的Thread对象与之相关联, 用我们上面的例子来说, 每个执行流, 也就是需要一个Thread对象来描述, 而Thread类的对象就是用来描述一个线程执行流的, JVM会将这些Thread对象组织起来, 用于线程调度, 线程管理.

例如:

Thread常见构造方法

方法

说明

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用 Runnable 对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用 Runnable 对象创建线程对象,并命名

Thread()和Thread(Runnable target) 我们上面都有举例过, 下面这个Thread(String name) 只是给这个线程取了个小名, 便于我们后面去调试这个线程, 例如下面的例子:

public class Main 
    public static void main(String[] args) 
        Thread t = new Thread("MyThread-0") 
            @Override
            public void run() 
                System.out.println("hello world!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    throw new RuntimeException(e);
                
            
        ;
        t.start();
    

这里传入了name = MyThread-0, 然后去jconsole去查看, 如下:

Thread(Runnable target, String name) 同理, 这个name不会影响程序执行, 只是方便调试的时候快速找到所需要的线程.

Thread常见属性

每个进程都有自己的状态, 上下文, 记账信息, 等属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

对其解释:

  • ID : 是线程的唯一标识, 不同的线程不会重复使用同一个ID

  • 名称: 是各种调试工具所要用到的, 便于寻找对应的线程

  • 状态 : 标识一个线程当前所处的情况

  • 优先级 : 优先级高的线程在理论上会更先辈调度到

  • 后台线程: JVM会在一个进程的所有非后台线程结束后, 才会结束运行, 后台进程不会阻止java进程结束, 哪怕后台线程没有执行完, java进程该结束的就会结束, true表示是 后台线程, false表示为前台进程. 必须进程中所有的前台线程都执行完成java进程才会结束.

  • 是否存活: run方法是否运行结束了

  • 线程中断:

例如


public class Main 
    public static void main(String[] args) 
        Thread t = new Thread("MyThread-0") 
            @Override
            public void run() 
                while (true) 
                    System.out.println("hello world!");
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                
            
        ;
        t.start();
        System.out.println("t->ID = " + t.getId()); // ID
        System.out.println("t->名称 = " + t.getName()); // 名称
        System.out.println("t->状态 = " + t.getState()); // 状态
        System.out.println("t->优先级 = " + t.getPriority()); // 优先级
        System.out.println("t->是否为后台线程 = " + t.isDaemon()); // 是否为后台线程
        System.out.println("t->是否存活 = " + t.isAlive()); // 是否存活
        System.out.println("t->是否被中断 = " + t.isInterrupted()); // 是否被中断

    

启动一个线程:start()方法

我们之前覆盖写了run方法, 来创建了一个线程对象, 但是线程对象被创建出来了, 并不代表线程就开始运行了.

例如:

public class Main 
    public static void main(String[] args) 
       Thread t = new Thread(() -> 
           System.out.println("hello MyThread!!!");
       );
    

使用lambda表达式来创建一个Thread匿名内部类. 单击运行, 可以发现系统没有任何提示输出:

这个时候就需要使用start方法来启动线程:

public class Main 
    public static void main(String[] args) 
       Thread t = new Thread(() -> 
           System.out.println("hello MyThread!!!");
       );
       t.start();
    

中断一个线程

如果线程执行到一半, 我们需要让他停下来, 该怎么做?

目前有两种办法来实现:

  1. 通过共享标记来进行实现

  1. 调用interrupt()方法来实现

标志位

看一个例子:

public class Main 
    public static void main(String[] args) 
       Thread t = new Thread(() -> 
          while (true) 
              System.out.println("hello MyThread!");
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  throw new RuntimeException(e);
              
          
       );
       t.start();
    

这个例子中, 线程t对象中使用了while(true)的死循环, 导致入口方法无法执行完毕, 自然线程就不能结束, 但是如果把这个变量换成全局变量来控制, 是否能达到我们所需要的效果?

public class Main 
    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException 
       Thread t = new Thread(() -> 
          while (!flag) 
              System.out.println("hello MyThread!");
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  throw new RuntimeException(e);
              
          
           System.out.println("线程终止!!");
       );
       t.start();
       Thread.sleep(3000);

       flag = true;
    

结果出现:

同我们的预想相同.

注意, 这里不能将flag设置为main方法中的局部变量:

下面是错误的写法:

public class Main 

    public static void main(String[] args) throws InterruptedException 
        boolean flag = false;
       Thread t = new Thread(() -> 
          while (!flag) 
              System.out.println("hello MyThread!");
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  throw new RuntimeException(e);
              
          
           System.out.println("线程终止!!");
       );
       t.start();
       Thread.sleep(3000);

       flag = true;
    

运行结果:

原因: lambda变量捕获, 我们实现的是lambda表达式Thread子类, lambda捕获的变量必须是final修饰的, 或者是实际上没有final的, 也就是没有被final修饰, 但是也没有被修改的.

解决方法: 将这个标志位设置为成员变量, 而非局部变量

Thread.interrupt() / Thread.currentThread().isInterrupted()

Thread.isInterrupted() : java里面的每个Thread类对象都有自带的一个标志位, 也就是Thread.isInterrupted(), isInterrupted()的值默认是false的.

Thread.currentThread() : 这个方法在哪个线程对象里面使用, 返回的就是哪个线程对象的引用

Thread.interrupt() : 将这个自带的标志位设置为true.

一个简单的例子 :



public class Main 

    public static void main(String[] args) throws InterruptedException 
       Thread t = new Thread(() -> 
          while (!Thread.currentThread().isInterrupted()) 
              System.out.println("hello MyThread!");
              try 
                  Thread.sleep(1000);
               catch (InterruptedException e) 
                  throw new RuntimeException(e);
              
          
           System.out.println("线程终止!!");
       );
       t.start();
       Thread.sleep(3000);
        t.interrupt();
    

   

我们将控制条件设置为自带的标志位, 然后使用t.interrupt()方法来将标志位值为true来结束循环. 点击运行, 结果如下:

为什么会抛出异常?

而interrupt的作用是: 将标志位设置为true, 如果线程正在阻塞态(例如sleep等), 此时就会把阻塞态立马唤醒.然后通过抛出异常的方法让sleep结束.

当sleep方法被唤醒的时候, sleep会自动把自带的标志位isInterrupted()清空(true -> false) 这下子导致循环任然可以继续执行.

所以在后面使用interrupt方法的时候, 程序正在sleep阻塞态, 立马被唤醒然后抛出上面的异常, 然后由于标志位被设为了false, 此时线程任然继续进行.

等待一个线程-join()

线程之间是并发执行的, 操作系统对线程的调度是无序的, 无法判断两个线程谁先执行结束.

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作. 可以在另外一个线程里面使用join()方法:

例如 : 这里有线程A 和线程B, 我们在线程A里面使用 B.join(), 也就意味着, A线程执行到B.join()之后, 就必须等B执行完才能继续执行A.

看下面这个案例:

public class Main 
    public static void main(String[] args) throws InterruptedException 
       Thread t = new Thread(()->
           System.out.println("hello t");
       );
       t.start();
       t.join();
        System.out.println("hello main!");
    

在main方法中加入t.join(), 也就是如果线程t还没有结束, 那么main线程就会阻塞(Blocking)等待, 代码走到这个t.join()就会停下来(main线程, 别的线程不受影响),

也就是: 如果main线程调用到t.join()的时候, 如果t线程没有结束, 那么main线程就会阻塞, 直到t线程结束, main线程才会解除阻塞, 然后继续执行下面的部分.

但是如果main线程调用t.join()时, t线程已经结束了, 那么main线程就会继续往下执行

获取当前线程的引用

这个在前面已经介绍到了,

使用Thread.currentThread(), 放回当前线程的引用, 例如:

public class Main 
    public static void main(String[] args) throws InterruptedException 
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
    

输出结果:

休眠当前进程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的:

方法

说明

public static void sleep(long millis) throws InterruptedException

休眠当前线程 millis毫秒

public static void sleep(long millis, int nanos) throws InterruptedException

更高精度的休眠

线程状态

线程状态是一个枚举类型(Thread.State)

public class Main 
    public static void main(String[] args) throws InterruptedException 
        for (Thread.State state :
                Thread.State.values()) 
            System.out.println(state );
        
        // Thread.State state 是单个枚举实例,
        // Thread.State.values()是这个Thread类所有状态类型 的枚举实例数组的引用
    

输出:

类型

解释

NEW

安排了工作, 还未开始行动

RUNNABLE

可以工作-> (正在工作 / 即将工作)

BLOCKING

表示排队等着其他线程先结束

WAITING

表示正在等待其他线程

TIMED_WAITING

表示正在等待其他线程

TERMINATED

工作完成了

深入聊聊Java多线程

一、背景

  在没有学习Java多线程以前,总觉得多线程是个很神秘的东西,只有那些大神才能驾驭,新年假期没事就来学习和了解一下Java的多线程,本篇博客我们就来从头说一下多线程到底是怎么回事。

二、概述

  1.进程的概念

    每一个正在运行的程序都是一个进程,它是系统进行资源分配和调用的独立单位。且 每一个进程都有自己的内存空间和系统资源。

  2.线程的概念

    是进程中的单个顺序控制流,是一条执行路径。每个进程都可以拥有一个或者多个线程。各个线程之间都共享所属的那个进程的内存空间和系统资源。

  3.单线程和多线程程序的辨别方式

    如果一个进程只有一条执行路径,则称为单线程程序。 如果一个进程有多条执行路径,则称为多线程程序(经典应用程序:扫雷、迅雷下载)。

    废话不多说,我们直接上图来解释

    

  4.站在线程的角度来解释Java程序运行某各类的main方法

    首先java 命令会启动 java 虚拟机,即启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。

三、多线程程序的实现方式

  1.方式一:继承Thread类,重写run方法。

 1 package cn.hafiz;
 2 
 3 /*
 4  *多线程demo  
 5  */
 6 public class MyThread extends Thread {
 7 
 8     @Override
 9     public void run() {
10         for (int x = 0; x < 200; x++) {
11             System.out.println(x);
12         }
13     }
14 
15 }

  几个需要注意的问题:

    为什么要重写run()方法?

    答:我们只有把想要在线程中运行的代码写在run方法里才能启动线程进行运行。

    启动线程使用的是那个方法?

    答:启动使用的是start方法。

    线程能不能多次启动?

    答:一个线程只能启动一次,否则会报IllegalThreadStateException异常。

    run()和start()方法的区别?

    答:run()方法是普通的方法调用,并不是启用线程,而start方法是首先启动一个新的线程,然后运行run()方法里面的代码。

  2.方式二:实现Runnable接口

 1 package com.hafiz;
 2 
 3 public class MyRunnable implements Runnable {
 4 
 5     @Override
 6     public void run() {
 7         for (int x = 0; x < 100; x++) {
 8             System.out.println(Thread.currentThread().getName() + ":" + x);
 9         }
10     }
11 
12 }

  该实现方式优点:

    可以避免由于Java单继承带来的局限性。 适合多个相同程序的代码去处理同一个资源的情况,

    把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

四、线程基本操作

  1.获取和设置线程名称

    public final String getName();

    public final void setName(String name);

    其实通过构造方法也可以设置线程名称.

    public static Thread currentThread():获取任意方法所在的线程名称.

  2.线程调度

    1).之所以出现线程调度是因为计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。

    2).线程的两种调度模型(Java使用的是抢占式调度模型).

      分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。

      抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

      获取和设置线程优先级的方法:

        public final int getPriority();

        public final void setPriority(int newPriority);

  3.线程控制

    线程休眠:public static void sleep(long millis);

    线程加入:public final void join();

    线程礼让:public static void yield();

    设置是否为守护线程(当所有的线程都为守护线程就不具有抢占CPU执行权的资格): 

      public final void setDaemon(boolean on);

    

  上图中的关羽和张飞可以看成守护线程,当刘备主线程消亡以后,两者都不可能再具有获得CPU执行权的资格。

    中断线程:

      public final void stop();(已过时,直接将虚拟机退出)

      public void interrupt();(建议使用,是中断线程并且抛出一个InterruptedException异常,虚拟机不会退出,线程之后的代码会继续执行)

  4.线程生命周期

    1).创建:新建线程对象。

    2).就绪:线程对象已经启动,已经具有获得CPU的资格,但没有获取执行权。

    3).运行:获得了CPU的执行权,执行线程。

    4).阻塞:没有CPU的执行权,只能等待会到就绪状态。

    5).死亡:线程代码运行完毕,线程消亡。

  嫌文字不好理解,直接上图说明:

  

以上是关于聊聊Java线程是个啥东西-Java多线程的主要内容,如果未能解决你的问题,请参考以下文章

深入聊聊Java多线程

聊聊 Java 的几把 JVM 级锁

Python多线程中队列到底是个啥概念?

Java之多线程详解

Java - JVM - 线程状态

首先了解下所谓的java nio是个什么东西!