Java多线程——线程的创建、Thread类以及多线程状态

文章目录

  • 学习目标
  • 一、认识线程
    • 1、线程是什么?
    • 2、为什么要有线程
    • 3、==进程和线程的区别==
  • 二、Thread类以及常见方法
    • 1.创建线程的几种方式
    • 2、Thread类属性及方法
      • 2.1、Thread的常见构造方法
      • 2.2、Thread的常见属性
    • 3、线程的中断-interrupt()
        • 中断一个线程:
    • 4、等待一个线程-join()
  • 三、线程的状态
    • 1、线程的所有状态
    • 2、线程的状态转移
    • 3、Jconsole调试工具
  • 总结
    • **线程和进程的对比**


学习目标

  • 认识多线程
  • 掌握多线程的创建
  • 掌握多线程的状态
  • 掌握什么是线程安全以及解决方法
  • 掌握synchronized、volatile关键字

提示:以下是本篇文章正文内容,下面案例可供参考

一、认识线程

1、线程是什么?

一个线程就是一个执行流。每个线程之间都可以按照顺序执行自己的代码,多个线程之间“同时”执行着多份代码。举个例子:

一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2、为什么要有线程

首先,“并发编程”成为“刚需”。

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源。
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。

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

  • 创建线程比创建进程更快。
  • 销毁线程比销毁进程更快。
  • 调度线程比调度进程更快。

最后,线程虽然比进程轻量,但是还是不能将多核cpu的性能发挥到极致,于是又有了“线程池”和“协程”

3、进程和线程的区别

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间。
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
    Java多线程——线程的创建、Thread类以及多线程状态_第1张图片
  • Java的线程和操作系统线程的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

二、Thread类以及常见方法

1.创建线程的几种方式

方法1 继承Thread类

class MyThread extends Thread {
     @Override//重写run方法
     public void run() {
         System.out.println("这里是线程运行的代码");
     }
}
public static void main(){
    MyThread t = new MyThread();
    t.start();//线程开始执行
}

方法2 实现Runnable接口

class MyRunnable implements Runnable{
    @Override//重写run方法
    public void run() { 
        System.out.println("my runnable");
    }
}
public class Demo {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

其他变形

/**
 * 匿名内部类的方式, 创建Thread子类
 */
public class Demo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println(4444);
            }
        };
        thread.start();
    }
}
-------------------------------------------------------------
/**
 * 匿名内部类的方式, 创建Runnable的子类
 */
public class Demo5 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(5555);
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
--------------------------------------------------------------------
/**
 * lambda表达式, 创建线程
 */
public class Demo6 {
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            System.out.println(5555);
        });
        thread.start();
    }
}

2、Thread类属性及方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
Java多线程——线程的创建、Thread类以及多线程状态_第2张图片

2.1、Thread的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名

2.2、Thread的常见属性

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

需要强调的是,通过覆写run()方法创建的是线程对象,但此时线程对象仅仅只是被创建出来了,只有调用了start()方法之后线程才真正独立去执行了,才真正的在操作系统的底层创建出一个线程。

以下是代码示例,演示线程的构造方法以及getName():

public class Demo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());

        Thread t2 = new Thread("一号线程"){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                System.out.println(this.getName());
            }
        };
        t2.start();

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
         //这里不能使用getName,因为Runnable没有name这个属性
         //    System.out.println(this.getName());
            }
        },"二号线程");
        t3.start();
    }
}

运行结果打印了主线程名称以及创建的两个线程(这里使用带名称参数的构造方法):

Java多线程——线程的创建、Thread类以及多线程状态_第3张图片

3、线程的中断-interrupt()

中断一个线程:

接着讲上面转账的例子,李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。

两种方式:

1.通过共享的标记进行沟通(volatile关键字)

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
    }
}

2.调用interrupt()方法进行通知

Thread内部包含了一个boolean类型的变量作为线程是否被中断的标记

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位(使用最多)

使用 thread 对象的 interrupted() 方法通知线程结束:

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
// 两种方法均可以
            while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
// 注意此处的 break
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
    }
}

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

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

4、等待一个线程-join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            + ": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
    }
}

