关于Java 中的那些“锁”事

63 阅读5分钟

一、公平锁&非公平锁

1.1公平锁是什么

  • 公平锁:线程按照申请锁的顺序来获取锁;在并发环境中,每个线程都会被加到等待队列中,按照 FIFO 的顺序获取锁。
  • 非公平锁:线程不按照申请锁的顺序来获取锁;一上来就尝试占有锁,如果占有失败,则按照公平锁的方式等待。

通俗来讲,公平锁就相当于现实中的排队,先来后到;非公平锁就是无秩序,谁抢到是谁的;

**1.2****优缺点**

公平锁

  • 优点:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。
  • 缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒线程的开销比非公平锁要大。

非公平锁

  • 优点:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。
  • 缺点:在高并发环境下可能造成线程优先级反转和饿死现象。
**1.3****Java中的公平&非公平锁**

在 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.3****Java中的可重入锁**

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****八锁问题**

点击查看我之前的博客 多线程之8锁问题,搞懂八锁问题,可以更深刻的理解 synchronized 锁的范围

**2.5****实现一个不可重入锁**
public class UnReentrantLockDemo {

    private AtomicReference<Thread> atomicReference = 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.3****Java中的自旋锁**

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() {
        AtomicReference<String> atomicReference = 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<String> 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
     */
    AtomicReference<Thread> atomicReference = 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.3****java中的共享锁&独占锁**

ReentrantReadWriteLock 中的读锁是共享锁、写锁是独占锁

class MyCache {
    private volatile Map<String, Object> map = 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相关技术文章或行业资讯,欢迎大家关注和转发文章!