2022-05-31
后端
00
请注意,本文编写于 962 天前,最后修改于 242 天前,其中某些信息可能已经过时。

目录

问题复现!
解决问题
1、使用synchronized
2、ReentrantLock
3、验证
扩展
Synchronized 与 ReentrantLock 的区别!
synchronized与Lock的区别及底层实现?
公平锁、非公平锁、可重入锁、自旋锁、独占锁、共享锁

业务

系统需要把待办推送到B系统. 情况1:用了定时任务每分钟推送。 情况2:如果A系统在生成待办时调用定时任务的方法手动推送。 所以出现了多线程执行,导致方法重复执行。 使用同步锁解决问题!

问题复现!

# 定时任务代码 @Scheduled(cron = "*/3 * * * * ?") public void ceShi() { try { log.info(Thread.currentThread().getName()); Thread.sleep(5000); } catch (Exception e) { log.error("待办portalWork表推送OA任务异常", e); } } # 普通接口 @RequestMapping("/ceShi") public void ceShi() { portalWorkTask.ceShi(); }

游览器重复调用/ceShi 接口

http://127.0.0.1:8077/ceShi

查看idea控制台,在方法中睡眠5秒钟重复进入方法!

image-20220531140528319

解决问题

1、使用synchronized

2、ReentrantLock

private final ReentrantLock lock = new ReentrantLock(); lock.lock(); try { log.info(Thread.currentThread().getName()); Thread.sleep(5000); } catch (Exception e) { log.error("待办portalWork表推送OA任务异常", e); } finally { lock.unlock(); }

3、验证

可以看到进入方法中,如果存在锁,必须等待,释放完锁后,再次进入方法!

image-20220531140847769

扩展

Synchronized 与 ReentrantLock 的区别!

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量

相同点:两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

主要区别如下

  1. ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  2. ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  3. ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
  4. 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象

synchronized与Lock的区别及底层实现?

:

  1. synchronized属于JVM层面,属于java的关键字
  • monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
  • Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
  1. Lock必须手动释放锁,否则会产生死锁,synchronized系统会自动释放锁,而且不可中断,lock方法中可以实现中断。
  2. Lock默认是非公平锁,也可以是公平锁,synchronized非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

使用方法:

  1. synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用
  2. ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。

等待是否可中断:

  1. synchronized不可中断,除非抛出异常或者正常运行完成
  2. ReentrantLock可中断

加锁是否公平:

  1. synchronized非公平锁
  2. ReentrantLock两者都可以,默认是非公平锁。

锁绑定多个条件Condition:

  1. synchronized没有。
  2. ReentrantLock可用来分组唤醒需要唤醒的线程。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。

多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:

  • AA打印5次,BB打印10次,CC打印15次
  • 紧接着
  • AA打印5次,BB打印10次,CC打印15次
  • 来10轮
java
class ShareResource { private int number = 1; private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); public void print5() { lock.lock(); try { while (number != 1) { condition1.await(); } for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } number = 2; condition2.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print10() { lock.lock(); try { while (number != 2) { condition2.await(); } for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } number = 3; condition3.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15() { lock.lock(); try { while (number != 3) { condition3.await(); } for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } number = 1; condition1.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class SyncAndReentrantLockDemo { public static void main(String[] args) { ShareResource shareResource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print5(); } }, "AAA").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print10(); } }, "BBB").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { shareResource.print15(); } }, "CCC").start(); } }

公平锁、非公平锁、可重入锁、自旋锁、独占锁、共享锁

公平锁: 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

可重入锁(也叫递归锁): 线程可以进入任何一个它已经拥有的锁所同步的代码块(如:租房,门外面是一把门锁,进去之后厕所、厨房不会安装锁,直接可以用)

手写自旋锁

java
public class AtomicTest { AtomicReference<Thread> atomicReference = new AtomicReference<Thread>(); public void myLock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "^_^"); while (!atomicReference.compareAndSet(null, thread)) { } } public void myOnLock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "关闭"); atomicReference.compareAndSet(thread,null); } public void test(Integer i){ System.out.println(Thread.currentThread().getName()+"打印"+i); } public static void main(String[] args){ AtomicTest atomicTest = new AtomicTest(); new Thread(() -> { atomicTest.myLock(); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } atomicTest.myOnLock(); },"t1").start(); new Thread(() -> { atomicTest.myLock(); atomicTest.myOnLock(); },"t2").start(); AtomicInteger atomicReference1 = new AtomicInteger(); AtomicInteger atomicReference2 = new AtomicInteger(5); System.out.println(1); } }

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

独占锁: 该锁一直只能被一个线程所持有。lock和synchronized都是独占锁

共享锁: 该锁可被多个线程所持有。读写分离。

本文作者:酷少少

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!