【JUC】2.多线程锁

心已赠人 2024-04-03 08:17 134阅读 0赞

文章目录

    1. 乐观锁与悲观锁
    1. 从8种锁的案例理解锁
    1. 公平锁与非公平锁
    1. 可重入锁(递归锁)
    • 4.1使用synchronized实现(隐式锁)
    • 4.2 使用ReentrantLock实现(显式锁)
    1. 死锁
    • 5.1 什么是死锁???
    • 5.2 尝试写出一个死锁
    • 5.3 死锁产生的原因
    • 5.4 如何排除死锁

1. 乐观锁与悲观锁

什么是悲观锁呢?

悲观锁就是认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的数据的时候会先枷锁,确保数据不会被别的线程修改

根据悲观锁的定义,不难看出,悲观锁适合写操作多的场景,先加锁可以保证操作时数据的正确

一个一个人来写,这样就不会写乱了

常见的悲观锁有

  • synchronized关键字
  • Lock的实现类

那么什么是乐观锁呢?

乐观锁则认为自己在使用数据的时候,不会有别的线程修改数据和资源,所以不会添加锁

在Java中是通过无所编程来实现的,只有在更新数据的时候去判断,之前有没有别的线程更新数据

  • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
  • 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改或者重试抢锁等等

根据乐观锁的定义。乐观锁适合读操作多的场景,不加锁的特点就是能够使得读操作性能大幅度提升

简而言之,乐观锁是一个佛系锁。

乐观锁判断规则

  • 版本号机制Version
  • 常见采用CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

2. 从8种锁的案例理解锁

①标准访问ab两个线程,请问先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public synchronized void sendEmail(){
  4. System.out.println("Send Email ~~~~");
  5. }
  6. //发短信
  7. public synchronized void sendSMS(){
  8. System.out.println("Send SMS ~~~");
  9. }
  10. }
  11. public class SynchronizedDemo {
  12. public static void main(String[] args) {
  13. Phone phone = new Phone();
  14. new Thread(() -> {
  15. phone.sendEmail();
  16. }, "a").start();
  17. //休眠200毫秒
  18. try {
  19. TimeUnit.MICROSECONDS.sleep(200);
  20. } catch (InterruptedException e) {
  21. throw new RuntimeException(e);
  22. }
  23. new Thread(() -> {
  24. phone.sendSMS();
  25. }, "b").start();
  26. }
  27. }

在这里插入图片描述

答案是先打印邮件后打印短信

原因是因为两个方法都有synchronized关键字,而两个进程发短信和发邮件使用的都是同一个对象,所以存在竞争关系

由于synchronized属于悲观锁,因此当a线程使用这个对象,这个实例对象就会锁住了,其他线程想调用实例的方法需要等待其他线程用完

简而言之,同一个实例对象,只要有一个线程调用一个synchronized方法,其他线程就必须等待

②sendEmail方法加入暂停3秒,打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public synchronized void sendEmail(){
  4. try {
  5. TimeUnit.MICROSECONDS.sleep(300);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. }
  16. public class SynchronizedDemo {
  17. public static void main(String[] args) {
  18. Phone phone = new Phone();
  19. new Thread(() -> {
  20. phone.sendEmail();
  21. }, "a").start();
  22. //休眠200毫秒
  23. try {
  24. TimeUnit.MICROSECONDS.sleep(200);
  25. } catch (InterruptedException e) {
  26. throw new RuntimeException(e);
  27. }
  28. new Thread(() -> {
  29. phone.sendSMS();
  30. }, "b").start();
  31. }
  32. }

在这里插入图片描述

从这个案例中,更能提现第一个案例的解释了

更能提现出锁的是new出来的Phone实例对象

③添加一个普通方法hello,先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. new Thread(() -> {
  23. phone.sendEmail();
  24. }, "a").start();
  25. //休眠200毫秒
  26. try {
  27. TimeUnit.SECONDS.sleep(1);
  28. } catch (InterruptedException e) {
  29. throw new RuntimeException(e);
  30. }
  31. new Thread(() -> {
  32. phone.hello();
  33. }, "b").start();
  34. }
  35. }

在这里插入图片描述

加了普通方法后则与同步锁无关,因此不存在竞争关系

④ 用两台手机,请问先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. Phone phone1 = new Phone();
  23. new Thread(() -> {
  24. phone.sendEmail();
  25. }, "a").start();
  26. //休眠200毫秒
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. new Thread(() -> {
  33. phone1.sendSMS();
  34. }, "b").start();
  35. }
  36. }

在这里插入图片描述

换成两个对象后,因为不是同一把锁了,所以先打印短信后打印邮件

