别在啃厚书了!线程知识点全在这里
一.认识线程1.1什么是线程线程是操作系统能够进行运算调度的最小单位被包含在进程之中是进程中的实际运作单位。一个进程可以包含多个线程所有线程共享进程的资源如内存、文件描述符等但每个线程拥有独立的程序计数器、栈和寄存器组。⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码,main()⼀般被称为主线程Main Thread。1.2为啥要有线程(线程的作用与优势)线程是操作系统能够进行运算调度的最小单位被包含在进程之中是进程中的实际运作单位。线程的出现主要是为了提高程序的执行效率和资源利用率。1.2.1提高程序并发性线程允许程序在同一时间内执行多个任务。通过将任务分解为多个线程可以充分利用多核处理器的计算能力。例如一个网络服务器可以同时处理多个客户端请求每个请求由一个独立的线程处理。1.2.2资源共享与通信效率同一进程内的多个线程共享进程的内存空间和资源线程间的通信比进程间通信更高效。共享内存使得数据交换无需通过复杂的IPC机制减少了系统开销。1.2.3响应性与用户体验(javaEE进阶后面会讲到)在图形用户界面GUI应用中主线程负责界面渲染而工作线程处理耗时操作如文件读写或网络请求。这种分工避免了界面冻结提升了用户体验。1.2.4资源开销较低(重点理解)创建和销毁线程比进程更轻量级,就是更快。线程的上下文切换成本低于进程切换因为线程共享相同的地址空间和系统资源。1.2.5简化复杂任务设计多线程模型可以将复杂任务分解为多个并行执行的子任务。例如数据处理任务可以分割为多个线程并行处理再合并结果显著缩短总执行时间。1.3线程与进程的区别进程是操作系统资源分配的基本单位拥有独立的地址空间、文件描述符、系统资源等PCB属性。每个进程运行在独立的内存空间中相互隔离一个进程崩溃通常不会影响其他进程。线程是CPU调度的基本单位属于进程的一部分共享同一进程的资源如内存、文件等。一个进程可以包含多个线程这些线程共享进程的地址空间和资源。根据以上定义,就是说进程是包含线程的,一个进程至少有一个线程,就是主线程,并且一个进程中只有一个主线程。值得注意的是进程与进程之间不共享内存空间,同一个进程的线程之间共享同一片内存空间。进程是系统分配资源的最小单位,线程是系统调度的最小单位。1.3.1资源占用与开销进程需要独立的内存空间和系统资源创建和销毁进程的开销较大。上下文切换时需要保存和恢复整个进程的状态如内存映射、寄存器等效率较低。线程共享进程的资源创建和销毁线程的开销较小。上下文切换时只需保存线程的寄存器状态效率更高。但线程间共享资源可能导致竞争条件需要同步机制如锁来避免冲突。1.3.2稳定性与隔离性进程具有更强的隔离性一个进程崩溃不会直接影响其他进程。适合需要高稳定性的场景如服务端程序。线程共享进程资源一个线程崩溃可能导致整个进程退出。调试多线程程序更复杂需处理竞态条件和死锁问题。也就是说一个进程出问题了,不会影响其他进程,所以说进程具有较强的隔离性较稳定。但是一个线程出问题,那么该线程所在的进程就可能出问题,导致整个进程崩溃,所以说线程隔离性较弱,不稳定。1.3.3适用场景进程适合需要高隔离性、独立运行的任务例如浏览器中每个标签页作为独立进程运行避免单个页面崩溃影响整个浏览器。线程适合需要高效协作、频繁通信的任务例如Web服务器用多线程处理并发请求共享资源以减少开销。二.创建线程方法一.继承Thread类//创建一个新的类,让这个类继承标准库的Thread类 class MyThread extends Thread{ //重写Thread类中的run方法 Override public void run(){ while(true){ System.out.println(hello thread); //休息1000ms1s //使用sleep方法时会发生中断异常 try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e); } } } } public class Demo1 { public static void main(String[] args) throws InterruptedException { //创建一个类实例 Thread tnew MyThread(); //启动线程 t.start(); //若执行下面代码,线程将不会开启,只会执行run方法 //t.run(); while(true){ System.out.println(hello main); //sleep是静态方法,只能是Thread自己调用,对象不能调用 Thread.sleep(1000); } } }方法二.实现Runnable接口class MyRunnable implements Runnable { Override public void run() { while (true) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo2 { public static void main(String[] args) throws InterruptedException { MyRunnable r new MyRunnable(); Thread t new Thread(r); t.start(); while (true) { System.out.println(hello main); Thread.sleep(1000); } } }方法三.匿名内部类创建Thread子类对象public class Demo3 { //创建匿名内部类来创建一个线程 public static void main(String[] args) throws InterruptedException{ Thread tnew Thread(){ Override public void run(){ while(true){ System.out.println(hello thread); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(); } } } }; //记住,调用start才是真正创建线程 while(true){ System.out.println(hello main); Thread.sleep(1000); } } }方法四.匿名内部类创建Runnable子类对象public class Demo4 { public static void main(String[] args) throws InterruptedException { // Runnable runnablenew Runnable() { // Override // public void run() { // // } // }; // Thread tnew Thread(runnable); Thread tnew Thread(new Runnable() { Override public void run() { while(true){ System.out.println(hello thread); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e);//运行时异常 } } } }); t.start();//开启线程 while(true){ System.out.println(hello main); Thread.sleep(1000); } } }方法五.lambda表达式创建Runnable子类对象(最为常见)public class Demo5 { //用lammbaa表达式创建一个线程 public static void main(String[] args) throws InterruptedException{ Thread tnew Thread(()-{ //里面的内容相当于run方法里面的内容 while(true){ System.out.println(hello thread); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e);//运行时异常 } } }); //上面的都是子线程 //下面的是主线程 t.start(); while(true){ System.out.println(hello main); Thread.sleep(1000); } } }使用lambda表达式创建多个线程public class Demo6 { //使用lambaa表达式创建多个线程 public static void main(String[] args) throws InterruptedException { Thread t1new Thread(()-{ while(true){ System.out.println(hello thread1); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e); } } },线程1); Thread t2new Thread(()-{ while(true){ System.out.println(hello thread2); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e); } } },线程2); Thread t3new Thread(()-{ while(true){ System.out.println(hello thread3); try{ Thread.sleep(1000); }catch(InterruptedException e){ throw new RuntimeException(e); } } },线程3); t1.start(); t2.start(); t3.start(); while(true){ System.out.println(hello main); Thread.sleep(1000); } //接下来上面的四个语句会交替执行 } }三.Thread类及其方法Thread 类是 JVM ⽤来管理线程的⼀个类换句话说每个线程都有⼀个唯⼀的 Thread 对象与之关联。⽤我们上⾯的例⼦来看每个执⾏流也需要有⼀个对象来描述类似下图所⽰⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的JVM 会将这些 Thread 对象组织起来⽤于线程调度线程管理。3.1Thread类常见的方法方法说明Thread()创建线程对象Thread(Runnable target)使用Runnable对象创建线程对象Thread(String name)创建线程对象,并命名Thread(Runnable target,String name)使用Runnable对象创建xianchengThread t1new Thread(); Thread t2new Thread(new Runnable()); Thread t3new Thread(这是我的名字); Thread t4new Thread(new Runnable(),这是我的名字);3.2Thread类常见的属性ID 是线程的唯⼀标识不同线程不会重复•名称是各种调试⼯具⽤到•状态表⽰线程当前所处的⼀个情况下⾯我们会进⼀步说明•优先级⾼的线程理论上来说更容易被调度到•关于后台线程需要记住⼀点JVM会在⼀个进程的所有⾮后台线程结束后才会结束运⾏。•是否存活即简单的理解为 run ⽅法是否运⾏结束了3.3如何获取当前线程的引用方法说明public static Thread currentThread()返回当前线程对象引用public class ThreadDemo { public static void main(String[] args) { Thread thread Thread.currentThread(); System.out.println(thread.getName()); } }3.4如何休眠当前线程也是我们⽐较熟悉⼀组⽅法有⼀点要记得因为线程的调度是不可控的所以这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。public class ThreadDemo { public static void main(String[] args) throws InterruptedException { //throws InterruptedException是因为sleep()方法可能会中断 System.out.println(System.currentTimeMillis()); Thread.sleep(3 * 1000); System.out.println(System.currentTimeMillis()); } }3.5线程中断public static void main(String[] args) { Thread t new Thread(() - { Thread cur Thread.currentThread(); System.out.println(子线程启动准备循环执行任务...); // 核心用 isInterrupted() 控制循环 while (!cur.isInterrupted()) { System.out.println(hello thread); try { // 模拟长时间任务/阻塞操作 Thread.sleep(1000); // 改成1秒方便测试 } catch (InterruptedException e) { // 1. 收到中断信号打印日志 System.out.println(子线程收到中断信号准备退出...); // 2. 关键恢复中断标志位因为sleep被中断时会自动清除标志位 cur.interrupt(); // 3. 释放资源、收尾工作比如关闭文件、网络连接等 System.out.println(正在释放资源...); // 这里可以写你的清理逻辑 // 4. 跳出循环优雅退出 //不需要break因为此时循环条件已经为假了 } } // 循环结束线程收尾 System.out.println(子线程已安全退出 ✅); });下面还有一种写法,不推荐public static void main(String[] args) { Thread tnew Thread(()-{ Thread curThread.currentThread();//获取当前线程的引用 while(!cur.isInterrupted()){ System.out.println(hello thread); try{ Thread.sleep(1000000); }catch(InterruptedException e){ //throw new RuntimeException(e); //0 e.printStackTrace(); //退出之前做一些释放资源工作 } } });上面这个代码Thread.sleep()被中断时的特殊行为线程在sleep()休眠时,如果被调用interrupt()会发生两件事:1.立即抛出异InterruptionException异常2.z自动清除线程的中断标记(把原来的true改成false)所以上面的代码的循环不会停止会一直打印hello thread3.6操作系统管理线程的两个关键队列就绪队列Ready Queue就绪队列是操作系统中用于管理准备执行但尚未获得CPU资源的进程或线程的队列。这些进程已经具备运行条件只需等待CPU调度器分配时间片即可执行。就绪队列通常采用优先级调度、时间片轮转等算法决定下一个运行的进程。进程进入就绪队列的条件包括新创建的进程完成初始化、阻塞状态的进程被唤醒如I/O操作完成、时间片用完的进程被重新调度等。操作系统通过维护就绪队列实现多任务的高效切换。阻塞队列Blocked Queue阻塞队列用于管理因等待某事件如I/O完成、信号量释放等而暂时无法执行的进程。这些进程会主动放弃CPU资源直到外部条件满足后被唤醒。阻塞队列中的进程不参与CPU调度直到其等待的事件发生。进程进入阻塞队列的典型场景包括请求未被满足的资源如文件读写、主动休眠sleep、等待同步信号如锁或信号量等。一旦事件完成进程会被移至就绪队列重新等待调度。核心区别状态依赖就绪队列中的进程仅需CPU资源即可运行阻塞队列中的进程需等待外部事件。调度参与就绪队列参与CPU调度决策阻塞队列不参与调度。转换方向阻塞队列的进程被唤醒后进入就绪队列就绪队列的进程可能因时间片耗尽或主动阻塞而离开。例如在Java的线程模型中Runnable状态对应就绪队列WAITING或BLOCKED状态对应阻塞队列。结合这两个队列解释一下sleep0和sleepsleep(0) 与 sleep() 的区别sleep(0)调用sleep(0)会主动让出当前线程的剩余时间片但不会真正进入休眠状态。操作系统会立即重新调度该线程可能继续执行当前线程如果优先级最高也可能切换到其他就绪线程。适用于需要避免忙等待的场景例如在自旋锁中短暂让出 CPU,就是说主动放弃CPU使用权。sleep()不带参数的sleep()通常会导致语法错误因标准库中sleep需要明确的时间参数如秒或毫秒。例如sleep(1)会让线程休眠至少 1 秒。休眠期间线程完全暂停不参与 CPU 调度直到指定时间结束或信号中断。3.7等待一个线程-join()有时我们需要等待⼀个线程完成它的⼯作后才能进⾏⾃⼰的下⼀步⼯作。例如张三只有等李四转账成功才决定是否存钱这时我们需要⼀个⽅法明确等待线程的结束。public class Demo10 { private static long result0; public static void main(String[] args) throws InterruptedException{ Thread tnew Thread(()-{ for (int i 0; i 100 ; i) { resulti; } System.out.println(t线程计算完毕); }); t.start(); // 给 t 线程留有一定的执行时间. // Thread.sleep(1); // 使用 join 是更优的解法. // join 等待 t 线程结束. //由于线程的随机调度,可能for循环还没有走完就打印result //使用join等待t线程执行完才打印result t.join(); System.out.println(result); } }四.线程的状态4.1观察线程的所有状态线程的状态是⼀个枚举类型 Thread.Statepublic 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: ⼯作完成了.4.2下面的图会好理解一点五.多线程带来的风险-线程安全问题(重点)5.1观察线程不安全public class Demo13 { private static int count 0; //private static Object locker new Object(); //private static Object locker2 new Object(); public static void main(String[] args) throws InterruptedException { // 创建两个线程, 分别对同一个变量进行 5w 次的 操作. // 最终主线程打印结果. Thread t1 new Thread(() - { for (int i 0; i 50000; i) { // synchronized (locker) { count; //} } }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { //synchronized (locker) { count; //} } }); //必须是多个线程共用同一把锁,互斥执行,一个一个来,才能保证线程安全 t1.start(); t2.start(); // 让主线程等待, 等待上述的两个线程结束 t1.join(); t2.join(); System.out.println(count count); } }上面这个代码目的是求0到5000的整数依次两次相加,但结果一直都比正确结果要小。这就是线程问题。5.2线程安全问题一个进程的多个线程,共享同一份内存资源.如果两个线程都尝试修改同一个变量就可能出现冲突使代码出现Bug.5.2.1为什么count会出现问题看似count只进行了一步操作但在CPU中每个操作都对应三个指令。1.load把内存的count加载到寄存器中2.add把寄存器里的数据13.save把寄存器里的数据写回数据并且这三步操作不是原子操作!操作系统可能在任何一步打断线程,切换到另一个线程执行就会导致冲突。举个例子时间点线程t1在做什么线程t2在做什么内存里的count值00初始值1load(count)-把内存里的0读到t1的寄存器里寄存器值002CPU切到t2线程t1线程被挂起load(count)-把内存里的0读到t2的寄存器里寄存器值003CPU切回t104add-t1的寄存器值变成01105save-把t1线程寄存器里的1写回内存内存里的count变为116CPU切回t217add-t2的寄存器里的值变成011注意他之前的load的值是018save-把t2寄存器里的1写回内存,内存里的count还是11t2在t1还没把save写回内存的时候,就已经load了count的值所以t2后续的add和save,都是基于它自己读到的0在操作,而不是t1写回后的1最终两个线程都把1写回内存相当于count被合并成一次结果就是1而不是预期的25.3线程安全的5大根源5.3.1操作系统线程调度是随机的1.线程什么时候上CPU什么时候被切下来完全有操作系统调度决定程序员无法控制2.刚才遇到的count被打断结果被覆盖就是因为调度器在load/add/save三步中间把CPU切给了另一个线程3.这是线程安全问题的“土壤”后面的问题都是在这个基础上发生的5.3.2多个线程同时修改同一个共享变量场景会不会出问题原因单线程修改一个变量没问题没有竞争不会被打断多个线程修改不同变量没问题变量不共享互不干扰多线程同时读取统一变量没问题读取不会改变变量的值不会冲突多线程同时修改同一变量出问题这是线程安全问题的必要条件5.3.3修改操作不是原子的count分三步不是原子性的1.count不是一条CPU指令,而是load-add-save三步操作2.解决方法用锁(synchronized/Lock)或者原子类(AtomicInteger),把这三步变成原子操作中间不被打断5.3.4内存可见性问题线程看不到其他线程的修改1.每个线程都有自己的工作内存寄存器/缓存修改变量时会先写到自己的缓存里再异步刷新到主内存2.这就导致线程A修改里变量的值但线程B看不到读到还是自己缓存里的旧值3.解决方法用volatile关键字修饰变量强制线程每次都从主内存读修改后立刻写回主内存保证所有线程都能看到最新值5.3.5指令重排CPU/编译器打乱代码执行顺序这是CPU/编译器为了优化性能会打乱代码执行顺序只要逻辑结果不变就行了看代码int a1; int b2;CPU可能会先执行b2,再执行a1单线程下结果没问题但多线程下可能会出现意外情况1.线程A执行a1和b2,CPU重排之后先写b22.线程B看到b2,以为a也已经被赋值了结果读到的a还是默认值03.解决方法volatile关键字也能禁止指令重排序保证代码按你写的顺序执行