java中的锁
synchronized(非公平锁)
public class Phone {
//修饰普通方法,锁的是当前类的实例
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------sendEmail");
}
//修饰普通方法,锁的仍然是当前类的实例
public void sendSMS() {
synchronized(this){
System.out.println("------sendSMS");
}
}
//修饰静态方法,如果是静态方法那就是类锁 锁的当前类的class对象
public static synchronized void sendEmailStatic(){
System.out.println("------sendEmailStatic");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//修饰静态方法中的代码块 ,锁的仍然是当前类的class对象
public static void sendSMSStatic(){
synchronized (Phone.class){
System.out.println("------sendSMSStatic");
}
}
}
//检测
//调用两个静态方法,获取到的都是类锁,第一个thread调用了静态方法phone.sendEmailStatic(),因此其获取到对锁,打印,延迟四秒,再进行锁释放,此时已经执行到第二个thread, phone.sendSMSStatic()因为也是静态方法,所以也要去获取对象锁,此时对象锁已经被获得,四秒过后获取到对象锁,打印sendSMSStatic
//调用两个普通方法与调用两个静态方法相同,都需要去争夺锁
//调用一个静态方法,一个普通方法,没有发生锁竞争
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmailStatic();
},"a").start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMSStatic();
}, "b").start();
}
synchronized的锁升级
当第一个线程第一次访问一个对象的同步块时,该对象的锁状态会被设置为偏向锁
无锁状态: 初始状态,锁没有被任何线程占用程序不会有锁的竞争。那么这种情况我们不需要加锁,所以这种情况下对象锁状态为 无锁。
偏向锁: 顾名思义,它会偏向于第一个访问锁的线程
如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁
锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争,发生了竞争偏向锁就升级为轻量级锁
轻量级锁:当一个线程访问该对象时,JVM会将对象头中的Mark Word复制一份到线程栈中,并在对象头中存储线程栈中的指针。此时如果另外一个线程想要访问该对象,会发现该对象已经处于轻量级锁状态,于是开始尝试使用CAS操作将对象头中的指针替换成自己的指针。自旋一定次数后(JDK1.8最多自旋15次),如果替换成功,则该线程获取锁成功,反之,升级为重量级锁
**重量级锁:**当锁升级到重量级锁时,JVM会将该对象的锁编程一个重量级锁,并在对象头中记录指向等待队列的指针。此时如果一个线程想要获取锁,需要先进入等待队列,等待锁被释放。当锁被释放时,JVM会从等待队列中选择一个线程唤醒,并将该线程设置为“就绪”状态。
使用synchronized关键字将会隐式地获取或释放,这种方式简化了同步管理,但扩展性没有显式的好。例如,针对一个场景,手把手锁释放和获取,先获取锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C…在这种场景下,synchronized就不那么容易实现了,而Lock却容易许多。接下来重点介绍一下它的是实现类——ReentrantLock
ReentrantLock(ReentrantLock可实现非公平锁和公平锁)
//和synchronized不同,ReentrantLock需要显示的获取和释放
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//获取锁
lock.lock();
try {
//代码逻辑
} finally {
//释放锁
lock.unlock();
}
}
公平锁与非公平锁的区别
- 非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁,获取不到,再进入队列等待。
- 公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证队列中的第一个线程先获得锁。
实现非公平锁,只需要使用ReentrantLock的有参构造即可
//只需要在new的时候指定其构造函数为true,就是公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁与非公平锁的优缺点
非公平锁:优点是减少了cpu唤醒线程的开销,整体的吞吐量会高一点。但它可能会导致队列中排队的线程一直获取不到锁或长时间获取不到,活活饿死。
公平锁:优点是所有的线程都能得到资源,不会饿死在队列中。但它的吞吐量相较非公平锁而言,就下降了很多,队列里面除了第一个线程,其它线程都会阻塞,cpu唤醒阻塞线程的开销是很大的缺点。
可重入性的解释和应用(synchronized和ReentrantLock都是可重入锁,即同一线程可以多次获取同一把锁)
所谓重入锁,是指一个线程拿到锁后,还可以多次获取同一把锁,而不会因为该锁已经被持有(尽管是自己持有的)而陷入等待状态。之前说的sychronized也是可重入锁。
ReentrantLock加锁的时候,看下当前持有锁的线程和当前请求的线程是否同一个,一样就可重入了。只需要简单的讲state加1,记录当前重入的次数即可。同时,在锁释放的时候,需要确保state=0的时候才执行释放的动作,简单的说就是重入多少次就得释放多少次。
可重入锁的例子详解
所谓重入锁,是指一个线程拿到锁后,还可以多次获取同一把锁,而不会因为该锁已经被持有(尽管是自己持有的)而陷入等待状态。之前说的sychronized也是可重入锁。
ReentrantLock加锁的时候,看下当前持有锁的线程和当前请求的线程是否同一个,一样就可重入了。只需要简单的讲state加1,记录当前重入的次数即可。同时,在锁释放的时候,需要确保state=0的时候才执行释放的动作,简单的说就是重入多少次就得释放多少次。
public class Chopsticks {
boolean getOne=false;
boolean getAnother=false;
//拿筷子,获取锁,该锁是当前Chopsticks对象
public synchronized void getOne() {
getOne=true;
System.out.println(Thread.currentThread().getName()+"拿到了一根筷子。");
//if语句块调用了另外的同步方法,需要再次获取锁,而该锁也是当前Chopsticks对象
if(getAnother) {
//有两根筷子,吃饭
canEat();
//吃完放下两根筷子
getOne=false;
getAnother=false;
}else {
//只有一根筷子,去拿另一根,然后吃饭
getAnother();
}
}
public synchronized void getAnother() {
getAnother=true;
System.out.println(Thread.currentThread().getName()+"拿到了一根筷子。");
if(getOne) {
//有两根筷子,吃饭
canEat();
//吃完放下两根筷子
getOne=false;
getAnother=false;
}else {
//只有一根筷子,去拿另一根,然后吃饭
getOne();
}
}
public synchronized void canEat() {
System.out.println(Thread.currentThread().getName()+"拿到了两根筷子,开恰!");
}
}
public class testChopstick {
public static void main(String[] args) {
Chopsticks chopsticks=new Chopsticks();
//线程A,模拟人A
Thread A=new Thread(new Runnable() {
@Override
public void run() {
chopsticks.getOne();
}
});
}
线程A调用getone()获取到了对象锁,在getone()中还需要调用,getnother(),发现锁就在自己这里,于是继续执行,这就是可重入锁
与synchronized的对比
两个的相同点是,都是用于线程同步控制,且都是可重入锁,但也有很多不同点:
synchronized是Java内置特性,而ReentrantLock是通过Java代码实现的。
synchronized可以自动获取/释放锁,而ReentrantLock需要手动获取/释放锁。
synchronized的锁状态无法判断,而ReentrantLock可以用tryLock()方法判断。
synchronized同过notify()和notifyAll()唤醒一个和全部线程,而ReentrantLock可以结合Condition选择性的唤醒线程。
在3.1小节提到过ReentrantLock的常用方法,所以它还具有响应中断、超时等待、tryLock()非阻塞尝试获取锁等特性。
ReentrantLock可以实现公平锁和非公平锁,而synchronized只是公平锁。
Condition接口
上面在介绍ReentrantLock的时候就提到过了Condition接口,它可以结合ReentrantLock选择性地唤醒线程,从而实现更复杂的线程同步操作。
同样,Conditon接口也来自java.util.concurrent.locks包下,任意一个Java对象,都拥有一组监视器方法(Object),主要包括wait、wait(long timeout)、notify()和notifyAll()方法,这些方法与sychronized关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式上还是有差异的。
Condition接口的使用
实现10次AB的循环打印
package com.jitui.entity.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class AB {
private int nums=0;
private ReentrantLock reentrantLock=new ReentrantLock();
//获取condition对象
Condition condition=reentrantLock.newCondition();
public void printA(){
reentrantLock.lock();
if(nums%2!=0)
{
try {
//当前线程释放锁,并进入Condition的等待队列中等待,直到被其它线程调用signal()唤醒它、或调用signalAll()、或被线程中断
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("A");
nums++;
//打印成功唤醒其他线程
condition.signal();
reentrantLock.unlock();
}
public void printB(){
reentrantLock.lock();
if(nums%2!=1)
{
try {
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("B");
nums++;
condition.signal();
reentrantLock.unlock();
}
public static void main(String[] args) {
AB ab=new AB();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
ab.printA();
}
},"A").start();
new Thread(()-> {
for (int i = 0; i <10 ; i++) {
ab.printB();
}
},"B").start();
}
}
这里只使用了一个condition,因为这里只有两个线程,当打印A的线程阻塞了,自然会唤醒打印B的线程,如果循环打印ABC,打印完A之后,再使用siagnal唤醒,可能唤醒的是打印c的线程,这并不是我们想要的,所以这里最好使用signalAll()进行唤醒,这样可以保证打印c的线程被唤醒。但是我们可以给打印ABC的三个线程各创建一个condition,在A线程完成打印之后,直接调用conditionB.signal(),这样就方便很多
Condition使用多个的好处
1. 细粒度的控制
多个 Condition
允许不同的条件进行单独的通知和等待。这使得线程可以更加精确地控制何时唤醒和等待,从而优化资源的使用。例如,在生产者-消费者模型中,可以为生产和消费各自创建一个 Condition
,这样生产者可以在有空间时通知消费者,而消费者可以在有新数据时通知生产者。
因为我们可能有多个生产者,消费者线程,如果此时缓冲区为空。我们使用单一的condition进行唤醒,调用signal(),可能唤醒的是一个消费者线程这是不合理的,如果调用condition.signalAll(),唤醒当前condition的所有线程,这样使得生产者线程能够百分百唤醒,单这也是不合理的因为消费者线程根本没必要唤醒
2. 避免不必要的唤醒
如果使用单一的 Condition
,任何一个线程的唤醒都可能导致所有等待的线程被唤醒,这会造成上下文切换开销。在多条件的情况下,只有满足特定条件的线程才会被唤醒,从而减少不必要的资源消耗。
3. 提升效率
通过分开管理不同的条件,可以减少竞争和增加并发性。当多个条件被同时使用时,只有相关的线程会被激活,这可以提升整体系统的效率。
ReentrantReadWriteLock(读写锁)
无论是synchronized还是ReentrantLock,同一时刻只能有一个线程访问临界资源,但是我们知道,读线程并不会导致并发问题,那么在读多写少的场景下,这两种锁就不太适合了,所以针对这个“痛点”,JUC中提供了另一种锁的实现——ReentrantReadWriteLock。
- 写——写:互斥,一个线程在写的同时,其它线程会被阻塞。
- 读——写:互斥,读的时候不能写,写的时候不能读。
- 读——读:不互斥,不阻塞。
StampedLock(邮戳锁)
StampedLock是Java8提供的一种乐观读写锁。相比于ReentrantReadWriteLock,StampedLock引入了乐观读的概念,就是在已经有写线程加锁的同时,仍然允许读线程进行读操作,这相对于对读模式进行了优化,但是可能会导致数据不一致的问题,所以当使用乐观读时,必须对获取结果进行校验。
StampedLock的三种模式
读模式:在读模式下,多个线程可以同时获取读锁,不互相阻塞。但当写线程请求获取写锁时,读线程会被阻塞。与ReentrantReadWriteLock类似。
写模式:写模式时独占的,当一个写线程获取写锁时,其它线程无法同时持有写或读锁。写锁请求会阻塞其它线程的读锁。与ReentrantReadWriteLock类似。
乐观读模式:注意,上述两个模式均加了锁,所以它们之间读写互斥,乐观读模式是不加锁的读。这样就有两个好处,一是不加锁意味着性能会更高一点,二是写线程在写的同时,读线程仍然可以进行读操作。(如果对数据的一致性要求,那么在使用乐观读的时候需要进行validate()校验)
CAS(乐观锁)
CAS全称Compare And Swap,顾名思义就是先比较再交换。主要应用就是实现乐观锁和锁自旋。CAS操作包含三个操作数——内存位置(V)、预期值(A)和新值(B)。在并发修改的时候,会先比较A和V的值是否相等,如果相等,则会把值替换成B,否则就不做任何操作。
oldValue =count.get()就是我们的预期值(A),newValue = oldValue + 1就是我们的新值(B),count.compareAndSet(oldValue, newValue),做的操作就是再次从内存中拿到count值 也就是内存位置(V),如果 预期值A与内存位置V相等,则将count更新成新值B,如果 预期值A与内存位置V不相等,则不更新,这就是cas操作。我们我们肯定是要更新的以到结果,所以while循环一直执行cas操作,失败了就重试。如果这里不加while循环的话一定是得不到想要的结果的
package com.jitui.entity.lock;
import java.util.concurrent.atomic.AtomicInteger;
public class CASCounter {
//AtomicInteger中的方法全是原子操作,即执行过程不能被打断,要么全部执行成功
private final AtomicInteger count = new AtomicInteger(0);
// 增加计数,模仿AtomicIntege.etAndIncrement,
public void increment() {
int oldValue;
int newValue;
do {
oldValue = count.get(); // 获取当前值
newValue = oldValue + 1; // 计算新值
} while (!count.compareAndSet(oldValue, newValue)); // 尝试更新
}
// 获取当前计数
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
CASCounter counter = new CASCounter();
Thread[] threads = new Thread[10];
// 启动10个线程并增加计数
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数
System.out.println("Final count: " + counter.getCount());
}
}
原子类就是基于cas的乐观锁,也就是无锁机制
CAS(自旋锁)
- CAS 操作:CAS 是一个原子操作,执行的过程通常包含三个操作数:内存位置(V)、预期值(A)和新值(B)。如果内存位置 V 的当前值等于预期值 A,则将内存位置 V 的值更新为新值 B。这个操作是原子的,即在这个过程中的其他线程无法干预。
- 自旋循环:自旋锁在获取锁时会使用一个循环(即“自旋”)。线程不断尝试获得锁,通常通过一个共享变量来表示锁的状态。在锁空闲(通常是 0)时,线程会尝试进行 CAS 操作将其更改为锁定状态(通常是 1)。如果 CAS 操作成功,线程就获得了锁;如果失败,线程将继续自旋,直到锁被释放。
- CPU 资源:由于自旋锁在锁被占用时会持续尝试获取锁,这可能导致 CPU 资源的浪费。在锁的持有时间很短的情况下,自旋锁可能会比传统的锁(如互斥锁)更有效率,但在锁持有时间较长时,则可能导致性能下降。
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
while (!owner.compareAndSet(null, currentThread)) {
// 自旋等待,锁被其他线程占用
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
if (owner.get() == currentThread) {
owner.set(null); // 释放锁
}
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
// 启动多个线程测试自旋锁
for (int i = 0; i < 5; i++) {
new Thread(() -> {
spinLock.lock();
try {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(100); // 模拟一些工作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
}).start();
}
}
}
基于cas的乐观锁和基于cas的自旋锁有什么区别,我认为基于cas的自旋锁是一种悲观锁,基于cas的乐观锁一种无锁机制,是允许多个线程进入临界区的,但是更新临界区的value,需要看cas是否比对成功,不成功则重新执行cas操作,而基于cas的自旋锁,cas操作是为了将当前锁的拥有者更新成当前线程,如果更新失败则继续cas操作,尝试更新锁的拥有者为当前线程,自旋锁只允许一个线程进入临界区。
部分内容源自https://blog.csdn.net/weixin_45433817/article/details/132216383,原文很不错,可以去看看原文
如果错误请指正