基础概念
1.同步和异步
形容一次方法的调用
同步方法一旦开始,必须等到调用的返回,才能继续后续行为
异步方法调用后立即返回,调用者可以继续后续行为,异步方法会在另一个线程中真实执行
2.并发和并行
并行是真正意义同时执行(多核cpu),并发可能多个任务交替执行
3.临界区
表示一种公共资源或者共享数据,每次只能被一个线程占用
4.阻塞和非阻塞
形容多线程间的相互影响
阻塞:一个线程占用临界区,其他线程都需要等待
非阻塞:没有一个线程能妨碍其他线程执行
5.死锁、饥饿、活锁
死锁:
饥饿:线程因为种种原因无法获得所需资源。高优先级线程不断抢占资源,导致低优先级无法执行。和死锁相比,饥饿在一段时间后是有可能解决的,比如高优先级线程已经完成。
活锁:谦让其他线程,主动将资源释放给其他线程,导致没有一个线程可以拿到所有资源而执行
6.并发级别:阻塞、无饥饿、无障碍、无锁、无等待
阻塞:在其他线程释放资源之前、当前线程无法执行。synchronized和可重入锁
无饥饿:非公平锁时,系统允许高优先级线程插队,导致低优先级线程产生饥饿
公平锁时,先来先服务,不会产生饥饿。
栗子:一个严格公平的互斥锁通常是无饥饿的。在JDK 8中的StampedLock有这样的性质,因为它创建了一个线程队列(链表)等待获取锁。这个队列的插入操作是无锁的,但是在插入之后,每个线程都会自旋或者让步从而被当前占有锁的线程锁阻塞。释放锁的线程采用unsafe.park()/unpark()机制,够唤醒下一个在队列中等待的线程,从而执行了严格的优先级。这个机制的意义是,如果给予其他线程(占有锁的线程)足够的时间去完成他们的操作,那么当前线程可以确保最终获取锁,然后完成自己的操作。
无障碍:最弱的非阻塞调度,不会因为临界区问题导致挂起。如果检查到数据竞争,对自己数据修改回滚。
但是在冲突严重时,所有线程不断回滚自己操作,有可能没有一个线程可以走出临界区
实现:可以依赖一致性标记(比如时间戳和版本号)来实现,线程操作之前和之后都读取下,如果一致说明没冲突,如果不一致说明有冲突。任何对资源有修改操作的线程,在修改数据前,都需要更新标记,表示数据已经不安全
无锁:都是无障碍的,所有线程都能尝试对临界区进行访问,无锁的并发保证必然有一个线程能在有限步内完成操作离开临界区。
实现:典型特点是包含一个无穷循环,在循环冲线程不断尝试修改共享变量,没冲突,修改成功,程序退出,否则尝试修改。比如CAS
无等待:要求所有线程必须在有限步内完成。
实现:RCU(read-copy-update),对数据读不加控制,读线程都是无等待对,写数据时候,先取得原始数据副本,接着只修改副本数据,修改完成后,合适时机回写数据。
JMM(java内存模型)
1.原子性
指一个操作是不可以中断的。
32位系统,long型数据的读写不是原子性的,因为long有64位。并行关系,会串位‘
2.可见性
指一个线程修改了某个共享变量的值,其他线程能立刻知道这个修改
不可见的原因:缓存、指令重排
3.有序性
指令重排保证串行语义的一致性。指令重排的原因是性能考虑,汇编指令执行采用流水线,为了减少中断流水线,当某指令中断以后,可以把后面当指令提上来。
4.哪些指令不能重排,happen-before规则
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。