java加锁问题详解

目录

调用系统api进行加锁

锁的主页特征

产生互斥条件

java代码中加锁

本质

加锁过程

创建锁对象

synchronized(锁对象)+代码块

 加锁的生命周期

死锁典型应用场景

死锁

一、锁是不可重入锁,一个线程针对一个锁对象加锁两次

二、两个线程两把锁

三、N个线程,M把锁

死锁的必要条件


调用系统api进行加锁

锁本质上也是操作系统提供的功能,应用程序通过调用操作系统提供关于锁的API达到加锁操作。关于锁最主要的操作在于加锁解锁两个操作。

锁的主页特征

互斥是锁的主要特征,一个线程枷锁后,另一个线程也尝试加同一个锁,就会进入阻塞状态。

产生互斥条件

一个进程中锁的数量可以是多个,多个线程竞争同一把锁时才会产生互斥。

java代码中加锁

本质

java(JVAM)对上述加锁的API进行了封装,通过使用synchronized关键字来完成加锁操作

加锁过程

创建锁对象

首先创建一个对象,使用这个对象作为锁,,这里我使用了Object对象作为锁对象。

Object object = new Object();

在Java中,任何一个类都是直接或者间接继承自Object类的,随便拿出一个对象都是可以 作为加锁的对象(其他语言不适用)。

锁对象的用途只有一个,就是用来区分线程是否针对同一个对象进行加锁,如果是同一个对象,就会出现锁竞争/锁冲突/互斥,如果不是针对同一个对象进行加锁,就不会出现锁竞争,就不存在阻塞等待的问题。

synchronized(锁对象)+代码块

使用synchronized关键字,用花括号将需要加锁的对象括起来,完成加锁操作。

这是加锁之前的代码:

public class SynchronizedTest {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
        });
        Thread t2 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

运行结果为:

 进行加锁操作后:

此时可以看到两个线程全部完成执行,打印结果也没有问题。此时的加锁操作是针对count++操作进行加锁,也就是说,当线程t1进入循环进行count++操作时,t2也进入循环,但却进入阻塞状态,当线程t1执行一次count++操作完毕后,t1线程释放锁,t2线程拿到锁,执行count++操作,此时的t1线程和t2线程针对cout++操作是串行执行的,但两个线程总体还是并行执行的。

切换加锁方式:

 synchronized (object) {
     for (int i = 0; i < 50000; i++) {
          count++;
     }
 }

将for循环和count++一起包含在代码块内部,此时针对它们进行加锁操作,观察结果:

 结果看起来和上面的一样,但是执行过程确实不相同的:

t1线程拿到锁后,执行完整个for循环后释放锁,然后t2线程才能拿到锁进入循环,执行完代码块中 的内容,释放锁。

 加锁的生命周期

加锁的生命周期和方法的生命周期是一样的,此时就可以把synchronized写到方法上去。写一个加法方法,放入多线程中执行,把锁加在方法上:

public synchronized static void add() {
        count++;
    }

放在多线程中执行,查看结果:

 此时可以看到结果正确,把锁加在方法上面,相当于对this加锁,此处的this就是锁对象,不过省略了,上述对方法加锁就等价与下面代码:

public void add() {
        synchronized (this) {
            count++;
        }
    }

注意:当synchronized修饰普通方法时,相当于对this加锁,当synchronized修饰static方法时,相当于针对该类的类对象进行加锁。

死锁典型应用场景

死锁

当一个线程进行加锁操作后,尝试第二次加锁,此时由于第一次加锁后未解锁,这时线程就会进入阻塞等待状态,这种等待永无休止,这种情况称为死锁。

    synchronized (link) {
                synchronized (link) {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                }
            }

 两次针对link对象进行加锁,此时程序应该进入阻塞等待状态,但在Java中使用synchorized进行加锁,则不会出现这种情况。

一、锁是不可重入锁,一个线程针对一个锁对象加锁两次

创建一个线程,执行这个线程,内部包含对同一对象两次加锁操作,查看上面代码的运行结果:

可以看到结果正常输出,没有进入阻塞状态,因为JVM在加锁中进行了特殊处理,这种特殊处理类似于:

每个锁对象会记录当前哪个线程持有了这个锁,当针对这个对象加锁操作时,就会先判定当前加锁线程是否已经持有锁。 

也就是说,java中使用synchorized加锁,实际上是可重入锁,当这个对象进行相同的二次加锁时,不会出现阻塞状态,而不可重入锁则会进入阻塞等待状态。

二、两个线程两把锁

现在,有两个线程t1和t2,锁A和锁B,让两个线程分别拿到一把锁,不解锁的情况下尝试获取对方的锁。

