美文网首页
多线程锁的分类学习

多线程锁的分类学习

作者: 不加糖的开水 | 来源:发表于2022-02-07 00:06 被阅读0次

    1. 公平锁和非公平锁

    • 定义:
      • 公平锁:多个线程按照申请锁的顺序来获取锁,按照FIFO规则从等待队列中拿到等待线程获取相应锁
      • 非公平锁:多个线程并不是按照申请锁的顺序来获取锁,有可能出现后申请锁的线程先申请到锁。在高
        并发环境下,非公平锁有可能造成 优先级反转 或者 饥饿 的现象。如果非公平锁抢占失败,就要继续采取类似公平锁的机制。非公平锁的优点在于吞吐量大。
    • 常见的非公平锁:
      • ReentrantLock可以通过指定构造函数的boolean类型来获取公平/非公平锁,默认情况下是非公平锁
      • 对于Synchronized而言,也是一种非公平锁

    2 可重入锁(递归锁)

    • 定义:可重入锁的定义要类比递归的定义来理解。指在同一个线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,
      即进入内层函数时会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。一个同步方法内部仍然存在一个同步方法,那么可以进入内层同步方法,且内存同步方法和外层同步方法持有的是同一把锁。

    具体看一个案例来理解可重入锁:synchronized就是可重入锁,现在问题是synchronized块中能够使用System.out.println()方法?

    public void println(String x) {
        // println方法内部使用了synchronized
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    
    /**
     * 演示可重入锁
     *
     * @author sherman
     */
    public class LockReentrantDemo1 {
        public static void main(String[] args) {
            // 程序正常运行输出:hello
            new LockReentrantDemo1().lockReentrant();
        }
    
        public synchronized void lockReentrant() {
            /**
             * 注意这个println方法内部就使用了synchronized关键字,锁住了this
             * 即synchronized块中仍然能够使用synchronized关键字 -> 可重入的
             */
            System.out.println("hello");
        }
    }
    

    可重入锁的意义有一点类似于事务的传播行为(一个方法运行在另一个开启事务的方法中,那么当前方法的事务行为是什么样的?),类比来说可重入锁意义就是:一个synchronized(锁)块运行在另一个synchronized(块)中,那么当前synchronized的具体表现行为是什么,是直接中断?还是阻塞等待?又或者是正常执行,因为两个synchronized锁住的是同一个对象?

    可重入锁的含义就是最后一种:正常执行,因为可重入锁,锁的是同一个对象。

    • 典型的可重入锁:ReentrantLock & synchronized关键字
    • 作用:最大作用就是防止死锁,因为多层嵌套的锁,其实锁的是同一个对象,另一个含义就是:嵌套方法持有的是同一把锁

    具体示例:

    /**
     * 可重入锁演示
     *
     * @author sherman
     */
    // 演示ReentrantLock是可重入的
    class ShareResouce implements Runnable {
        private Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            get();
        }
    
        private void get() {
            lock.lock();
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + ": get()");
                set();
            } finally {
                lock.unlock();
            }
        }
    
        private void set() {
            lock.lock();
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + ": set()");
            } finally {
                lock.unlock();
            }
        }
    }
    
    public class LockReentrantDemo2 {
        // outer()和inner()方法演示synchronized是可重入的
        private synchronized void outer() {
            System.out.println(Thread.currentThread().getName() + ": outer method()");
            inner();
        }
    
        // outer()和inner()方法演示synchronized是可重入的
        private synchronized void inner() {
            System.out.println(Thread.currentThread().getName() + ": inner method()");
        }
    
        public static void main(String[] args) {
            // 验证synchronized是可重入的
            LockReentrantDemo2 lrd = new LockReentrantDemo2();
            new Thread(lrd::outer, "thread-1").start();
            new Thread(lrd::outer, "thread-2").start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 验证ReentrantLock是可重入的
            System.out.println("===================");
            new Thread(new ShareResouce(), "thread-3").start();
            new Thread(new ShareResouce(), "thread-4").start();
        }
    }
    

    补充:

    在使用ReentrantLock类演示可重入锁时,lock.lock()和lock.unlock()数量一定要匹配,否则:

    • 当lock.lock()数量 > lock.unlock():程序一直运行
    • 当lock.lock()数量 < lock.unlock():抛出java.lang.IllegalMonitorStateException异常

    3 自旋锁(SpinLock)

    自旋锁尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是避免线程上下文切换的消耗,缺点是如果一直自旋会消耗CPU:

    /**
     * 自旋锁演示
     *
     * @author sherman
     */
    public class LockSpin {
        AtomicReference<Thread> ar = new AtomicReference<>();
    
        private void lock() {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + ": come in!");
            while (!ar.compareAndSet(null, thread)) {
    
            }
        }
    
        private void unlock() {
            Thread thread = Thread.currentThread();
            ar.compareAndSet(thread, null);
            System.out.println(thread.getName() + ": get out!");
        }
    
        public static void main(String[] args) throws InterruptedException {
            LockSpin lockSpin = new LockSpin();
            new Thread(() -> {
                lockSpin.lock();
                try {
                    Thread.sleep(8000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockSpin.unlock();
            }, "线程A").start();
    
            // 保证线程A先进行获取到锁,让线程B之后自旋
            Thread.sleep(1000);
    
            new Thread(() -> {
                lockSpin.lock();
                lockSpin.unlock();
            }, "线程B").start();
        }
    }
    

    4 读写锁

    • 写锁(独占锁):指该锁一次只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁

    • 读锁(共享锁):指该锁可以被多个线程所持有

    • 读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的

      /**

      • 演示读写锁

      • @author sherman
        */
        class Cache {
        private volatile HashMap<String, Object> map = new HashMap<>();
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        public Object get(String key) {
        lock.readLock().lock();
        Object res = null;
        try {
        System.out.println(Thread.currentThread().getName() + ": 正在读取+++");
        Thread.sleep(100);
        res = map.get(key);
        System.out.println(Thread.currentThread().getName() + ": 读取完成---");
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.readLock().unlock();
        }
        return res;
        }

        public void put(String key, Object value) {
        lock.writeLock().lock();
        try {
        System.out.println(Thread.currentThread().getName() + ": 正在写入>>>");
        Thread.sleep(1000);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + ":写入完成<<<");
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.writeLock().unlock();
        }
        }
        }

      public class LockReadWrite {
      public static void main(String[] args) {
      Cache cache = new Cache();

          // 写入操作是被一个线程独占的,一旦写线程开始
          // 其它线程必须等待其完成后才能继续执行
          for (int i = 0; i < 10; i++) {
              final int tmp = i;
              new Thread(() -> cache.put(tmp + "", tmp + ""), String.valueOf(i)).start();
          }
      
          // 读操作可以被多个线程持有
          // 其它线程不必等待当前读操作完成才操作
          for (int i = 0; i < 10; i++) {
              final int tmp = i;
              new Thread(() -> cache.get(tmp + ""), String.valueOf(i)).start();
          }
      }
      

      }

    5 CountDownLatch

    CountDownLatch是一个计数器闭锁,它通过一个初始化定时器latch,在latch的值被减到0之前,其它线程都会被await()方法阻塞。

    以模拟火箭发射过程解释CountDownLatch使用:

    /**
     * CountDownLatch模拟火箭发射过程:
     * 火箭发射之前需要十个线程进行前期检查工作,每个线程耗时0-4s,
     * 只有10个线程对应的检查工作全部完成后,火箭才能发射
     *
     * @author sherman
     */
    
    public class CountDownLatchDemo implements Runnable {
        public static final int TASK_NUMBERS = 10;
        private static CountDownLatch cdl = new CountDownLatch(TASK_NUMBERS);
    
        public static void main(String[] args) throws InterruptedException {
            CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.submit(countDownLatchDemo);
            }
            cdl.await();
            System.out.println("检查工作检查完毕:fire!");
            executorService.shutdown();
        }
    
        @Override
        public void run() {
            try {
                // 模拟火箭发射前的各种检查工作
                int millis = new Random().nextInt(5000);
                Thread.sleep(millis);
                System.out.println(Thread.currentThread().getName() + ":检查完毕! 耗时:" + millis + "ms");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 每次检查完毕后都将计数器减1
                cdl.countDown();
            }
        }
    }
    

    6 CyclicBarrier

    CyclicBarrier是可循环使用的屏障,它的功能是:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会被打开,所有被屏障阻塞的方法都会被打开。

    A synchronization aid that allows a set of threads to all wait for each other to reach a **common

    barrier point**. CyclicBarriers are useful in programs involving a fixed sized party of threads that

    must occasionally wait for each other. The barrier is called cyclic because it can be re-used

    after the waiting threads are released.

    示例:模拟集齐七颗龙珠才能召唤神龙:

    /**
     * CyclicBarrier模拟集齐七颗龙珠才能召唤神龙
     * 设置common barrier point为7,每个线程收集到七颗龙珠之前都会被阻塞
     * 每个线程都到达common barrier point时候才会召唤神龙
     *
     * @author sherman
     */
    public class CyclicBarrierDemo implements Runnable {
        private static CyclicBarrier cb = new CyclicBarrier(7, () -> System.out.println("召唤神龙"));
    
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + ": 到达同步点(收集到一个龙珠)!");
                cb.await();
                System.out.println(Thread.currentThread().getName() + ": 阻塞结束,继续执行!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            CyclicBarrierDemo cbd = new CyclicBarrierDemo();
            ExecutorService executorService = Executors.newFixedThreadPool(7);
            for (int i = 0; i < 7; i++) {
                try {
                    Thread.sleep(new Random().nextInt(2000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                executorService.submit(cbd);
            }
            executorService.shutdown();
        }
    }
    

    7 Semaphore

    Semaphore信号量主要有两个目的:

    • 用于多个共享资源的互斥使用;
    • 用于并发数量的控制(是synchronized的加强版,当并发数量为1时就退化成synchronized);

    主要方法:

    • Semaphore(int permits):构造函数,允许控制的并发数量;
    • acquire():请求一个信号量,导致信号量的数量减1;
    • release():释放一个信号量,信号量加1;

    示例:使用Semaphore模拟请车位过程(3个车位,10辆车):

    /**
     * 使用Semaphore模拟抢车位过程(3个车位,10辆车)
     * 任意时刻只有3辆车持有线程
     *
     * @author sherman
     */
    public class SemaphoreDemo {
        public static void main(String[] args) {
            // 模拟三个车位,十辆车
            // 任意时刻只有三辆车持有车位
            Semaphore semaphore = new Semaphore(3);
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + ": 抢到车位");
                        // 每辆车占有车位[3,8]秒时间
                        Thread.sleep((new Random().nextInt(6) + 3) * 1000);
                        System.out.println(Thread.currentThread().getName() + ": 释放车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                }).start();
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:多线程锁的分类学习

          本文链接:https://www.haomeiwen.com/subject/wdbpkrtx.html