JAVA中常见的锁,你知道的有哪些?
公平锁/非公平锁/可重入锁/递归锁/自旋锁/读写锁 等等;谈谈对各自的理解;
一、JAVA锁之公平锁和非公平锁
1、是什么?
-
公平锁:多个线程按照申请所的顺序来获取锁,类似排队打饭,先到先得;
-
非公平锁:是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁;在多线程的情况下有可能会造成优先级反转或者饥饿现象;
2、两者区别?
-
公平锁:就是很公平,在并发环境下,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁;否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己;
-
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的那种方式,加入到等待队列;
并发包中的ReentrantLock的创建时通过制定构造函数的boolean类型来得到公平锁或非公平锁的,默认什么都不传,就是非公平锁;这点可以很容易的从源码构造函数中读懂;
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
非公平锁的优点在于:吞吐量比公平锁大;
对于Synchronized而言,也是一种非公平锁;
二、JAVA锁之可重入锁(递归锁)
1、是什么?
指的是,同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码;
在同一个线程在外层方法获取到锁的时候,在进入内层方法的时会自动获取锁;
也就是说:线程可以进入任何一个 它已经拥有的锁 所同步着的代码块;
2、作用?
最大的作用就是:可以有效地避免死锁;
3、代码验证synchronized是可重入锁;
package com.jiguiquan.www; /** ** 代码验证可重入锁(递归锁) * @author jiguiquan * */ public class ReentrantLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendSMS(); },"t1").start(); new Thread(() -> { phone.sendSMS(); },"t2").start(); } } class Phone { public synchronized void sendSMS() { System.out.println(Thread.currentThread().getName()+"\t invoked sendSMS"); sendEmail(); } public synchronized void sendEmail() { System.out.println(Thread.currentThread().getName()+"\t invoked sendEmail"); } }
代码执行结果如下:
很明显,一个synchronized同步方法可能很直接地进入了它其中的另一个同步方法(同一个线程在外层方法获取到锁的时候,在进入内层方法的时会自动获取锁);
4、代码验证ReentrantLock是可重入锁;
package com.jiguiquan.www; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** ** 代码验证可重入锁(递归锁) * @author jiguiquan * */ public class ReentrantLockDemo { public static void main(String[] args) { Dog dog = new Dog(); Thread t3 = new Thread(dog, "t3"); Thread t4 = new Thread(dog, "t4"); t3.start(); t4.start(); } } class Dog implements Runnable{ Lock lock = new ReentrantLock(); @Override public void run() { get(); } public void get() { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t invoked get()"); set(); } finally { lock.unlock(); } } public void set() { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t invoked set()"); } finally { lock.unlock(); } } }
执行结果如下:
注意这里可没有使用synchronized,而是用时的ReentrantLock可重入锁;
补充:增加两层lock.lock();和两层lock.unlock(),程序时没有问题的,只要lock和unlock的数量配对,如果不一样多,那么程序会无法结束,但是并不会报错;——加锁几次,那么必须解锁几次;
三、JAVA锁之自旋锁(SpinLock)
1、是什么?
是指:尝试获取锁的线程不会立即堵塞,而是采用循环的方式去尝试获取锁——用循环代替堵塞;
这样的好处是:减少线程上下文切换的消耗,缺点是循环会消耗CPU;
讲到这里,我们必须要联想到CAS中的Unsafe.getAndAddInt()方法的实现,do…while…循环;
2、请手写一个自旋锁,即不使用synchronized或者ReentrantLock的方式实现一个Lock锁;(重要)
package com.jiguiquan.www; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** ** 手写一个自旋锁 * @author jiguiquan * */ public class SpinLockDemo { //原子引用线程,默认的reference初始值为null AtomicReference<Thread> atomicReference = new AtomicReference<Thread>(); public void myLock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t come in***"); while (!atomicReference.compareAndSet(null, thread)) { //如果是null,那么就将当前线程放入其中 //第一次必定成功,成功后不会进入循环; //后面的线程再想要进入的时候,会发现已经不为null,compareAndSet无法成功,取!后为true,进入循环(死循环) } } public void myUnlock() { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock"); } public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.myLock(); //占有时间长一点,看看BB线程是否可以加塞进来;如果无法加塞,就说明此自旋锁起作用了; try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} spinLockDemo.myUnlock(); },"AA").start(); //停一秒,是为了确保BB线程在AA线程后面执行 try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} new Thread(() -> { spinLockDemo.myLock(); spinLockDemo.myUnlock(); },"BB").start(); } }
执行结果如下:
分析:
在AA进入之后,立即给上了一把自旋锁;此时BB也进来了,但是由于已经被AA占有了,此时BB就一直循环等待询问,直到AA释放锁后,BB才进行后面的操作;
有点像单坑卫生间;
四、JAVA锁之独占锁(写锁)/共享锁(读锁)/互斥锁——ReadWriteLockDemo
1、是什么?
-
独占锁(写锁):该锁一次只能被一个线程所持有;对ReentrantLock和Synchronized而言都是独占锁;
-
共享锁(读锁):该锁可以被多个线程同时持有;
-
对ReentrantReadWriteLock,其读锁时共享锁,其写锁是独占锁:该锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的。
2、手写一个缓存MyCache,为了下一步的代码验证:
此时是一个不带任何锁的缓存类:
package com.jiguiquan.www; /** ** 手写一个缓存类,为了读写锁验证时候使用 * 所有缓存框架的3大方法:写、读、清空 * @author jiguiquan * */ import java.util.HashMap; import java.util.Map; public class MyCache { private volatile Map<String, Object> map = new HashMap<>(); public void put(String key, Object value) { System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key); //模拟网络延时拥堵 try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();} map.put(key, value); System.out.println(Thread.currentThread().getName()+"\t 写入完成。"); } public void get(String key) { System.out.println(Thread.currentThread().getName()+"\t 正在读取:"); //模拟网络延时拥堵 try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();} Object result = map.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+result); } }
3、代码验证,五个线程写,五个线程读:
package com.jiguiquan.www; /** ** 多个线程同时读一个资源类,没有任何问题,所以为了满足并发量,读取共享资源应该是可以同时进行的; * 但是 * 如果有一个线程想去写共享资源就不应该再有其他线程可以对该资源进行读或写操作了 * 写:原子+独占,中间不可被加塞打断 * 读: * 小总结: * 读-读能共存 * 读-写不能共存(互斥) * 写-写不能共存(互斥) * @author jiguiquan * */ public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); //让5个线程来写 for (int i = 0; i < 5; i++) { //由于是lambda表达式,我们需要让i为final final int tempInt = i; new Thread(() -> { myCache.put(tempInt+"", tempInt+""); },String.valueOf(i)).start(); } for (int i = 0; i < 5; i++) { //由于是lambda表达式,我们需要让i为final final int tempInt = i; new Thread(() -> { myCache.get(tempInt+""); },String.valueOf(i)).start(); } } }
此时执行结果:
0 正在写入:0 3 正在写入:3 1 正在写入:1 2 正在写入:2 4 正在写入:4 0 正在读取: 2 正在读取: 3 正在读取: 1 正在读取: 4 正在读取: 1 写入完成。 3 写入完成。 0 写入完成。 0 读取完成:null 3 读取完成:null 2 读取完成:null 1 读取完成:1 4 写入完成。 2 写入完成。 4 读取完成:null
很显然,非常乱
4、此时我们使用读写写ReentrantReadWriteLock对MyCache缓存类进行修改
package com.jiguiquan.www; /** ** 手写一个缓存类,为了读写锁验证时候使用 * 所有缓存框架的3大方法:写、读、清空 * @author jiguiquan * */ import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; public class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); public void put(String key, Object value) { rwlock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key); //模拟网络延时拥堵 try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();} map.put(key, value); System.out.println(Thread.currentThread().getName()+"\t 写入完成。"); } catch (Exception e) { e.printStackTrace(); } finally { rwlock.writeLock().unlock(); } } public void get(String key) { rwlock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在读取:"); //模拟网络延时拥堵 try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();} Object result = map.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+result); }catch (Exception e) { e.printStackTrace(); } finally { rwlock.readLock().unlock(); } } }
测试代码不变,我们再次进行测试,结果如下:
0 正在写入:0 0 写入完成。 2 正在写入:2 2 写入完成。 1 正在写入:1 1 写入完成。 3 正在写入:3 3 写入完成。 4 正在写入:4 4 写入完成。 1 正在读取: 0 正在读取: 3 正在读取: 2 正在读取: 4 正在读取: 0 读取完成:0 1 读取完成:1 3 读取完成:3 4 读取完成:4 2 读取完成:2
很显然,读可以被加塞,但是写不可以被加塞;
之所以用ReentrantReadWriteLock而不是使用简单的ReentrantLock的原因就在于,我们只需要控制写操作的独占,而不需要控制读操作的独占,而后者是直接为读、写操作都加了锁,这样很影响读的性能;