public class TwoThread {
    private static Object A = new Object();
    private  static Object B = new Object();
    private static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (A) {
                System.out.println("t1进入锁A,,,尝试获取锁B");
                synchronized (B) {
                    count++;
                }

            }
        });
        Thread t2 = new Thread(()->{
            synchronized (B) {
                System.out.println("t2进入锁B,,,尝试获取锁A");
                synchronized (A) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
    }
}

运行查看结果:

 可以看到,程序没有终止,两个线程同时等待对方释放锁,这样就会陷入阻塞状态中,此时就产生了死锁,这里使用jdk提供的jconsole工具查看线程状态,先打印一下锁A和锁B。

通过工具追踪线程状态:

 

两个线程都被互相的锁阻塞。

三、N个线程,M把锁

 随着线程和锁的数量增加,情况更加复杂,更容易出现死锁,这里和第二种有相似指出,就不过多介绍了。

死锁的必要条件

1.锁具有互斥性,一个线程拿到锁后,其他线程就得阻塞等待,

2.锁不可抢占(不可被剥夺),一个线程拿到锁后,只能自己释放锁,其他线程不能抢占。

3.请求和保持,一个线程拿到一把锁后,不释放这个锁的前提下,在尝试获取其他锁。

4.循环等待,多个线程获取多个锁的过程中,出现了循环等待。

<think>好的,我现在需要详细解释Java中ReentrantLock的加锁机制和使用方法。首先,用户提供的引用资料中提到了ReentrantLock的基本用法,比如lock()和unlock()方法,以及其与AQS(AbstractQueuedSynchronizer)的关系。我需要将这些信息整合起来,形成一个清晰的讲解结构。 首先,应该从ReentrantLock的基本概念入手,说明它是一个可重入的互斥锁,具有与synchronized相似的功能,但更灵活。然后,引用示例代码展示基本用法,比如在doSomething方法中加锁和解锁的过程。这里要注意代码的正确性,比如在finally块中释放锁,确保异常时也能解锁。 接下来,需要深入加锁机制。根据引用[3],ReentrantLock的lock()方法调用了sync.lock(),而sync是继承自AQS的内部类。这里要区分公平锁和非公平锁的不同实现。非公平锁会直接尝试获取锁,如果失败则进入AQS队列;公平锁则会先检查队列是否有等待线程。需要详细解释这两个过程,并结合源码片段,如compareAndSetState(0,1)来展示CAS操作。 然后,可重入性的实现是关键点。要说明锁内部维护了一个计数器,记录线程重入的次数,每次lock()计数器加1,unlock()减1,直到为0时释放锁。这部分需要例子说明,比如同一个线程多次加锁的情况。 此外,使用注意事项也很重要,比如确保在finally中释放锁,避免死锁。还可以提到ReentrantLock的高级功能,如尝试获取锁、可中断锁等,但根据用户的问题,可能需要简要提及,保持重点在基本机制和用法。 最后,生成相关问题时要围绕ReentrantLock的特性、AQS的作用、公平与非公平锁的区别、使用注意事项等,确保问题能帮助用户进一步理解。 需要检查引用资料的正确性,比如引用[4]提到Doug Lea的贡献和AQS,可以用来支持AQS的讲解部分。同时,确保各部分引用标注正确,比如源码部分引用[3]和[4]。 现在组织内容的结构:概述、基本用法、加锁机制(公平与非公平)、可重入性、注意事项、相关问题。确保每个部分都有引用支持,并且语言简洁明了,符合用户要求的中文回答,使用正确的LaTeX格式,如代码块用反引号包裹,行内公式用$符号。</think>### ReentrantLock加锁机制及使用详解 #### 1. 基本概念 `ReentrantLock`是Java提供的可重入互斥锁,属于`java.util.concurrent.locks`包。与`synchronized`关键字相比,它支持**公平锁策略**、**可中断锁**和**超时获取锁**等特性[^4]。 #### 2. 基本用法 通过`lock()`加锁,`unlock()`释放锁,需在`finally`块中确保释放: ```java private ReentrantLock lock = new ReentrantLock(); public void doSomething() { lock.lock(); // 加锁 try { // 临界区代码 } finally { lock.unlock(); // 释放锁 } } ``` #### 3. 加锁机制 ##### 3.1 非公平锁(默认) 调用`lock()`时通过CAS操作尝试直接获取锁: ```java final void lock() { if (compareAndSetState(0, 1)) // CAS操作 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 进入AQS队列等待 } ``` 若失败则调用`acquire()`,进入AQS队列排队[^3]。 ##### 3.2 公平锁 构造函数传入`true`启用公平锁: ```java ReentrantLock fairLock = new ReentrantLock(true); ``` 公平锁会先检查AQS队列是否有等待线程,避免插队[^4]。 #### 4. 可重入性实现 通过`state`变量记录重入次数: ```java protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 同一线程重入 int nextc = c + acquires; setState(nextc); // 增加重入次数 return true; } return false; } ``` #### 5. 注意事项 1. **必须手动释放锁**:`unlock()`需在`finally`中调用,避免死锁 2. **避免锁嵌套**:重入次数需与释放次数严格匹配 3. **性能权衡**:公平锁保证顺序但吞吐量低,非公平锁反之 #### 6. 典型应用场景 - 需要细粒度控制的并发资源访问 - 实现带超时的锁获取(`tryLock(long timeout, TimeUnit unit)`) - 处理可中断的阻塞操作(`lockInterruptibly()`)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值