0
点赞
收藏
分享

微信扫一扫

JUC之各种锁

码农K 2022-04-15 阅读 44
java

公平锁和非公平锁

回头看一下之前的一个多线程买票案例

public class LockSaleTicket {
    public static void main(String[] args) {
        LockTicket lockTicket = new LockTicket();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "AA").start();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "BB").start();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "CC").start();
    }
}

class LockTicket {
    private static int number = 20;
    private final ReentrantLock lock = new ReentrantLock();
    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + " : 卖出一张票, 剩余票数: " + (--number));
            }
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYN2erI9-1650025917875)(/img/2022-04-05/01.png)]

CC线程几乎卖出了一多半的票, 而BB线程只卖出了2张票, 这是因为在线程抢占的过程中, CC总能抢到锁, 这样就容易导致其他线程饥饿, 那么为什么会这样呢? 来看一下ReentrantLock的源码:

/**
 * 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();
}

从字面意思上也能看出来, ReentrantLock在创建的时候如果不传入任何参数, 默认创建的是一个非公平锁, 当传入一个布尔值为true时, 创建的则是一个公平锁, 使用公平锁修改上面的案例:

public class LockSaleTicket {
    public static void main(String[] args) {
        LockTicket lockTicket = new LockTicket();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "AA").start();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "BB").start();
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
                lockTicket.sale();
            }
        }, "CC").start();
    }
}

class LockTicket {
    private static int number = 20;
    private final ReentrantLock lock = new ReentrantLock(true);
    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + " : 卖出一张票, 剩余票数: " + (--number));
            }
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r71I7ssZ-1650025917876)(/img/2022-04-05/02.png)]

可以看到, 当三个线程都创建完成后, 遵循的就是一个线程卖一张票的的规则, 很公平, 但是公平锁也有缺点, 那就是效率低下, 所以使用的时候还是要视情况而定。

可重入锁

什么是可重入锁?

  • 可重入锁又称递归锁, 是指在同一个线程在外层方法获取锁的时候, 再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象), 不会因为之前已经获取过还没有释放而阻塞
  • Java中synchronized和ReentrantLock都是可重入锁, 可重入锁的存在就是为了在一定程度上避免死锁

只看概念不太能理解可重入锁这个东西, 来通过代码演示一下:

//synchronized版本
/**
 * 外中内三层代码块上的都是同一把锁o
 */
Object o = new Object();
new Thread(()->{
    synchronized(o) {
        System.out.println(Thread.currentThread().getName()+"进入外层");
        synchronized (o) {
            System.out.println(Thread.currentThread().getName()+"进入中层");
            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+"进入内层");
            }
        }
    }
},"AA").start();

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cqpcLGW1-1650025917877)(/img/2022-04-05/03.png)]

可以看到因为三层代码块用的是同一把锁, 所以不用等到上一层得到锁的代码块释放锁就能进入, 接着看一下ReentrantLock版本

//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
    //上锁
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName()+"进入外层");
        //上锁
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"进入内层");
        }finally {
            //释放锁
            lock.unlock();
        }
    }finally {
        //释放锁
        lock.unlock();
    }
},"BB").start();

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JK1AWVDS-1650025917878)(/img/2022-04-05/04.png)]

依旧是不用等到上一层代码块的锁释放就可以得到锁进入代码块

死锁

什么是死锁?

  • 死锁是指两个或两个以上的线程在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力干涉那它们都将无法推进下去, 如果资源充足, 进程的资源请求都能够得到满足, 死锁出现的可能性就很低, 否则就会因争夺有限的资源而陷入死锁

  • 如果有两个线程各自持有一把锁, 却同时想获取对方的锁而双发都未释放锁, 这时就产生了死锁

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eB5JjV8L-1650025917879)(/img/2022-04-05/05.png)]

来看一下代码演示:

public class DeadLock {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        new Thread(()->{
            synchronized (a) {
                System.out.println("获取到锁a, 等待锁b释放");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("获取到锁b");
                }
            }
        }, "A").start();
        new Thread(()->{
            synchronized (b) {
                System.out.println("获取到锁b, 等待锁a释放");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println("获取到锁a");
                }
            }
        }, "B").start();
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWWXFc5k-1650025917880)(/img/2022-04-05/06.png)]

两个线程都在等待对方的锁释放, 然而如果它们获取不到对方的锁就不会释放自己的锁, 这就产生了死锁, 如果我们不人为去干预, 会一直卡在这里占用系统资源, 但是很多时候思索地产生是因为资源分配不当导致的, 我们很难这么清晰的观察到, 那么就需要通过命令的方式排查死锁并人为关闭死锁线程

