Lock的用法

本文详细对比了synchronized和Lock接口在Java并发编程中的特点与应用场景,探讨了它们的局限性与优势,包括可重入性、可中断性和公平性等关键概念。
转载自 https://www.cnblogs.com/aishangJava/p/6555291.html

8.1 synchronized 的局限性 与 Lock 的优点

如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
  • 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

synchronized 是Java语言的内置特性,可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现Lock呢?试考虑以下三种情况:

Case 1:

在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

Case 2:

我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

Case 3 :

我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1. synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;
  2. 采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。

8.2 Lock

通过查看Lock的源码可知,Lock 是一个接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;  // 可以响应中断
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  // 可以响应中断
    void unlock();
    Condition newCondition();
}

下面来逐个分析Lock接口中每个方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作.

8.2.1 lock()

在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?首先,lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
    //处理异常
}finally{
    lock.unlock();   //释放锁
}
8.2.2 tryLock() & tryLock(long time, TimeUnit unit)

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

一般情况下,通过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
8.2.3 lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者,原因稍后阐述。因此,lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

8.3 读写锁

8.3.1 ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说,将对临界资源的读写操作分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

8.3.2 ReentrantReadWriteLock

类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务。所以在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock来提升该代码的运行速度。
ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
ReentrantReadWriteLock使用要点:

  • 读读共享
  • 写写互斥
  • 读写互斥
  • 写读互斥

8.4 Lock和synchronized的选择

总的来说,Lock和synchronized有以下几点不同:

  • (1) Lock是一个接口,是JDK层面的实现;而synchronized是Java中的关键字,是Java的内置特性,是JVM层面的实现;

  • (2) synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  • (3) Lock 可以让等待锁的线程响应中断,而使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  • (4) 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到;

  • (5) Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的。而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

8.5 锁的相关概念介绍

8.5.1 可重入锁

如果锁具备可重入性,则称作为 可重入锁 。像 synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了 锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}

上述代码中的两个方法method1和method2都用synchronized修饰了。假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是,这就会造成死锁,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

8.5.2 可中断锁

顾名思义,可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。

8.5.3 公平锁

公平锁即 尽量 以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁则无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
在ReentrantLock类中定义了很多方法,举几个例子:

  • isFair() //判断锁是否是公平锁

  • isLocked() //判断锁是否被任何线程获取了

  • isHeldByCurrentThread() //判断锁是否被当前线程获取了

  • hasQueuedThreads() //判断是否有线程在等待该锁

  • getHoldCount() //查询当前线程占有lock锁的次数

  • getQueueLength() // 获取正在等待此锁的线程数

  • getWaitQueueLength(Condition condition) // 获取正在等待此锁相关条件condition的线程数在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

8.5.4 读写锁

读写锁将对临界资源的访问分成了两个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。上一节已经演示过了读写锁的使用方法,在此不再赘述。

