为什么会有多线程
CPU多核心意味着操作系统有更多的并行计算资源可以使用。操作系统以线程作为基本的调度单元。
单线程是最好处理不过的。线程越多,管理复杂度越高。
线程实现的方式
- runnable接口
- Thread类
- 线程池
Thread.sleep: 释放 CPU
Object#wait : 释放锁
Thread的状态操作
- Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入 TIMED_WAITING 状态,但不释放对象锁,millis 后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的 CPU 时间片,但不释放锁资源,由运行状态变为就绪状态,让 OS 再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。该方法与sleep() 类似,只是不能由用户指定暂停多长时间。
- t.join()/t.join(long millis),当前线程里调用其它线程 t 的 join 方法,当前线程进入WAITING/TIMED_WAITING 状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者 millis 时间到,当前线程进入就绪状态。
- obj.wait(),当前线程调用对象的 wait() 方法,当前线程释放对象锁,进入等待队列。依靠 notify()/notifyAll() 唤醒或者 wait(long timeout) timeout 时间到自动唤醒。
- obj.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll() 唤醒在此对象监视器上等待的所有线程。
线程安全问题
多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。不进行恰当的控制,会导致线程安全问题。
并发相关的特性
- 原子性:原子操作,注意跟事务 ACID 里原子性的区别与联系。
对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 - 可见性:对于可见性,Java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
volatile 并不能保证原子性。
- 有序性:Java 允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影 响到多线程并发执行的正确性。可以通过 volatile 关键字来保证一定的“有序性”(synchronized 和 Lock 也可以)。
happens-before 原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码先后顺序
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
- Volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出 A 先于 C
- 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始
synchronized
在JVM中,对象由三部分组成:对象头、实例数据、以及padding。
对象头是synchronized实现锁的基础,对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。
一开始是无锁状态,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
ObjectMonitor中有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,_owner指向持有ObjectMonitor对象的线程;若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
volatile
- 每次读取都强制从主内存刷数据
- 适用场景: 单个线程写;多个线程读
- 原则: 能不用就不用,不确定的时候也不用
- 替代方案: Atomic 原子操作类
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
缓冲队列
BlockingQueue 是双缓冲队列。BlockingQueue 内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
- ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是 FIFO 顺序排序的。
- LinkedBlockingQueue:大小不固定的 BlockingQueue,若其构造时指定大小,生成的 BlockingQueue 有大小限制,不指定大小,其大小有 Integer.MAX_VALUE 来决定。其所含的对象是 FIFO 顺序排序的。
- PriorityBlockingQueue:类似于 LinkedBlockingQueue,但是其所含对象的排序不是 FIFO,而是依据对象的自然顺序或者构造函数的 Comparator 决定。
- SynchronizedQueue:特殊的 BlockingQueue,对其的操作必须是放和取交替完成
拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
Callable
- Runnable#run()没有返回值
- Callable#call()方法有返回值
Future
可以方便的用于异步结果的获取
Java.util.concurrency juc并发包
- 锁机制类 Locks : Lock, Condition, ReadWriteLock
- 原子操作类 Atomic : AtomicInteger
- 线程池相关类 Executer : Future, Callable, Executor
- 信号量三组工具类 Tools : CountDownLatch, CyclicBarrier, Semaphore
- 并发集合类 Collections : CopyOnWriteArrayList, ConcurrentMap
synchronized与Lock对比
- 使用方式灵活可控
- 性能开销小
- 锁工具包: java.util.concurrent.locks
ReentrantReadWriteLock读写锁 readLock && writeLock
ReentrantLockReadWriteLock中的state同时表示写锁和读锁的个数。为了实现这种功能,state的高16位表示读锁的个数,低16位表示写锁的个数。AQS有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式;
当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
condition接口
类比: Object 自带的 monitor
Condition是用来替代传统的Object的wait()、notify()实现线程间的协作,相比Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
每个Condition对象包含一个等待队列。等待队列中的节点复用了同步器中同步队列中的节点。Condition对象的await和signal操作就是对等待队列以及同步队列的操作。
await 操作:将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,最后进入等待状态。
signal 操作:会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前会将节点移到同步队列中。唤醒后的节点尝试竞争锁(自旋)。
LockSupport与wait/notify对比
LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。
它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程);
- wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以锁住线程。
- notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
用锁的最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
Atomic 工具类
- 基本数据类型
AtomicBoolean AtomicInteger AtomicLong - 数组
AtomicIntegerArray AtomicLongArray AtomicReferenceArray - 叠加器(没有使用CAS并性能高于基本数据类型)
DoubleAccumulator DoubleAdder LongAccumulator LongAdder - 引用类型
AtomicReference AtomicStampedReference【使用版本号解决ABA问题】 AtomicMarkableReference【使用Boolean解决ABA问题】 - 对象属性更新器
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
LongAdder 对 AtomicLong 的改进
AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的;
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
AQS
- Sync extends AbstractQueuedSynchronizer
- 抽象队列式的同步器
- Lock 的底层实现原理两种资源共享方式: 独占 | 共享
- 子类负责实现公平 OR 非公平
Semaphore - 信号量
信号量,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。主要用于限流。
CountdownLatch
场景: Master 线程等待 Worker 线程把任务执行完
示例: 等所有人干完手上的活,一起去吃饭。
CyclicBarrier
场景: 任务执行到一定阶段, 等待其他任务对齐
示例: 等待所有人都到达,再一起开吃。
CompletableFuture
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。
从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
除了异步回调机制,CompletableFuture更强大的功能是,多个CompletableFuture可以串行执行。
集合类List
List:ArrayList、LinkedList、Vector、Stack
Set:LinkedSet、HashSet、TreeSet
Queue->Deque->LinkedList
集合类Map
Map:HashMap、LinkedHashMap、TreeMap
Dictionary->HashTable->Properties
hashmap
在JDK1.7中,HashMap是由数组+链表构成的,在JDK1.8中HashMap是由数组+链表+红黑树构成,数组中的每个元素存储的是一个链表的头结点。通过hash(key)%len、也就是元素的key的哈希值对数组长度取模计算得到存储到数组中的位置。 HashMap里面实现一个静态内部类Entry,其重要的属性有key , value, next。
Java8中加入了红黑树;Java7链表插入方式为头插法,Java8为尾插法。 扩容时,Java7对每个元素都重新放置,Java8只对 e.hash & oldCap 不为0的元素移动位置。
插入流程
- 计算出key的哈希值hash并以数组长度为模求余,得到该K-V对应节点在数组中的存放位置;
- 如果这个格子里面没有元素,那么放入以该K-V对创建的节点作为 链表的头节点 ;放入之后数组占用率如果达到了扩容的阈值,那么就进行扩容 ;
- 如果这个格子中已经有元素了,则会遍历这个链表或者红黑树,如果找到了key的哈希值与 hash 相等的节点,那么就直接更新这个节点的 value ;否则就将这个新节点插入到链表或者红黑树中,其中链表插入到尾部,红黑树插入到适当的位置并进行重平衡。
为什么引入红黑树?
哈希碰撞是无可避免的。当最坏情况发生时,大量的节点累计在一个桶中,即数组中的同一项。这时,HashMap的增删改查效率退化到O(n),n为链表的长度。 在链表的长度达到8时,链表会转换为红黑树。红黑树的优势为查找效率为O(logn),在冲突数据量大时效率优于链表。 劣势 为在建树和拆分的时候需要额外的时间、操作小步骤多、占用内存空间大,所以只有在链表长度长到足以抵消红黑树的劣势时,才进行转换。 当数组长度小于64时,即使链表长度达到8,HashMap也会优先考虑扩容而不是转红黑树。
concurrentHashMap
在JDK7中, ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。
- 数据结构 Java 7 采用 Segment 分段锁来实现,而 Java 8 中的 ConcurrentHashMap 使用数组 + 链表 + 红黑树,在这一点上它们的差别非常大。
- 并发度 Java 7 中,每个 Segment 独立加锁,最大并发个数就是 Segment 的个数,默认是 16。 但是到了 Java 8 中,锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。
- 保证并发安全的原理 Java 7 采用 Segment 分段锁来保证安全,而 Segment 是继承自 ReentrantLock。 Java 8 中放弃了 Segment 的设计,采用 Node + CAS + synchronized 保证线程安全。
- 遇到 Hash 碰撞 Java 7 在 Hash 冲突时,会使用拉链法,也就是链表的形式。 Java 8 先使用拉链法,在链表长度超过一定阈值时,将链表转换为红黑树,来提高查找效率。
- 查询时间复杂度 Java 7 遍历链表的时间复杂度是 O(n),n 为链表长度。 Java 8 如果变成遍历红黑树,那么时间复杂度降低为 O(log(n)),n 为树的节点个数。
ThreadLocal
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
对象实例与threadlocal变量的映射关系通过Thread来维护的,具体数据结构是放在一个类似Map中的,然后这个map是Thread的一个对象,key是threadlocal对象,value是具体保存的值。
java8 parallelStream
fork/join框架
fork/join框架
Fork/Join框架会把大任务分割成若干个小任务,最终汇总每个小任务的结果然后得到大任务结果。Fork/Join框架要完成两件事情;
- 任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割;
2 .执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
在Java的Fork/Join框架中,使用两个类完成上述操作: - ForkJoinTask:我们要使用Fork/Join框架,首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
a. RecursiveAction:用于没有返回结果的任务
b. RecursiveTask:用于有返回结果的任务 - ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行;
任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)。
加锁需要考虑的问题
- 粒度
- 性能
- 重入
- 公平
- 自旋锁(spinlock)
- 场景: 脱离业务场景谈性能都是耍流氓
线程间协作与通信
1. 线程间如何共享
- static/实例变量(堆内存)
- Lock
- synchronized
2. 线程间协作:
- Thread#join()
- Object#wait/notify/notifyAll
- Future/Callable
- CountdownLatch
- CyclicBarrier
什么是可重入,什么是可重入锁? 它用来解决什么问题?
可重入就是同一个线程可以多次获取锁。
可以避免死锁。
可重入互斥锁Lock的语义与使用方法和隐式监视器锁synchronized相同,但具有扩展功能。
上一次成功锁定但尚未解锁的线程拥有ReentrantLock,当该锁不属于另一个线程时,调用ReentrantLock的线程将返回并成功获取该锁。 如果当前线程已经拥有该锁,则该方法将立即返回。 可以使用方法isHeldByCurrentThread和getHoldCount进行检查。
ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗? 说说其类内部结构关系。
ReentrantLock里面有三个内部类,一个是Sync,继承自AQS,另外两个继承自Sync,分别是FairSync和NonfairSync。reentrantLock持有Sync对象实例,它的方法基本数调用的sync实现的。
ReentrantLock是如何实现公平锁的? ReentrantLock是如何实现非公平锁的?
非公平:尝试获取锁的时候不去做任何判断,直接去获取,成功了就返回,不成功就入队等待。
公平:获取的时候要判断有没有前驱,详见代码中NonfairSync和FairSync的tryAcquire方法
ReentrantLock默认实现的是公平还是非公平锁?
非公平
ReentrantLock和Synchronized的对比?
1、底层实现不一样,synchronized它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。也就是说synchronized隐式获得释放锁,ReentrantLock显示的获得、释放锁;
2、synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在 finally块中释放锁;
3、synchronized是同步阻塞,使用的是悲观并发策略lock是同步非阻塞,采用的是乐观并发策略;
4、ReentrantLock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。通过ReentrantLock可以知道有没有成功获取锁,而synchronized却无法办到。最重要的是ReentrantLock可以提供公平锁,而synchronized只能是非公平锁。
如何用synchronized实现ReentrantLock?
- ReentrantLock类关键是实现lock() 和unlock();
- 用wait()方法让线程等待,用notify()方法让线程唤醒;