多线程(7)-java多线程之Lock物语

本文深入讲解Java中的Lock接口及其实现ReentrantLock的使用方法,包括lock、unlock、tryLock等核心方法,并探讨了Lock与synchronized关键字的区别,以及可重入锁、公平锁、非公平锁和读写锁的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Lock概要介绍

先来一张锁的框架图

image

  • Lock接口

JUC包中的 Lock 接口支持那些语义不同(重入、公平等)的锁规则。所谓语义不同,是指锁可是有"公平机制的锁"、"非公平机制的锁"、"可重入的锁"等等。"公平机制"是指"不同线程获取锁的机制是公平的",而"非公平机制"则是指"不同线程获取锁的机制是非公平的","可重入的锁"是指同一个锁能够被一个线程多次获取。主要实现的类是ReentrantLock。

  • ReadWriteLock接口

ReadWriteLock 接口以和Lock类似的方式定义了一些读取者可以共享而写入者独占的锁。JUC包只有一个类实现了该接口,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。

  • Condition接口

Condition需要和Lock联合使用,它的作用是代替Object监视器方法,可以通过await(),signal()来休眠/唤醒线程。 Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

二、Lock用法

1、lock,unlock

void lock() ;获取锁
  • 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回,将锁的保持计数设置为 1。
  • 如果该锁被其他线程保持,在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。
void unlock(); 释放锁
  • 对应于lock()、tryLock()、tryLock(xx)、lockInterruptibly()等操作,如果成功的话应该对应着一个unlock(),这样可以避免死锁或者资源浪费
lock unlock 一般这样使用
 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }}

注意点
  • lock unlock 一定要成对出现。获取锁,必须要释放锁,不然很有可能引起死锁
  • unlock 一定要在finally里使用,不然业务代码如果发生异常,直接抛出异常时,锁没有得到释放,也是很有可能引起死锁
代码示例
package wang.conge.javasedemo.core.thread;

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

public class LockTest {

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();

		Model model = new Model();

		Runnable numCountThread = (() -> {
			lock.lock();

			try {
				model.addNum();
				
				System.out.println(model.getNum());
			} finally {
				lock.unlock();
			}
		});

		for (int i = 1; i <= 10; i++) {
			new Thread(numCountThread).start();
		}
	}

	static class Model {
		private int num = 0;

		public int getNum() {
			return num;
		}

		public void addNum() {
			num = num + 1;
		}
	}

}

代码解析
  • ReentrantLock 是Lock独占锁的一种实现
  • 多个线程执行Runnable时,先去获取锁,然后再执行具体的addNum操作
  • 通过锁,线程可以安全的执行

2、tryLock

boolean tryLock();尝试获取锁
  • 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回true,将锁的保持计数设置为 1。
  • 如果该锁被其他线程保持,则立即返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;尝试获取锁
  • 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回true,将锁的保持计数设置为 1。
  • 如果该锁被其他线程保持,在time 毫秒内等待获取锁,
    • 如果time时间内其他线程释放了锁,则当前线程获取该锁并返回true,将锁的保持计数设置为 1。
    • 如果time时间内其他线程没有释放锁,则返回false
tryLock 一般这样使用
 Lock lock = ...;
 if (lock.tryLock()) {
   try {
     // manipulate protected state
   } finally {
     lock.unlock();
   }
 } else {
   // perform alternative actions
 }}

注意点
  • 尝试获取锁,然后执行相关的业务,最后也一定要unlock
  • 获取锁失败,执行其他的业务
代码示例
package wang.conge.javasedemo.core.thread;

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

public class TryLockTest {

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();

		Model model = new Model();

		Runnable numCountThread = (() -> {
			if (lock.tryLock()) {
				try {
					model.addNum();
					System.out.println(model.getNum());
				} finally {
					lock.unlock();
				}
			} else {
				System.out.println("nolock:"+model.getNum());
			}
		});

		for (int i = 1; i <= 10; i++) {
			new Thread(numCountThread).start();
		}
	}

	static class Model {
		private int num = 0;

		public int getNum() {
			return num;
		}

		public void addNum() {
			num = num + 1;
		}
	}

}

代码解析
  • 线程执行addNum之前,尝试获取锁
  • 获取锁成功,再去执行addNum
  • 没有获取锁,只打印出当前num
  • 这样做的好处是,不会阻塞当前线程,可以先去执行其他业务

3、lockInterruptibly

void lockInterruptibly() throws InterruptedException;可中断的获取锁
  • 当前线程等待获取锁,如果被其他线程调用了该线程的interrupt,会停止等待获取锁,直接抛出InterruptedException异常

例如,两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

  • 当前线程获取锁之后,是不会再被interrupt方法打断的。也就是说线程不能打断正在运行过程中的线程,只能打断阻塞过程中的线程(例如线程调用了wait(),join(),sleep())
  • 而使用lock获取锁,或者synchronized,也是出于阻塞状态等待锁的过程,是无法被打断的,只会一直阻塞等待下去
