文章目录
多线程(Thread)
线程定义
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码
线程的意义
首先, 并发编程 成为 刚需其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
使用多线程好处
- 能够充分利用多核CPU,能够提高效率
- 只是创建第一个线程的时候,需要申请资源,后续再创建新的线程,都是共用同一份资源(节省申请资源的开销)销毁线程的时候,也只是销毁到最后一个的时候,才真正的释放资源,前面的进程销毁,都不必真释放资源
线程和进程
- 操作系统内核是通过一组PCB来描述一个进程的,每个PCB对应一个线程. 一个进程至少有一个线程,也可以有多个
- 这一组PCB的内存指针,和文件描述符表其实是同一份东西(也就是文件资源和内存资源)
- 而状态、上下文、优先级、记账信息、则是每个PCB(每个线程)自己有一份
进程和线程的区别🎉
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 线程比进程更轻量,创建更快,销毁也块
- 进程和进程之间不共享内存空间. 同一个进程的多个线程之间共享同一个内存空间
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位
创建线程
线程调度执行的顺序"随机"的
-
继承Thread类
把线程要完成的工作和线程本身耦合在一起
Class MyThread extends Thread{ @Override//重写run方法 public void run(){ //run里面的逻辑就是线程要执行的工作 System.out.println("hello thread!"); } } public class Test{ //如果我们没有创建新的线程,会有默认的线程,就是main方法所在的线程 //也就是主线程 public static void main(String[] args){ //创建MyThread实例 此时并不会真正创建线程 MyThread t = new MyThread(); //调用start方法才会创建出真正的新线程,并执行run里面的代码 //直到代码执行完,新的线程就结束了 t.start(); //main主线程和我们创建的线程是一个并发执行的关系(并发+并行) //并发执行就是两边同时执行,各自执行各自的 } }
-
实现 Runnable 接口
使用Runnable专门表示**“线程要完成的工作”**
class MyRunnable implements Runnable{//实现Runnable接口 @override public void run(){ System.out.println("hello thread!"); } } public class Test{ public static void main(String[] args){ MyRunnable runnable = new MyRunnable(); //创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数. Thread t = new Thread(runnable); t.start(); } }
-
使用匿名内部类,创建Thread子类的方式
public class Test{ public static void main(String[] args){ //创建Thread的子类,同时实例化出一个对象 Thread t1 = new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } }; } t1.start(); }
-
使用匿名内部类,实现Runnable接口方式
public class Test{ public static void main(String[] args){ // 使用匿名类创建 Runnable 子类对象 Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("使用匿名类创建 Runnable 子类对象"); } }); t2.start(); } }
-
Lambda表达式创建 Runnable 子类对象
public class Test{ public static void main(String[] args){ Thread t = new Thread(() ->{ System.out.println("使用匿名类创建 Thread 子类对象"); }); t.start(); } }
Thread常见的方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组.了解即可 |
Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
-
ID 是线程的唯一标识,不同线程不会重复。身份标识可以有多个,不同的环境使用不同的标识。
一个线程:JVM中有个id;在操作系统的线程API有个id;在内核PCB中有个id
-
前后台线程:咱们默认创建的线程是前台线程,前台线程会阻止进程退出。也就是说如果main运行完了,前台线程还没结束,进程不会退出;后台线程不会阻止进程退出,如果main等其他的前台线程都执行完了,这时,即使后台线程没执行完,进程也会退出。
-
线程是否存活,判断内核的线程在不在
Thread对象虽然和内核中的线程是一一对应的关系,但是生命周期并非完全相同
Thread对象创建出来了,内核里的线程还不一定有,调用start方法,内核线程才有;
当内核里的线程执行完了(也就是说run的代码运行完了),内核的线程就销毁了.但是Thread对象还在
启动一个线程start()
调用start()才真正创建了一个线程
注意run()和start()的区别覆写run方法是提供给线程要做的事情,直接调用run并没有创建线程,只是在原来的线程中运行的代码;而调用start()才真正创建了线程,并在新线程中执行代码(和原来的线程是并发的)
线程的中断
-
自己定义一个标志位
public class Test{ private static boolean isQuit = false; public static void main(String[] args) { Thread t = new Thread(()->{ while (!isQuit){ System.out.println("hello thread"); try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t线程执行完了"); }); t.start(); try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } isQuit = true; System.out.println("设置让t线程结束"); } }
-
调用 interrupt() 方法来通知
interrupt()方法的行为有2种
1.线程在运行状态,会Thread.currentThread().isInterrupted()设置标志位为true
2.如果线程因为调用 wait/join/sleep 等方法而阻塞挂起则以InterruptedException 异常的形式通知,清除中断标志。此时我们可以忽略这个异常、也可立即结束线程、也可稍后处理,这就取决于catch中的代码怎么去写(不是没设置标志位,而是wait/join/sleep阻塞方法会清除标志位)
public class Test{ public static void main(String[] args) { Thread t = new Thread(()->{ //Thread.currentThread()是Thread类的静态方法,通过这个方法可以拿到当前线程对应的Thread对象 //isInterrupted就是在判定标志位 while (!Thread.currentThread().isInterrupted()){ System.out.println("hello thread"); try{ Thread.sleep(1000); }catch (InterruptedException e) { //e.printStackTrace(); //方式一立即结束线程 //break; //方式二啥都不做 线程继续执行 //方式三线程稍后处理 //Thread.sleep(1000); //break; } } System.out.println("t线程执行完了"); }); t.start(); try{ Thread.sleep(1000); }catch (InterruptedException e) { e.printStackTrace(); } //在主线程中通过这个方法来中断线程 设置标志位为true来中断线程 t.interrupt(); System.out.println("设置让t线程结束"); } }
线程等待join()
在main中调用join效果就是让main线程阻塞等待,等到t执行完,main才继续执行
如果在调用join之前,t线程已经结束了,此时join不需要阻塞等待.
public class Test {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("main 线程 join 之前");
try {
//在main中调用join效果就是让main线程阻塞等待
//等到t执行完,main才继续执行
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 线程 join 之后");
}
}
休眠当前线程(sleep)
线程的状态
- NEW: Thread对象创建出来了,但是内核的PCB还没创建(还没有真正的创建线程)
- RUNNABLE: 就绪状态(正在CPU上运行+在就绪队列中排队)
- BLOCKED: 等待锁的时候进入的阻塞状态
- WAITING: 特殊的阻塞状态 调用wait
- TIMED_WAITING: 按照一定的时间,进行阻塞 sleep
- TERMINATED: 内核的PCB销毁了,但是Thread对象还在
线程间的相互转换
线程不安全的原因
- 抢占式执行(线程的调度)
- 多个线程修改同一个变量
- 修改操作不是原子的
- 内存可见性
- 指令重排序 编译器对于指令重排序的前提是 “保持逻辑不发生变化”.
synchronized 关键字
synchronized 的特性
-
互斥。某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待如果一个线程A针对一个锁对象进行加锁,加锁就能成功
如果是一个线程A和一个线程B都针对锁对象1进行加锁,谁先来谁就能成功(取决于代码写法),另一个就得阻塞等待。
如果是一个线程A和一个线程B分别针对锁对象1和锁对象2进行加锁,,都可以获取到锁,不会产生阻塞等待.
重点结合代码理解锁对象是谁
class Counter3 { public int count = 0; static private Object Locker = new Object(); public void increase() { //此时locker是一个静态成员(类属性) //类属性是唯一的(一个进程中,类对象只有一个,类属性也只有一份) synchronized(Locker) { count++; } } } public class Demo3 { private static Counter3 counter = new Counter3(); private static Counter3 counter2 = new Counter3(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { //counter2.increase(); counter2.increase(); } }); //虽然counter2和counter3是两个实例 //但是这俩里面的locker其实就是同一个locker 所以会产生锁竞争 t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("counter: " + counter.count); } }
-
刷新内存(存疑)
-
可重入
synchronized基本写法
1.修饰普通方法 锁对象相当于this
2.修饰代码块 锁对象在()指定
3.修饰静态方法 锁对象相当于 类对象(不是锁整个类)
可重入
可重入锁:不会产生死锁的锁
不可重入锁:会产生死锁的锁
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会死锁
可重入锁的实现要点
- 让锁里持有线程对象,记录是谁加了锁
- 维护一个计数器,用来判断什么时候是真解锁、真加锁、以及放行(例如每次加锁计数器++,每次解锁计数器–,当计数器为0的时候真加锁,同样计数器为0的时候真解锁)
volatile 关键字
volatile 修饰的变量, 能够保证 “内存可见性”. 被
volatile禁止了编译器优化,避免了直接读取CPU寄存器(工作内存)中的缓存数据,而是每次都重新读内存(主内存)
内存可见性:在编译器优化的条件下,一个线程把内存给改了,但是另一个线程不能及时感知到
解决方案:避免让编译器做出不读内存的优化
volatile起到的效果是保证内存可见性,但是并不能保证原子性也就是说:
针对一个线程读,一个线程修改,这个场景使用volatile是合适的。
针对两个线程修改,这个场景,volatile是没有办法的
wait和notify
让当前线程进入等待状态
wait是Object的方法
wait的任务:
-
使当前执行代码的线程进行等待
-
释放当前的锁
-
满足一定条件时被唤醒
注意wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
wait结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
notify
notify 方法是唤醒等待的线程
notify也是要包含在synchronized里面的
线程1如果没有释放锁的话,线程2也就无法调用notify(因为锁阻塞等待)
线程1调用了wait,在线程里面就释放锁了,这个时候线程1代码虽然阻塞在synchronized里面
但是此时锁是释放的状态,线程2可以拿到锁
wait和notify代码示例和图解
package Threading;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 刘尚辉
* Date: 2022-10-10
* Time: 14:09
*/
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
// 专门准备一个对象, 保证等待和通知是同一个对象
Object object = new Object();
// 第一个线程, 进行 wait 操作.
Thread t1 = new Thread(() -> {
while (true) {
//先进行加锁
synchronized (object) {
System.out.println("wait 之前");
try {
//调用wait阻塞等待 等待线程2调用notify唤醒线程
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此处写的代码, 一定是在 notify 之后执行的.
System.out.println("wait 之后");
}
}
});
t1.start();
Thread.sleep(500);
// 第二个线程, 进行 notify
Thread t2 = new Thread(() -> {
while (true) {
synchronized (object) {
System.out.println("notify 之前");
// 此处写的代码, 一定是在 wait 唤醒之前执行的
//调用notify唤醒线程1
object.notify();
System.out.println("notify 之后");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
}
执行结果:
关于wait和notify注意点:
- 要保证加锁的对象和调用wait的对象是同一个对象,保证调用wait的对象和调用notify的对象是同一个对象
- 如果t1先调用了wait,t2后调用了notify,此时notify会唤醒wait
- 如果t2先执行了notify,t1后执行了wait,程序并不会达到我们预期的效果
notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程
但是即使notifyAll唤醒了所有的wait,这些线程需要重新竞争锁,所以并不是同时执行的,仍然还是有先后顺序的。