一、synchronized
1、一些基本概念
1、synchronized可以在任意对象和方法上加锁,加锁的这段代码称为“互斥区”或“临界区”。当一个线程想要执行同步方法里面的代码时,会首先尝试拿到这把锁,如果拿到则能执行,如果不能拿到,则会不断去拿这把锁,直到拿到为止。
2、如果多个线程同时对同一个对象中的同一个实例变量进行操作时,可能会出现值被更改、值不同步的情况,进而影响程序的执行流程,这也叫做非线程安全。
3、synchronized既能保证了原子性,又能保证可见性。
4、synchronized是非公平锁。当一个线程想获取锁时,先试图插队,如果占用锁的线程释放了锁,下一个线程还没来得及拿锁,那么当前线程就可以直接获得锁;如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,只能等到前面所有线程都执行完才能获得锁。
5、synchronized是可重入锁。如果是一个同步方法调用另一个同步方法,它们同一个线程在调用且加的是同一把锁,则该线程在第二个同步方法申请锁时仍然会得到该对象的锁。每得到该锁一次,标记会+1,释放一次,标记会-1,当标记为0时,会完全释放该锁,其它线程可以去争抢该锁。
6、程序在执行过程中,如果出现异常,默认情况下锁会被释放。在并发处理的过程中,有异常时需要多加小心,不然很可能会发生不一致的情况。 例如多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
7、以对象为锁时,最好在其引用前加final关键字,防止对象改变。
8、禁止以基本类型作为锁对象。例如以String类型为锁对象,但String类型是存在常量池中,可能造成多个锁对象其实是同一个锁对象。
2、synchronized实现同步的基础:
java中每一个对象都可以作为锁,具体表现为以下三个形式:
① 对于普通同步方法:锁的是当前实例对象,等同于synchronized(this)。一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它线程无论调用哪一个同步方法都只能等待,换句话说,只能有一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它synchronized方法(对象锁)
② 对于静态同步方法:所得是当前类的Class对象,等同于synchronized(T.class)。和普通同步方法一样,一个类中如果有多个静态同步方法时,只要有一个线程去调用了其中的一个静态方法时,其它线程无论调用哪一个静态方法都只能等待(类锁)。
注:T.class是单例吗?如果是在同一个ClassLoader空间T.class是单例,但不在同一个类加载器时,T.class不是单例。不同的类加载器互相之间不能访问。如果能访问它,T.class则是单例。
③ 对于同步方法块:锁的是synchronized括号里面的对象。
3、synchronized的底层实现
1、早期:
在jdk早期时,synchronized的底层实现是重量级锁,也就是synchronized每次都需要去找操作系统申请锁,导致synchronized效率非常低。
2、锁升级:
由于synchronized效率很低,在Java SE 1.6后对synchronized进行了一些改进,引入了锁升级的概念。在HotSpot的实现中,synchronized有四种锁状态,分别为:无锁、偏向锁、轻量级锁(自旋锁)、重量级锁。
了解四种锁状态前,首先了解一下对象头里的mark word
锁状态 | mark word | ||||
bit field | tag bits | ||||
25bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否可偏向 | 锁标志位 | ||
无锁 | 对象hashcode | 对象分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 | 指向线程栈中锁记录(LockRecord)的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级锁的指针) | 10 | |||
GC标志 | 空 | 11 |
① 无锁:对象没有被synchronized锁住时,为无锁状态。
② 偏向锁:当只有一个线程访问同步块并获取锁时,会在对象头中储存当前获取锁的线程的id,这时有其他线程来申请锁资源时,偏向锁会升级为轻量级锁。偏向锁是可重入锁,如果对象头中的线程id为当前线程的id,当前线程再次申请锁时,无需重新进行加锁和解锁。当前线程释放锁后,锁对象并没有真正地释放,只有当其他线程尝试竞争偏向锁时,偏向锁才会释放锁,且此时锁对象会升级为轻量级锁。
③ 轻量级锁(自旋锁):为了解决重量级锁效率低下的问题,JVM引入了轻量级锁概念。JVM会在当前线程的栈针中创建一个用于存储锁记录的空间LockRecord,会将锁对象头中的mark word信息复制到LockRecord中,并让LockRecord的Owner指针指向锁对象,然后会尝试使用CAS将对象头中的mark word信息替换为LockRecord的指针,如果替换成功则获取到锁对象,失败表示锁对象被其它线程抢到,然后会继续通过CAS的方式尝试获取到锁对象,一定次数之后,若还没获取到锁对象,则锁对象升级为重量级锁。
④ 重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间较慢。
注:
1、锁升级后是无法降级的。如果并发量减少,且是重量级锁时,可以替换锁对象进行降级。
2、当一个锁对象是无锁时,如果调用过了hashcode方法,就不会再进入偏向锁,因为对象头中的bit位已经被hashcode占用(对象的hashcode一旦生成就不能被改变),没有空间再去储存线程id,如果此时有线程来申请锁时,会直接进入轻量级锁状态。同样地,如果当前锁状态为偏向锁时,调用hashcode方法会让锁升级为轻量级锁。
想要更深刻理解锁升级概念,可以查看马士兵老师写的文章:《没错,我就是厕所所长》
二、Volatile
1、volatile作用:
① 使变量在多个线程中可见
为什么线程之间不可见:每个线程都有一块属于自己的区域,当线程去访问某一个变量时,会将它的值从共享空间复制一份放在自己的工作空间里,该线程对这个值进行了任何的改变,首先会在自己的工作空间里改变它,然后立马写回到共享空间里。但是其它线程不会立马收到这个值改变的消息。
volatile如何实现线程间的可见性:本质是MESI缓存一致性协议。
② 禁止指令重排序
为什么会指令重排序:由于CPU为了提高执行效率,会将指令并发地执行,就会导致没有相关性的指令乱序执行。
volatile如何实现禁止重排序:JVM在指令前后增加了读屏障和写屏障。
为什么懒汉式单例需要加volatile关键字:如果没有加volatile关键字,可能会导致创建对象后先赋值给引用,此时对象内的成员变量还未完成初始化,当有其它线程获取这个单例进行使用时,发现引用已经有值,于是直接拿去使用,而这时会使用到对象还未完成初始化的成员变量。
三、CAS
1、什么是CAS:
CAS是Java的Unsafe类中的一个方法:CompareAndSet,比较并设定。
2、CAS的实现原理:
CompareAndSet方法有三个参数:v(要改变值的对象)、expected(期望的当前值)、newValue(新的值)。当想要改变一个值时,会判断它的当前值和期望的值是否一致,若一致则将对象改为新值,若不一致,则说明其它线程对该对象进行过修改,此次修改失败,于是重新尝试修改这个对象,直到它满足期望为止。
注:cas是cpu的原语支持,比较并修改在cpu指令上是连续执行的,中间不能被打断,所以不必担心在比较一致后之后修改之前被其它线程修改的情况。
3、ABA问题:
如果一个对象从A变为B又变为A后,CAS是无法识别的,因为它只和当前值进行比较。如果是基础类型,由于不影响结果值,可以不用关注。若是引用类型,可以考虑加版本解决。
4、CAS的底层实现—Unsafe类:
Unsafe类提供了类似C++手动管理内存的能力,但由于Unsafe类为final类,且构造方法为private,故不能继承和实例化,只能通过反射的方式创建其实例对象。Unsafe类中的方法几乎都为native方法,它提供了直接操作内存、直接生成类实例、直接操作类或实例变量、CAS相关的功能。
5、一些基于CAS实现的Atomic类:
AtomicInteger(CAS)、AtomicLong(CAS)、LongAdder(CAS,分段式锁,在并发较高时,把线程分组进行CAS相加,最后把各段的和相加)