synchronized介绍
synchronized关键字是Java加锁的一种方式,用于修饰代码块和方法(静态方法和普通方法),根据修饰范围的不同,可以分为类锁和对象锁。同时,synchronized也是一种可重入锁。
synchronized实现原理
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 JVM 处理字节码会出现相关指令。
jvm 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。
1.代码块的同步
利用 monitorenter 和 monitorexit 这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当 jvm 执行到 monitorenter 指令时,当前线程试图获取 monitor 对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器 + 1;当执行 monitorexit 指令时,锁计数器 - 1;当锁计数器为 0 时,该锁就被释放了。如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
2. 方法级的同步
是隐式的,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构 (method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成 (无论是正常完成还是非正常完成) 时释放 monitor。
synchronized优化
synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高。
Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。这个过程又叫做锁膨胀
LOCK
Lock 有三个实现类,一个是 ReentrantLock, 另两个是 ReentrantReadWriteLock 类中的两个静态内部类 ReadLock 和 WriteLock。
public interface Lock {//lock 相关代码
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
LOCK 的实现类其实都是构建在 AbstractQueued-Synchronizer 上,为何图中没有用 UML 线表示呢,这是每个 Lock 实现类都持有自己内部类 Sync 的实例,而这个 Sync 就是继承 AbstractQueuedSynchronizer (AQS)。为何要实现不同的 Sync 呢?这和每种 Lock 用途相关。
FairSync 与 NonfairSync 的区别在于,是不是保证获取锁的公平性,因为默认是 NonfairSync(非公平性)
可以看到Lock锁的**底层实现是AQS,**那么说明是AQS呢?
AQS
1.定义
AQS(AbstractQuenedSynchronizer ),抽象的队列式同步器,除了 java 自带的 synchronized 关键字之外的锁机制。
2.核心思想
被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁(CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性)实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 是将每一条请求共享资源的线程封装成一个 CLH 锁队列(该队列是一个双向链表,没有实现)的一个结点(Node),来实现锁的分配。
3.实现
AQS 基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
4.根据AQS,得出Lock的实现过程
-
lock 的存储结构:一个 int 类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
-
lock 获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
-
lock 释放锁的过程:修改状态值,调整等待链表。
可以看到在整个实现过程中,lock 大量使用 CAS + 自旋。因此根据 CAS 特性,lock 建议使用在低锁冲突的情况下。目前 java1.6 以后,官方对 synchronized 做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用 synchronized 做同步操作。
atomic 包底层实现原理
Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。因此atomic证原子性就是通过**:自旋 + CAS(乐观锁)**
仔细分析 concurrent 包的源代码(lock和atomic均在这个包下) 实现,会发现一个通用化的实现模式:
-
首先,声明共享变量为 volatile;
-
然后,使用 CAS 的原子条件更新来实现线程之间的同步;
-
同时,配合以 volatile 的读 / 写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
可以看出,Lock类和Atomic包底层实现都是通过 CAS+自旋的方式解决多线程同步问题。那这二者有什么区别呢?
Atomic在竞争激烈时能维持常态,比 lock 性能好,但是只能同步一个变量。
volatile关键字
Java 因为指令重排序,优化我们的代码,让程序运行更快,也随之带来了多线程下,指令执行顺序的不可控。
volatile关键字的作用:
-
内存可见性,修饰的变量发生改变之后对所有线程立即可见
-
禁止指令重排序
volatile的底层是通过内存屏障实现的,第一个作用是禁止指令重排。内存屏障另一个作用是强制更新一次不同 CPU 的缓存。
synchronized 看作重量级的锁,而 volatile 看作轻量级的锁 。synchronized 使用的锁的层面是在JVM层面,虚拟机处理字节码文件实现相关指令。volatile 底层使用多核处理器实现的 lock 指令,更底层,消耗代价更小。
CAS
CAS 的全称是 Compare-And-Swap , 它是一条 CPU 并发原语。
CAS 并不是一种实际的锁,它仅仅是实现乐观锁的一种思想,java 中的乐观锁(如自旋锁)基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
乐观锁一般会使用版本号机制或 CAS 算法实现
1.版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
2.CAS 算法
即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数
-
需要读写的内存值 V
-
进行比较的值 A
-
拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
3.CAS在Java中的实现
CAS 并发原语现在 Java 语言中就是 Unsafe 类的各个方法,调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现 CAS 汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作,由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由于诺干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 原子指令,不会造成所谓的数据不一致问题。
4.synchronized与CAS的比较
synchronized涉及线程之间的切换,存在用户状态和内核状态的切换,耗费巨大。CAS只是CPU的一条原语,是一个原子操作,消耗较少。
CAS 相对于其他锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu 资源会消耗很高。CAS + 自旋适合使用在低并发有同步数据的应用场景。
synchronized 与Lock的区别
1.实现层面不一样。synchronized 是 Java 关键字,JVM 层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁,(但是Lock的底层CAS乐观锁比synchronized更底层,是CPU原语,属于操作系统层面的)
2.是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
3.是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时。
4.获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
5.功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率。