此时运行程序,程序会像我们预想的那样 按部就班的工作。如图:

Java多线程——线程的创建、Thread类以及多线程状态_第4张图片
当我们注释掉那两行join后 运行结果如图:

Java多线程——线程的创建、Thread类以及多线程状态_第5张图片

解释一下为什么:

t1.join()后,main线程就必须等待t1执行完,才能接着往下执行,t1未执行完时,main线程阻塞,t2同理。 这里t2是在执行完t1后启动的。假如t1线程和t2线程同时启动,那么将这两个线程join后,main线程会阻塞的时间是这两个线程运行时间的最大值,即阻塞时是同时在等这两个线程执行完毕。

谁调用join() 谁阻塞,即join写在哪个线程内,该线程就阻塞。

三、线程的状态

1、线程的所有状态

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

public class ThreadState {
	public static void main(String[] args) {
		for (Thread.State state : Thread.State.values()) {
				System.out.println(state);
			}
	}
}
  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了。

2、线程的状态转移

Java多线程——线程的创建、Thread类以及多线程状态_第6张图片

还是我们之前的例子:

刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;

当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;

如果李四、王五已经忙完,为 TERMINATED 状态。

所以,前面提到的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

Java多线程——线程的创建、Thread类以及多线程状态_第7张图片

在具体的代码中,线程的状态是如何进行转移的呢?

Java多线程——线程的创建、Thread类以及多线程状态_第8张图片
如图所示,

  • 创建Thread实例后,线程状态为New
  • 调用start方法后,线程变为Runnable状态,
  • 此时再调用wait()、join()等方法又会进入Waitting状态,
  • 当给wait()、sleep()、join()传入时间参数时进入的是Timed_Waitting状态,
  • 若线程需要资源竞争,比如CPU资源已被其他线程加锁,则进入Blocked状态。
  • run()方法执行结束,线程进入Terminated状态。
  • 除了New状态和Terminated状态 线程都属于存活状态。

在代码中通过isAlive方法判定线程的存活状态:

public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100_000_000; i++) {
            }
            System.out.println("线程执行结束");
        });
        System.out.println("start 方法之前:" + t1.getState());
        t1.start();
        System.out.println("start方法之后:"+ t1.getState());
        Thread.sleep(1000);
        System.out.println(t1.getState());
    }
}

Java多线程——线程的创建、Thread类以及多线程状态_第9张图片

3、Jconsole调试工具

在观察线程状态的具体转移过程时,我们可以使用JDK自带的调试工具Jconsole,通常在JDK的安装目录bin目录下,当我们运行一个多线程代码后 可以双击打开Jconsole查看各线程的状态。

Java多线程——线程的创建、Thread类以及多线程状态_第10张图片

Java多线程——线程的创建、Thread类以及多线程状态_第11张图片

点击Demo20 链接后可以看到该线程此时状态为Timed_Waitting,若将sleep内时间删除重新运行,状态则会变为Waitting状态。

Java多线程——线程的创建、Thread类以及多线程状态_第12张图片

我们通过加锁的方式,创建两个线程去竞争同一把锁,这样没有竞争到的就进入block状态。这里Thread-0是t1(因为默认从0号开始创建),此时Thread-1就没有竞争到锁进入Blocked状态,等待锁释放资源。

Java多线程——线程的创建、Thread类以及多线程状态_第13张图片

Java多线程——线程的创建、Thread类以及多线程状态_第14张图片
线程的六种状态及其转移过程如上,总结如下:

  • BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
  • 只有在New (刚创建)和 Terminated(run方法执行结束)状态时,线程是“死”的。

总结

线程和进程的对比

1、线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

2、线程和进程的区别

  • 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  • 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  • 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  • 线程的创建、切换及终止效率更高

以上就是今天要讲的内容,本文算是多线程的入门吧,介绍了什么是线程、线程的创建、常见方法以及线程的状态和转移。后续将继续深入学习多线程以及JavaEE的其他内容,感兴趣的朋友可以点点订阅~

你可能感兴趣的:(JavaEE,从初阶到进阶,java,学习,jvm)