一,认识线程
1.1 什么是线程
一个线程就是一个"执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间"同时"执行着多份代码。同时,线程也是 CPU 真正执行任务的单位。
举个栗子:去奶茶店买奶茶(奶茶店中有自己独立的"程序"让我们拿到奶茶,所以也可把这个看为一个线程),奶茶店有不同岗位员工,每个员工都是独立的"执行流"。如果店里只有一个员工(单线程),那速度将会很慢效率极低;多个员工分工合作(多线程),那效率会得到大幅提高,这就是多线程的优势。
2.2 线程和进程的区别
进程包含线程的 ,每个进程至少有一个线程存在(主线程)
|------|-------------------------------------------|-------------------|
| | 线程(Thread) | 进程 |
| | 线程时cpu上调度执行的基本单位 | 进程是操作系统中资源分配的基本单位 |
| 资源 | 共享进程内存 | 独立内存,互不影响 |
| 开销 | 轻量,快速(只有第一个线程也就是和进程一起创建时,需要申请资源,后续创建则不需要) | 创建/销毁慢,消耗资源 |
| 奔溃影响 | 一个崩溃,可能把同进程内的线程都带崩溃 | 一个崩溃不影响其他 |
二,创建线程
2.1 继承Thread类,重写run方法
class Thread1 extends Thread{
public void run(){
System.out.println("线程运行代码");
}
}
//创建实例
Thread1 t = new Thread();
2.2 实现Runnable接口,重写run方法
clas Runnable1 implements Runnable{
public void run(){
System.out.println("线程运行代码");
}
}
//创建实例
Thread t = new Thread(new Runnable());
2.3 匿名内部类(本质就是方法1,2)
2.3.1 用匿名内部类创建Tread子类对象
Thread t1 = new Thread(){
public void run(){
System.out.println("使用匿名类创建Tread子类对象");
}
}
2.3.2 用匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable(){
public void run(){
System.out.println("使用匿名类创建Runnable子类对象");
}
});
这两个线程对象(
t1和t2)的start()方法都只能调用一次,所以从 "启动线程" 的角度来说,它们是一次性的;但第二种写法里的Runnable任务逻辑,是可以被复用的。
2.4 用lambda表达式(常用)
//使用lambda 表达式创建Runnable 子类对象
Thread t3 = new Thread (() -> System.out.println("使用匿名类创建Thread 子类对象"));
Thread t4 = new Thread (() -> {
System.out.println("使用匿名类创建Thread 子类对象");
});
2.5 实现Callable
// 用匿名内部类创建Callable对象
Callable<String> callableTask = new Callable<String>() {
public String call() throws Exception {
return "Callable任务执行完成,返回结果";
}
};
// 包装成FutureTask,再交给Thread
FutureTask<String> futureTask = new FutureTask<>(callableTask);
Thread t5 = new Thread(futureTask);
2.6 线程池(ThreadFactory)
// 用匿名内部类创建ThreadFactory
ThreadFactory factory = new ThreadFactory() {
private int count = 0;
public Thread newThread(Runnable r) {
// 自定义线程名,方便调试
return new Thread(r, "自定义线程-" + (++count));
}
};
// 创建固定大小线程池
ExecutorService pool = Executors.newFixedThreadPool(3, factory);
// 提交任务(用匿名内部类Runnable)
pool.submit(new Runnable() {
public void run() {
System.out.println("线程池中的任务执行");
}
});
// 关闭线程池
pool.shutdown();
三,Thread类
2.1 常见构造方法
|-------------------------------------------|-----------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
| Thread(ThreadGroup gruop,Runnable target) | 线程可以被用来分组管理,分好的组为线 |
2.2 常见属性
|--------|------------------|-----------------------------|
| 属性 | 获取方法 | 说明 |
| ID | getID( ) | ID是线程的唯一标识,不同的线程不会重复 |
| 名称 | getName( ) | 名称在各种调试工具用到 |
| 状态 | getState( ) | 表示线程当前所处的一个情况 |
| 优先级 | getPriority( ) | 优先级高的线程理论上来说更容易被调度到 |
| 是否后台线程 | isDaemon( ) | JVM会在一个进程的所有非后台线程结束后,才会结束运行 |
| 是否存活 | isAlive( ) | 简单的理解,为run 方法是否运行结束了 |
| 是否被中断 | isInterrupted( ) | 检查线程是否收到中断信号 |
四,线程相关操作
4.1 启动线程 start( )
上面我们介绍了覆写run( )方法创建一个线程对象,但线程对象被创建出来并不意味着线程已经开始运行了,而调用start( )方法,才真的在操作系统的底层创建出一个线程,线程才真正的独立执行
举个栗子:我(主线程)奶茶店店长,我先写了一份完整的"工作流程"(new Thread()),我在纸上写下了做奶茶的步骤(run( ) 方法),这是我自己按照流程做一杯(调用run( ),并不会启动新的线程)。然后我画了个店员的名牌,写着 "店员 A"(创建了线程对象),此时店员A开始按步骤工作(调用start( )),这是店里就我和店员A同时干活,效率提升。
4.2 中断线程
举个栗子:我(主线程)安排店员A(子线程)去做奶茶,3秒之后,我告诉店员A"先不做奶茶了,过来点单",店员A立刻停下做奶茶,去点单。
常见的有以下两种方法:
a)通过共享的标记来进行沟通,使用自定义变量作为标志位(volatile)
b)调用interrupt(方法来通知)
代码实例:
//采用 a)方法
public class demo {
private static class MyRunnable implements Runnable{
//volatile相当于一个主线程和子线程公用的开关
public volatile boolean isQuit=false;
@Override
public void run() {
while(!isQuit){
//如果没有收到停止指令,循环一直满足,开始做奶茶
System.out.println(Thread.currentThread().getName()+":我正在忙着做奶茶");
try{
//做一杯奶茶需要1s,也就是1s打印一次
Thread.sleep(1000);//这里把光标放在sleep下用alt+enter快捷键可以直接生成try,catch
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//不进入while循环也就是收到停止指令时
System.out.println(Thread.currentThread().getName()+":我现在来帮忙点单");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target=new MyRunnable();
Thread thread=new Thread(target,"店员A");
System.out.println(Thread.currentThread().getName()+":A,快去做奶茶");
//让A开始工作
thread.start();
//我去做3秒的事,a接着做奶茶每秒打印一次
Thread.sleep(3000);
//sleep结束,我叫a停止做奶茶
System.out.println(Thread.currentThread().getName()+":A先别做奶茶了,快过来帮忙点单");
target.isQuit=true;
}
}
volatile就是保证:主线程改了isQuit,子线程可以立刻获取 。其核心就是修改了内存可见性问题(内存可见性将下面线程安全产生原因中进行详细讲解)
//采用 b)方法
public class demo2 {
private static class MyRunnable implements Runnable{
@Override
public void run() {
//while(!Thread.isterrupted())
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,"A");
System.out.println(Thread.currentThread().getName()+":快去做奶茶");
thread.start();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+":A先别做奶茶了过来点单");
thread.interrupt();
}
}
| 方法 | 类型 | 作用 | 是否清除中断标志 |
|---|---|---|---|
Thread.currentThread().isInterrupted() |
实例方法 | 检查当前线程的中断状态 | ❌ 不清除 |
Thread.interrupted() |
静态方法 | 检查当前线程的中断状态 | ✅ 会清除(调用后标志位变为false) |
使用Thread.interrupted( )方法来通知线程结束或Thread.currentThread( ).isInterrupted( )代替自定义标志位
在这里thread收到通知有两种方法
1.如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InterruptedException 异常的形式通知,清除中断标志,这个时候可以在catch中加入break;
2.只是内部的一个中断标志被设置,thread 可以通过Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在sleep 也可以马上收到。
4.3 等待线程 join( )
join可以要求多个线程间结束的线程顺序
t.join( )就是等t线程结束在完成别的,只要t不结束就一直等
|-----------------------------------------|--------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等millis毫秒 |
| public void join(long millis,int nanos) | 等待线程结束,精度更高 |
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Runnable target=()->{
for(int i=0;i<3;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,"A");
Thread thread2=new Thread(target,"B");
System.out.println("先让A开始工作");
thread1.start();
thread1.join();
System.out.println("A工作结束,B开始工作");
thread2.start();
thread2.join();
System.out.println("B工作结束");
}
}
这里的抛异常也不需要手动去写,alt+enter快捷键即可
4.4 获取当前线程引用
已经很熟悉了就不再赘述了
public static Thread currentThread( ); //返回当前线程对象的引用
4.5 休眠线程 sleep
因为线程的调度是不可控的,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
|----------------------------------------------------------------------------|----------------|
| 方法 | 说明 |
| public static void sleep(long millis)throws InterruptedException | 休眠当前线程millis毫秒 |
| public static void sleep(long millis,int nanos)throws InterruptedException | 更高精度的休眠 |
这里的抛异常也不需要手动去写,alt+enter快捷键即可
五,线程状态
查看所有线程状态代码:
public class demo4 {
public static void main(String[] args) {
for (Thread.State state:Thread.State.values()) {
System.out.println(state);
}
}
}
代码运行结果:

NEW:安排了工作,还未开始行动
RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.(就绪:线程在cpu上执行;线程随时可以去cpu上执行)
BLOCKED:表示排队等着其他事情(由于锁导致的阻塞)
WAITING:表示排队等着其他事情(死等,无超时时间阻塞等待)
TIMED_WAITING:表示排队等着其他事情(线程阻塞,不参与cpu调度,不继续执行,阻塞时间是有上限的)
**TERMINATED:**工作完成了,内核中的线程已结束,但Thread对象还在

六,线程安全
6.1 什么是线程安全
一段代码,如果多线程环境下代码运行的结果是符合预期的(在单线程环境应该的结果),则说这个程序是线程安全的。
6.2 线程安全问题产生原因
1)操作系统对于线程的调度使抢占式随即执行的(根本原因)
2)多个线程同时修改同一个变量
3)修改操作不是原子的(如果修改操作只是对应一个cpu指令,就认为是原子的)
4)内存可见性问题(内存可见性:即一个线程对共享变量值的修改能够及时的被其他线程看见)
5)指令重排序问题
七,小结
到这两天感冒才稍微好一点,大家一定要注意身体哦!多线程是一个新的内容块,和之前的单线程比麻烦了很多。多敲吧,沉淀ing~~