同一个对象的时候使用的是该实例做锁

⑤有两个静态同步方法,有一部手机,请问打印的是邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public static synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public static synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. Phone phone1 = new Phone();
  23. new Thread(() -> {
  24. phone.sendEmail();
  25. }, "a").start();
  26. //休眠200毫秒
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. new Thread(() -> {
  33. phone.sendSMS();
  34. }, "b").start();
  35. }
  36. }

在这里插入图片描述

和第二种例子一样,先打印邮件再打印短信

在这里是看不出区别,先看看下面的这种情况

⑥有两个静态方法,有两部手机,请问先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public static synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public static synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. Phone phone1 = new Phone();
  23. new Thread(() -> {
  24. phone.sendEmail();
  25. }, "a").start();
  26. //休眠200毫秒
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. new Thread(() -> {
  33. phone1.sendSMS();
  34. }, "b").start();
  35. }
  36. }

在这里插入图片描述

还是先打印邮件后打印短信,这是为什么呢?

这是因为对于普通同步方法,锁的对象是当前实例对象,通常指this,具体的一部手机,所有普通同步方法都是同一把锁,也就是实例对象本身

而对于静态同步方法,锁的对象是当前类的class对象,如Phone.class文件,也就是每new一个对象,这多个对象对应的都是同一把锁

对于同步方法块,锁的是synchronized括号内对象

  1. synchronized(o) {
  2. }

⑦有一个静态同步方法,有一个普通同步方法,有一部手机,请问先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public static synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. Phone phone1 = new Phone();
  23. new Thread(() -> {
  24. phone.sendEmail();
  25. }, "a").start();
  26. //休眠200毫秒
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. new Thread(() -> {
  33. phone.sendSMS();
  34. }, "b").start();
  35. }
  36. }

在这里插入图片描述

前面说到所有普通同步方法用的都是同一把锁——实例对象,就是new出来的实例对象本身。本类this

也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放后才能获取锁

所有静态同步方法用的同一把锁——类对象本身,就是我们说过的唯一模板class

具体实例对象this和唯一模板class,这两把锁是不同对象,所以静态方法和普通同步方法之间是不会有竞态关系的,但是一旦一个静态同步方法获取锁后,其他静态同步方法就必须等待该方法释放锁后才能获取锁

⑧有一个静态同步方法,有一个普通同步方法,两台手机,请问先打印邮件还是短信

  1. class Phone{
  2. //发邮件
  3. public static synchronized void sendEmail(){
  4. try {
  5. TimeUnit.SECONDS.sleep(3);
  6. } catch (InterruptedException e) {
  7. throw new RuntimeException(e);
  8. }
  9. System.out.println("Send Email ~~~~");
  10. }
  11. //发短信
  12. public synchronized void sendSMS(){
  13. System.out.println("Send SMS ~~~");
  14. }
  15. public void hello(){
  16. System.out.println("hello ~~");
  17. }
  18. }
  19. public class SynchronizedDemo {
  20. public static void main(String[] args) {
  21. Phone phone = new Phone();
  22. Phone phone1 = new Phone();
  23. new Thread(() -> {
  24. phone1.sendEmail();
  25. }, "a").start();
  26. //休眠200毫秒
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. new Thread(() -> {
  33. phone.sendSMS();
  34. }, "b").start();
  35. }
  36. }

在这里插入图片描述

无论是一台手机还是多台手机,静态同步方法和普通同步方法不存在竟态关系


3. 公平锁与非公平锁

公平锁:指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到

非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发情况下,有可能造成优先级反转和饥饿现象

而在java.util.concurrent.locks.ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁。其默认为非公平锁

  1. public ReentrantLock(boolean fair) {
  2. sync = fair ? new FairSync() : new NonfairSync();
  3. }

为什么会有公平锁/非公平锁的设计?(为什么默认非公平锁)

  • 恢复挂起线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是比较明显的。所以非公平锁能更加充分利用CPU的时间片,尽量减少CPU空闲状态的时间
  • 使用多线程很重要的考量点是线程切换的开销,当使用非公平锁的时候,当一个线程请求锁获得同步状态然后释放同步状态,所以刚释放锁的线程在此刻再次获得锁状态概率变大,所以减少了线程开销

总而言之,非公平锁的效率比公平锁的效率要高

既然有两种锁,那么什么时候用公平锁?什么时候用非公平锁呢?

前面说到非公平锁的效率比公平锁的效率要高,所以如果是为了更高的吞吐量,很显然非公平锁更加合适,因为节省了很多线程切换时间,吞吐量自然就上去了。否则那就使用公平锁,大家公平使用。


4. 可重入锁(递归锁)

