java并发基础
线程生命周期
- new(初始状态)
- runable(可运行/运行状态)
- blocked(阻塞状态)
- waiting(无限时阻塞)
- time_waiting(有限时阻塞)
- terminated(终止状态)
在操作系统层面: blocking、waiting、time_waiting都没有cpu使用权,因此可以简化为
- new
- runable
- (waiting,blocking,time_waiting)
- terminated
线程的状态转换
runable与blocking
runable与blocking的转换只有在等待synchronized
隐式锁时触发.
synchronized修饰的方法,代码块同一时刻只允许一个线程调用,此时,等待的线程会进入blocked状态.
获得synchronized
隐式锁后,又会恢复为runable
状态.
线程调用阻塞方法,并不会改变线程状态,在操作系统层,其会转为blocked,jvm层仍为runable状态.
runable与waiting
- 获得synchronized隐式锁,调用Ojbect.wait()
- 调用无参得Thread.join()方法.如在线程A中调用线程B.join(),A会由Runable进入waiting,等待b执行完毕后,从waiting恢复到Runable
- LockSupport.park方法.(java中得并发锁,都是基于LockSupport实现).
runable与time_waiting
与Waiting状态得转换类似,只是调用了带超时参数的方法.如Object.wait(long millis)
new与runable
初始化的线程处于new,转为runable则需要调用线程的start方法.
runable与terminated
线程执行玩run,自动转为terminated.
stop与interrupt
stop()方法直接杀死线程,因此不会释放需要手动释放的锁(隐式锁Synchronized会释放)
interupt() 只是通知线程有个中断标记,具体处理需要自己实现.
阻塞方法的中断异常
一旦catch了IntterruptException
,默认调用者就会处理,jvm会清除中断状态,防止方法退出后上层调用代码再一次处理异常.如果只是简单地catch,而不打算处理,需要恢复被清除得中断位(Thread.currentThread().interrupt()),让上层知道
java同步模块
同步容器
- vector,HashTable 二者是早期jdk的一部分
- jdk1.2之后加入了由
Collections.synchronizedXxx
工厂方法创建的同步容器
这些类实现线程安全的方式是: 将它们的状态封装起来,对每个公有方法都进行同步。
同步容器的问题
迭代器与CurrentModificationException:同步器容器的迭代器,没有考虑并发修改的问题,他们表现得行为是及时失败(fail-fast)
,当迭代器发现容器再迭代过程中被修改,就会抛出CurrentModificationException
异常。其实现方式是,将计数器得变化与容器关联起来:如果迭代期间计数器被修改,那么hasnext与next将抛出异常。
并发容器
java 5.0提供了多种并发容器改进同步容器得性能。同步容器将所有对容器状态得访问都串行话,以实现线程安全。
- ConcurrentHashMap,替代同步基于散列的map
- CopyOnWriteArrayList,用于在遍历操作未主要操作的情况下,代替同步的List
java 6.0 - ConcurrentSkipListMap 作为同步的SortedMap替代品(TreeMap)
- ConcurrentSkipListSet 作为同步的SortedSet替代品(TreeSet)
并发容器替代同步容器,可以极大提高伸缩性并降低风险
并发容器(concurrentHashmap)迭代器具有弱一致性,不会抛出ConcurrentModificaitionException,弱一致性迭代器可以容忍并发修改,创建迭代器时会遍历已有元素,并可以(不保证)在迭代器被构造后修改操作反应给容器
CopyOnWrite容器,每当修改时都会复制底层数组,仅当迭代操作远远多于修改操作时,才应该使用
双端队列与密取
java 6增加了 Deque和BlockingDeque,分别对应Queue和BlockingQueue做了扩展。
Deque是一个双端队列,实现了在队列头和队列尾得高效插入和移除。具体实现包括ArrayDeque
,LinkedBlockingDeque
双端队列适用于工作密取模式(Work Stealing),在生产者消费者模式中,所有消费者共享一个工作队列,在工作密取设计中,每个消费者都有自己得双端队列,如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者队列末尾秘密地获取工作.密取工作模式比传统的生产者模式具有更高的可伸缩性.这是因为工作线程不会在单个共享的任务队列上发生竞争.多数情况下,它只取自己的双端队列,从而极大减少了竞争.当工作者线程需要访问另一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队列上的竞争程度
阻塞方法与中断方法
线程阻塞或暂停有多种原因,如:io等待,等待获得锁,等待从sleep中醒来,或者等待另一个线程计算结果.当线程阻塞时,通常会被挂起并处于某种阻塞状态(BLOCKED
,WAITING
,TIME_WAITING
).
阻塞与执行时间长的操作的区别在于,被阻塞的线程必须等待某个不受他控制的事件发生后才能继续执行.当某个外部事件发生后,线程被置回RUNNALBE状态,并可以再次被调度执行.
BlockingQueue的put和take等方法会抛出受检异常(CheckedException)InteruptedException
同步工具类
java 内存模型
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
happens-before
前面一个操作的结果对后续操作是可见的。
- 程序的顺序性
- volatile 变量规则,指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
- 传递性
- synchronized的解锁,happens-before后续对他的加锁
- 线程 start() 规则,它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
- 线程 join() 规则
lock与condition
并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题
Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践,它们分别是:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
lock的可见性
java里多线程的可见性通过happens-before
规则保证,synchronized的解锁happens-before后续对这个锁的加锁.
而lock利用了volatile相关的happens-before
规则.
java SDK 里面的ReentrantLock
,内部持有一个volatile
的成员变量state
,获取锁的时候,会读写state
的值;解锁的时候,也会读写state
的值。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。
java SDK 并发包通过Lock
和Condition
两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
闭锁-Latch
可以延迟线程的进度直到其达到终止状态.通过闭锁来启动一组操作,或者等待一组相关操作结束.闭锁时一次性对象,一旦进入终止状态,就不能被重置.
一个闭锁相当于一扇大门,在大门打开之前所有线程都被阻断,一旦大门打开所有线程都将通过,但是一旦大门打开,所有线程都通过了,那么这个闭锁的状态就失效了,门的状态也就不能变了,只能是打开状态。也就是说闭锁的状态是一次性的,它确保在闭锁打开之前所有特定的活动都需要在闭锁打开之后才能完成。
场景:
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行。二元闭锁(包括两个状态)可以用来表示“资源R已经被初始化”,而所有需要R的操作都必须先在这个闭锁上等待。
- 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
- 等待直到某个操作的所有参与者都就绪在继续执行。(例如:多人游戏中需要所有玩家准备才能开始)
CountDownLatch
CountDownLatch
,是一种灵活的闭锁实现.闭锁状态包括一个计数器,该计数器被初始化为1个整数,表示需要等待的事件数量.countDown()
方法递减计数器,表示有一个事件发生.await()
方法等待计数器清零,若计数器值非0,则阻塞至计数器为0,或者等待中的线程中断,或者超时.
futureTask
futureTask
也可以用作闭锁,其表示得计算通过Callable实现.future.get()将阻塞直到任务进入完成状态,返回结果或者抛出异常.
信号量
计数信号量(Counting Semaphore
),用来控制同时访问某个特定资源的操作数量.或者同事执行某个操作的数量.计数信号量还可以用来实现某种资源池,或者对容器施加边界(将任何一种容器变成有界阻塞容器).
Semaphore
中管理一组虚拟许可(permit),许可的初始数量可通过构造函数来指定.执行操作时可以首先获得许可(只要还有剩余许可),并在使用以后释放许可.如果没有许可,那么acquire
将阻塞直到有许可(或者直到被中断或者操作超时).release方法返回一个许可给信号量.
栅栏
Barrier
类似闭锁,它能阻塞一组线程到某个事件发生.栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行.闭锁用于等待事件,栅栏用于等待其他线程.栅栏用于实现一些协议.如:几个家庭决定在某个地方集合,所有人6:00在麦当劳碰头,到了以后等待其他人,再讨论下一步.
CyclicBarrier
可以使一定数量的参与方反复的在栅栏处集汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题.当线程都到达了栅栏位置,那么栅栏打开,所有线程被释放,栅栏将被重置以便下次使用.如果对await的调用超时,或者await阻塞的线程被打断,那么栅栏就被认为时打破了,所有对await的调用都将终止并抛出BrokenBarrierException
,如果成功通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,可以利用这些索引来选举产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊操作.
Exchanger
是一种两方(Two-party)栅栏,各方在栅栏上交换数据,当两方执行不对称的操作时,exchanger非常有用(只适用于两个线程)