java并发编程

111 阅读9分钟

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锁