可重入锁是指同一线程再外层方法获取锁的时候,再次进入该线程内层会自动获取锁(前提,锁的对象是同一个锁),不会因为之前已经获取过还没有释放而阻塞。

如果没有可重入锁,当内外层锁对象是一样的话,准备进入内部的时候需要获取该锁对象,但是该锁对象在进入外层的时候就已经获取了,需要等待自己释放最外层的锁才可以获取内层的锁,这就会出现自我阻塞

Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是在一定程度上避免了死锁


4.1使用synchronized实现(隐式锁)

隐式锁支持同步块和同步方法

同步块

  1. public class ReEntryLockDemo {
  2. public static void main(String[] args) {
  3. final Object object = new Object();
  4. new Thread(() -> {
  5. synchronized (object){
  6. System.out.println(Thread.currentThread().getName() + "\t---外层调用");
  7. synchronized (object){
  8. System.out.println(Thread.currentThread().getName() + "\t---中层调用");
  9. synchronized (object){
  10. System.out.println(Thread.currentThread().getName() + "\t---内层调用");
  11. }
  12. }
  13. }
  14. }).start();
  15. }
  16. }

同步方法

  1. public class ReEntryLockDemo {
  2. public static void main(String[] args) {
  3. new ReEntryLockDemo().m1();
  4. }
  5. public synchronized void m1(){
  6. System.out.println(Thread.currentThread().getName() + "\t---come in m1");
  7. m2();
  8. System.out.println(Thread.currentThread().getName() + "\t---end in m1");
  9. }
  10. public synchronized void m2(){
  11. System.out.println(Thread.currentThread().getName() + "\t---come in m2");
  12. m3();
  13. }
  14. public synchronized void m3(){
  15. System.out.println(Thread.currentThread().getName() + "\t---come in m3");
  16. }
  17. }

4.2 使用ReentrantLock实现(显式锁)

  1. public class ReEntryLockDemo {
  2. public static void main(String[] args) {
  3. ReentrantLock lock = new ReentrantLock();
  4. lock.lock();
  5. try {
  6. System.out.println(Thread.currentThread().getName() + "\t---外层调用");
  7. lock.lock();
  8. try {
  9. System.out.println(Thread.currentThread().getName() + "\t---中层调用");
  10. lock.lock();
  11. try {
  12. System.out.println(Thread.currentThread().getName() + "\t---内层调用");
  13. } finally {
  14. lock.unlock();
  15. }
  16. } finally {
  17. lock.unlock();
  18. }
  19. } finally {
  20. lock.unlock();
  21. }
  22. }
  23. }

5. 死锁

5.1 什么是死锁???

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

在这里插入图片描述


5.2 尝试写出一个死锁

  1. public class DeadLockDemo {
  2. public static void main(String[] args) {
  3. final Object objectA = new Object();
  4. final Object objectB = new Object();
  5. new Thread(() -> {
  6. synchronized (objectA){
  7. System.out.println(Thread.currentThread().getName() + "\t自己持有A锁,希望获得B锁");
  8. try {
  9. TimeUnit.SECONDS.sleep(1);
  10. } catch (InterruptedException e) {
  11. throw new RuntimeException(e);
  12. }
  13. synchronized (objectB){
  14. System.out.println(Thread.currentThread().getName() + "\t成功获得B锁");
  15. }
  16. }
  17. }, "A").start();
  18. new Thread(() -> {
  19. synchronized (objectB){
  20. System.out.println(Thread.currentThread().getName() + "\t自己持有B锁,希望获得A锁");
  21. try {
  22. TimeUnit.SECONDS.sleep(1);
  23. } catch (InterruptedException e) {
  24. throw new RuntimeException(e);
  25. }
  26. synchronized (objectA){
  27. System.out.println(Thread.currentThread().getName() + "\t成功获得A锁");
  28. }
  29. }
  30. }, "B").start();
  31. }
  32. }

5.3 死锁产生的原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

5.4 如何排除死锁

有两种方法

使用命令行,我这里使用idea,需要以管理员身份运行idea

然后在命令行输入

  1. jps -l

然后就可以查看到进程编号

在这里插入图片描述

再使用另一个命令查看进行信息

  1. jstack 进程号

在这里插入图片描述

输出显示发现一个死锁

还有一种方法就是通过图形化

首先win + r输入jconsole

在这里插入图片描述

找到进程进行连接

在这里插入图片描述

点击线程,下面有个检测死锁的按钮

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


发表评论

表情:
评论列表 (有 0 条评论,134人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线JUC

    进程 进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有他自己的内存空间和系统资源 多进程意义在于计算机可以执行多个任务,提高cpu使用率