线程8大核心基础

思维导图
1线程8大核心基础.png

实现多线程

实现多线程的官方正确方式:2种

方法一:实现Runable接口
方法二:继承Thread类

两种方法实现原理对比

方法一和方法二,也就是“实现Runnable接口并传入Thread类”和“继承Thread类然后重写run()”在实现多线程的本质上,并没有区别,都是最终调用了start()方法来新建线程。这两个方法的最主要区别在于run()方法的内容来源:

  1. @Override
  2. public void run() {
  3. if (target != null) {
  4. target.run();
  5. }
  6. }

方法一:在Thread.run()中调用Runable.run();
方法二:Thread.run()整个被重写

两种方法的优劣对比

实现Runable接口更好
1、代码架构、结偶
实现Runable接口把业务代码与线程进行了结偶
2、资源节约
继承Thread类,每次都需要新建一个独立线程
使用Runable,可以利用线程池,大大减小创建线程、销毁线程带来的损耗
3、扩展性
继承Thread类,由于java单继承无法再继承其他类,扩展性差

总结:最精准的描述

通常我们可以分为两类,Oracle官网就是这么说的
准确的讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有2种方式

启动线程

Start()方法含义

启动新线程:通知JVM在有空闲时就启动新线程,何时启动是有线程调度器决定
准备工作:让自己处于就绪状态(获取到除CPU以外的其他资源,设置了上下文、栈、线程状态、PC,PC是寄存器指向了程序运行的位置)
不能重复执行start()

start()源码解析

启动新线程检查线程状态
加入线程组
调用start0()

停止线程

原理介绍

使用interrupt来通知那个线程,以及被通知线程如何配合停止线程,而不是强制
**
在Java中,最好的停止线程的方式是使用中断interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。