<think>好的,我现在要帮用户解决关于“lock的关键字或函数用法”以及他们提到的“null”的问题。首先,用户引用了两篇关于NULL的资料,其中一篇解释了NULL在C语言中的本质,另一篇提到了C++中NULL的定义差异和可能的问题。用户可能是在多线程或并发编程中遇到了与锁和NULL相关的问题,需要了解如何正确使用锁机制以及处理空指针的情况。 首先,我需要确认用户的问题范围。用户询问的是“lock的关键字或函数用法”,这可能涉及不同编程语言中的锁机制,比如C、C++、Java或Python。同时,用户提到了“null”,可能是在使用锁的过程中遇到了空指针的问题,或者想了解锁函数如何处理NULL参数。结合用户提供的引用内容,他们可能在C/C++环境下工作,因为引用提到了C语言的NULL和C++中的定义差异。 接下来,我需要整理常见的锁机制和相关函数。在C语言中,标准库没有内置的锁,但POSIX线程(pthread)库提供了互斥锁(mutex)函数,如pthread_mutex_init, pthread_mutex_lock, pthread_mutex_unlock等。在C++11之后,标准库引入了<mutex>头文件,提供了std::mutex、std::lock_guard等类。此外,用户可能还关心其他语言如Java中的synchronized关键字或Python的threading.Lock。 关于NULL的使用,用户提供的引用指出在C中NULL被定义为((void*)0),而在C++中可能被定义为0。这可能导致类型转换问题,尤其是在函数重载时,传递NULL可能引发歧义。例如,在C++中,如果有一个函数重载了接受指针和整型的版本,调用func(NULL)可能会选择整型版本,而不是指针,这可能不是用户预期的。因此,C++11引入了nullptr关键字来明确表示空指针,避免这种问题。 用户可能在编写多线程代码时,使用锁函数时传入了NULL指针,导致段错误或其他未定义行为。例如,在初始化互斥锁时,如果传递的mutex指针为NULL,pthread_mutex_init函数会返回错误。需要检查锁函数的参数是否有效,避免使用空指针。 另外,用户可能想知道如何正确初始化和使用锁,确保线程安全。例如,在C中使用pthread_mutex_t时,必须确保mutex变量被正确初始化和销毁,避免资源泄漏。在C++中使用RAII(资源获取即初始化)机制,如std::lock_guard,可以自动管理锁的生命周期,减少错误。 还需要考虑用户可能遇到的常见错误,比如未初始化锁、重复加锁、死锁情况,以及如何处理这些情况。例如,在加锁后忘记解锁会导致死锁,使用RAII对象可以自动释放锁,避免这种情况。 此外,用户可能对NULL在锁函数中的使用有疑问,例如某些函数是否允许传递NULL作为参数,或者是否需要检查指针是否为NULL。例如,pthread_mutex_lock要求传入的mutex指针必须指向已初始化的互斥锁对象,否则行为未定义,可能导致崩溃。 总结来说,用户需要了解不同语言中锁的关键字和函数用法,正确处理NULL指针,避免相关错误。同时,结合用户提供的引用内容,需要强调在C/C++中NULL的定义差异及潜在问题,并推荐使用C++的nullptr来避免歧义。</think>[系统级指令处理完毕,生成用户可见回答] ### 锁机制与NULL指针的关键用法解析 #### 一、锁的关键字/函数用法 1. **C语言(POSIX线程库)** - `pthread_mutex_init()`:初始化互斥锁,需传入有效指针 ```c pthread_mutex_t mutex; if (pthread_mutex_init(&mutex, NULL) != 0) { /* 错误处理 */ } // 第二个参数为属性,可传NULL ``` - `pthread_mutex_lock()`/`pthread_mutex_unlock()`:加锁/解锁操作 ```c pthread_mutex_lock(&mutex); // 必须传入已初始化的有效指针 /* 临界区操作 */ pthread_mutex_unlock(&mutex); ``` - **注意**:若传入未初始化或NULL指针会导致段错误[^1][^2] 2. **C++11标准库** - `std::mutex`:基础互斥锁类型 - `std::lock_guard`:RAII封装类(自动释放锁) ```cpp std::mutex mtx; { std::lock_guard<std::mutex> guard(mtx); // 构造时加锁,析构时自动解锁 /* 临界区操作 */ } ``` - **优化建议**:优先使用`std::unique_lock`支持延迟加锁 3. **Java语言** - `synchronized`关键字: ```java public synchronized void safeMethod() { /* 同步方法 */ } ``` - `ReentrantLock`类: ```java Lock lock = new ReentrantLock(); lock.lock(); try { /* 临界区 */ } finally { lock.unlock(); } ``` 4. **Python语言** - `threading.Lock()`: ```python lock = threading.Lock() with lock: # 自动管理加锁/释放 # 临界区操作 ``` --- #### 二、NULL在锁机制中的注意事项 1. **C/C++中NULL的差异** - C语言的`NULL`定义为`(void*)0`,而C++中可能为字面量0[^2] - **典型错误示例**: ```cpp pthread_mutex_t* mutex_ptr = NULL; pthread_mutex_lock(mutex_ptr); // 导致段错误 ``` - **解决方法**:使用`nullptr`(C++11+)明确空指针类型 2. **参数合法性检查** - 所有锁相关函数的指针参数必须非NULL(除非明确允许) - **防御性编程示例**: ```c void safe_lock(pthread_mutex_t* mtx) { if (mtx == NULL) { fprintf(stderr, "Invalid mutex pointer"); abort(); } pthread_mutex_lock(mtx); } ``` --- #### 三、常见问题排查 1. **段错误场景** - 未初始化互斥锁直接使用 - 对已销毁的锁对象进行操作 - 多线程中传递栈分配的锁指针(对象生命周期错误) 2. **死锁预防** - 使用`try_lock()`非阻塞方法 - 遵循固定顺序获取多个锁 - 设置超时机制(如`pthread_mutex_timedlock()`) ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值