一、线程有关的基本概念
-- 1、线程安全
什么是线程安全?
线程安全:一个类被多个线程以任意方式同时调用,且不需要外部额外同步和协同的情况下,仍然保持内部数据正确且表现正确的行为,那么这个类就是线程安全的。
线程安全的等级
线程对立,线程兼容,有条件的线程安全,线程安全,不可变。
(1)不可变类:不可变的对象一定是线程安全的。如:final类修饰的不可变类,String,Integer等;enum枚举类。
1、编译javac;反编译jad
2、enum底层是一个final类
3、是不是final修饰的类就是线程安全的呢?不是。final可以使得另一个线程不会访问处于“部分创建”的对象,除了此种情况,线程不安全是可能会发生的。final只能保证值不被覆盖。
(2)线程安全的类
1、线程安全类的任意方法都不会使该对象的数据处于不一致或者数据污染的情况。
2、java.util.concurrent下的类。如ConcurrentHashMap,LinkedBlockingQueue
(3)有条件的线程安全
1、单独的访问类的方法,是线程安全的;复合方法时,不保证。
举例 Vector
Vector复合操作
(4)线程兼容类
1、使用同步
2、使用锁或者synchronized块包含每一个方法
举例:
使用synchronized控制同步访问每个代码块或者类,
如:使用Collections.synchronizedList 来包装一个List使其变成线程安全的。
(5)线程对立
1、外部使用同步也不能保证线程安全
2、比如静态变量
思考:为什么java设计者设计非线程安全类?
-- 2、线程的同步异步,阻塞非阻塞
1、区分同步异步、阻塞非阻塞
2、梳理同步异步的使用场景
3、分清同步异步的优势劣势
同步异步
同步的概念
阻塞式调用,调用方必须等待响应方执行完毕才会返回
同步的使用场景
1、大多数非异步场景(不用异步,就用同步来调用)
如:百度搜索,客户端同步调用服务端搜索接口,等待服务端实时结果
2、在编排的流程中,必须等待拿到响应结果才能去做下一步操作,且在实时链路中相互之间有串联或关联数据的。
如:电商中商品详情页的查询接口的内部实现
异步
非阻塞式调用,立即返回,调用方无需等待响应方返回实际结果,响应方会通过状态、通知或回调来告知调用方
异步使用场景
1、耗时任务,主线程中提交耗时任务到线程池,然后通过Feture来异步获取任务执行结果,这里也可以由异步任务发消息等途径来通知主线程。
1、电商下单链路的非核心链路调用,为了下单的性能考虑,将订单下发的发货仓库等非实时流程放在后续操作,提高下单的响应速度。 如:这里交易系统通过消息自发自接等方式来驱动后续流程
同步异步优劣势比较
| 同步 | 异步 |
优势 | 1、可以拿到实时结果进行处理,上下文信息始终在一个代码块,代码处理上更加方便直观 2、对错误和异常处理可以做到实时
| 1、不影响主流程的执行,降低响应时间,提高应用的性能和效率 2、及时释放系统资源,如线程占用,让系统去做更多有价值的事
|
劣势 | 1、耗时的接口响应会影响整个流程的性能
| 1、为了保障数据最终一致性,需要对账系统去做好监控和保障 2、需要更多异步任务去补偿系统间的数据一致性
|
阻塞非阻塞
阻塞
调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,而会立即返回。
怎么理解同步异步、阻塞非阻塞
同步异步
关注点是得到结果的方式
同步是实时返回结果,异步是通过共享变量、通知消息或回调来得到结果
阻塞非阻塞
关注点在程序在等待调用结果返回时的状态
思考:还有哪些场景下适合使用异步调用,为什么?
-- 3、线程的并发和并行
1、了解并发并行的基本概念
2、区分线程的并发并行
单线程程序
程序执行过程中始终只有一个线程在运行,后面的代码段必须等到前面代码段的任务执行完毕后才能执行,这样的程序可以认为是单线程程序。
多线程程序
程序由多个线程构成,方法内各代码块执行顺序是不确定的,这样的程序可以认为是多线程程序。
并发
定义
逻辑上的同时处理。能处理多个事件,但是并不一定是同时进行的。
说明
这里可能只有一个处理器的多个线程,由系统的调度,让不同的线程在不同的小的时间段内执行各自的线程任务逻辑。
并发并行-举例说明
并发
并行
定义
物理或是实际的同时处理。能在同一时刻处理多个事件。
说明
并行具有并发的含义,并发不一定是并行的,因为不是同时进行。
并发并行-举例说明
两个队列共用咖啡机
小结
- 并行具有并发的含义,并发不一定是并行的
- 多核处理器处理能力强于单核处理能力,且有更少的系统调度和上下文切换
思考:举一个生活中并发并行的场景
-- 4、线程状态及Java中线程常见方法
1、了解线程的状态
2、了解线程不同状态下的切换
3、几个常见的Thread方法用途和示例
线程的状态
看源码
NEW:线程创建但是还没有调用start()方法
RUNNABLE :可运行线程的状态,线程已经在JVM虚拟机中执行,但是可能需要等待操作系统资源,如处理器。RUNNABLE包括RUNNING和READY
BLOCKED :阻塞的线程意味着正在等待监视器锁,来进入或重入synchronized代码块或者方法
WAITING :等待状态,需要其他线程中断或通知来唤醒
TIMED_WAITING :定时等待状态,在指定等待时间后返回,或提前被其他线程中断或通知返回
TERMINATED :终止线程的线程状态,线程已执行完成
线程状态切换(一张图理清思路)
线程方法讲解
Thread.yield()
线程让步,使用了这个方法,当前线程就会退出CPU时间片,让其他线程或当前线程使用CPU时间篇执行
Thread.sleep()
线程休眠,主动让出当前CPU时间,在指定时间过后,CPU会返回继续执行该线程。sleep方法不会释放当前所持有的锁
Thread.join()
等待该线程死亡/终止,当前线程会等待调用该方法的线程执行完毕后才能继续执行
Object.wait()
Object类的方法,调用前必须拥有对象锁,例如在synchronized代码块内,调用wait方法后,对象锁会释放,线程进入WAITING等待状态
小结
线程的几种状态
线程不同状态的定义
线程不同状态的切换
线程中常见方法的语义和使用场景示例
思考:当服务器线程异常时,如何快速定位线程状态及异常代码行?
-- 5、线程死锁和避免
- 了解线程死锁的发生原因
- 发现线程死锁的方法
- 如何避免代码出现线程死锁
线程死锁
百度百科
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁的原因
两个及以上的线程,抢占2把及以上的锁,抢占锁的顺序不一致
避免和处理死锁问题
- 不使用锁,不使用2把及以上的锁
- 必须使用2把及以上锁的时候,确保在整个应用程序中对获取锁的顺序是一致的
- 尝试获取具有超时释放的锁,例如Lock中的tryLock来获取锁
- 当发生了Java-level的锁时,重启程序来干掉进程/线程
定位死锁问题
jps
列举正在运行的虚拟机进程并显示虚拟机执行的主类以及这些进程的唯一ID(PID)
jstack
用于JVM当前时刻的线程快照,得到JVM当前每一条线程正在执行的堆栈信息,定位线程长时间卡顿问题,如死锁、死循环等问题
思考:写一段死循环代码,使用jps和jstack命令,发现死循环代码出现的具体位置
扩展:数据库死锁问题
操作步骤
选择一个数据表
开启两个会话,每个会话调用“start transaction;”
第一个会话更新id=1的行
第二个会话更新id=2的行
第一个会话更新id=2的行,此时在等待id=2行的锁
第二个会话更新id=1的行,此时数据库检测死锁,终止触发死锁的事务,第一个会话的等待状态变为正常状态
调用“commit”提交第一个会话事务,数据成功
小结
- 了解死锁发生的原因
- 避免死锁的方法
- 学会发现死锁,定位问题
思考
银行账号相互转账的时候,A给B转账,B给A转账,先锁定自己的账户,再锁定对方的账户,会有死锁问题,如何解决?