【Java多线程】基础知识笔记

本文将会介绍Java多线程中的重点知识,本文内容参考了网上的资料整理,主要为了自己看着方便,方便查找。
主要来源有:

  1. Guide哥
  2. 小林Coding
  3. 菜鸟教程

多线程

  • 一、为什么要使用多线程?
  • 二、什么是进程和线程
    • 1、进程
    • 2、线程
    • 3、进程与线程区别
      • 3.1 程序计数器
      • 3.2 虚拟机栈和本地方法栈
      • 3.3 堆和方法区
    • 4、线程的生命周期(状态)
    • 5、进程的状态
    • 6、什么是上下文切换?
  • 三、创建线程
    • 1、继承Thread类
    • 2、实现Runnable接口
    • 3、实现Callable接口
    • 4、为什么调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?
  • 四、Thread类及常见方法
    • 1、常见构造方法
    • 2、常见属性
    • 3、线程的启动
    • 4、中断一个线程
    • 5、等待一个线程
    • 6、获取当前线程引用
    • 7、休眠当前线程

一、为什么要使用多线程?


  • 单核CPU的发展遇到了瓶颈,使用多核CPU来提高算力,并发编程能够更好的利用多核CPU的资源。
  • 当遇到一个复杂任务时,如果我们只用一个线程的话,那么系统无论有多少个CPU核心,都会只使用其中的一个CPU,而创建多线程,可以将这些CPU充分地利用起来,任务执行效率便会显著提升。

二、什么是进程和线程


在Java中线程是以轻量的进程的方式实现的。
其他语言的实现方式可能不同,例如现在很火的Go语言,就是以协程的方式进行实现的。

1、进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

2、线程

线程与进程相似,一个进程中的执行过程中会产生多个线程,线程是一个比进程更小的执行单位。
同类的多个线程共享进程的方法区资源,每个线程有自己独立的程序计数器虚拟机栈本地栈
系统中产生一个线程或者在多个线程之间切换工作时,消耗要比进程小的多,所以线程也被叫做轻量级进程

3、进程与线程区别

  • 进程包含线程。每个进程至少包含一个线程,即主线程(main函数所在)。
  • 进程与进程之间不共享内存空间;同一个进程的线程共享同一块内存空间。
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

下面放一张Guide哥的图解,方便理解:
【Java多线程】基础知识笔记_第1张图片

3.1 程序计数器

程序计数器为什么是私有的?
程序计数器的私有主要是为了线程切换后能恢复到正确的执行位置

3.2 虚拟机栈和本地方法栈

  • **虚拟机栈:**每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

虚拟机栈和本地方法栈为什么是私有的?
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

3.3 堆和方法区

堆和方法区是所有线程共享的资源

  • 堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)。
  • 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

4、线程的生命周期(状态)

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)
【Java多线程】基础知识笔记_第2张图片
线程的的状态不是固定的,而是随着代码的执行在不同的状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):【Java多线程】基础知识笔记_第3张图片

5、进程的状态

进程的状态与线程的生命周期大同小异。
在一个进程活动期间,至少具备三种状态:运行状态、就绪状态以及阻塞状态。
【Java多线程】基础知识笔记_第4张图片
上图中各状态的的意义:

  • 运行状态: 该时刻进程占用CPU。
  • 就绪状态: 等待运行,由于其他进程的运行状态而暂时停止运行的进程。
  • 阻塞状态: 正在等待某一事件的发生(如等待输入/输出操作的完成)而暂时停止运行,此时就算CPU给它资源,它也无法运行。
    进程还有两种基本状态:
  • 创建状态: 进程正在被创建。
  • 结束状态: 进程正在被销毁,正在从系统中消失。
    所以,完整进程状态图:
    【Java多线程】基础知识笔记_第5张图片
    状态转换:
状态转换 转换原因
NULL -> 创建状态 一个新进程被创建时的第一个状态
创建状态 -> 就绪状态 当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的
就绪态 -> 运行状态 处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程
运行状态 -> 结束状态 当进程已经运行完成或出错时,会被操作系统作结束状态处理
运行状态 -> 就绪状态 处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行
运行状态 -> 阻塞状态 当进程请求某个事件且必须等待时,例如请求 I/O 事件
阻塞状态 -> 就绪状态 当进程要等待的事件完成时,它从阻塞状态变到就绪状态

6、什么是上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。例如:

  • 调用sleep()、wait()方法,主动让出CPU。
  • 时间片用完,切换到下一个线程或者进程。
  • 调用阻塞类的系统中断,比如IO请求,线程被阻塞。
  • 被终止或者结束。
    其中,前三种都发生了线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的上下文切换

三、创建线程


线程的创建在Java中有三种方式

  • 继承Thread,重写run方法。
  • 实现Runnable接口,重写run方法。
  • 实现Callable接口。

1、继承Thread类

  1. 继承Thread来创建一个线程类
	class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("my thread run");
        }
    }
  1. 创建MyThread实例
	Thread t = new MyThread();
  1. 调用start方法启动线程
	t.start();

也可以使用匿名内部类的方式进行实现

     Thread t2 = new Thread(){
         @Override
         public void run() {
             System.out.println("匿名内部类的 run");
         }
     };

整体代码:

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

        //匿名内部类重写run方法,本质还是继承Thread
        Thread t2 = new Thread(){
            @Override
            public void run() {
                System.out.println("匿名内部类的 run");
            }
        };
        t2.start();
    }

    //继承Thread类重写run方法
    private static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("my thread run");
        }
    }
}

