文章目录
- 学习目标
- 一、认识线程
- 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的其他内容,感兴趣的朋友可以点点订阅~