多线程锁的分类学习

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();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容

  • 多线程状态 新建(NEW):新创建了一个线程对象。 可运行(RUNNABLE):线程对象创建后,其他线程(比如ma...
    奇点一氪阅读 655评论 1 8
  • 1. 计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行;当...
    AI乔治阅读 539评论 0 12
  • 参考链接:http://smallbug-vip.iteye.com/blog/2275743 在多线程开发的过程...
    时之令阅读 1,554评论 2 5
  • 乐观锁认为读多写少,遇到并发的可能性低,拿数据的时候认为别人不会修改,所以不上锁,但是更新数据时,会判断一下有没有...
    繁星追逐阅读 378评论 0 0
  • 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 Java 平台下的多线程编...
    业志陈阅读 477评论 0 0