目录
- 一、线程(Thread)
- 1.1 Thread类中的构造方法
- 1.2 启用线程的方法
- 二、创建第一个多线程
- 三、多线程并发执行简单演示
- 四、多线程并发执行的优势
- 五、Thread的常见构造方法和属性
- 5.1 属性
- 5.2 方法
- 六、中断线程
- 七、线程等待
一、线程(Thread)
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使
用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
1.1 Thread类中的构造方法
1.2 启用线程的方法
二、创建第一个多线程
方法一:继承Thread类
1.继承Thread 来创建一个线程类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello Thread");
}
}
2.创建MyThread 类的实例
MyThread t = new MyThread();
3.调用start方法启动线程
t.start();
方法二:实现Runnable接口
1.实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello Thread");
}
}
2.创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());
3.调用 start 方法
t.start();
方法三:匿名内部类创建Thread子类对象
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello Thread");
}
};
方法四:匿名内部类创建Runnable子类对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello Thread");
}
});
方法五:lambda表达式创建Runnable子类对象
Thread t1 = new Thread(() -> System.out.println("hello Thread"));
Thread t2 = new Thread(() -> {
System.out.println("hello Thread");
});
三、多线程并发执行简单演示
public class Demo {
public static void main(String[] args) {
//thread 线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("thread线程执行中!");
//为了让这里的打印慢一点使效果更加明显,可以使用sleep方法设定线程睡眠时间
try {
Thread.sleep(1000);//每执行一次循环就睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
//main 线程
for (int i = 0; i < 10; i++) {
System.out.println("main线程执行中!");
//为了使效果更加明显,可以使用sleep方法设定线程睡眠时间
try {
Thread.sleep(1000);//每执行一次循环就睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
发现结果中thread线程和main线程打印顺序是不固定的,这里究其更本在于操作系统调度线程的时候, 线程是 “抢占式执行” 的, 具体哪个线程先执行, 哪个线程后执行是不确定的, 取决于操作系统调度器的具体实现策略.
四、多线程并发执行的优势
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
- 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
- serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.
假设当前有两个变量, 需要把两个变量各自自增 1000w 次(典型的 CPU 密集型的场景), 可以一个线程, 先针对 a 自增, 然后再针对 b 自增; 还可以两个线程, 分别对 a 和 b 自增; 我们来看看使用两个线程和单独使用一个线程分别所需的时间是多少.
public class Demo { public static void main(String[] args) { //多线程 concurrency(); //单线程 serial(); } // 串行执行, 一个线程完成 public static void serial() { // currentTimeMillis 获取到当前系统的 ms 级时间戳. long beg = System.currentTimeMillis(); long a = 0; for (long i = 0; i < 100_0000_0000L; i++) { a++; } long b = 0; for (long i = 0; i < 100_0000_0000L; i++) { b++; } long end = System.currentTimeMillis(); System.out.println("单线程执行时间: " + (end - beg) + " ms"); } public static void concurrency() { // 使用两个线程分别完成自增. Thread t1 = new Thread(() -> { long a = 0; for(long i = 0; i < 100_0000_0000L; i++) { a++; } }); Thread t2 = new Thread(() -> { long b = 0; for(long i = 0; i < 100_0000_0000L; i++) { b++; } }); // 记录开始执行的时间戳 long beg = System.currentTimeMillis(); t1.start(); t2.start(); try { // 等待两个线程结束后,再获取结束时的时间戳 t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 获取执行结束时的时间戳 long end = System.currentTimeMillis(); System.out.println("多线程执行时间: " + (end - beg) + " ms"); } }
多线程在IO密集型的任务中, 也是有作用的, 日常使用一些程序,经常会看到"程序未响应", 这是因为程序进行了一些耗时的IO操作(比如一些程序启动要加载数据文件,就涉及到大量的读硬盘操作), 阻塞了界面的响应, 这种情况下使用多线程也是可以有效改善的(让一个线程负责IO, 另一个线程用来响应用户的操作).五、Thread的常见构造方法和属性
5.1 属性
关于java线程的属性, 我们可以通过java官方的jconsole调试工具查看java线程的一些属性, 这个工具一般在jdk的bin目录下.
双击打开出现如下界面, 选择需要查看的进程连接查看
选择需要查看的进程属性查看:
5.2 方法
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程
就开始运行了。
覆写 run 方法是提供给线程要做的事情的指令清单
线程对象可以认为是把 李四、王五叫过来了
而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
调用 start 方法, 才真的在操作系统的底层创建出一个线程.线程调用start和直接调用run的区别
run 单纯的只是一个普通的方法, 描述了任务的内容。
start 则是一个特殊的方法, 内部会在系统中创建线程。
直接通过线程对象调用run方法就是单纯地调用了Thread对象中的一个普通方法而已, 并没有创建一个新线程来执行run方法, 而是通过main线程来执行的run方法, 而使用start方法, 会创建一个新线程并执行run方法.六、中断线程
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们
需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如
何通知李四停止呢?这就涉及到我们的停止线程的方式了。目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
方法一:自己设置一个标志位来来控制线程是否要停止
public class Demo { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (flag) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(3000); // 在主线程里就可以随时通过 flag 变量的取值, 来操作 t 线程是否结束. flag = false; } }
方法二:使用Thread类中自带标志位
public class Demo { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(3000); t.interrupt(); } }
thread 收到通知的方式有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
忽略这个异常, 也可以跳出循环结束线程.
2. 否则,只是内部的一个中断标志被设置,thread 可以通过Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。七、线程等待
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转
账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。public class Demo { public static void main(String[] args) { Thread t = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); // 此处的 join 就是让当前的 main 线程来等待 t 线程执行结束 (等待 t 的 run 执行完) try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t线程执行完之后再执行这里!"); } }