几种获取锁大致区别
  • 内部锁(synchronized) 优先响应锁获取再响应中断
  • Lock.lock() 优先响应锁获取再响应中断
  • Lock.tryLock() 判断锁的状态不可用后马上返回不等待
  • tryLock(long time, TimeUnit unit) 优先响应中断再响应锁获取
  • Lock.lockInterruptibly() 优先响应中断再响应锁获取
lockInterruptibly 一般这样用

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

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

或者

public void method() {
	Lock lock = ...;
	try {
		lock.lockInterruptibly();
	} catch (InterruptedException e) {
		// todo InterruptedException
	}

	try {
		// .....
	} finally {
		lock.unlock();
	}
}
注意点
  • 一定要妥善处理InterruptedException
  • 如果获取锁之后,一定要unlock
代码示例
package wang.conge.javasedemo.core.thread;

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

public class LockInterruptiblyTest {
	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		
		Model model = new Model();
		
		Runnable run = (()->{
			try{
				lock.lockInterruptibly();
			}catch(InterruptedException e){
				System.out.println("Interrupt");
				return;
			}
			
			try {
				model.addNum();
				
				System.out.println(model.getNum());
			} finally {
				lock.unlock();
			}
			
		}) ;
		
		int threadNum = 100;
		Thread [] threads = new Thread[threadNum];
		for(int i=0;i<threadNum;i++){
			threads[i] = new Thread(run);
			threads[i].start();
		}
		
		for(int i=0;i<threadNum;i++){
			if(i%10==0){
				threads[i].interrupt();
			}
		}
	}
	
	static class Model {
		private int num = 0;

		public int getNum() {
			return num;
		}

		public void addNum() {
			num = num + 1;
		}
	}
}

代码解析
  • 每个线程都是采用lockInterruptibly方式去获取锁
  • 如果被打断,输出打断信息,线程结束
  • 如果已经获取锁,不被打断,直接继续运行结果
  • 所以该程序运行的结果,是不确定的,threads[i],i%10==0,也有可能已经获取锁,不被中断,已经运行结果

4、Condition

通过lock获取Condition方法定义
package java.util.concurrent.locks;

public interface Lock {
    ...
    Condition newCondition();
    ...
}
Condition接口定义的方法
package java.util.concurrent.locks;

import java.util.concurrent.TimeUnit;
import java.util.Date;

public interface Condition {
	/**
     * 等待
	 */
    void await() throws InterruptedException;
	
	/**
     * 不可中断的等待
	 */
    void awaitUninterruptibly();
	
	/**
     * 等待nanosTimeout时间
	 */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
	
	/**
     * 等待time + nanosTimeout时间
	 */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
	
	/**
     * 在deadline之前,一直等待
	 */
    boolean awaitUntil(Date deadline) throws InterruptedException;
	
	/**
     * 唤醒等待在此Condition上的随机一个线程
	 */
    void signal();
	
	/**
     * 唤醒等待在此Condition上的所有线程
	 */
    void signalAll();
}

Condition说明
  • 在Object方法上定义了wait(),notify(),notifyAll()等方法,是用来协调基于object对象锁的线程,而Condition上定义的方法,正是和这些方法有着类似的功能
  • Object的wait() 基于该对象锁的等待,让出锁,阻塞线程,等待其他线程唤醒
  • Condition的await(),基于Condition对象的等待,让出Condition对应的lock锁, 阻塞当前线程,等待该其他线程通过该Condition唤醒
  • Object的notify(),notifyAll(),唤醒等待在此object上的线程
  • Condition的signal(),signalAll(),唤醒等待在此Condition上的线程
  • 一个Object对象只有一个锁,也只能基于一个对象进行await,notify,nofifAll
  • 一个Lock可以有多个Condition,通过不同的Condition切换,可以更精细的协调线程运行
代码示例
package wang.conge.javasedemo.core.thread;

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

public class LockConditionTest {

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		Condition condition = lock.newCondition();
		
		lock.lock();//1.主线程获取锁
		
		System.out.println("main thread lock and run");//2.主线程业务执行
		
		new Thread (()->{
			lock.lock();//6.主线程阻塞让出lock锁,子线程获取锁
			
			System.out.println("child thread lock and run");//7.子线程业务执行
			
			condition.signalAll();//唤醒等待此condition下面的所有线程//8.子线程唤醒等待在condition下面的线程
			
			System.out.println("child thread unlock");
			lock.unlock();//9.子线程unlock,让出锁
		}).start();//4.子线程start
		
		try {
			condition.await();//5.主线程执行condition.await(),让出condition对应的lock锁, 主线程进入阻塞状态
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("main thread continue run");//10.因为被子线程唤醒,而且子线程让出锁,主线程重新获取锁,继续运行
		
		System.out.println("main thread unlock");
		lock.unlock();//11.主线程unlock,让出锁,结束整个流程
	}

}

代码解析
  • 代码里详细的说明每步执行的顺序
  • 通过condition可以很好的协调线程

三、Lock与synchronized对比

相同点

  • ReentrantLock提供了synchronized类似的功能和内存语义

不同点

  • Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
  • Lock可以提高多个线程进行读操作的效率
  • ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性
  • ReentrantLock 的性能比synchronized好
  • ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁
  • 一个Lock可以通过多个Condition协调不同条件下的线程

