Java锁示例 – ReentrantLock

欢迎来到Java锁示例教程。通常在多线程环境中工作时,我们使用synchronized关键字来保证线程安全。

Java锁

大多数情况下,synchronized关键字是最常用的方式,但它存在一些缺点,这导致了Java并发包中引入了Lock API。Java 1.5并发API引入了java.util.concurrent.locks包,其中包括Lock接口和一些实现类,以改进对象锁定机制。Java Lock API中的一些重要接口和类包括:

  1. Lock:这是Lock API的基本接口。它提供了synchronized关键字的所有功能,还提供了创建不同条件进行锁定、为线程提供等待锁的超时时间等额外的方式。一些重要的方法包括lock()获取锁、unlock()释放锁、tryLock()等待一段时间获取锁、newCondition()创建条件等。

  2. 条件:条件对象类似于对象的等待-通知模型,但具有创建不同等待集合的附加功能。条件对象总是由锁对象创建。一些重要的方法包括await()(类似于wait()方法)和signal()、signalAll()(类似于notify()和notifyAll()方法)。

  3. 读写锁:它包含一对相关的锁,一个用于只读操作,另一个用于写操作。只读锁可以同时被多个读线程持有,只要没有写线程。写锁是独占的。

  4. ReentrantLock:这是Lock接口最广泛使用的实现类。这个类以与synchronized关键字类似的方式实现了Lock接口。除了Lock接口的实现,ReentrantLock还包含一些实用方法,用于获取持有锁的线程、等待获取锁的线程等。synchronized块具有可重入性,即如果一个线程持有监视器对象的锁,且另一个synchronized块需要在同一个监视器对象上获得锁,则线程可以进入该代码块。我认为这就是类名为ReentrantLock的原因。让我们通过一个简单的例子来理解这个特性。

    public class Test{
    
    public synchronized foo(){
        //做一些事情
        bar();
      }
    
      public synchronized bar(){
        //做更多的事情
      }
    }
    

    如果一个线程进入foo(),它就持有Test对象的锁,因此当它尝试执行bar()方法时,线程被允许执行bar()方法,因为它已经持有Test对象的锁,即与synchronized(this)相同。

Java锁示例 – Java中的ReentrantLock

现在让我们看一个简单的例子,我们将用Java的Lock API替换synchronized关键字。假设我们有一个Resource类,其中包含一些需要线程安全的操作,还有一些不需要线程安全的方法。

package com.journaldev.threads.lock;

public class Resource {

	public void doSomething(){
		//进行一些操作,例如数据库读写等
	}
	
	public void doLogging(){
		//记录日志,不需要线程安全
	}
}

现在假设我们有一个Runnable类,在这个类中我们将使用Resource方法。

package com.journaldev.threads.lock;

public class SynchronizedLockExample implements Runnable{

	private Resource resource;
	
	public SynchronizedLockExample(Resource r){
		this.resource = r;
	}
	
	@Override
	public void run() {
		synchronized (resource) {
			resource.doSomething();
		}
		resource.doLogging();
	}
}

请注意,我在使用同步块来获取Resource对象的锁。我们可以在类中创建一个虚拟对象,并将其用于锁定目的。现在让我们看看如何使用Java Lock API并在不使用synchronized关键字的情况下重写上述程序。我们将在Java中使用ReentrantLock。

package com.journaldev.threads.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrencyLockExample implements Runnable{

	private Resource resource;
	private Lock lock;
	
	public ConcurrencyLockExample(Resource r){
		this.resource = r;
		this.lock = new ReentrantLock();
	}
	
	@Override
	public void run() {
		try {
			if(lock.tryLock(10, TimeUnit.SECONDS)){
			resource.doSomething();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			//释放锁
			lock.unlock();
		}
		resource.doLogging();
	}

}

正如您可以看到的那样,我使用tryLock()方法确保我的线程只等待一定的时间,如果无法获取对象的锁,则仅记录并退出。另一个重要的注意事项是使用try-finally块,确保在doSomething()方法调用引发任何异常时也释放锁定。

Java Lock vs synchronized

根据上述详细信息和程序,我们可以很容易地得出Java Lock和synchronization之间的以下区别。

  1. Java Lock API提供更多的可见性和锁定选项,不同于synchronized,其中一个线程可能会无限期地等待锁定,我们可以使用tryLock()确保线程仅等待特定的时间。
  2. 同步代码更加清晰和易于维护,而使用Lock我们被迫使用try-finally块,以确保在lock()和unlock()方法调用之间即使抛出某些异常也释放锁定。
  3. 同步块或方法只能覆盖一个方法,而使用Lock API可以在一个方法中获取锁并在另一个方法中释放锁。
  4. 使用synchronized关键字无法提供公平性,而创建ReentrantLock对象时可以设置公平性为true,以便最长等待的线程首先获得锁。
  5. 我们可以为Lock创建不同的条件,不同的线程可以为不同的条件等待(await)。

这就是Java Lock示例、Java中的ReentrantLock和与synchronized关键字的比较分析的全部内容。

Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock