1, 锁状态
锁的状态只能升级不能降级.
无锁
没有锁对资源进行锁定, 所有线程都能访问并修改同一个资源, 但同时只有一个线程能修改成功. 其他修改失败的线程会不断重试, 直到修改成功, 如 CAS 原理和应用是无锁的实现.
偏向锁
偏向锁是指一段同步代码一直被一个线程访问, 那个该线程会自动获取锁, 降低获取锁的代价.
轻量级锁
是指当锁是偏向锁的时候, 被另外的线程所访问, 偏向锁就会升级为轻量级锁, 其他线程会通过自旋的形式尝试获取锁, 不会阻塞, 从而提高性能. 通过 cas 操作和自旋来解决加锁问题, 自旋超过一定的次数或者已经有一个线程在自旋, 又来一个线程获取锁时, 轻量级锁会升级为重量级锁.
重量级锁
升级为重量级锁, 等待锁的线程都会进入阻塞状态.
2, 乐观锁与悲观锁
乐观锁, 每次拿数据的时候认为别人都不会修改, 在更新的时候再判断在此期间有没有更新数据, 可以使用版本号等机制, 适合读取多场景, 提高性能.
悲观锁, 每次拿数据都认为别人会修改, 都会上锁, 可以使用 synchronized, 独占锁 Lock, 读写锁等机制, 适合写多的场景, 保证写入操作正确.
3, 自旋锁与适应性自旋锁
自旋锁: 指当一个线程在获取锁的时候, 如果锁已经被其他线程获取, 那么该线程将循环等待, 然后不断判断锁是否能获取成功, 直到获取到锁才退出循环.
优点: 线程不进行上下文切换, 减少了上下文切换的时间.
存在的问题: 如果线程持有锁的时间较长, 其他线程进入循环, 消耗 CPU.
自适应自旋锁: 指的是自旋的时间不固定, 由前一个在同一个锁上自旋的时间和锁拥有者的状态来决定. 如果在同一个对象上, 刚刚通过自旋成功获取过锁, 且持有锁的线程正在运行中, 那么虚拟机就会认为这次自旋很有可能再次成功. 反之自旋操作很少成功获取锁, 那么后面获取这个锁可能直接省略掉自旋的过程, 直接阻塞线程.
4, 公平锁与非公平锁
公平锁是指多个线程按照申请锁的顺序直接进入队列排队, 队列中的第一个线程才能获取锁.
非公平锁是指线程先尝试获取锁, 获取不到进入队列中排队, 如果能获取到, 则无需阻塞直接获取锁.
5, 重入锁与非重入锁
重入锁: 同一个线程在外层方法获取锁的时候, 在进入内层方法会自动获取锁, 前提是锁对象是相同的.
6, 共享锁与排他锁
共享锁是指一个锁可以被多个线程锁持有.
排它锁或者叫独享锁或者互斥锁 指锁一次只能被一个线程所持有.
7, 读写锁
读锁是共享的, 写锁是独占的.
读读之间不会互斥, 读写互斥, 写写互斥, 读写锁提高了读的性能.
8,CAS
CompareAndSwap 比较与交换, 是一种无锁算法, 原子类使用了 CAS 实现了乐观锁.
带来的问题:
ABA 问题
解决思路在变量前面加版本号, 每次变量更新的时候都将版本号 1, 每次更新的时候要求版本 >= 当前版本 (AtomicStampedReference)
循环时间长开销大, CAS 操作如果长时间执行不成功, 会导致其一直自旋, CPU 消耗大.
只能保证一个共享变量的原子操作.
可以把多个变量放在一个对象里面进行 CAS 操作.
9, 锁优化
9.1, 锁升级
偏向锁的升级
线程 A 获取锁对象时, 会在 java 对象头和栈帧中记录偏向的线程 A 的 id, 线程 A 再次获取锁时, 只需要比较 java 头中的线程 id 与当前 Id 是否相等, 如果一致则无需通过 cas 加锁解锁. 如果不一致, 说明有线程 B 来获取锁, 那么要判断 java 头中偏向锁的线程是否存活, 如果没有存活, 锁对象被置为无锁状态, 线程 B 可将锁对象置为 B 的偏向锁. 如果存活, 则查看 A 是否还需要继续持有对当前锁, 如果不需要持有, 则将锁置为无锁状态, 偏向新的线程, 如果还继续持有锁对象, 则暂停 A 线程, 撤销偏向锁, 将锁升级为轻量级锁.
轻量级锁的升级
线程 A 获取轻量级锁时会把锁的对象头复制到自己的线程栈针中, 然后通过 cas 把对象头中的内容替换为 A 所记录的地址. 此时线程 B 也想获取锁, 发现 A 已经获取锁, 那么线程 B 就自旋等待. 等到自旋次数到了或者线程 A 正在执行, 线程 B 自旋等待, 此时来了线程 C 来竞争锁对象, 这个时候轻量级锁就会膨胀为重量级锁. 重量级锁会把未获得到锁对象的线程全部变为阻塞状态该, 防止 CPU 空转.
9.2, 锁粗化
将多个连续的加锁, 解锁操作连接在一起, 扩展成为一个范围更大的锁, 避免频繁的加解锁操作.
9.3, 锁消除
通过逃逸分析, 去除不可能存在共享资源竞争的锁, 通过这种方式消除没有必要的锁.
10,synchronized 底层实现
synchronized 通过 Monitor 实现同步, Monitor 依赖于底层操作系统互斥锁来实现线程同步.
java 对象头是由 markword(标记字段) 和 klass point(类型指针) 组成. markword 存储对象的 hashcode, 分代年龄和锁标志位信息. Klass point 指向对象元数据的指针, 虚拟机通过这个指针来确定对象是哪个类的实例.
synchronized 修饰同步代码块, 是使用 monitorenter 和 monitorexit 来控制的, 通过 java 对象头中的锁计数器.
修饰方法时会将方法标识为 ACCSYNCHRONIZE,JVM 通过这个标志来判断方法是不是同步方法.
11,synchronized 与 ReentrantLock 的区别
两者都是悲观锁, 可重入锁.
ReentrantLock 可中断, 可以实现公平锁, 可以绑定多个条件.
ReentrantLock 需要显示的调用锁和释放锁, synchronized 属于 java 关键字, 不需要显式的释放.
12,volatile 关键字
保证变量内存可见.
禁止指令重排序.
volatile 和 synchronized 的区别:
volatile 不会阻塞, synchronized 会阻塞.
volatile 保证数据的内存可见性但不能保证原子性, synchronized 两者都能保证.
volatile 主要解决变量在线程之间的可见性, 而 synchronized 主要解决多线程访问资源的同步性.
13,Atomic 原子类实现
使用 cas 操作 volatile native 方法保证同步.
14,AQS
AQS(AbstractQueuedSynchronizer) 内部维护的是一个 FIFO 的双向同步队列, 如果当前线程竞争锁失败, AQS 会把当前线程以及等待状态信息构造成一个 Node 加入到同步队列中, 同时在阻塞该线程. 当获取锁的线程释放锁以后, 会从队列中唤醒一个阻塞的节点线程. 使用内部的一个 state 来控制是否获取锁, 当 state=0 时表示无锁状态, state>0 时表示已经有线程获取了锁.
15,AQS 的组件
semaphore 可指定多个线程同时访问某个共享资源.
countDownLatch 一个线程 A 等待其他线程执行完成之后才继续执行.
cyclicBarrier 一组线程等待至某个状态之后同时执行.
countDownLatch 和 CyclicBarrier 的区别
countDownLatch 是一个线程等一组线程执行完成之后才执行, cyclicBarrier 是一组线程互相等待至某个状态之后, 同时执行.
countDownLatch 不能复用, cyclicBarrier 可以重用.
16, 锁降级
锁降级是指将写锁降级为读锁, 这个过程就是当前线程已经获取到写锁的时候, 再获取到读锁, 随后释放写锁的过程, 这么做的目的为的就是保证数据的可见性.
17, 逃逸分析
逃逸分析就是分析对象的动态作用域, 当一个对象在方法中被定义后, 他可能被外部方法所引用, 作为参数传递到其他方法中, 成为方法逃逸, 赋值给类变量或者可以被其他线程访问的实例变量成为线程逃逸.
使用逃逸分析, 编译器可以对代码做优化. 比如: 同步省略 (锁消除), 将堆分配转化为栈分配, 标量替换.
使用逃逸分析的缺点, 没法保证逃逸分析的性能一定高于其他性能. 极端的话经过逃逸分析后, 所有的对象都逃逸了, 那么逃逸分析的过程就浪费了.
来源: http://www.bubuko.com/infodetail-3338666.html