JMM(JAVA内存模型)中,针对多线程并行操作的场景,主要是围绕原子性、可见性、有序性保证多个线程间可以有效地、正确地协同工作。
原子性( Atomicity )
指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
比如:在32位操作系统中,对long类型(64位)赋值是要执行两步的.当多个线程同时对某一long类型变量赋值时, 高32位由线程A赋值,低32位由线程B赋值, 此时就会出现问题.而原子性操作就是要保证整个赋值操作不会被打断,能够正确赋值,可以使用原子类:比如AtomicLong避免.
可见性(Visibility)
指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
可见性问题是一个综合性问题。由于内存屏障的存在, 缓存优化、硬件优化(有些内存读写可能不会立即触发,而会先进入一个硬件队列等待),指令重排以及编译器优化,都有可能导致不可见问题.可以使用 volatile 关键字修饰变量,避免不可见性问题
内存屏障: 所有的共享变量都存储于主内存。每一个线程还存在私有的工作内存,线程的工作内存中保留了被线程使用的变量副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
有序性( Ordering )
因为指令流水线的存在,CPU才能真正高效的执行。但是,流水线总是害怕被中断的。流水线满载时,性能确实相当不错,但是一旦中断,所有的硬件设备都会进入一个停顿期,再次满载又需要几个周期,因此,性能损失会比较大。所以,我们必须要想办法尽量不让流水线中断-指令重排.
Happen-Before规则
程序顺序原则:一个线程内保证语义的串行性。
volatile规则: volatile 变量的写,先发生于读,这保证了volatile变量的可见性。
锁规则:解锁( unlock )必然发生在随后的加锁( lock )前。
传递性:A先于B,B先于C,那么A必然先于C。
线程的 start() 方法先于它的每一个动作。
线程的所有操作先于线程的终结 Thread.join() 。
线程的中断 interrupt() 先于被中断线程的代码。
对象的构造函数执、结束先于 finalize() 方法。
线程状态和操作
线程状态
● NEW(新建):表示刚刚创建的线程,这种线程还没开始执行。
● RUNNABLE(就绪):当调用 start() 方法时,处于该状态,线程所需的一切资源都已准备好 。
● BLOCKED(阻塞):如果线程在执行过程中遇到了锁 ,就会进入该状态。
● WAITING(无限等待):处于无时间限制的等待状态 。
● TIMED_WAITING(有限等待):处于有时间限制的等待状态 。
● TERMINATED(结束):当线程执行完毕,就进入结束状态 。
【注意】从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED的线程也不能再回到RUNNABLE状态。
线程中断
线程中断并不会使线程立即退出,而是给线程发送一个中断通知,至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
interrupt 执行中断线程操作,添加中断状态
isInterrupted 判断线程是否被中断
interrupted 判断线程是否被中断,并清除当前中断状态
等待&唤醒(wait¬ify)
如果一个线程调用了obj.wait(),那么它就会进入object对象的等待队列,并释放资源。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当obj.notify()被调用时,它就会从这个等待队列中,随机选择一个线程将其唤醒。这种选择是不公平的,是完全随机的。notifyAll() 则是唤醒所有等待线程.
Object的 wait() 和 notify()方法必须包含在对应的synchronized语句中,因为wait()或notify()都需要首先获得目标对象的一个监视器。
join&yield
当一个线程的输入非常依赖于另外一个或者多个线程的输出,此时这个线程就需要等待依赖线程执行完毕,才能继续执行.
join() 的本质就是让调用线程 wait() 在当前线程对象实例上.线程执行完成后,被等待的线程会在退出前调用 notifyAll() 通知所有的等待线程继续执行。
yield()方法会使当前线程让出CPU。让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。
ThreadGroup
在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
Daemon
守护线程是一种特殊的线程,它会在后台默默地完成一些系统性的服务。当一个Java应用内,只有守护线程时,Java虚拟机就自然退出了。我们可以通过调用setDaemon(true)的方式,设置线程为守护线程.
synchronized(锁)
synchronized关键字是多线程场景下保证线程安全的直接方式. 它会使所有竞争不到锁的线程进入到阻塞状态,是java语言中一个重量级的同步操作,被称为重量级锁. 但如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间甚至要比用户代码执行的时间还要长.
为了缓解上述性能问题,JVM从1.5开始,陆续引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。这些锁之间就形成了一个锁升级过程:
无锁——>偏向锁——>轻量级锁——>自旋锁——>重量级锁
对象在内存中存储的布局可以分为3块区域:对象头( Header )、实例数据( Instance Data )和对齐填充( Padding )。其中对象头中的markword 就是用于存储对象自身的运行时数据 ,如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
markword 的内部结构结构,以32位虚拟机为例:
偏向锁
偏向锁是Java6引入的一项多线程优化。顾名思义,它会偏向于第一个访问锁对象的线程,如果同步锁只有一个线程访问,则线程是不需要触发同步的,就会给该线程加一个偏向锁;如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级到轻量级锁,然后再唤醒原持有偏向锁的线程。
程序启动开始4s内,偏向锁是不启用的,也就是偏向锁的延迟策略。原因是 JVM 内部的代码有很多地方用到了synchronized,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗。
当一个对象计算过一致性hash后,或者从偏向锁升级后, 就再也无法进入偏向锁状态了。而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁, 以便于记录hashcode值。
偏向撤销
偏向撤销只能发生在有线程竞争的情况下。想要撤销偏向锁,还不能对持有偏向锁的线程有影响,所以就要等待持有偏向锁的线程到达一个safepoint安全点 (JVM为了保证在垃圾回收的过程中引用关系不会发生变化设置的一种安全状态,在这个状态上会暂停所有线程工作,即STW),在这个安全点会挂起获得偏向锁的线程。
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁.
主要是通过CAS操作将对象头中的markword修改为当前线程的锁记录指针,成功则说明获得锁,并修改锁标志位为"00",此时即为轻量级锁; 如果修改失败,则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”, markword 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
自旋锁
锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会再做最后的努力—— 自旋。虚拟机会让当前线程做几个空循环(这也是自旋的含义),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真正地将线程在操作系统层面挂起,升级为重量级锁。
JDK1.6中,通过 -XX:+UseSpinning 开启自旋锁,通过 -XX:PreBlockSpin=10 设置自旋次数;JDK1.7后,已去掉此参数,由jvm自主控制.
重量级锁
- JVM每次从队列的尾部取出一个数据用于锁竞争候选者( OnDeck ),但是并发情况下, ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner线程会在unlock时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList中的某个线程为 OnDeck线程 (一般是 最先进去 的那个线程)。
- Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“ 竞争切换 ”。
- OnDeck线程获取到锁资源后会变为 Owner线程 ,而没有得到锁资源的仍然停留在 EntryList 中。如果Owner线程被wait()方法阻塞,则转移到 Waiting Queue 中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去 EntryList 中。 处于 ContentionList 、 EntryList 、 WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的.
- Synchronized是非公平锁。 Synchronized在线程进入 ContentionList 前,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能 直接抢占 OnDeck线程的锁资源。
锁消除
锁消除是一种更彻底的锁优化 。Java虚拟机在编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。锁消除涉及的一项关键技术为逃逸分析: 观察某一个变量是否会逃出某一个作用域。
比如局部变量是在线程栈上分配的,属于线程私有的数据,因此不可能被其他线程访问。所以,如果虚拟机检测到这种情况,就会将这些无用的锁操作去除掉。如下方代码中的Vector:
无锁
CAS
CAS采用无锁的乐观策略,由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小得多。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销。因此,它要比基于锁的方式拥有更优越的性能。
CAS(object, offset, expectdValue, newValue)
object:待更新的对象
offset:待更新变量的offset偏移量
expectdValue:表示预期值
newValue:表示新值
仅当 object 和 offset 定位到的值等于 expectdValue 值时,才会将其值设为newValue ,如果与 expectdValue 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V 的真实值。
Atomic原子类
AtomicInteger
AtomicReference
AtomicStampedReference(解决ABA问题)
AtomicIntegerArray
AtomicIntegerFieldUpdater
ThreadLocal
ThreadLocal 是一个线程的本地变量,当某个线程创建了ThreadLocal, 也就意味着这个变量是这个线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。
ThreadLocal内部都维护了一个ThreadLocalMap内部类, 它是一个Entry(k,v)数组,类似HashMap,其中key就是ThreadLocal实例对象, value就是变量值.Thread线程类中有一个ThreadLocal.ThreadLocalMap threadLocals 的属性值, 这也就是为什么说ThreadLocal是当前线程的本地变量了。
ThreadLocal和加锁方式(synchronized、Lock)是有本质的区别的:
关于资源的管理
○ 当资源是多个线程共享的,访问的时候可以通过加锁的方式,逐一访问资源。
○ ThreadLocal是每个线程都有一个资源副本,是不需要加锁的。
关于实现方式
○ 锁是通过时间换空间的做法。
○ ThreadLocal是通过空间换时间的做法。
在我们平常的项目中, 对于读写分离/多数据源切换常常用到ThreadLocal,因为每个线程进来会访问不同的数据库,此时我们就可使用ThreadLocal将当前线程需要访问的数据源作为变量存入到ThreadLocal中,保证每个线程访问对应的数据库,互不干扰, 访问结束后, 调用remove()方法清除调数据源变量.(在使用线程池时, 不进行remove的话下次该线程进来访问的还是同一个数据源,并且还可能会导致内存泄漏)