Java锁结构图
(一) 乐观锁 悲观锁
悲观锁和乐观锁是一种广义的概念,体现的是看待线程同步的不同的角度
悲观锁认为自己在使用数据的时候,一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。
锁实现:关键字synchronized、接口Lock的实现类
使用的场景:写操作较多,先加锁可以保证写操作是数据正确
乐观锁认为自己在使用数据的时候不会有其他的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
锁实现:CAS算法 --> 详细查看 多线程之CAS和ABA
使用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅提升
(二)自旋锁 适应性自旋锁
1 自旋锁
阻塞或唤醒一个Java进程,需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。
如果同步代码块中的内容过于简单,状态转换花费的时间有可能比用户代码执行的时间还长。
而为了让当前线程“稍等一下”,我们就需要让当前线程进行自旋,如果自旋完成后,持有锁的线程已经释放了锁,当前线程就可以不进入阻塞状态而是直接获得同步资源,
避免线程切换的开销,这就是自旋锁。
缺陷 : 自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋的效果就很好。反之,如果所被占用的时间很长,自旋就是在白白浪费处理器时间。
所以,自旋等待的时间必须要有限度,默认情况下是10次,也可以通过 -Xx:PreBloackSpin 来更改。如果在自旋10次都没有获得锁,就应该挂起线程。
2 适应性自旋锁
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作源码中的do…while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直到成功
自适应意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态共同决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,
并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很可能再次成功的,进而它将会允许线程自旋相对更长的时间。如果对于某个锁,线程很少成 功获得过,
则会相应减少自旋的时间甚至直接进入阻塞的状态,避免浪费处理器资源。
(三)公平锁 不公平锁 区别 this.hasQueuedPredecessors()
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
优点 :等待锁的线程不会饿死。
缺点 :是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,
那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
优点 : 是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
缺点 : 是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
(四)可重入锁 (ReentrantLock) 不可重入锁(NonReentrantLock)
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
Java中 ReentrantLock 和 Synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
public class Do{ public synchronized void doSomething() { System.out.println("方法1执行..."); doOthers(); } public synchronized void doOthers() { System.out.println("方法2执行..."); } }
不可重入锁,在所没有释放的时候,其他方法是不能进行执行的,只能进行等待锁的释放。等拿到锁之后,才能继续执行。
(五)共享锁 互斥锁(排它锁)
共享锁 : 获得共享锁后,其他线程也可以获得共享锁完成读操作,但都不能修改和删除数据。
互斥锁(排它锁) : 获得锁后 即能读也能写,但其他线程就不能在获取这个锁了。
JVM 锁
可以通过-XX:-UseBiasedLocking 来禁用偏向锁
Synchronized 原理
真希望雨能下不停