Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)

Java各类锁对比及应用案例


文章目录

  • Java各类锁对比及应用案例
  • 简介
  • 案例介绍
    • 1. 悲观锁
    • 2. 乐观锁
    • 3. 公平锁和非公平锁
    • 4. 读写锁
    • 5. 可重入锁
    • 6. 自旋锁


简介

  • 乐观锁、悲观锁
  • 公平锁、非公平锁
  • 可重入锁
  • 读写锁
  • 自旋锁

案例介绍

我们将围绕卖票案例,用各种锁去解决卖票重复和溢出等情况

  • 经典的卖票案例(不加锁)
public class SellDemo {
    /**
     * 库存
     */
    static int ticketSum = 100;
    /**
     * 卖票
     */
    public static void sell() {
        for (int i = 0; i < 100; i++) {
            try {
                if (ticketSum > 0) {
                    // 模拟业务处理时间
                    Thread.sleep(10);
                    ticketSum--;
                    System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + ticketSum);
                }
            } catch (InterruptedException ignored) {
            }

        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> SellDemo.sell());
        executorService.execute(() -> SellDemo.sell());
        executorService.execute(() -> SellDemo.sell());
        executorService.execute(() -> SellDemo.sell());
    }
}
  • 由于没有加锁,出现卖票重复和溢出等情况
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第1张图片

1. 悲观锁

  • 介绍
    悲观锁比较悲观,它认为如果不锁住这个资源,别的线程就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失。

  • JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock,都是悲观锁

  • synchronized 加锁

public class SynchronizedDemo {
    /**
     * 库存
     */
    static int ticketSum = 100;

    /**
     * 卖票
     */
    public synchronized static void sell() {
        for (int i = 0; i < 100; i++) {
            try {
                if (ticketSum > 0) {
                    // 模拟业务处理时间
                    Thread.sleep(10);
                    ticketSum--;
                    System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + ticketSum);
                }
            } catch (InterruptedException ignored) {
            }

        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> SynchronizedDemo.sell());
        executorService.execute(() -> SynchronizedDemo.sell());
        executorService.execute(() -> SynchronizedDemo.sell());
        executorService.execute(() -> SynchronizedDemo.sell());
    }
}
  • ReentrantLock 加锁
public class ReentrantLockDemo {

    /**
     * 锁
     */
    private static final Lock LOCK = new ReentrantLock();

    /**
     * 库存
     */
    static int ticketSum = 100;

    public static void sell() {
        for (int i = 0; i < 100; i++) {
            LOCK.lock();
            try {
                if (ticketSum > 0) {
                    // 模拟业务处理时间
                    Thread.sleep(10);
                    ticketSum--;
                    System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + ticketSum);
                }
            } catch (InterruptedException ignored) {
            } finally {
                LOCK.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
    }
}

  • 加锁后效果
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第2张图片

2. 乐观锁

  • 介绍
    通过版本号一致与否,即给数据加上版本,不会上锁,判断版本号,可以多人操作。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

  • 乐观锁的典型案例就是原子类,例如 AtomicInteger 在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量。

  • 乐观锁实现

public class OptimisticDemo {
    /**
     * 用原子类定义变量
     * 库存
     */
    static AtomicInteger ticketSum = new AtomicInteger(100);

    public static void sell() {
        for (int i = 0; i < 100; i++) {
            try {
                    // 获取进行-1后的票数,修改和获取必须同时进行。
                    int now = ticketSum.addAndGet(-1);
                    if (now < 0) {
                      break;
                    }
                    // 模拟业务处理时间
                    Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + now);
                
            } catch (InterruptedException ignored) {
            }

        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> OptimisticDemo.sell());
        executorService.execute(() -> OptimisticDemo.sell());
        executorService.execute(() -> OptimisticDemo.sell());
        executorService.execute(() -> OptimisticDemo.sell());
    }
}
  • 效果
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第3张图片

3. 公平锁和非公平锁

  • 介绍
    公平锁:先来先到(效率相对低)
    非公平锁:不是按照顺序,可插队(效率高,但是线程容易饿死)
  • 和我们第一步创建的ReentrantLock一样,Lock lock = new ReentrantLock(true)在构造中传入true即可开启公平锁,false 表示非公平锁。
public class ReentrantLockDemo {

    /**
     * true 表示公平锁
     */
    private static final Lock LOCK = new ReentrantLock(true);

    /**
     * 库存
     */
    static int ticketSum = 100;

    public static void sell() {
        for (int i = 0; i < 100; i++) {
            LOCK.lock();
            try {
                if (ticketSum > 0) {
                    // 模拟业务处理时间
                    Thread.sleep(10);
                    ticketSum--;
                    System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + ticketSum);
                }
            } catch (InterruptedException ignored) {
            } finally {
                LOCK.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
        executorService.execute(() -> ReentrantLockDemo.sell());
    }
}

  • 效果
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第4张图片

4. 读写锁

  • 介绍
    读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享(写锁独占,读锁共享,写锁优先级高于读锁)

    读写锁ReentrantReadWriteLock
    读锁为ReentrantReadWriteLock.ReadLockreadLock()方法
    写锁为ReentrantReadWriteLock.WriteLockwriteLock()方法
    创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock()
    写锁 加锁 rwLock.writeLock().lock(),解锁为rwLock.writeLock().unlock()
    读锁 加锁rwLock.readLock().lock(),解锁为rwLock.readLock().unlock()

