广州公司网站提供,iis 架设 wordpress,qq邮箱网页版,东胜做网站图片引用自#xff1a;不可不说的Java“锁”事 - 美团技术团队
1 java内存模型
java内存模型(JMM)是线程间通信的控制机制。JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存中#xff0c;每个线程都有一个私有的本地内存#xff0c;本地内存中存储了该…
图片引用自不可不说的Java“锁”事 - 美团技术团队
1 java内存模型
java内存模型(JMM)是线程间通信的控制机制。JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存中每个线程都有一个私有的本地内存本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念并不真实存在。它涵盖了缓存写缓冲区寄存器以及其他的硬件和编译器优化。
2 锁的概念
2.1 什么是锁
在Java中的锁主要是用于保障线程在多并发的情况下数据的一致性。就是实现并发的原子性。
2.2 为什么加锁
在多线程编程中为了保证数据的一致性我们通常需要在使用对象或者调用方法之前加锁这时如果有其他线程也需要使用该对象或者调用该方法则首先要获得锁如果某个线程发现锁正在被其他线程使用就会进入阻塞队列等待锁的释放直到其他线程执行完成并释放锁该线程才有机会再次获取锁并执行操作。这样做可以保障了在同一时刻只有一个线程持有该对象的锁并修改该对象从而保障数据的安全性。 3 悲观锁 VS 乐观锁
悲观锁悲观锁认为自己在使用数据的时候一定有别的线程来修改数据因此在获取数据的时候会先加锁确保数据不会被别的线程修改阻塞直到拿到锁。
实现方式Java中悲观锁是通过synchronized关键字或ReentrantLock接口来实现的。
应用场景悲观锁适合写操作多的场景先加锁可以保证写操作时数据正确。
乐观锁认为在使用数据时不会有别的线程修改数据所以不会添加锁只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新则根据不同的实现方式执行不同的操作例如报错或者自动重试。
实现方式乐观锁在Java中是通过使用无锁编程来实现最常采用的是CAS算法Java原子类中的递增操作就通过CAS自旋实现的。相对于对于 synchronized 这种阻塞算法CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。当然如果并发非常严重那么会导致CAS的自旋非常严重此时性能反倒不如直接使用悲观锁。
应用场景乐观锁适合读操作多的场景不加锁的特点能够使其读操作的性能大幅提升这样可以提高吞吐量。
// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {lock.lock();// 操作同步资源lock.unlock();
}// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1
3.1 synchronized VS Lock 1、synchronized是Java关键字在JVM层面实现加锁和解锁Lock是一个接口在代码层面实现加锁和解锁。
2、synchronized可以用在代码块上、方法上Lock只能写在代码里。
3、synchronized在代码执行完或出现异常时自动释放锁Lock不会自动释放锁需要在finally中显示释放锁。
4、synchronized会导致线程拿不到锁一直等待Lock可以设置获取锁失败的超时时间。
5、synchronized无法得知是否获取锁成功Lock则可以通过tryLock得知加锁是否成功。
6、synchronized锁可重入、不可中断、非公平Lock锁可重入、可中断、可公平/不公平并可以细分读写锁以提高效率
7、都是悲观锁都可以解决线程安全问题
3.2 CAS
CAS全称 Compare And Swap比较与交换是一种无锁算法。在不使用锁没有线程被阻塞的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数
需要读写的内存值 V。进行比较的值 A。要写入的新值 B。
4 volatile
参考另外一篇文章java并发编程中的四个关键字ThreadLocal、Volatile、Synchronized和Atomic-CSDN博客
5 线程安全
5.1 定义
当多个线程同时访问共享资源时可能出现数据竞争、死锁等问题导致程序运行出错或异常
5.2 常见问题
1数据竞争当多个线程同时访问同一个资源时会导致数据不一致比如在多线程环境下多条线程同时修改同一个变量可能导致该变量值不可测。
2死锁多线程间同时等待对方释放资源导致程序无法继续运行进入死锁状态。
3非原子操作某些操作需要多条指令才能完成如果多条线程同时执行这些操作就会出现部分执行的情况导致程序结果不正确。
4内存泄漏由于程序设计不当可能出现内存无法回收的情况导致内存泄漏。
多个线程可以共享一个进程的变量时如果线程需要对这个变量进行修改操作则可能会因为数据更新不及时导致变量信息不准确而引发线程不安全。如果线程对这个变量只有读操作没有更新操作则这个线程没有线程安全问题。
5.3 原因
多个线程同时访问同一个共享资源且存在修改该资源
5.4 解决方法--线程同步
为了解决线程安全问题就要引出线程同步这个概念。
如何保证线程安全队列和锁 保证线程同步的安全性让多个线程实现先后依次访问共享资源这样就解决了安全问题。
加锁把共享资源进行上锁每次只能一个线程进入访问完毕以后解锁然后其他线程才能进来。
1、同步代码块Synchronized
2、同步方法Synchronized
3、Lock锁ReentrantLock类实现了Lock。比较常用的是ReentrantLock,可以显式加锁释放锁
6 死锁
5.1 什么是死锁
不同的线程分别占用对方需要的同步资源不放弃都在等待对方放弃自己需要的同步资源就形成了线程的死锁。
出现死锁后不会出现异常不会出现提示只是所有的线程都处于阻塞状态无法继续。
如果没有外部干预线程会一直阻塞无法往下执行这些一直处于相互等待资源的线程就称为死锁线程。
6.2 产生死锁的四个必要条件 1.资源互斥:对所分配的资源进行排它性控制锁在同一时刻只能被一个线程使用。
2.不可剥夺:线程已获得的资源在未使用完之前不能被剥夺只能等待占有者自行释放锁。
3.请求等待:当线程因请求资源而阻塞时对已获得的资源保持不放
4.循环等待:线程之间的相互等待
6.3 避免死锁
按照死锁发生的四个条件只需要破坏其中的任何一个就可以解决但是互斥条件是没办法破坏的因为这是互斥锁的基本约束其他三方条件都有办法来破坏
1、设置超时时间超时可以退出防止死锁
2、降低锁的使用粒度尽量不要几个功能用同一把锁
3、可以一次性申请所有的资源这样就不存在等待了
7 分布式锁
7.1 为什么
假设我们把代码部署到多台服务器上还能生效吗?答案是否定的这时分布式锁应运而生。
public synchronized void test1() {System.out.println(获取到锁1);
}
public void test2() {synchronized (Test.class) {System.out.println(获取到锁2);}
}
7.2 Redis分布式锁 图片来源于Java分布式锁面试题_殷十娘的博客-CSDN博客
8 sleep() VS wait() 1、sleep方法没有释放锁而wait方法释放了锁。
2、都可以暂停线程的执行。
3、wait()通常被用于线程交互/通信sleep()通常被用于暂停执行。
4、wait()方法被调用后线程不会自动苏醒需要别的线程调用同一个对象上的notify()或者notifyAll方法sleep方法执行完成后线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
9 单例模式
加入violate可以避免重排序
class SingletonClass {private static violate SingletonClass instance null;public static SingletonClass getInstance() {if (instance null) {synchronized(SingletonClass.class) {if (instance null) {instance new SingletonClass();}}}return instance;}
}