0%

Java Concurrency in Practice

目录

[TOC]

1. 线程安全性

对象的状态是指存储在状态变量(例如实例域或者静态域)中的数据

Java主要的同步机制是关键字synchronized,它提供了一种独占的加锁方式,但是”同步”这个术语还包括volatile类型的变量,显示锁(Explicit lock)以及原子变量

完全由线程安全的类构成的程序不一定就是线程安全的, 而在线程安全类中也可以包含非线程安全的类

正确性的含义就是:ok_hand: :某个类的行为与其规范完全一致

线程安全性的定义:taurus:: 当多个线程访问某个类时, 这个类始终都能表现出正确的行为, 那么就称这个类是线程安全的

或者这样定义:alien::一个在并发环境和单线程环境中都不会被破坏的类

类的无状态:yum::它既不包含任何域, 也不包含任何对其他类中域的引用. 计算过程中的临时状态仅存在于线程栈上的局部变量上, 并且只能由正在执行的线程访问

无状态对象一定是线程安全的

竞态条件:由于不恰当的执行时序而出现的不正确的结果

当某个计算的正确性取决于多个线程的交替执行时时序时, 就会产生竞态条件

数据竞争:rofl:: 如果在访问共享的非final类型的域时没有采取同步进行协同, 那么就会出现数据竞争

并非所有的竞态条件都是数据竞争, 同样并非所有的数据竞争都是竞态条件

复合操作: 包含一组必须以原子方式执行的操作

当在无状态的类中添加一个状态时, 如果该状态完全由线程安全的对象来管理,那么这个类仍是线程安全的

原子性: 一组语句作为一个不可分割的单元被执行

Java内置锁(synchronized) 线程在进入代码块之前会自动获得锁, 并且在退出同步代码块时自动释放锁(无论是正常退出还是异常退出)

重入:camel:: 当某个线程请求一个由其他线程持有的锁时, 发出的请求就会阻塞, 由于内置锁是可重入的, 因此如果某个线程试图获得一个已由它持有的锁时, 那么这个请求就会成功

如果在复合操作的执行过程中持有一个锁, 那么会使复合操作成为原子操作

一种常见的错误:x: 认为: 只有在写入共享变量时才需要同步 对于可能被多个线程同时访问的可变状态变量, 在访问它时都需要持有同一把锁

每个共享变量和可变的变量都应该只由一个锁来保护

串行访问:first_quarter_moon::多个线程以此以独占的方式访问对象, 而不是并发地访问

通常 在简单性和性能之间存在着相互制约因素, 当实现某个同步策略时, 一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)

当执行时间较长的计算或者可能无法快速完成的操作时(网络I/O或者控制台I/O) 一定不要持有锁

2. 对象的共享

:one: 可见性

重排序(Reordering): 在没有同步的情况下,编译器、处理器以及运行时等都可能对操纵的执行顺序进行一些意想不到的调整

只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显的的看到该线程中重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行

在没有同步的情况下, 编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整

非原子的64位共享:shaved_ice::

Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但是对于非volatile类型的long和double变量,JVM允许将64位的读操作和写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位可另外一个值的低32位

在访问某个共享且可变的变量时要求所有的线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其它线程来说都是可见的

加锁的含义不仅仅局限于互斥行为,还包括内存可见性,所有执行读操作或者写操作的线程都必须在同一个锁上同步

volatile的特性:

  • 保证了不同线程对这个变量进行操时的可见性, 即一个线程修改了某个变量的值, 这个新值对其他线程来说是立即可见的(实现可见性)
  • 禁止进行指令重排序(实现有序性)

volatile变量:telephone_receiver::当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值

volatile变量的内存可见性是基于内存屏障(Merry Barrier)实现的, JMM内部会有指令重排, 并且会有as-if-serial和happen-before的理念来保证指令的正确性

内存屏障就是基于4个基于汇编级别的关键字来禁止指令重排序的

volatile变量不会造成线程阻塞,因此volatile是一种比synchronized关键字更轻量级的同步机制

在当前大多数处理器架构上, 读取volatile变量的开销只比读取非volatile变量的开销略高一些

