1、基本
参考:
《锁介绍名词解释》
1.1 分类:
- 可重入锁:在执行对象中所有同步方法不用再次获得锁。
- 可中断锁:在等待获取锁过程中可中断
- 公平锁:按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
- 读写锁:读的时候可以多线程一起读,写的时候必须同步地写
1.2 乐观锁与悲观锁
-
乐观锁 只是在更新数据的时候才去判断之前有没有别的线程更新了这个数据。 CAS(Compare And Swap)算法:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,当前内存值V,旧的预期值A,新值B。当且仅当预期值A和当前内存值V相同时,将当前内存值V修改为B,否则什么都不做。
-
悲观锁 自己在使用数据的时候,其他线程无法访问,只能等待。 Java中,
synchronized关键字和Lock的实现类都是悲观锁
1.3 公平锁与非公平锁
-
非公平锁
就是一种获取锁的抢占机制,即所有等待的任务随机获得锁,或者说获取锁的顺序与申请锁的顺序无关。
实现:
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
synchronized关键字
优点: 效率高(如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁)
缺点: 可能产生饥饿问题 -
公平锁
表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先加锁先执行、先来先得、先进先出FIFO的顺序。
背景: 当多个任务等待获取锁时,CPU是按照“在等待队列里随机挑选一个线程”的调度规则执行的。那么就可能造成某一个任务永远得不到执行的问题,即造成饥饿问题。
优点: 公平锁可以避免饥饿问题的产生。
缺点: 与非公平锁相比,公平锁的效率会比较低。因为需要维持一个有序的序列。
实现:Lock lock = new ReentrantLock(true);
1.4 可重入锁与非可重入锁
- 可重入锁:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,该线程再进入的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。 举例: 当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。 Java中ReentrantLock和synchronized都是可重入锁。 优点: 可一定程度避免死锁。 - 非可重入锁:
非可重入锁NonReentrantLock容易引起死锁。
1.5 synchronized和volatile的区别
volatile具备两层语义:
- 保证了不同线程对这个变量进行操作时的可见性:工作内存中发生变化之后,必须马上回写到主内存,而线程读取
volatile变量的时候,必须马上到主内存中去取最新值而不是读取本地工作内存的副本。volatile并不能保证在多线程下是安全的,因为Java中的运算并非原子操作 - 禁止进行指令重排序。为了获取更好的性能,在程序执行过程中, 编译器和CPU可能会对指令重新排序。因此多线程下可能会出现一些意想不到的问题。
区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、和类级别的
注:
volatile不能保证线程安全,因为volatile不保证原子性。比如操作:从内存中读取值 --> 加1 --> 写回内存。在两个线程中进行此操作,结果不一定“加2”。- 在需要同步的时候,第一选择应该是
synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。 尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。 - 用
synchronized来加锁的方法抛异常的时候,锁仍然可以由JVM来自动释放。
1.6 synchronized与Lock的区别
- synchronized是Java关键字,由JVM实现同步,释放由JVM来完成,不用人工干预;
- Lock是一个接口interface,ReentrentLock实现了该接口。
Lock的优点:Lock对锁的操作更加灵活:
- synchronized不可中断,Lock可中断:使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断 (interrupt) 的,而使用
Lock.lockInterruptibly() 获取锁时被中断,线程将抛出中断异常。 - Lock可非阻塞获取锁 :使用
synchronized关键字获取锁时,如果没有成功获取,只有被阻塞,而使用lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
synchronized的优点:
-
与
Lock相比,synchronized使用起来更加简单 -
synchronized同步由JVM层实现,效率更加高(引入很多优化措施如偏向锁、轻量锁等) -
synchronized是非公平锁,Lock有公平锁和非公平锁
2、FAQ
2.1 CAS算法中的ABA问题
-
什么是ABA?
就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过。 其实很多场景如果只追求最后结果正确,这是没关系的。但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。 -
那怎么解决ABA问题?
用版本号去保证就好了,就比如说,在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。 除了版本号,使用时间戳也是可以的:查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证