图片引用自:不可不说的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;
}
}