synchronized在内存可见性上的作用比volatile变量更强

加锁 (synchronized 悲观锁 互斥锁)并发执行 ——–> 序列化执行

怎么理解volatile变量(volatile是怎么保证多个线程对被修饰变量的可见性): 从内存可见性的角度来看,写入volatile变量相当于退出同步代码块(即monitorexit 将本地缓存的变量值刷新到主存),而读取volatile变量就相当于进入同步代码块(即monitorenter 从主存中读取最新的值到本地缓存中)

虽然volatile变量很方便,但也存在一些局限性:santa::volatile变量通常用做某个变量完成、发生、中断或者状态的标志,但是volatile的语义不足以确保递增操作(count++)的原子性,除非你可以确保只有一个线程对变量执行写操作

:jack_o_lantern: 加锁机制既可以保证可见性又可以确保原子性, 而volatile变量只能确保可见性

:ear_of_rice: 当且仅当满足以下条件时才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值, 或者你能保证只有单个线程更新变量的值
  • 该变量不会与其他变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

:two: ​发布与逸出

发布: 指使一个对象能够在当前作用域范围之外的代码中使用

当某个不应该发布的对象被发布时,这种情况就被成为逸出

封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得困难

6. 任务的执行

在理想状态下, 各个任务之间是相互独立的:任务并不依赖于其他任务的状态、结界或者边界效应。独立性有助于实现并发

在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性

在单线程服务器中,阻塞不仅会推迟当前请求的完成时间:而且还会彻底阻止等待中的请求被处理,服务器的资源利用率非常低,因为当单线程在等待I/O操作完成时,CPU将处于空闲状态

串行处理机制通常都无法提供高吞吐率或者快速响应性

8. 线程池的使用

在线程池中:sailboat: , 如果任务依赖于其他任务,那么可能产生死锁

线程饥饿死锁(Thread Starvation Deadlock): 如果所有正在执行任务的线程都由于等待其他仍然处于工作队列中的任务而阻塞的现象

1
2
3
4
5
6
7
8
// ThreadPoolExecutor的通用构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}

线程的创建和销毁:new::

线程池的基本大小(Cool Pool Size) 、最大大小(Maximum)以及存活的时间等因素共同负责线程的创建和销毁