  • 读写锁案例:修改卖票案例,新增读写方法,分开加锁。

public class ReentrantReadWriteDemo {

    /**
     * 创建读写锁
     */
    private static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock();

    /**
     * 库存票数
     */
    static int ticketSum = 100;
    
    /**
     * 获取库存
     */
    public static int getSum() {
        RW_LOCK.readLock().lock();
        try {
            return ticketSum;
        } finally {
            RW_LOCK.readLock().unlock();
        }
    }
    /**
     * 设置库存
     */
    public static int setSum() {
        RW_LOCK.writeLock().lock();
        try {
            if(ticketSum > 0){
               ticketSum--;
            }
            return ticketSum;
        } finally {
            RW_LOCK.writeLock().unlock();
        }
    }
    
    /**
     * 卖票
     */
    public static void sell() {
        for (int i = 0; i < 100; i++) {
            try {
                    int sum = setSum();
                    if (sum  <= 0) {
                        System.out.println("卖完啦!");
                        break;
                    }
                    // 模拟业务处理时间
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + sum );
                
            } catch (InterruptedException ignored) {
            }

        }
    }

    /**
     * 打印剩余票数,模拟用户查询票数
     */
   public static void print() {
      while(true) {
          try {
                    System.out.println("用户查询票数,当前剩余票数:" +  getSum() );
                    Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
      }
   }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> ReentrantReadWriteDemo.sell());
        executorService.execute(() -> ReentrantReadWriteDemo.sell());
        executorService.execute(() -> ReentrantReadWriteDemo.sell());
        executorService.execute(() -> ReentrantReadWriteDemo.sell());
        // 模拟1个线程查询剩余票数
        executorService.execute(() -> ReentrantReadWriteDemo.print());
    }

}

5. 可重入锁

  • 可重入锁,也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。 就是同一个线程再次进入同样代码时,可以再次拿到该锁。
  • 其实我们之前案例用到的synchronizedReentrantLock 都是可重入锁
  • synchronized案例
public class Reentrant {

    public static void test() {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + "买1");
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + "送1");
                    synchronized (o) {
                        System.out.println(Thread.currentThread().getName() + "再送");
                    }
                }
            }

        }, "线程:").start();
    }

    public static void main(String[] args) {
        test();
    }
}
  • ReentrantLock案例
public class ReentrantLockTest {

    private static final Lock LOCK = new ReentrantLock();

    public static void test() {
        LOCK.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "买1");
            LOCK.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "送1");
                LOCK.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "再送");
                } finally {
                    LOCK.unlock();
                }
            } finally {
                LOCK.unlock();
            }
        } finally {
            LOCK.unlock();
        }
    }
    public static void main(String[] args) {
        new Thread(() -> {
            test();
        }, "线程:").start();
    }
}
  • 效果
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第5张图片

6. 自旋锁

  • 介绍
    自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
    优点: 多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。
    缺点: 自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。

    总结:由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

  • 利用CSA实现自旋锁

public class SpinLock {

    private static final AtomicReference<Thread> CAS = new AtomicReference<>();

    public static void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS,compare比较CAS中值是否为空,为空则把值更新为新的线程并返回true,否则一直循环运行,线程保持运行不被挂起
        while (!CAS.compareAndSet(null, current)) {
        }
        // DO nothing
        System.out.println(Thread.currentThread().getName() + "获得锁");
    }

    public static void unlock() {
        // 解锁也很简单,compare比较当前线程是否拥有锁,拥有则把CAS中的值重新设空即可
        Thread current = Thread.currentThread();
        CAS.compareAndSet(current, null);
    }
}
  • 结合到我们的卖票案例
public class SpinLockSellDemo {

    private static final AtomicReference<Thread> CAS = new AtomicReference<>();

    /**
     * 库存
     */
    static int ticketSum = 100;

    public static void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS,compare比较CAS中值是否为空,为空则把值更新为新的线程并返回true,否则一直循环运行
        while (!CAS.compareAndSet(null, current)) {
        }
    }

    public static void unlock() {
        // 解锁也很简单,compare比较当前线程是否拥有锁,拥有则把CAS中的值重新设空即可
        Thread current = Thread.currentThread();
        CAS.compareAndSet(current, null);
    }

    /**
     * 卖票
     */
    public static void sell() {
        for (int i = 0; i < 100; i++) {
            try {
                lock();
                try {
                    if (ticketSum > 0) {
                        // 模拟业务处理时间
                        Thread.sleep(10);
                        ticketSum--;
                        System.out.println(Thread.currentThread().getName() + "卖票成功,当前剩余票数:" + ticketSum);
                    }
                } catch (InterruptedException ignored) {
                }
            } finally {
                unlock();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 模拟4个线程卖票
        executorService.execute(() -> SpinLockSellDemo.sell());
        executorService.execute(() -> SpinLockSellDemo.sell());
        executorService.execute(() -> SpinLockSellDemo.sell());
        executorService.execute(() -> SpinLockSellDemo.sell());
    }
}
  • 自旋锁卖票效果
    Java各类锁对比及应用案例(乐观锁、悲观锁、公平锁、非公平锁、可重入锁、读写锁、自旋锁)_第6张图片

你可能感兴趣的:(Java基础知识,java,ReentrantLock,lock,Synchronized,卖票)