1.认识线程(Thread)
1.1概念
1)线程是什么 ⼀个线程就是⼀个"执⾏流".每个线程之间都可以按照顺序执⾏⾃⼰的代码.多个线程之间"同时"执⾏ 着多份代码.
*还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业** *务。我们进⼀步设想如下场景:*
*⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。*
*如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两* *位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有* *了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。*
*此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队* *执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(MainThread)。*
2)为啥要有线程 ⾸先,"并发编程"成为"刚需".
- 单核CPU的发展遇到了瓶颈.要想提⾼算⼒,就需要多核CPU.⽽并发编程能更充分利⽤多核CPU资源.
- •有些任务场景需要"等待IO",为了让等待IO的时间能够去做⼀些其他的⼯作,也需要⽤到并发编程.
其次,虽然多进程也能实现并发编程,但是线程⽐进程更轻量.
- 创建线程⽐创建进程更快.
- 销毁线程⽐销毁进程更快.
- 调度线程⽐调度进程更快.
最后,线程虽然⽐进程轻量,但是⼈们还不满⾜,于是⼜有了"线程池"(ThreadPool)和"协程" (Coroutine)
3)进程和线程的区别
- 进程是包含线程的.每个进程⾄少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间.同⼀个进程的线程之间共享同⼀个内存空间.
*⽐如之前的多进程例⼦中,每个客⼾来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。*
- 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
- ⼀个进程挂了⼀般不会影响到其他进程.但是⼀个线程挂了,可能把同进程内的其他线程⼀起带⾛(整个进程溃).
4)Java的线程和操作系统线程的关系
线程是操作系统中的概念.操作系统内核实现了线程这样的机制,并且对⽤⼾层提供了⼀些API供⽤⼾使⽤(例如Linux的pthread库).
Java标准库中Thread类可以视为是对操作系统提供的API进⾏了进⼀步的抽象和封装.
1.2第⼀个多线程程序
感受多线程程序和普通程序的区别:
- 每个线程都是⼀个独⽴的执⾏流
- 多个线程之间是"并发"执⾏的.
import java.util.Random;
public class ThreadDemo {
private static class MyThread extends Thread {
@Override
public void run() {
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停⽌运⾏ 0-9 秒
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
// 随机停⽌运⾏ 0-9 秒
e.printStackTrace();
}
}
}
}
使⽤ jconsole 命令观察线程
1.3创建线程
⽅法1继承Thread类 继承Thread来创建⼀个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
创建MyThread类的实例
MyThread t = new MyThread();
调⽤start⽅法启动线程
t.start(); // 线程开始运⾏
⽅法2实现 Runnable 接⼝
- 实现Runnable接⼝
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
- 创建Thread类实例,调⽤Thread的构造⽅法时将Runnable对象作为target参数.
Thread t = new Thread(new MyRunnable());
- 调⽤start⽅法
t.start(); // 线程开始运⾏
对⽐上⾯两种⽅法:
- 继承Thread类,直接使⽤this就表⽰当前线程对象的引⽤.
- 实现Runnable接⼝,this表⽰的是 MyRunnable 的引⽤.需要使⽤Thread.currentThread()
其他变形
- 匿名内部类创建Thread⼦类对象
// 使⽤匿名类创建 Thread ⼦类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
- 匿名内部类创建Runnable⼦类对象
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
- lambda表达式创建Runnable⼦类对象
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
});
1.4多线程的优势-增加运⾏速度
可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
- 使⽤ System.nanoTime() 可以记录当前系统的纳秒级时间戳.
- serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算.
public class ThreadAdvantage {
// 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使⽤并发⽅式
concurrency();
// 使⽤串⾏⽅式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利⽤⼀个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运⾏结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串⾏: %f 毫秒%n", ms);
}
}
并发:399.651856毫秒
串⾏:720.616911毫秒
2.Thread类及常⻅⽅法
Thread类是JVM⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的Thread对象与之关联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽Thread类的对象就是⽤来描述⼀个线程执⾏流的,JVM会将这些Thread对象组织起来,⽤于线程调度,线程管理。
2.1Thread的常⻅构造⽅法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2Thread的⼏个常⻅属性
- ID是线程的唯⼀标识,不同线程不会重复
- 名称是各种调试⼯具⽤到
- 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
- 优先级⾼的线程理论上来说更容易被调度到
- 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
- 是否存活,即简单的理解,为run⽅法是否运⾏结束了
• 线程的中断问题,下⾯我们进⼀步说明
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} System.out.println(Thread.currentThread().getName() + ": 我即将死去")
});
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());
}
}
2.3启动⼀个线程-start()
之前我们已经看到了如何通过覆写run⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。
- 覆写run⽅法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把李四、王五叫过来了
- ⽽调⽤start()⽅法,就是喊⼀声:”⾏动起来!“,线程才真正独⽴去执⾏了。
调⽤start⽅法,才真的在操作系统的底层创建出⼀个线程.
2.4中断⼀个线程
李四⼀旦进到⼯作状态,他就会按照⾏动指南上的步骤去进⾏⼯作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如⽼板突然来电话了,说转账的对⽅是个骗⼦,需要赶紧停⽌转账,那张三该如何通知李四停⽌呢?这就涉及到我们的停⽌线程的⽅式了。
⽬前常⻅的有以下两种⽅式:
- 通过共享的标记来进⾏沟通
- 调⽤interrupt()⽅法来通知
⽰例-1:使⽤⾃定义的变量来作为标志位.
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: 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
Thread内部包含了⼀个boolean类型的变量作为线程是否被中断的标记.
- 使⽤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.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种⽅式通知收到的更及时,即使线程正在sleep也可以⻢上收到。
2.5等待⼀个线程-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("王五⼯作结束了");
}
}
2.6获取当前线程引⽤
这个⽅法我们已经⾮常熟悉了
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7休眠当前线程
也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}