Java没有提供任何机制来安全地终止线程。但它提供了中断( Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。

通常线程会在什么情况下停止

1.run方法所有代码运行完毕
2.运行出现异常

正确的停止方法:interrupt

普通情况

  1. public class RightWayStopThreadWithoutSleep implements Runnable{
  2. @Override
  3. public void run(){
  4. //如果线程调用了interrupt()方法来请求停止线程,当前线程可以处理停止线程请求也可以不处理
  5. //Thread.currentThread().interrupted() 该方法为获取是否请求停止线程
  6. while(true && !Thread.currentThread().interrupted()){
  7. Log.d("打印一次");
  8. }
  9. }
  10. public static void main(String[] args){
  11. Thread thread=new Thread(new RightWayStopThreadWithoutSleep());
  12. thread.start();
  13. thread.sleep(1000);
  14. thread.interrupt();
  15. }
  16. }

线程可能被阻塞
在sleep状态下,调用interrupt()方法,线程将抛出InterruptedException异常,并清除interrupted标记位,即Thread.currentThread().interrupted=false

  1. public class RightWayStopThreadWithSleep implements Runnable{
  2. @Override
  3. public void run(){
  4. try{
  5. Thread.sleep(1000);
  6. }catch(InterruptedException e){
  7. e.printStackTrce();
  8. }
  9. }
  10. public static void main(String[] args){
  11. Thread thread=new Thread(new RightWayStopThreadWithSleep());
  12. thread.start();
  13. thread.sleep(500);
  14. thread.interrupt();
  15. }
  16. }

如果线程在每次迭代后都阻塞
如果调用了线程interrupt()方法后,在run方法里执行Thread.sleep()将抛出InterruptedException异常

  1. public class RightWayStopThreadWithSleepEveryLoop implements Runnable{
  2. @Override
  3. public void run(){
  4. try{
  5. while(true){
  6. Log.d("打印一次");
  7. Thread.sleep(10);
  8. }
  9. }catch(InterruptedException e){
  10. e.printStackTrce();
  11. }
  12. }
  13. public static void main(String[] args){
  14. Thread thread=new Thread(new RightWayStopThreadWithoutSleep());
  15. thread.start();
  16. thread.sleep(1000);
  17. thread.interrupt();
  18. }
  19. }

实际开发中的两种最佳实践

1、优先选择:传递中断(方法中的异常应该往上层调用方抛出)
2、不想或无法传递:恢复中断
如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。

3、不应屏蔽中断

正确停止带来的好处

保证数据的完整性

错误的停止方法

被弃用的stop,suspend和resume方法(无法保障线程退出数据的安全性)
用volatile设置boolean标记位(不够全面,不适用与阻塞的场景)

线程的生命周期

线程的一生-6个状态(生命周期)

New:创建还未启动,通过new 创建了Thread 但还未调用start()方法
Runnable:可运行状态,调用start()后将进入Runable状态,不管是否获取到CPU资源
Blocked:由synchronized 修饰的代码,未获取到锁时,将阻塞等待其他线程释放锁
Waiting 等待其他线程唤醒
Timed Waiting 等待其他线程唤醒,记时等待
Terminated 已终止状态

状态间的转化图示

截屏2020-01-20上午10.32.46.png

阻塞状态是什么

一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态
不仅仅时Blocked

状态转换特殊情况

从Object.wait()状态刚被唤醒时,通常不能立刻抢到monitor锁,那就会从Waiting先进入到Blocked状态,抢到锁后再进入到Runnable状态
如果发生异常,可以直接跳到终止Terminated状态,不必再遵循路径,比如可以从Waiting直接到Terminated状态

Thread和Object类中和线程相关的重要方法

方法概览

截屏2020-01-20上午11.27.10.png

wait、notify、notifyAll 方法详解

属于Object类
必须执行在synchronized修饰的代码块中,即必须先拥有monitor
notify唤醒其中的一个等待线程
notifyAll唤醒所有等待线程,由JVM调度执行

wait原理
1、Entry Set 为Runnable的状态的线程,由JVM调度执行
4、Wait Set 为等待唤醒线程
紫色线程,唤醒Wait Set 后,Wait Set里的线程将变为Runnable状态
5、唤醒后的线程,由JVM调度执行
**
截屏2020-01-20下午1.56.49.png

sleep方法详解

sleep可以让线程进入Waiting状态,
并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,
休眠期间如果被中断,会抛出异常并清空中断状态

join方法

作用:因为新的线程加入了我们,所以我们要等他执行完再出发
用法:main等待Thread1执行完毕,注意谁等谁
在join期间线程是Waiting状态

yield方法

作用:释放我的CPU时间片
定位:JVM不保证遵循

获取当前执行线程的引用:Thread.currentThread()方法

start和run 方法

stop,suspend,resume方法

线程属性

截屏2020-01-20下午6.27.30.png

守护线程

作用:给用户线程提供服务
特性:

  • 线程类型默认继承自父线程(在守护线程中创建出来的线程默认就是守护线程,在用户线程创建出来的默认就是用户线程,主线程就是用户线程)
  • 被谁启动(通常是由JVM自己启动的)
  • 不影响JVM退出(用户线程都结束了,JVM将结束该进程)

守护线程和普通线程的区别

整体无区别
位于区别在于是否影响JVM退出

线程优先级

10个级别,默认为5
程序设计不应该依赖优先级
1)不同操作系统优先级不一样
2)优先级会被操作系统改变

线程未捕获的异常

java异常体系

截屏2020-01-20下午7.20.43.png

Error、Exception都继承自Throwable
Error是java运行时系统内部错误,我们无法通过try catch来处理
Exception 异常
RuntimException 运行时异常,编译器无法预测,统称非受检查异常
除RuntimeException以外的其他异常,统称受检查异常

如何处理全局异常

在主线程中调用如下方法,给主线程未处理异常设置处理器,子线程出现未捕获异常时,会先获取父线程异常处理类进行处理,在调用子线程的,如都未发现异常处理类,最后将异常抛出
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() )

线程安全

线程安全定义

《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。

这句话的意思是:不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。

相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的。

