一、公平锁&非公平锁
1.1公平锁是什么
公平锁:线程按照申请锁的顺序来获取锁;在并发环境中,每个线程都会被加到等待队列中,按照 FIFO 的顺序获取锁。
非公平锁:线程不按照申请锁的顺序来获取锁;一上来就尝试占有锁,如果占有失败,则按照公平锁的方式等待。
通俗来讲,公平锁就相当于现实中的排队,先来后到;非公平锁就是无秩序,谁抢到是谁的;
1.2优缺点
公平锁
优点:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒线程的开销比非公平锁要大。
非公平锁
优点:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。
缺点:在高并发环境下可能造成线程优先级反转和饿死现象。
1.3Java中的公平&非公平锁
在 Java 中,synchronized 是典型的非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁,可以在初始化的时候指定。
查看 ReentrantLock 的源码会发现,初始化时可以传入 true 或 false,来得到公平或非公平锁。
//源码//默认为非公平public ReentrantLock() { sync = new NonfairSync(); }public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
public class FairLockDemo { public static void main(String[] args) { //公平锁 Lock fairLock = new ReentrantLock(true); //非公平锁 Lock unFairLock = new ReentrantLock(false); } }
二、可重入锁
2.1是什么
可重入锁也叫递归锁,是指线程可以进入任何一个它已经拥有的锁所同步的代码块。通俗来讲,就好比你打开了你家的大门,就可以随意的进入客厅、厨房、卫生间......
如下图,线程 M1 和 M2 是被同一把锁同步的方法,M1 中调用了 M2,那么线程 A 访问 M1 时,再访问 M2 就不需要重新获取锁了。
2.2优缺点
优点:可以一定程度上避免死锁
缺点:暂时不知道
2.3Java中的可重入锁
synchronized和ReentrantLock都是典型的可重入锁
synchronized
public class ReentrantDemo1 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendSMS(); }).start(); new Thread(() -> { phone.sendSMS(); }).start(); } }class Phone { public synchronized void sendSMS() { System.out.println(Thread.currentThread().getId() + ":sendSMS()"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } sendEmail(); } public synchronized void sendEmail() { System.out.println(Thread.currentThread().getId() + ":sendEmail()"); } }
ReentrantLock
public class ReentrantDemo2 { public static void main(String[] args) { User user = new User(); new Thread(() -> { user.getName(); }).start(); new Thread(() -> { user.getName(); }).start(); } }class User { Lock lock = new ReentrantLock(); public void getName() { lock.lock(); try { System.out.println(Thread.currentThread().getId() + ":getName()"); TimeUnit.SECONDS.sleep(1); getAge(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void getAge() { lock.lock(); try { System.out.println(Thread.currentThread().getId() + ":getAge()"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
2.4八锁问题
搞懂八锁问题,可以更深刻的理解 synchronized 锁的范围
2.5实现一个不可重入锁
public class UnReentrantLockDemo { private AtomicReferenceatomicReference = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); //自旋 while(!atomicReference.compareAndSet(null, current)) { } } public void unlock() { Thread current = Thread.currentThread(); atomicReference.compareAndSet(current, null); } }
三、自旋锁
3.1是什么
尝试获取锁的线程不会立即阻塞,而是以循环的方式不断尝试获取锁
3.2优缺点
优点:减少线程上下文切换的消耗
缺点:循环消耗CPU
3.3Java中的自旋锁
CAS:CompareAndSwap,比较并交换,它是一种乐观锁。
CAS 中有三个参数:内存值V、旧的预期值A、要修改的新值B;只有当预期值A与内存值V相等时,才会将内存值V修改为新值B,否则什么都不做
public class CASTest { public static void main(String[] args) { AtomicInteger a1 = new AtomicInteger(1); //V=1, A=1, B=2 //V=A,所以修改成功,此时V=2 System.out.println(a1.compareAndSet(1, 2) + "," + a1.get()); //V=2, A=1, B=2 //V!=A,修改失败,返回false System.out.println(a1.compareAndSet(1, 2) + "," + a1.get()); } }
源码解析:以 AtomicInteger 中的 getAndIncrement() 方法为例
//获取并增加,相当于i++操作public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }//调用UnSafe类中的getAndAddInt()方法public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //获取当前内存值 var5 = this.getIntVolatile(var1, var2); //循环比较内存值和预期值 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
CAS 也存在一些问题:
如果一直交换不成功,会一直循环,开销大
只能保证一个共享变量的原子操作
ABA 问题:即 A 被修改为 B,又被改为 A,虽然值没发生变化,但这种操作还是存在一定风险的
可以通过加时间戳或版本号的方式解决 ABA 问题:
public class ABATest { public static void main(String[] args) { showABA(); } /** * 重现ABA问题 */ private static void showABA() { AtomicReferenceatomicReference = new AtomicReference<>("A"); //线程X,模拟ABA问题 new Thread(() -> { atomicReference.compareAndSet("A", "B"); atomicReference.compareAndSet("B", "A"); }, "线程X").start(); //线程Y睡眠一会儿,等待X执行完 new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } atomicReference.compareAndSet("A", "C"); System.out.println("最终结果:" + atomicReference.get()); }, "线程Y").start(); } /** * 解决ABA问题 */ private static void solveABA() { //初始版本号为1 AtomicStampedReference asr = new AtomicStampedReference<>("A", 1); new Thread(() -> { asr.compareAndSet("A", "B", 1, 2); asr.compareAndSet("B", "A", 2, 3); }, "线程X").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } asr.compareAndSet("A", "C", 1, 2); System.out.println(asr.getReference() + ":" + asr.getStamp()); }, "线程Y").start(); } }
3.4动手实现一个自旋锁
public class SpinLockDemo { /** * 初始值为 null */ AtomicReferenceatomicReference = new AtomicReference<>(null); public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.lock(); spinLockDemo.unLock(); }, "线程A").start(); new Thread(() -> { spinLockDemo.lock(); spinLockDemo.unLock(); }, "线程B").start(); } public void lock() { //获取当前线程对象 Thread thread = Thread.currentThread(); do { System.out.println(thread.getName() + "尝试获取锁..."); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //当赋值成功才会跳出循环 } while (!atomicReference.compareAndSet(null, thread)); } public void unLock() { //获取当前线程对象 Thread thread = Thread.currentThread(); //置为null,相当于释放锁 atomicReference.compareAndSet(thread, null); System.out.println(thread.getName() + "释放锁..."); } }
四、共享锁&独占锁
4.1是什么
共享锁:也可称为读锁,可被多个线程持有
独占锁:也可称为写锁,只能被一个线程持有,synchronized和ReentrantLock都是独占锁
互斥:读读共享、读写互斥、写写互斥
4.2优缺点
读写分离,适用于大量读、少量写的场景,效率高
4.3java中的共享锁&独占锁
ReentrantReadWriteLock 中的读锁是共享锁、写锁是独占锁
class MyCache { private volatile Mapmap = new HashMap<>(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /** * 写锁控制写入 */ public void put(String key, Object value) { lock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "开始写入..."); //睡一会儿 TimeUnit.SECONDS.sleep(1); map.put(key, value); System.out.println(Thread.currentThread().getName() + "写入完成..."); } catch (Exception e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } /** * 读锁控制读取 */ public Object get(String key) { lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "开始读取..."); //睡一会儿 TimeUnit.SECONDS.sleep(1); Object value = map.get(key); System.out.println(Thread.currentThread().getName() + "读取结束...value=" + value); return value; } catch (Exception e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } return null; } public void clear() { map.clear(); } }
public class ReentrantReadWriteLockDemo { public static void main(String[] args) { MyCache cache = new MyCache(); for (int i = 1; i <= 5; i++) { int finalI = i; new Thread(() -> { cache.put(String.valueOf(finalI), String.valueOf(finalI)); cache.get(String.valueOf(finalI)); }, "线程" + i).start(); } cache.clear(); } }
最后
感谢大家看到这里,文章有不足,欢迎大家指出;如果你觉得写得不错,那就给我一个赞吧。
也欢迎大家关注我的公众号:程序员麦冬,麦冬每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!