四、可重入锁

ReentrantLock 是Lock接口的一个实现,事实上上面所有的示例都是基于ReentrantLock去实现,而该ReentrantLock也是一个可重入锁,But,什么是可重入锁呢?

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

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

public class ReentrantTest {
	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		
		Model model = new Model();
		
		Runnable numCountThread = (() -> {
			lock.lock();
			try {
				model.addNum();
				System.out.println("num:"+ model.getNum());
				
				lock.lock();
				try{
					model.addFlg();
					System.out.println("flg:"+ model.getFlg());
				}finally{
					lock.unlock();
				}
				
			} finally {
				lock.unlock();
			}
		});
		
		for (int i = 1; i <= 10; i++) {
			new Thread(numCountThread).start();
		}
	}
	
	static class Model {
		private int num = 0;
		private int flg = 0;
		
		public int getNum() {
			return num;
		}

		public void addNum() {
			num = num + 1;
		}
		
		public int getFlg() {
			return flg;
		}
		
		public void addFlg() {
			flg = flg + 1;
		}
	}
}

代码解析
  • 当前线程再已经获取锁的情况下,如果执行的业务代码中还需要再次获取锁,这个时候锁直接分配给了当前线程
  • 可重入锁,基于线程分配锁
  • synchronized和ReentrantLock都是可重入锁
还有一些其他锁分类
  • 可中断锁,比如lock 的lockInterruptibly
  • 公平锁,非公平锁,见下文
  • 读写锁,见下文
  • 自旋锁,即是利用CAS原子操作,CPU不断轮询不断试错获取锁
  • 还有更多的锁。。。

五、公平锁,非公平锁

先来一张ReentrantLock图:

image

ReentrantLock实现Lock接口时,依赖了一个变量

private final Sync sync;

而该变量有两种内部实现

static final class NonfairSync extends Sync {
    ...
}

static final class FairSync extends Sync {
    ...
}
  • FairSync是公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁
  • NonfairSync是非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁
  • ReentrantLock 默认是非公平锁
  • public ReentrantLock(boolean fair);通过此方法创建公平锁
什么是CLH等待队列
  • Craig, Landin, and Hagersten lock queue
  • CLH队列是AQS中“等待锁”的线程队列。在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。
  • CLH是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。
CLH大致原理
  • CLH lock queue其实就是一个FIFO的队列,队列中的每个Node就是每个线程
  • 如果队列中没有线程等待,那么该线程设为占有锁的线程,返回成功
  • 如果队列中已经有线程占有锁,把获取锁的线程排队到队尾
  • 然后设置当前线程为阻塞状态,等待其他线程释放锁
  • 随着其他线程unlock,删除队列中的线程,这样整个流程就串通了
公平锁和非公平锁的区别
  • 在获取锁的机制上的区别,表现在,在尝试获取锁时
  • 公平锁,只有在当前线程是CLH等待队列的表头时,才获取锁
  • 非公平锁,只要当前锁处于空闲状态,则直接获取锁,而不管CLH等待队列中的顺序。只有当非公平锁尝试获取锁失败的时候,它才会像公平锁一样,进入CLH等待队列排序等待。
  • unlock流程都一样

六、读写锁

ReadWriteLock接口定义极其简单

package java.util.concurrent.locks;


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

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

  • 返回两种锁,很好理解,一个是读锁,一个是写锁
  • 读锁是共享锁,能同时被多个线程获取
  • 写入锁用于写入操作,它是独占锁,写入锁只能被一个线程锁获取。
  • 读锁与读锁不会冲突
  • 读锁与写锁会冲突
  • 写锁与写锁会冲突
  • ReentrantReadWriteLock 类实现了ReadWriteLock接口

ReentrantReadWriteLock的UML类图如下: image

代码示例
package wang.conge.javasedemo.core.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ThreadSafeArrayList<E> {
	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

	private final Lock readLock = readWriteLock.readLock();

	private final Lock writeLock = readWriteLock.writeLock();

	private final List<E> list = new ArrayList<>();

	public void set(E o) {
		writeLock.lock();
		try {
			list.add(o);
			System.out.println("Adding element by thread" + Thread.currentThread().getName());
		} finally {
			writeLock.unlock();
		}
	}

	public E get(int i) {
		readLock.lock();
		try {
			System.out.println("Printing elements by thread" + Thread.currentThread().getName());
			return list.get(i);
		} finally {
			readLock.unlock();
		}
	}

	public static void main(String[] args) {
		ThreadSafeArrayList<String> threadSafeArrayList = new ThreadSafeArrayList<>();
		threadSafeArrayList.set("1");
		threadSafeArrayList.set("2");
		threadSafeArrayList.set("3");

		System.out.println("Printing the First Element : " + threadSafeArrayList.get(1));
	}
}
代码解析
  • 在ThreadSafeArrayList的set 写操作时,使用writeLock ,确保写的过程中是独占锁
  • 在ThreadSafeArrayList的get 读操作时,使用readLock,多个读操作不会冲突,但写操作不能操作

引用文章

转载于:https://my.oschina.net/haoran100/blog/719249

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值