基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小(在创建ThreadPoolExecutor初期, 线程并不会立即启动,而是等到有任务提交时才会启动(除非调用prestartAllCoreThreads),并且只有在工作队列满了的情况下才会创建超出这个数量的线程

线程池的最大大小表示可以同时活动的线程数量的上限,如果某个线程的的空闲时间超过了等待时间, 那么将标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止

管理队列任务:golf::

newFixedThreadPool和newSingleThreadPool在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有的工作者线程都处于忙碌状态,那么任务将在队列中等候,如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加

使用有界队列(ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue)有助于避免资源耗尽的情况发生

当队列已满时,新的任务该怎么办:ambulance::

:one: 在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节,如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换, 但付出的代价是可能限制吞吐量

:two:对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程

newCachedThreadPool工厂方法中使用了SynchronousQueue

CPU工作线程数(线程池中的线程数量)设多少合适?
Nthreads = NCPU * UCPU * (1 + W/C)

  • NCPU 是处理器的核数目, 可以通过Runtime.getRuntimr().availableProcessors()得到

  • UCPU是期望的CPU利用率(该值应该介于0-1之间)

  • W/C是等待时间与计算时间的比率

13. 显式锁

在jdk5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile 在jdk5.0之后增加了一种新的机制: ReentrantLock

Reentrant并不是一种替代内置锁的办法,而是当内置加锁机制不使用时,作为一种可选择的高级功能

Lock提供了一种可轮询、无条定时的以及可中断的锁获取操作

15. 原子变量与非阻塞同步机制

非阻塞算法用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性

非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞, 因此它能在粒度更细的层次上进行协调, 并且极大地减少调度开销。而且, 在非阻塞算法中不存在死锁和其他活跃性问题:smiley:

锁的劣势: 如果有多个线程同时请求锁, 那么JVM就要借助操作系统的功能。在没有竞争的情况下能够运行的很好, 但是在竞争的情况下, 其性能会由于上下文切换的开销和调度延迟而降低。如果锁持有的时间非常短, 那么当在不恰当的时间请求锁时, 使线程休眠将付出很高的代价。当一个线程等待锁时, 它不能做任何事情, 如果持有锁的线程被永久地阻塞(无限循环、死锁、活锁、其他的活跃性故障), 所有等待这个锁的线程就永远无法执行下去:sob:

:rainbow_flag:与锁相比, volatile变量是一种更轻量级的同步机制, 因为在使用这些变量时不会发生上下文切换或线程调度等操作。但是同样存在一些局限: 虽然它们提供了相似的可见性保证, 但不能用于构建原子的复合操作, 因此当一个变量依赖其他变量时或者当变量的新值依赖于旧值时, 就不能使用volatile变量

独占锁是一种悲观技术

硬件对并发的支持:ng_man:: cas操作 cpu本身有指令支持 cmpxchg (汇编指令), 其不支持原子性, OS和JVM使用这些指令来实现锁和并发的数据结构, 但是在JDK5之前 Java还不能直接使用这些指令

cas: 借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰, 如果存在, 这个操作将失败,并且可以重试(也可以不重试)

cas(Compare And Set / Compare And Swap)是一种乐观的技术

关于cas失败重试问题: 如果cas失败时不执行任何操作, 那么是一种明智的做法,当cas失败时,意味着其他线程已经完成了你想要执行的操作

当竞争程度不高时,基于cas的计数器在性能上远远超过基于锁的计数器, 而在没有竞争时甚至更高

cas不一定就比悲观锁效率高,取决于单个线程的执行时间以及等待线程的数量

cas的主要缺点是: 它将使调用者处理竞争问题(通过重试、回退、放弃),而在锁中能自动处理竞争问题(线程在获得锁之前将一直阻塞)

cas存在的问题: ABA问题

  • 版本号(AtomticStampedReference)
  • 使用 Boolean(AtomticMarkableReference)标记

LOCK_IF_MP cmpxchg === lock cmpxchg

单个cpu不用加lock

lock优先锁定cache line 其次锁定北桥信号

能用synchronized解决问题的 优先使用synchronized

CFS(操作系统线程调度算法)

JDK1.5之后synchronized内部有锁升级的过程 ,偏向锁 —>自旋锁(轻量级cas锁)—->重量级锁(悲观排队锁)

内存屏障 (memory barrier) lfency sfency mfency

JVM对cas的支持: 在原子变量类(java.util.concurrent.atomic)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的cas操作,而在java.util.concurrent中的大多数类在实现时则直接或者间接地使用了这些原子变量

原子变量类:atom_symbol: :原子变量比锁的粒度更细(原子变量将发生竞争的范围缩小到单个变量上),量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的

尽管原子的变量类扩展了Number类,但是并没有扩展一些基本包装的类,例如Integer和Long 原因:基本类型的包装类是不可修改的,而原子变量类则是可修改的。在原子变量类中同样没有重新定义hashCode方法和equals方法,每个实例都是不同的,与其他可变对象相同, 也不宜用做基于散列的容器中的键值

AtomicInteger直接利用了硬件对并发的支持

在高度竞争的情况下, 锁的性能将超过原子变量的性能, 但在更真实的竞争情况下,原子变量的性能将超过锁的性能,这是因为锁在发起竞争时会挂起线程,从而降低了CPU的使用率和共享内存总线上的同步信号量

16. Java内存模型

Happens-Before :happy:

  • 程序顺序规则: 如果程序中的A操作在操作B之前,那么在线程中A操作将在B操作之前执行
  • 监视器锁规则: 在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行
  • volatile变量规则: 对volatile变量的写入操作必须在对该变量的读操作之前执行
  • 线程启动规则: 在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行
  • 线程结束规则: 线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false
  • 中断规则: 当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)
  • 终结器规则: 对象的构造函数必须在启动该对象的终结器之前执行完成
  • 传递性: 如果操作A在操作B之前执行,并且操作B在操作C之前执行, 那么操作A必须在操作C之前执行

欢迎关注我的其它发布渠道