文章目录
- 学习目标
 - 一、认识线程
 - 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的线程和操作系统线程的关系
 
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 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 对象组织起来,用于线程调度,线程管理。
 
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();
    }
}
 
运行结果打印了主线程名称以及创建的两个线程(这里使用带名称参数的构造方法):

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收到通知的方式有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志。
 
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程。
 
- 否则,只是内部的一个中断标志被设置,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("王五工作结束了");
    }
}
 
此时运行程序,程序会像我们预想的那样 按部就班的工作。如图:

 当我们注释掉那两行join后 运行结果如图:

解释一下为什么:
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、线程的状态转移

还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
 当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,前面提到的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

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

 如图所示,
- 创建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());
    }
}
 

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


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

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


 线程的六种状态及其转移过程如上,总结如下:
- BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
 - TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
 - 只有在New (刚创建)和 Terminated(run方法执行结束)状态时,线程是“死”的。
 
总结
以上就是今天要讲的内容,本文算是多线程的入门吧,介绍了什么是线程、线程的创建、常见方法以及线程的状态和转移。后续将继续深入学习多线程以及JavaEE的其他内容,感兴趣的朋友可以点点订阅~


