乐观锁和悲观锁

悲观锁:

  • 认为自己在使用数据的时候一定有别的线程来修改数据, 因此在获取数据的时候会先加锁, 确保数据不会被别的线程修改
  • 适合写操作多的场景, 先加锁可以保证写操作时数据正确(写操作包括增删改), 显式的锁定之后再操作同步资源
  • synchronized关键字和Lock的实现类都是悲观锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlD57wMB-1650025917880)(/img/2022-04-05/07.png)]

乐观锁:

  • 乐观锁认为自己在使用数据时不会有别的线程修改数据, 所以不会添加锁, 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据, 如果这个数据没有被更新, 当前线程将自己修改的数据成功写入, 如果数据已经被其他线程更新, 则根据不同的实现方式执行不同的操作
  • 适合读操作多的场景, 不加锁的特点能够使其读操作的性能大幅度提升
  • 乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
  • 乐观锁在Java中通过使用无锁编程来实现, 最常采用的时CAS算法, Java原子类中的递增操作就通过CAS自旋实现的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3YK3kdk-1650025917882)(/img/2022-04-05/08.png)]

读写锁

在多线程对同一资源进行操作的时候肯定是要加锁的, 对于写操作(增删改)来说, 肯定是要考虑并发场景下的操作原子性的, 所以写操作必须是独占锁, 但是对于读操作来说, 多线程同时读一个资源并没有任何问题, 为了满足并发情况下的效率, 读操作应为共享锁。为了满足这种需求就出现了读写锁

读写锁特点:

  • 读-读可以共享
  • 读-写互斥
  • 写-写互斥

用一个小案例来演示一下读写锁的使用: 多个线程对Map进行写入和读取操作

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //创建写线程
        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {
                try {
                    myCache.put(number + "", number + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "" + i).start();
        }
        //创建读线程
        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {
                try {
                    myCache.get("" + number);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "" + i).start();
        }
    }
}

//资源类
class MyCache {
    //创建读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //创建map集合
    private volatile static Map<String, Object> map = new HashMap<>();
    //放数据
    public void put(String key, Object value) throws InterruptedException {
        //添加写锁, 独占锁啊
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在进行写操作");
            TimeUnit.SECONDS.sleep(1);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写操作结束");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
    //取数据
    public Object get(String key) throws InterruptedException {
        //添加读锁, 共享锁
        readWriteLock.readLock().lock();
        try {
            Object result = null;
            System.out.println(Thread.currentThread().getName() + "正在进行读操作");
            TimeUnit.SECONDS.sleep(1);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读操作结束");
            return result;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgnO5roB-1650025917883)(/img/2022-04-05/09.png)]

可以看到, 写操作未结束时, 不会有其他的线程进行操作, 但是读操作未结束时, 其他读操作线程也可以进行操作, 这就说明写锁是独占的, 读锁是共享的。

锁降级

如果我们在做完写操作之后又想进行读操作, 但是由于写锁是独占锁, 这时候进行读操作很浪费资源, 这时候就可以将写锁降级为读锁, 这样读取的时候其他线程也能共享读取

具体步骤:

获取写锁->进行写操作->获取读锁->释放写锁->进行读操作->释放读锁

代码演示:

public class ReadWriteLockDemo2 {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        //写锁上锁
        writeLock.lock();
        //进行写操作
        System.out.println("进行写操作");
        //锁降级, 先读锁上锁再释放写锁
        readLock.lock();
        writeLock.unlock();
        //进行读操作
        System.out.println("进行读操作");
        //释放读锁, 操作完成
        readLock.unlock();
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkIrl8z8-1650025917885)(/img/2022-04-05/10.png)]

成功从写锁降级成读锁

写锁可以降级为读锁, 那么读锁可不可以升级为写锁呢, 把上面的代码改一下试试

public class ReadWriteLockDemo2 {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        //读锁上锁
        readLock.lock();
        System.out.println("进行读操作");
        //锁升级, 先写锁上锁再释放读锁
        writeLock.lock();
        readLock.unlock();
        //进行写操作
        System.out.println("进行写操作");
        //释放写锁
        writeLock.unlock();
    }
}

看一下运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aErHQ8OA-1650025917887)(/img/2022-04-05/11.png)]

卡在这里了, 说明不能将读锁升级为写锁!

我的个人主页: www.ayu.link
本文连接: ┏ (゜ω゜)=☞

举报

相关推荐

0 条评论