java锁
简介
java多线程中可能出现数据不一致问题,而锁在对于多线程执行是必不可少的,且单体项目(部署到一台机器)和部署多台机器上锁的用法是不一样的
什么是锁:
锁用于控制多线程对共享资源的访问,保证同一时刻只有一个线程能够访问共享资源。它的基本作用是:
**保证线程安全:**避免多个线程同时对共享数据进行修改,导致数据不一致或程序错误。
**数据一致性:**加锁确保了在加锁范围内的代码只能被一个线程执行,从而保证数据的一致性。
为什么要加锁
**多线程竞争:**在多线程环境下,多个线程可能同时尝试访问或修改共享资源。如果不加锁,就可能会出现线程之间的竞态条件(race condition),导致数据不一致或者错误的执行结果。
**保证原子性:**锁确保一段代码在执行时,其他线程无法中断或同时执行。
单体项目:
例如一个Springboot项目部署到一台服务器上
加锁方式
一般加锁方式
1.synchronzied关键字
synchronized 是一个 关键字,使用C++实现的,没办法控制锁的开始、锁结束,也没办法中断线程的执行
2.lock接口
是 java层面的实现,可以获取锁的状态,开启锁,释放锁,通过设置可以中断线程的执行,更加灵活
是否自动是否锁:synchronized 会自动是否锁,而 lock 需要手动调用unlock 方法释放,否则会死循环,我们平时加的锁是ReentrantLock实现lock接口
synchronzied(内置关键字加锁)
synchronzied可以用于一下场景:
加在实例方法上
实例方法上的 synchronized 锁的是 当前实例对象。也就是说,只有在同一个实例上调用该方法时,多个线程会争夺锁。如果有多个不同的实例,每个实例的锁是独立的,不会互相影响。多个线程在操作不同实例时,互不干扰。
public synchronized void instanceMethod() {
// 同步代码
}
加在静态方法上
静态方法上的 synchronized 锁的是 类对象(Class 对象)。也就是说,同一个类的所有实例在访问该静态方法时会争夺同一把锁,不管你创建多少个实例,静态方法的同步机制总是锁住 类的 Class 对象。
public static synchronized void staticMethod() {
// 同步代码
}
加在代码块
实际上是对代码块中实例加锁
public class SynchronizedBlockExample {
private int counter;
private Object lock = new Object();
public void increment() {
synchronized(lock) {
counter++;
}
}
public int getCount() {
synchronized(lock) {
return counter;
}
}
}
这里是对lock加锁,lock是属于SynchronizedBlockExample 这个类的,所有当多个线程操作同一个SynchronizedBlockExample 类实例,执行increment操作时候会对lock加锁,其他线程执行代码块里面的方法的时候需要看lock锁释放没有
this
代码块中this和在类中定义一个对象实例的区别
public class SynchronizedBlockExample {
private int counter;
private Object lock = new Object();
public void increment() {
synchronized (this) {
counter++;
}
}
public int getCount() {
synchronized (this) {
return counter;
}
}
}
使用 this 会锁住 当前对象实例,确保同一个实例的同步代码块在同一时刻只有一个线程可以访问,多个线程访问同一实例时会发生锁竞争。
这种方式和 synchronized(lock) 在功能上是 等价的,也就是说,它们会表现得一样,锁住的是当前对象实例。
那如果创建一个不是Object对象的实例呢(Integer lock2=new Integer())
public class SynchronizedBlockExample {
private int counter;
private Integer lock2 = new Integer();
public void increment() {
synchronized (lock2 ) {
counter++;
}
}
public int getCount() {
synchronized (lock2 ) {
return counter;
}
}
}
使用 new Integer(0) 创建的 lock2 对象作为锁是 有效的,并且它与 this 或 lock 在逻辑上没有本质区别——它仅仅是一个替代的 锁对象。
lock2 作为一个锁对象,会确保同步代码块在执行时只有一个线程能够访问。
潜在的问题:
性能问题:使用 new Integer(0) 每次创建一个新的对象作为锁,这在性能上会有所浪费,尤其是在多线程情况下,频繁创建新的对象可能会引入不必要的开销。
使用 Integer 作为锁不推荐:虽然可以使用任意对象作为锁,但 Java 提倡使用 Object 或 私有的、专门用于锁定的对象(如 lock)。Integer 是一个不可变对象,使用它作为锁并不直观,容易导致不必要的误解。特别是对于 常量 或 基础数据类型的包装类,如果有不小心的地方(如对 Integer 对象进行了不可预料的共享),可能会引发错误。
Integer 是不可变对象:虽然在实际场景中,Integer 作为锁对象通常是安全的,但在某些特殊情况下,Integer 作为锁的行为不如 Object 直观,因为它是不可变的。通过 new Integer(0) 创建的新对象可以作为锁对象,但是它没有额外的语义,难以表明它的真正目的(即作为锁对象)。
synchronzied是可重入锁吗
可重入锁(Reentrant Lock) 的含义是,如果一个线程已经获得了锁,它可以再次进入该锁而不会被阻塞。这种锁的特性允许同一个线程多次获取同一个锁。
为什么 synchronized 是可重入锁?
当一个线程进入一个 synchronized 方法或代码块时,如果它再次请求进入同一个锁(例如,调用同一个对象的另一个 synchronized 方法),它可以成功获得该锁,而不会导致死锁。
synchronized 锁内部会维护一个锁的计数器(重入计数器),每当同一线程再次请求锁时,计数器会增加,直到线程释放锁时,计数器减少。当计数器减到零时,锁被完全释放。
例如:
class ReentrantLockExample {
synchronized void methodA() {
System.out.println("Entering methodA");
methodB(); // 线程可以再次进入 methodB,因为它已经持有锁
}
synchronized void methodB() {
System.out.println("Entering methodB");
}
}
synchronized 是非公平锁吗?
是的
非公平锁 指的是线程在竞争锁时,不保证按照请求的顺序(即先来先得)获取锁。当一个线程尝试获取锁时,即使队列中其他线程排队等待,它也可能直接获取锁,而不管它在队列中的顺序。
为什么 synchronized 是非公平锁?
synchronized 的实现机制是基于 偏向锁、轻量级锁 和 重量级锁 的,这些机制并没有显式地保证线程的公平性。
在高并发情况下,当多个线程竞争同一锁时,后到的线程有可能直接获取到锁,而不需要按照先到先得的顺序排队。
锁的竞争过程是“抢占式”的,即线程尝试获得锁时,如果锁空闲,它会立刻成功获取,而不等待前面的线程。
lock(ReentrantLock)加锁
Lock 接口的主要实现类是 ReentrantLock,它提供了与 synchronized 类似的基本功能,但具有更多的可控制性,如可中断、可公平性、定时锁等。
关键方法:
lock():获取锁。如果锁不可用,调用线程会被阻塞,直到获得锁。
unlock():释放锁。释放后,其他等待锁的线程可以获取锁。
tryLock():尝试获取锁。如果锁不可用,立即返回 false,而不阻塞。
lockInterruptibly():获取锁,但如果线程在等待过程中被中断,它会抛出 InterruptedException。
newCondition():创建一个 Condition 对象,用于线程间通信,类似于 Object.wait() 和 Object.notify()。
实现方式
底层实现原理
Lock的底层是 AQS+CAS机制 实现。
默认是非公平锁,可设置为公平锁
CAS
这里的CAS机制,主要是指一个线程获取到锁之后,会将state变量+1,这里的+1操作底层就是用CAS实现的。
不太了解CAS的可以看看这篇文章
java CAS
AQS
AQS是AbstractQueuedSynchronizer的缩写,是一个抽象类,属于JUC的一个基类,JUC下的很多内容都是通过AQS实现的,例ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
AQS在内部维护了2个数据结果
1.由volatile修饰的state成员变量,保证了可见性和有序性(禁止指令重排)
2.双向链表Node头节点和尾节点
AQS参考下面这篇文章
java AQS
state
当一个线程
双向链表
这里双向链表存储的是每个线程
为什么是双向链表
主要原因是它可以高效地管理线程的排队和唤醒
1. 线程的前后依赖关系:
当一个线程阻塞在获取锁时,它的状态和位置会被记录在队列中。双向链表可以轻松地维护线程之间的前后关系。
如果某个线程需要被唤醒,它通常会需要查看排队中的 前驱线程 或 后继线程,以决定唤醒顺序。双向链表通过 前驱指针 和 后继指针 使得这种操作变得容易。
线程可能会因 条件变量 或 信号量 等条件被唤醒,双向链表能够灵活地管理线程的等待和唤醒。
2. 线程的公平性控制:
在公平锁模式下,AQS 使用同步队列来确保线程按顺序获取锁(先来先服务)。双向链表支持 FIFO(先进先出) 的排队机制,便于实现公平性策略。
前驱和后继 指针帮助 AQS 在唤醒线程时遵循队列中的顺序,避免出现饥饿现象(即某些线程始终无法获取锁)。
如果一个线程被唤醒后,它会检查自己的 后继线程,以便唤醒队列中排队最久的线程。
3. 线程的插入与移除效率:
线程在等待队列中的 插入 和 移除 操作是非常频繁的,双向链表在这方面比单向链表更具优势:
当一个线程进入队列时,AQS 会将它插入到队列的尾部,操作相对简单且高效。
线程退出队列时,它可能是 队头线程 或者 队尾线程,双向链表可以迅速移除头尾节点。
双向链表不需要像单向链表那样遍历整个链表来找到节点,能更高效地进行节点的插入和删除操作。
4. 支持中断、等待和超时操作:
双向链表能帮助 AQS 实现线程的中断、等待和超时控制:
中断线程:当线程在等待队列中被中断时,可以快速找到线程在队列中的位置并将其从队列中移除。
等待超时:当线程的等待时间到达最大限制时,双向链表能够帮助它快速检查并从队列中移除。
ReentranLock是非公平锁吗
默认非公平锁,可以创建对象的时候设置为公平锁
非公平性体现在那里
当线程尝试获取锁时,并不是 严格按照队列的顺序 来获取锁。非公平锁 允许一个 正在等待的线程 在它的前面排队的线程还没有尝试重新获取锁时,直接抢占锁。
也就是说,如果 队列头的线程 还没有尝试获取锁(它可能正在等待某个条件),其他排队的线程 可以绕过它,直接尝试获取锁。
集群项目
有时候咱们项目的扩大,导致单台服务器CPU压力过大,这时候就会采用集群的方法将同一个springboot项目部署到多台机器,利用负载均衡分担压力
集群项目多线程问题
如果这时候前端用户有同时调用了一个接口,其中这个接口有个共享变量