资讯详情 【JavaEE】多线程初阶
📅 2026/10/14 3:02:41
第一章 多线程初阶文章目录第一章 多线程初阶前言一、进程和线程的关系(面试)二、创建线程方式1.继承Thread2.实现Runnable3. 使用匿名内部类--继承Thread4. 使用匿名内部类--Runnable5. lambda表达式其他三、线程常见方法和属性四、后台线程创建提问五、线程终止/中断1. 设置标志位2. 用interrupt设置标志位下划线六、线程等待1.获取线程引用七、线程休眠八、线程状态九、线程安全(面试)1.synchronized2. volatile十、wait notify十一、多线程编程案例1. 单例模式2. JDBC案例3. 阻塞队列BlockingQueue十二、线程池十三、案例1. 自己实现线程池2. 定时器总结前言JavaEE 是Java开发web后端的技术体系一、进程和线程的关系(面试)线程是啥:轻量级进程.线程也是操作系统调度执行的基本单位.和进程的关系:一个进程包含多个线程和线程区别每个进程有自己独立的资源进程和进程之间不能共享;同一个进程的线程之间共享相同的资源;进程之间不会相互影响一个进程挂了其他进程不会有事;同一个进程线程挂了可能把其他线程一起带走;进程之间通常(不绝对)不会有“资源冲突”情况.同一个进程的线程之间特别容易出现资源访问冲突;进程是操作系统资源分配的基本单位线程是操作系统调度执行的基本单位.Java生态中不鼓励使用多进程编程非常鼓励多线程的~~二、创建线程方式1.继承Thread线程的相关操作 , 操作系统提供了对应的API , JAVA标准库对操作系统线程的API统一封装 ---- Thread先继承Thread , 再重写run方法(main是Java入口 , run就是线程入口)创建实例 , 启动线程用start调用操作系统API在系统内部创建一个线程这个线程被创建好之后就会自动执行上面重写的run这样一类方法 , 你写了由别人给你调用 , 该方法 / 函数成为回调函数(callback)classMyThreadextendsThread{Overridepublicvoidrun(){while(true){System.out.println(hello thread);}}}用两个循环来体会多线程main方法自身也对应一个线程 , start 又创建了一个线程publicclassdemo1{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadtnewMyThread();t.start();while(true){System.out.println(hello class);}}多线程编辑中 , 没有父/子线程说法 , 只有主线程一般线程这样的说法转太快了 , 我们可以让他休息一下 , sleep是Thread的静态方法 , sleep起到阻塞作用try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}为什么sleep在main里面有两个办法 , 在run里面只有一个方法重写中要求 ,父类子类方法标签名完全一致(包括方法名 , 参数列表 , 声明的抛出异常)线程的调度是操作系统负责 , 操作系统的调度过程可以认为是随机的 , 因此不一定是交替的调试的时候也可以看到所包含的线程2.实现RunnableThread就是给线程执行的Runnable不关心 ,实现这个接口创建一个runnable实例 , 创建Thread对象 , 把前面的传进这里 , 再去调用Thread的startclassMyRunnableimplementsRunnable{Overridepublicvoidrun(){while(true){System.out.println(hello Runnable);try{Thread.sleep(1000);}catch(InterruptedExceptione){}}}}publicclassDemo2{publicstaticvoidmain(String[]args)throwsInterruptedException{MyRunnablernewMyRunnable();ThreadtnewThread(r);t.start();while(true){System.out.println(hello main);Thread.sleep(1000);}}}runnable这样的写法更加解耦合3. 使用匿名内部类–继承Thread继承Thread , 使用匿名内部类方便 , ,没有名字 , 一次性记住调用start才是真正创建线程publicstaticvoidmain(String[]args){ThreadtnewThread(){publicvoidrun(){while(true){System.out.println(hello thread);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}};t.start();while(true){System.out.println(hello main);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}4. 使用匿名内部类–Runnable此处针对Runnable定义匿名内部类 , 返回结果是一个Runnable的引用 , 把这个传入Threadpublicstaticvoidmain(String[]args){ThreadtnewThread(newRunnable(){publicvoidrun(){while(true){System.out.println(hello thread);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}});t.start();while(true){System.out.println(hello main);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}5. lambda表达式线程捕获规则publicstaticvoidmain(String[]args){ThreadtnewThread(()-{while(true){System.out.println(hello thread);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});t.start();while(true){System.out.println(hello main);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}其他Java约定 , 一个Thread对象和一个操作系统中的线程, 是一一对应的关系因此一个Thread对象就只能strat一次想要复用就重新创建对象三、线程常见方法和属性线程起名字Threadt1newThread(()-{while(true){System.out.println(hello t1);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}},t1);也就是方法括号里 , 逗号后面就是写名字的四、后台线程创建main线程包括代码中手动创建的线程默认都是前台线程后台线程结束不会阻止线程结束可以设置后台线程thread.setDaemon(true);publicstaticvoidmain(String[]args){Threadt1newThread(()-{for(inti0;i5;i){System.out.println(hello t1);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println(t线程结束);});t1.setDaemon(true);t1.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println(主线程结束);}提问如果成员变量改为局部变量能否达到一样效果?publicstaticvoidmain(String[]args){Threadt1newThread(()-{for(inti0;i5;i){System.out.println(hello t1);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println(t线程结束);});t1.setDaemon(true);t1.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println(主线程结束);}改为局部变量编译不通过 , 触发语法变量捕获 , 捕获的变量必须是final或事实final , 匿名内部类必须是事实final , 由于后续的地方要改running , running不再是事实final写成成员变量情况不一样 , 触发的语法是内部类访问外部类 ,外部类的成员可以被内部类随时进行访问五、线程终止/中断1. 设置标志位publicclassDemo8{privatestaticbooleanrunningtrue;publicstaticvoidmain(String[]args){ThreadtnewThread(()-{while(running){System.out.println(hello thread);try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println(t线程结束);});t.start();ScannerscannernewScanner(System.in);System.out.println(输入0退出);intiscanner.nextInt();if(i0){runningfalse;}}}2. 用interrupt设置标志位此方法不光可以设置标志位 , 还能唤醒sleep等导致线程阻塞的办法 , 会使sleep抛出异常cur和t指向同一个对象 ,不能直接使用tpublicstaticvoidmain(String[]args){Threadt1newThread(()-{ThreadcurThread.currentThread();while(!cur.isInterrupted()){System.out.println(hello thread);try{Thread.sleep(100_000);}catch(InterruptedExceptione){thrownewRuntimeException(e);break;}}});t1.start();ScannerscannewScanner(System.in);System.out.println(输入0退出);intnscan.nextInt();if(n0){t1.interrupt();}}改进 在实际开发中 , catch中一般不会再次抛出异常 , 那怎么修改呢让他打印 , 而不是抛出异常throw new RuntimeException(e);改成e.printStackTrace();可运行的时候为什么不会中断呢一旦唤醒了sleep , sleep会清标志位 , 把当前线程中断的位置重新设置回false那怎么正常退出呢 , 直接break下划线可以把整数常量加上下划线_ , 也就是分隔符 , 提高可读性100000改成100_000 , 不会改变原意六、线程等待执行发现得到的结果是t线程执行之前的 , 执行顺序是概率的 , 那加一个sleep , 等待t线程结束 , 但时间不好评估 ,使用join是最优解join 的等待时间是不确定的 , 取决于t何时结束1.获取线程引用我们也可以让t等待main线程 , 获取main指向那程序一直不推出怎么办可以加数字 , 文档里面可以看到一般用第二种 , Windows/Linux很难做到ns级别join等待 , t线程确实没有执行完 , 如果调用join之前t已经执行完了 , 在其调用此时不会阻塞七、线程休眠sleep(0)线程主动放弃cpu(放权)八、线程状态NEW: 安排了工作, 还未开始⾏动• TERMINATED: 工作完成了.• RUNNABLE: 可工作的. ⼜可以分成正在⼯作中和即将开始⼯作.• BLOCKED: 这⼏个都表⽰排队等着其他事情 , 特指由于锁引起的阻塞• WAITING: 这⼏个都表⽰排队等着其他事情 , 死等进入阻塞状态 , join• TIMED_WAITING: 这⼏个都表⽰排队等着其他事情 , 带有超时间的阻塞状态九、线程安全(面试)一个进程的多个线程共享同一分内存资源 , 如果两个线程都尝试改变某个变量 , 就可能出现冲突线程安全问题的原因1.[根本原因]操作系统对于线程的调度是随机的~(没有办法去应对)2.两个线程针对同一个变量进行修改操作3.修改操作不是原子的~4.内存可见性5.指令重排序1.synchronized处理从修改操作不是原子的入手锁的核心原则:互斥 , 独占对应操作 加锁 , 解锁synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待synchronized 几个要点:1.进入{就是加锁进入}就是解锁2.加锁操作是会防止其他线程插队不影响本线程调度出cpu(和临界区做区分)3.锁对象两个线程针对同一个对象加锁才会有锁竞争~~不同对象则不会有具体操作synchronized(obj){代码}synchronized 几个要点:1.进入{就是加锁进入}就是解锁2.加锁操作是会防止其他线程插队不影响本线程调度出cpu(和临界区做区分)3.锁对象两个线程针对同一个对象加锁才会有锁竞争不同对象则不会有synchronized其他写法synchronized 修饰一个实例方法 , 给this加锁synchronized 还可以修饰一个静态方法 , 针对类对象加锁无论哪种写法, synchronized针对啥对象加锁不重要 , 重要的是两个线程是否针对同一个对象加锁死锁涉及到的情况一个线程一把锁 , 连续多次加锁(可重入锁直接解决了两个线程两把锁 , 钥匙锁在车里 , 车钥匙锁家里了n个线程m把锁死锁的必要条件(背下来 , 面试4. 锁是互斥的(锁的基本特点5. 锁不可被抢占(也是基本特点6. 请求和保持(取决于代码的基本结构 , a线程在获取到lok1的情况下保持lok1的状态 , 再尝试获取lok27. 循环等待/环路等待 , 钥匙锁在车里 , 车钥匙锁家里了解决打破请求和等待 , 避免锁的嵌套打破循环等待 , 约定加锁顺序2. volatile处理内存可见性问题Demo16中出现的问题 , 为什么修改了可是代码没有反应在此前编译器flag每次读到的都是相同的值 , 编译器无法分析出另一个线程的执行时机 , 因此编译器有一个大胆的决定 , 把load操作优化掉 , 后续的循环只从寄存器中读取flag我们使用关键字volatile , 用这个关键字修饰某个变量 , 此时编译器知道了这个变量容易变 , 此后编译器针对这个变量读写操作就不会涉及到优化了加上sleep并把voilate删掉问题不见了 ,为什么内存可见性问题本质是编译器优化导致的 , 因为引入sleep , 这个代码中load没有被编译器优化掉 , 因为sleep背后涉及的指令消耗时间比load要多很多 , sleep是性能瓶颈 , 把load干掉于事无补 , sleep背后的调度过程是省略不了的十、wait notifyjoin只影响线程结束的顺序那如果执行的过程中就能按照一定顺序进行一个影响 , 那怎么办呢需要用到我们的wait和notify线程t1执行到wait阻塞 , 在线程t1中遇到notify唤醒这个机制也可以应对线程饿死问题wait和notify是object类的对象 , 不是Thread的方法 , 可以使用任意对象调用wait必须放到synchronized进行使用 , notify也是加锁的对象必须和调用wait的对象使用一个wait会等待和释放锁 , 最后再拿回来notify不会释放以上是死等版本 , 也有带有超时时间的版本notify也有一个版本 , notify如果有若干个等待线程 , 随即唤醒其中一个notifyAll唤醒所有wait和sleep的区别都可以让线程阻塞 , 都可以指定线程阻塞时间wait的设计是为了被notify , 超时间只是后手 ; sleep的设计就是为了按照一定时间阻塞wait必须搭配锁使用 , sleep不需要wait一进来会先释放锁 , 在获取到锁 , sleep防到锁内部 , 休眠时就不会释放锁wait虽然也可以通过interrupt唤醒 , 实际上更希望通过notify唤醒(正常情况) , notify唤醒之后还可以随时再wait , 再notify , sleep和interrupt就不是了 , interrupt是可以把线程终止掉十一、多线程编程案例1. 单例模式可以保证一个Java进程中某个类只有唯一一个实例 , 实例在这里指类在这个进程中只能有一个对象 , 不允许创建多个某个类 , 本身从需求功能上一个实例就够了 , 完全可以使用单例模式饿汉模式 : 类加载的时候创建对象懒汉模式 : 首次使用的时候创建实例通常认为懒汉模式更好 , 避免启动就占用大量内存正题 : 这两种模式下多线程是否线程安全饿汉模式天然就是线程安全的 , getlnstance中只是进行读操作 , 多线程对同一个变量进行读取 , 没有问题懒汉模式线程不安全 , 赋值和读取放在一起不一定是原子 , 可能会插队执行为解决这一问题 , 我们通过加锁这一方式加完锁了 , 但是每次执行的时候都要加一下锁 , 会产生阻塞在if外面再加一个锁一个这个判断是否要加锁 , 一个判断是否创建实例双重if , 避免枷锁开销指令重排序也是编译器的优化手段调整指令的顺序 , 从而使代码的效率更高锁只是在t1执行到锁的部分的时候, 线程还是可以同时调用t2的 , t2不能进入自己锁的部分在这个例子里 , 还是有可能出现线程安全问题 , 指令重排序instance new SinglentontLazy();这一行, 指令顺序变为分配内存后 , 先引用赋值在初始化 , 引用赋值完后 , 就直接调用t2的线 , t2判断其不为空了跳过括号里面内容直接开始执行下面的代码 , 进一步使用未初始化对象 , 使逻辑出现一些问题解决方法加volatile编写这段代码的时候涉及三个要到点加锁双重if判定volatile避免指令重排序产生的问题2. JDBC案例微信3. 阻塞队列BlockingQueue也是先进先出和其他队列的区别线程安全阻塞功能 a. 队列为空 , 尝试出队列 , 产生阻塞 , 知道队列不为空 b.队列为满 , 尝试入队列 , 产生阻塞 , 直到队列不满后端开发中这个东西非常重要非常常用有大佬把这样的阻塞队列数据结构单独提取出来封装成了一组服务器消息队列MessageQueue(MQ)生产者消费者模型能够产生的作用~~1)降低资源的竞争2)解耦合3)削峰填谷弊端 :对于单个请求响应时间的影响服务器结构更复杂Java标准库提供了阻塞队列 , 后面有容量值把sleep加到消费者里面后 , 生产者满了之后 , 消费一个才能继续生产一个现在我们自己造一个阻塞队列后续使用wait的时候使用while , 防止使用完if条件还是不成立十二、线程池线程虽然相较于进程更加轻量 , 但是线程创建销毁开销不能忽略不计 , 为了进一步提高效率削减这里的开销 , 于是引入线程池线程池就是提前把线程创建好 , 在创建线程 , 就不需要调用操作系统的API来获取(这会进操作系统内核执行逻辑 , 不可控) , 从池子里是用户态线程池构造函数参数都代表什么(经典面试题)ThreadPoolExecutor ( int corePoolSize, int maximumPoolSize, , long keepAliveTime, TimeUnit unit, BlockingQueue Runnable workQueue, TreadFactory threadFactory. RejectedExecutionHandler )1)自动扩容缩容int corePoolSize : 核心线程数 , 线程数最小是这些int maximumPoolSize : 最大线程数核心线程数非核心线程数2)描述了非核心线程释放时机long keepAliveTime : 非核心线程允许空闲的最大时间TimeUnit unit : 相当于枚举类型 , 选择时间单位 , 毫秒什么的3)工作队列BlockingQueue Runnable workQueue : 阻塞队列 , 放任务的 , 队列为空的时候阻塞 , 满的时候通过拒绝策略处理 , 而不是简单粗暴的阻塞4)线程工厂TreadFactory threadFactory : 工厂模式的体现 , 给线程池的线程统一初始化提供了方案 , 就是Thread类的工厂类 , 这个类就会提供一个/N个工厂方法 , 创建出Thread对象工厂模式当我们需要用两个构造方法但是参数的类型个数都一样的时候通过一个工厂类 , 在类里面提供一个工厂方法去创建一个指定对象 , 创建对象的时候可以通过一个普通的构造方法new出来 , 再通过一系列set来设置这里的属性微信里面有具体代码5)拒绝策略 (经典面试题)RejectedExecutionHandler线程池中有任务队列 , 任务队列长度是有上限的 , 任务达到上限 , , 继续尝试添加任务会触发拒绝策略四种策略任务队列满 , 继续添加的时候 , 抛出异常 , 处理不好 , 程序直接崩溃莫个线程直接调用submit提交任务 , 由于队列满了 , 新任务无法加入到队列中 , 看可以让调用submit的这个线程负责执行这个任务把任务队列中最老的任务给丢了 , 然后空余位置留给新任务吧任务队列最新任务丢掉 , 把空余位置留给最新任务如果按照阻塞的方法来处理 , 接下来新的请求就会处理不了 , 超时不合适 , 如果采用上面的拒绝策略 , 可以使得新来的任务及时处理ThreadPoolExecutor太麻烦了 , 标准库提供了一个方法Executors , 把ThreadPoolExecutor进一步封装executors是工厂类这两个用法在实际开发中 , 根据项目情况综合考虑十三、案例1. 自己实现线程池这个是创建一定的数目的线程 , 线程里面去while , 都并发的去一直take这个任务2. 定时器Java标准库也提供了定时器的实现 , Timer类Timer这个类内部有专门的线程执行任务 ,内置线程是前台线程 , 因此想要让他退出需要用到cancel方法现在模拟实现一下有地方要注意 , 线程安全问题 , 主方法中 , 我们往里放 , 同时线程又要取出, 有可能出现线程安全问题 , 需要加锁还有地方 , 我们创建新线城的时候 , 有continue , 则会个循环过程会消耗很多CPU , 并且反复获取锁 , 出发县城饿死问题 , 用wait解决 ,如果不用优先级对队列 , 而是用优先级阻塞队列 , 有可能会出现思索死锁情况 , 放入和抓取加锁顺序不一致就循环等待了总结