运行结果错误:a++多线程下出现运行结果错误

  1. /**
  2. * 描述: 第一种:运行结果出错。 演示计数不准确(减少),找出具体出错的位置。
  3. */
  4. public class MultiThreadsError implements Runnable {
  5. static MultiThreadsError instance = new MultiThreadsError();
  6. int index = 0;
  7. static AtomicInteger realIndex = new AtomicInteger();
  8. static AtomicInteger wrongCount = new AtomicInteger();
  9. static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
  10. static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
  11. final boolean[] marked = new boolean[10000000];
  12. public static void main(String[] args) throws InterruptedException {
  13. Thread thread1 = new Thread(instance);
  14. Thread thread2 = new Thread(instance);
  15. thread1.start();
  16. thread2.start();
  17. thread1.join();
  18. thread2.join();
  19. System.out.println("表面上结果是" + instance.index);
  20. System.out.println("真正运行的次数" + realIndex.get());
  21. System.out.println("错误次数" + wrongCount.get());
  22. }
  23. @Override
  24. public void run() {
  25. marked[0] = true;
  26. for (int i = 0; i < 10000; i++) {
  27. try {
  28. cyclicBarrier2.reset();
  29. cyclicBarrier1.await();
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. } catch (BrokenBarrierException e) {
  33. e.printStackTrace();
  34. }
  35. index++;
  36. try {
  37. cyclicBarrier1.reset();
  38. cyclicBarrier2.await();
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. } catch (BrokenBarrierException e) {
  42. e.printStackTrace();
  43. }
  44. realIndex.incrementAndGet();
  45. synchronized (instance) {
  46. if (marked[index] && marked[index - 1]) {
  47. System.out.println("发生错误" + index);
  48. wrongCount.incrementAndGet();
  49. }
  50. marked[index] = true;
  51. }
  52. }
  53. }
  54. }


活跃性问题:死锁、活锁、饥饿

对象发布和初始化的时候的安全问题

什么是发布:将对象提供给外部访问
什么是逸出:
方法返回一个private对象(private本意是不让外部访问)
还未完成初始化(构造函数还未执行完毕)就把对象提供给外界
1)在构造函数中未初始化完毕就this赋值
2)隐式逸出-注册监听事件
3)构造函数中运行线程

解决逸出
返回private对象的副本,这样外部就无法修改原始类
使用工厂模式,在初始化完毕后,在进行发布

各种需要考虑线程安全的情况

  • 访问共享变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write(读取->修改->写入)、check-then-act(检查->执行)
  • 不同数据之间存在绑定关系的时候
  • 我们使用其他类时,如果该类未声明自己是线程安全的

性能问题

性能问题有哪些体现、什么是性能问题

访问慢

为什么多线程会带来性能问题

调度:上下文切换

  • 什么是上下文
  • 缓存开销
  • 何时会导致密集的上下文切换

协作:内存同步

什么时候会发生线程调度?

就是可运行线程数,超过了cpu数量,操作系统就要调度线程,以便让所有线程都有机会运行

什么是上下文?:保存现场

线程切换时,保存的线程状态,下次切换回来所必须的内容,就是上下文,主要包括(线程目前执行到那一个指令了,指令位置)主要是跟寄存器相关的

什么是上下文切换

上下文切换可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行以下活动:

  1. 挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处
  2. 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程

缓存开销:缓存失效

程序很大概率会访问,之前访问过的数据,cpu为加快执行的速度,会根据不同算法做很多预测,把数据缓存到cpu里面,这样下次使用时,很快就能使用到了。
但是进行上下文切换,cpu执行不同线程的不同代码,原先缓存就没有价值了,所以cpu需要重新进行缓存,这导致线程被调度后,开始的执行速度会有点慢,因为之前的缓存大部分失效了,所以cpu为防止过于频繁的上下文切换带来过于大的缓存开销,通常会设置一个最小执行时间,以防止开销大于切换带来的收益

何时会导致密集的上下文切换:锁、IO

频繁的竞争锁,或者由于IO读写等原因导致频繁阻塞

协作:内存同步

比如使用synchronized、volatile,使子线程数据失效同步主线程带来的开销,这样就无法在cpu中的缓存必须使用主存,这就降低了效率