一、公平锁&非公平锁

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 就不需要重新获取锁了。

关于Java 中的那些“锁”事_第1张图片

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 AtomicReference 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.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() {
        AtomicReference 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 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 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.3java中的共享锁&独占锁

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

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