java并发编程
并发理论
为什么需要多线程
众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献
- CPU 增加了缓存,以均衡与内存的速度差异
- 存在可见性问题
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异
- 存在原子性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
- 存在有序性问题
并发中存在的问题
可见性问题
-
该问题是由CPU缓存引起的
-
一个线程对共享变量的修改,另外一个线程能够立刻看到
-
解决方式
- 例如:使用volatile修饰符修饰共享对象
原子性问题
- 该问题是分时复用引起的
- 即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 例如:a+=1
- 解决方式
- 例如:使用原子类
有序性问题
- 该问题是重排序引起的
- 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
- 解决方式
- 例如:使用synchronized或者Lock(锁)来控制
- 例如:遵守Happens-Before 规则
- 单一线程原则
- 管程锁定规则
- volatile 变量规则
- 线程启动规则
- 线程加入规则
- 线程中断规则
- 对象终结规则
- 传递性
线程安全的实现方案
互斥同步
非阻塞同步
- 乐观锁
- 原子类
无同步方案
- 栈封闭
- 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
- 线程本地存储(Thread Local Storage)
- 使用ThreadLocal存储线程独享变量
- 可重入代码(Reentrant Code)
并发编程模型
共享内存
- 共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信
- 在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行
消息传递
- 消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信
- 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
并发编程
CAS(Compare And Swap)
简述
- 是一种用于在多线程环境下实现同步功能的机制
- Java中CAS是通过自旋操作完成赋值,若值不相等再更新预期值、重新计算新值,接着进行CAS操作,直到成功为止
- 底层是JVM调用操作系统原语指令unsafe,并由CPU完成原子操作,你要知道并发/多线程环境下如果CPU没有原子操作我们是无法完成
分析
- 优点
- 没有引用锁的概念,并发量不高情况下提高效率
- 减少线程上下文切换
- 缺点
- cpu开销大,在高并发下,许多线程,更新一变量,多次更新不成功,循环反复,给cpu带来大量压力
- 只是一个变量的原子性操作,不能保证代码块的原子性
关键字
volatile
- volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性
- volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现,即预防指令重排
- volatile 变量通过缓存一致性协议(MESI),使得每个线程都能及时更新为该变量的最新值
final
- 声明不可变常量
锁
分类
- 同步资源维度
- 悲观锁
- 乐观锁
- 阻塞维度
- 阻塞
- 不阻塞。自旋锁、自适应自旋锁
- 锁竞争等待维度
- 公平锁
- 非公平锁
- 可重复持有锁(同一个线程)
- 可重入锁
- 不可重入锁
- 可重复持有锁(多个线程)
- 共享锁
- 排它锁
实现方式
synchronized
-
Synchronied有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级
-
锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率
-
锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的)
-
synchronized使用的地方
- 同步方法
- 同步类
- 同步块
- 同步静态方法
ReentrantLock
- ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁
线程
线程状态
- New(新建)。创建后尚未启动
- Runnable(就绪/准备)。正在等待 CPU 时间片
- Running(运行)。正在运行
- Blocking(阻塞)。等待获取一个排它锁,如果其线程释放了锁就会结束此状态
- Waiting(无限等待)。等待其它线程显式地唤醒,否则不会被分配 CPU 时间片
- Timed Waiting(限期等待)。无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒
- Terminated(结束/死亡)。可以是线程结束任务之后自己结束,或者产生了异常而结束
线程实现方式
- 实现Runnable接口
- 实现Callable接口
- 继承Thread类
- 线程池
线程的常见操作
- setDaemon
- 使用 setDaemon() 方法将一个线程设置为守护线程
- sleep
- Thread.sleep(millisec) 方法会休眠当前正在执行的线程
- yield
- Thread.yield()方法会交出cpu执行时间片
- join()
- notify
- 通过Object.notify唤醒挂起的线程
- notifyAll
- 通过Object.notifyAll唤醒所有线程
- 线程中断
- Object.interrupt()。Object.interrupted()可以查看线程中断状态
- ThreadPool.shutdown()。方法会等待线程都执行完毕之后再关闭
- ThreadPool.shutdownNow()
JUC常用类
ReentrantLock
- 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大
ReentrantReadWriteLock
- 读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁
StampedLock
- StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁
CountDownLatch
- 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
CyclicBarrier
- 是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier
Phaser
- 可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量
Semaphore
- 是一个计数信号量,从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目
Exchanger
- 用于线程协作的工具类, 主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了
线程池
常用工具包
- 使用commons-lang3包下的类进行创建
- 使用com.google.guava包下的类进行创建
- 使用spring配置线程池方式进行创建
规避事项
说明
- 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
问题分析
- newFixedThreadPool和newSingleThreadExecutor
- 主要问题是堆积的请求处理队列是无界队列,可能会耗费非常大的内存,甚至OOM
- newCachedThreadPool和newScheduledThreadPool
- 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
配置线程池需要考虑因素
- CPU密集型: 尽可能少的线程,Ncpu+1
- IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
- 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分
- 从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。尽可能地使用有界的工作队列
常见问题
sleep和wait的区别
- sleep是线程中的方法,但是wait是Object中的方法
- sleep方法不会释放资源锁,但是wait会释放资源锁,而且会加入到等待队列中
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
- sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)
类锁和对象锁的区别
- 类锁
- 类锁是加在类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的
- 作用于静态变量锁、静态方法锁、类锁
- 对象锁
- 作用于实例对象上,对同一个对象进行锁定区调用时,会进行锁竞争
- 作用于非静态变量锁、非静态方法锁、this锁