2、实现Runnable接口

  1. 实现Runnable接口
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("my Runnable run");
        }
    }
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数。
	Thread t = new Thread(new MyRunnable());
  1. 调用start方法启动线程
	t.start();

同样可以使用匿名内部类的方式进行实现,也可使用lambda方法实现

     //以匿名内部类的方式实现Runnable接口
     Thread t2 = new Thread(new Runnable() {
         @Override
         public void run() {
             System.out.println("匿名内部类 Runnable");
         }
     });
    
     //使用lambda的方法实现
     Thread t3 = new Thread(()->{
         System.out.println("lambda方式实现");
     });

整体代码:

public class CreateThread2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();

        //以匿名内部类的方式实现Runnable接口
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类 Runnable");
            }
        });
        t2.run();

        //使用lambda的方法实现
        Thread t3 = new Thread(()->{
            System.out.println("lambda方式实现");
        });
        t3.run();
    }

    //实现Runnable接口实现线程
    private static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("my Runnable run");
        }
    }
}

3、实现Callable接口

Callable 是一个 interface。相当于把线程封装了一个 “返回值”。方便开发人员借助多线程的方式计算结果。
Callable 和 Runnable 相对, 都是描述一个 “任务”。Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务。
Callable 通常需要搭配 FutureTask 来使用。FutureTask 用来保存 Callable 的返回结果。因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。
FutureTask就负责这个等待结果出来的工作。
使用Callable可以简化代码,不需要再去写线程同步的代码了。

实现Callable接口创建线程:

public class CreateThread3 {
    static int i = 0;

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //先实现callable
        Callable<Integer> r = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                try {
                    Thread.sleep(1000);
                    //将值返回出去
                    return 1;
                } catch (InterruptedException e) {
                    throw new RuntimeException("出错了");
                }
            }
        };

        //再创建futuretask
        FutureTask<Integer> task = new FutureTask<>(r);
        //通过futuretask获取到线程中的值
        Thread t = new Thread(task);
        t.start();
        System.out.println(task.get());
    }
}

例如:创建线程计算 1 + 2 + 3 + … + 1000

  1. 不使用Callable:
  • 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象。
  • main 方法中先创建 Result 实例, 然后创建一个线程 t。 在线程内部计算 1 + 2 + 3 + … + 1000。
  • 主线程同时使用 wait 等待线程 t 计算结束。 (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了)。
  • 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果。
static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}
  1. 使用Callable:
  • 创建一个匿名内部类, 实现 Callable 接口。 Callable 带有泛型参数. 泛型参数表示返回值的类型。
  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果。
  • 把 callable 实例使用 FutureTask 包装一下。
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中。
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果。
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);

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

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

四、Thread类及常见方法


1、常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("Thread name");
Thread t4 = new Thread(new MyRunnable(), "Thread name");

2、常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否为后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解为 run 方法是否运行结束了
  • 线程的中断

代码示例:

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

        //线程各种方法的使用
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName()+":alive");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+":die");
            }
        },"线程1");

        System.out.println(Thread.currentThread().getName()+"ID是:"+thread.getId());
        System.out.println(Thread.currentThread().getName()+"名字是:"+thread.getName());
        System.out.println(Thread.currentThread().getName()+"状态:"+thread.getState());
        System.out.println(Thread.currentThread().getName()+"优先级:"+thread.getPriority());
        System.out.println(Thread.currentThread().getName()+"后台线程:"+thread.isDaemon());
        System.out.println(Thread.currentThread().getName()+"是否存活:"+thread.isAlive());
        System.out.println(Thread.currentThread().getName()+"是否被中断:"+thread.isInterrupted());

        thread.start();
        while(thread.isAlive()){}

        System.out.println(Thread.currentThread().getName()+"的状态:"+thread.getState());


    }
}

【Java多线程】基础知识笔记_第6张图片

3、线程的启动

只有调用了start方法,才是真正在操作系统底层创建出一个线程。

4、中断一个线程

两种方式来中断线程

  • 共享的标记来进行交流,如使用带有volatile关键字的标志位。
  • 调用 interrupt() 方法来通知,如 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。
方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位

thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
  • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  1. 否则,只是内部的一个中断标志被设置,thread 可以通过
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
    这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

示例代码:

public class ThreadInterrupt {
    //线程中断
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (!Thread.currentThread().isInterrupted()){
                        Thread.sleep(100000);
                        System.out.println(Thread.currentThread().getName()+"run");
                    }
                } catch (InterruptedException e) {
                    //是否中断,由线程决定
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(10000);
        t.interrupt();
    }
}

睡眠10s后中断
【Java多线程】基础知识笔记_第7张图片

5、等待一个线程

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 与上相同,但可以有更高的精度

示例代码:

package Thread_api;

public class Join {
//    当前线程加入t线程,当前线程在此处等待
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 3; i++) {
                        System.out.println(Thread.currentThread().getName()+" run:"+i);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
//        t.join();
        System.out.println(Thread.currentThread().getName()+"run");
    }
}

6、获取当前线程引用

使用public static Thread currentThread();方法获取,它的作用是返回当前线程对象的引用。

7、休眠当前线程

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis 毫 秒
`public static void sleep(long millis, int nanos) throws
InterruptedException` 可以更高精度的休眠

你可能感兴趣的:(Java进阶,java,jvm,开发语言)