导图社区 架构师必会的java多线程
高级开发、架构师必会的java多线程,如进程:系统进行资源分配和调度的独立单位(资源:例如内存,文件,socket等);线程:系统分配处理器时间资源的基本单元(程序执行的最小单位)。
编辑于2023-10-11 10:27:11Java多线程
进程和线程
基本概念
进程:系统进行资源分配和调度的独立单位(资源:例如内存,文件,socket等)
线程:系统分配处理器时间资源的基本单元(程序执行的最小单位)
进程与线程关联关系
所属关系:进程是线程的容器,一个线程只能属于一个进程,而一个进程可以有多个线程
资源关系:资源分配给进程,同一进程的所有线程共享该进程的所有资源。进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
调度关系:线程是指进程内的一个执行单元,也是进程内的可调度实体
线程状态切换
New(新建):创建后尚未启动
Runnable(可运行):可能正在运行,也可能正在等待 CPU 时间片
Blocking(阻塞):等待获取一个排它锁,如果其线程释放了锁就会结束此状态
Waiting(无限期等待)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片
Timed Waiting(限期等待)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入
Terminated(死亡):可以是线程结束任务之后自己结束,或者产生了异常而结束
多线程带来的问题
安全性问题
现象:不进行数据保护,多个线程并发访问同一共享变量,导致多个线程先后更改数据造成所得到的数据是脏数据
原因
线程间变量的共享机制:JVM存在一个主内存,每个线程有各自的工作内存,一个线程对变量进行操作时,都要把变量从主内存读取一份副本到工作内存中。当多个线程对同一变量没有进行访问控制,就可能导致变量在不同线程的工作内存中存在不同的副本,当多个线程把同一变量的各种不同值从工作内存写入到主内存时,会导致冲突问题
安全性问题存在条件
1、有共享变量
2、处在多线程环境下
3、共享变量有修改操作
解决思路
1、在安全的位置存储变量,例如在方法栈调用栈(本地方法内)或线程本地存储
2、将共享变量修改为不可变
3、保证同时只有1个线程修改变量,例如使用互斥同步锁机制、乐观锁(CAS)、令牌桶、或者在单线程内使用变量
活跃性问题
基本概念:当某个操作无法继续执行下去的时候,就会发生活跃性问题。在串行程序中,活跃性问题就是无意中造成的无限循环,从而使得循环之后的代码无法得到执行
现象
死锁:多个线程,各自占对方的资源,都不愿意释放,从而造成死锁
活锁:活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行
饥饿:多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿
性能问题
在设计良好的并发应用程序中,线程能提升程序的性能。但无论如何,线程总会带来某种程度的运行时开销。在多线程程序中,但线程调度器临时挂起并转而运行另一个线程时,就会频繁地出现 上下文切换(Context Switch),这种操作将带来开销。当线程共享数据时,必须使用同步机制,而这些同步机制往往会压抑某些编译器优化,使内存缓冲区中的数据无效,以及增加共享内存总线的同步流量。这些操作都将带来额外开销
线程安全常见方案
使用不可变变量
简介:不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全
不可变的类型
final 关键字修饰的基本数据类型
String
枚举类型
Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的
同步
基本概念
线程同步:Java中的同步是控制多个线程对任何共享资源的访问的能力
线程同步的意义
防止多线程之间互相冲突写入共享变量
保证数据在多线程中的可见性
互斥同步(阻塞同步)
相关概念
定义:互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁
可见性保证:通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性
同步锁synchronized
synchronized 是Java中的关键字,是一种同步互斥锁(synchronized的锁非公平锁)机制,是来控制多个线程对共享资源的互斥访问
对象锁和类锁
对象锁:在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。一个对象实例的对象锁只能同时被一个线程独占,若对象是静态类型变量,那么因为只有1个实例,最多有一个线程能获得锁
类锁:在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁,所以最多只有一个线程能获得锁
用法分类
显示锁
传统synchronized内置锁的局限性
问题1 无法中断一个正在等待锁的线程
问题2 采用synchronized关键字实现同步的话,导致多线程同时读数据时只能又一个线程可以进行读操作
问题3 当多个锁时,容易造成因为获取锁的顺序不一致导致死锁
问题4 无法判断当前锁是否被线程持有
问题5 所有竞争锁的对象都是公平的,性能可以优化
解决方案
解决问题1
tryLock(long time, TimeUnit unit) 限制锁的等待时间
// 获取锁,支持响应线程中断thread.interrupt()void lockInterruptibly() throws InterruptedException;
解决问题2
基于读写锁,一个资源可以被多线程同时读,或者被一个线程写,但不允许读和写的情况同时发生
解决问题3
基于tryLock实现轮询锁机制,如果获取不到当前锁,主动释放之前已获取的锁
解决问题4
ReentrantLock使用isLocked()方法
解决问题5
支持非公平锁机制,如果锁被释放时,刚好有竞争者来竞争锁,会把锁直接给该竞争者。如果释放锁时没有恰好来的竞争者,则把锁分配给等待最近的竞争者
使用显示锁的时机:建议仅当内置锁不能满足需求时,才考虑使用显示锁
非阻塞同步
定义:基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,代表操作成功,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
CAS
定义
Compare And Swap(CAS) 简单的说就是比较并交换
操作
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
Java中具体的CAS操作类sun.misc.Unsafe的compareAndSwapXXX方法实现的
代码
存在问题
ABA问题:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化
解决:J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,通过控制变量值的版本保证CAS正确性
使用例子
AtomicInteger类
JDK8 ConcurrentHashMap的put方法
无同步方案
基本概念
要保证线程安全,并不是一定就要进行同步。如果线程之间本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性
栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
线程本地存储
线程本地的存储的概念是保证变量的存储的线程私有性,把共享数据的可见范围限制在同一个线程之内, 这样,无须同步也能保证线程之间不出现数据争用的问题
基本原理,每个线程各自维护独立一份Map(ThreadLocalMap)实例,Map的key是ThreadLocal,value是线程私有的变量对象。 基于ThreadLocal对象提供的get/set/remove方法访问更新调用方所在线程的本地存储变量
使用场景
每个线程需要有自己单独的对象实例
实例需要在多个方法中共享,但不希望被多线程共享
例如:存储客户端请求的session上下文会话信息、存储JDBC SQL操作的上下文信息
使用注意点:在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况, 应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险
死锁
死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象
产生死锁必要条件
互斥条件,一个资源每次只能被一个线程使用
请求与保持条件,一个线程因请求资源而阻塞时,对已获得的资源保持不放
不被剥夺条件,进程已经获得的资源,在未使用完之前,不能强行被剥夺
循环等待条件,若干线程之间形成一种头尾相接的循环等待资源关
产生死锁的示例代码
避免死锁
破坏请求与保持条件,加锁设置超时时限,如果超过时限释放已获得锁
破坏不被剥夺条件,死锁检测,基于map保存锁与线程的信息进行检测
破坏循环等待条件,基于固定顺序获取锁
线程协作
join
t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,调用方法的时候线程再继续
应用场景:当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能
示例代码
wait、notify、notifyAll
wait():告诉当前线程释放锁并进入休眠状态,直到某个其他线程进入同一个对象的同步锁其所并调用notify()
notify(): 唤醒正在当前相同对象的同步锁上等待的单个线程
notifyAll: 它唤醒在当前相同对象的同步锁内上调用wait()的所有线程
为何wait,、notify和notifyAll 这些方法不在thread类里面:wait()和notify()在对象同步锁级别工作,并且锁是分配给示例化对象,而不是特定线程的对象
notify和notifyAll的区别:notify只能唤醒一个,但究竟是哪一个不能确定,notifyAll则唤醒这个对象上的休息室中所有的线程
wait和sleep的区别:sleep是Thread的静态方法,线程让出CPU的使用,仍然占有锁。wait是Object类的方法,释放内置对象锁。
notify示例代码
notifyAll示例代码
yield
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行
线程中断
为什么需要线程中断:希望打断执行中的线程任务以达到暂停任务的目的。例如中断上传,中断下载
InterruptedException:通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞
实现
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程
实现代码
并发工具类
CountDownLatch
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务
Semaphre(信号量)
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量
Barrier(栅栏)
CyclicBarrier是一个同步工具类,它允许一组线程在到达某个栅栏点(common barrier point)互相等待,发生阻塞,直到最后一个线程到达栅栏点,栅栏才会打开,处于阻塞状态的线程恢复继续执行.它非常适用于一组线程之间必需经常互相等待的情况
CyclicBarrier字面理解是循环的栅栏,之所以称之为循环的是因为在等待线程释放后,该栅栏还可以复用