目录
synchronized关键字
基本概念
synchronized 属于独占式悲观锁,是通过 JVM 隐式实现的,synchronized 只允许同一时刻只有一个线程操作资源。
在 Java 中每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,当线程进入字节码 monitorenter 指令之后,线程将持有 monitor 对象,执行 monitorexit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞等待获取该对象。
三种使用方式
修饰代码块:需要一个引用对象作为锁对象
修饰实例方法:锁对象为当前对象
修饰静态方法(static):锁对象为当前类的class对象
特性
原子性
操作不可被中断 全执行 全都不执行
synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。 这两个字节码对应的内存模型的操作是 lock(上锁) 和 unlock(解锁) 。因为这两个操作之间运行的都是原子的(这个操作保证了变量为一个线程独占的,也就是说只有获得锁的线程才能够操作被锁定的内存区域。
可见性
资源信息对于所有线程可见
调用的unlock解锁这个操作规定,放开对某个变量的锁的之前,需要把这个变量从缓存更新到主内存。
有序性
按照代码先后顺序执行
synchronized无法禁止指令重排,却能保证有序性
在一个线程内部,他不管怎么指令重排,他都是 as if serial 的,也就是说单线程即使重排序之后的运行结果和串行运行的结果是一样的,是类似串行的语义。而当线程运行到同步块时,会加锁,其他线程无法获得锁,也就是说此时同步块内的方法是单线程的,根据 as if serial ,可以认为他是有序的。
可重入性
一个线程拥有锁后还可以重复申请锁
锁实现
同步方法
flags标志 ACC_SYNCHRONIZED
标志,这标志用来告诉JVM这是一个同步方法。在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。
同步代码块
monitorenter和monitorexit指令操作
由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
原理
执行monitorenter字节码时,如果这个对象没有被上锁,或者当前线程已经持有了该锁,那么锁的计数器会+1,而在执行monitorexit字节码时,锁的计数器会-1,当计数器为0时,锁被释放。如果获取对象的锁失败,那么该线程会被阻塞等待,直到之前把这个对象上锁的线程释放这个锁为止。
每个对象都有一个monitor(监视器)与之关联,所谓的上锁,就是获得对象的monitor的独占权(因为只用获得monitor才能访问这个对象)。执行monitorenter字节码的时候,线程就会尝试获得monitor的所有权,也就是尝试获得对象的锁。只有获得了monitor,才能进入同步块,或者执行同步方法。独占对象的本质是独占对象的monitor。
对象实例组成
对象头
Mark Word:存储对象的hashCode、锁信息或分代年龄或GC标志等信息。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
Class Metadata Address:是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
实例数据
类的属性数据信息,包括父类的属性信息。
填充
不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
JDK1.6之后JVM对synchronized的优化
锁升级
锁升级其实就是从偏向锁到轻量级锁再到重量级锁升级的过程,也称之为锁膨胀。
偏向锁
是指在无竞争的情况下设置的一种锁状态。偏向锁的意思是它会偏向于第一个获取它的线程,当锁对象第一次被获取到之后,会在此对象头中设置标示为“01”,表示偏向锁的模式,并且在对象头中记录此线程的 ID,这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作,如 Locking、Unlocking 等,直到另一个线程尝试获取此锁的时候,偏向锁模式才会结束,偏向锁可以提高带有同步但无竞争的程序性能。但如果在多数锁总会被不同的线程访问时,偏向锁模式就比较多余了,此时可以通过 -XX:-UseBiasedLocking 来禁用偏向锁以提高性能。即 锁不存在多线程竞争,总是由同一线程多次获得。
轻量锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块
是相对于重量锁而言的,在 JDK 1.6 之前,synchronized 是通过操作系统的互斥量(mutex lock)来实现的,这种实现方式需要在用户态和核心态之间做转换,有很大的性能消耗,这种传统实现锁的方式被称之为重量锁。
而轻量锁是通过比较并交换(CAS,Compare and Swap)来实现的,它对比的是线程和对象的 Mark Word(对象头中的一个区域),如果更新成功则表示当前线程成功拥有此锁;如果失败,虚拟机会先检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,则说明当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用了。当两个以上的线程争抢此锁时,轻量级锁就膨胀为重量级锁,这就是锁升级的过程。
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
自旋锁
让线程执行循环等待锁的释放,不让出CPU。
优点:如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起。
缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁
自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
比如在同一个锁对象上,如果通过自旋等待成功获取了锁,那么虚拟机就会认为,它下一次很有可能也会成功 (通过自旋获取到锁),因此允许自旋等待的时间会相对的比较长,而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是自适应自旋锁的功能。