1. 用法
  2. 原理
  3. 正常写程序基本用不着volatile(专为面试而生)

可重入锁

  1. 一个线程锁一下之后,还可以对同样的锁再锁一下
  2. synchronized和ReentrantLock都是可重入
  3. synchronized必须可重入===>否则子类调用父类无法实现
  4. 父子类是同一把锁===>当synchronized(this)时!
  1. /**
  2. * reentrantlock用于替代synchronized
  3. * 本例中由于m1锁定this,只有m1执行完毕的时候,m2才能执行
  4. * 这里是复习synchronized最原始的语义
  5. * @author mashibing
  6. */
  7. package com.mashibing.juc.c_020;
  8. import java.util.concurrent.TimeUnit;
  9. public class T01_ReentrantLock1 {
  10. synchronized void m1() {
  11. for(int i=0; i<10; i++) {
  12. try {
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(i);
  18. // 在m1中调用m2===>在同一个线程中都要synchronized
  19. if(i == 2) m2();
  20. }
  21. }
  22. synchronized void m2() {
  23. System.out.println("m2 ...");
  24. }
  25. public static void main(String[] args) {
  26. T01_ReentrantLock1 rl = new T01_ReentrantLock1();
  27. new Thread(rl::m1).start();
  28. try {
  29. TimeUnit.SECONDS.sleep(1);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. //new Thread(rl::m2).start();
  34. }
  35. }

新型锁ReentrantLock

  1. 可以替换synchronized{ }
  2. lock.lock();
  3. lock.unlock();
  4. synchronized是自动解锁的,而lock是手动解锁的
  5. lock.unlock();一定要写在try{}catch(){}finally{}中,假如不这么写,在中间出问题了,就永远不会解锁,就会永远锁住资源,让别人拿不到锁
  6. 一定要写try……finally……===>可以没有catch!!!===>要在finally中解锁
  7. 遇到异常时,synchronized会自动释放锁,而lock必须在finally块中手动释放锁unlock
  8. ReentrantLock比synchronized强大的地方:
    1. ReentrantLock可以用tryLock尝试锁定;而synchronized上锁的时候锁不了就直接阻塞了(wait);用ReentrantLock可以自己决定到底要不要wait(下面的例子为5s之内得到这把锁,如果得到就继续执行,如果得不到就什么都不干也不阻塞了)===>拿到锁locked变为true,没拿到锁locked仍为false;即便拿到了,这句话下面的部分也不是真正的上锁了,只是人为尝试了
    2. tryLock只是尝试一下,并不是真正意义上的上锁!tryLock没有上锁的功能
    3. 使用tryLock进行尝试锁定, 不管锁定与否,方法都将继续执行(拿着zhao第二声也不锁定,没拿着自己做一些处理)
    4. 可以根据tryLock的返回值来判定是否锁定,以此来决定到底要进行什么操作
    5. 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unlock的时机

image.png

  1. lock可以使用lockInterruptibly这种可以被打断的加锁方式;不管有没有上锁,只要调用个interrupt就可以打断现成的等待===>实验中抛出了异常===>没加锁报了异常(监视器=锁对象+等待队列+同步队列)===>判断的是之后来看lockInterruptibly返回值是void,怎么判断是个小问题!

image.png

  1. 用lockInterruptibly去抢别人的锁!!!
  2. ReentrantLock可以在等待时响应,被别人打断;别人要打断自己时可以做出响应,synchronized调用wait之后必须要notify才能让别的线程获取锁醒过来,不然醒不过来
  3. 对c的补充===>lock上锁要拿到锁才能上锁,锁住的是临界区

公平锁与非公平锁

  1. 默认为非公平锁===>在new ReentrantLock对象时传个true可以得到公平锁
  2. 竞争方式的公平与不公平
  3. 会不会检查等待队列中的内容是公平锁的关键,锁不公平,一些线程可以上来就抢(可以抢到)
  4. 公平锁会先检查队列中有没有原来等着的线程,如果有就让之前排队的线程先执行,而后来的线程就进入队列去等着
  5. 用公平锁也不意味着是严格按照顺序执行的,严格按照顺序执行需要用到线程之间的通信!!!
  6. 不公平锁更好?看具体业务情况!

ReentrantLock V.S synchronized

  1. 底层CAS V.S sync锁升级
  2. tryLock、lockInterruptibly V.S 阻塞锁
  3. 公平与非公平 V.S 非公平

IllegalMonitorStateException异常

  1. 如果wait()方法不在同步块中,代码的确会抛出IllegalMonitorStateExeception
  2. Lost Wake-Up Problem
  3. 假如我们有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠:
    1. 生产者伪代码:count+1; notify();
    2. 消费者伪代码:while(count<=0) wait(); count—;
    3. 熟悉多线程的朋友一眼就能够看出来,这里面有问题。什么问题呢?

生产者是两个步骤: count+1; notify(); 消费者也是两个步骤: 检查count值; 睡眠或者减一; 万一这些步骤混杂在一起呢?比如说,初始的时候count等于0,这个时候消费者检查count的值,发现count小于等于0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了…… 问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。 这就是一种很常见的竞态条件。 很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。

  1. 即便是我们自己在实现自己的锁机制的时候,也应该要确保类似于wait()和notify()这种调用,要在同步块内,防止使用者出现lost wake up问题。
  2. 为了避免出现这种lost wake up问题,在这种模型之下,总应该将我们的代码放进去的同步块中。Java强制我们的wait()/notify()调用必须要在一个同步块中,就是不想让我们在不经意间出现这种lost wake up问题。不仅仅是这两个方法,包括java.util.concurrent.locks.Condition的await()/signal()也必须要在同步块中

🤏随想

  1. synchronized可以单独使用,为什么还要和wait和notify搭配使用???
    1. wait相当于某一个线程获取到锁可以运行之后,自己主动进入阻塞队列让出锁,先让别的线程去执行,知道别的线程释放了锁,自己再去拿到这把锁执行临界区代码,并且在最后notify释放自己获取到的锁
    2. 一个线程不是一下子执行完的,之前的例子都是在线程一下子执行完的前提下,现在不是一下子执行完,就开始向进程之间的同步协作===>线程间通信转变了
  2. synchronized和wait、notify的关系就好比是ReentrantLock和await、signal的关系!