[多线程] - Lock对象的使用详解

一、Lock对象简介

按照惯例,我们在介绍一个对象前首先要翻译一下他的语义:
在这里插入图片描述
什么,锁?是不是就是这种🔒?没错,作为JDK1.5版本后新增的特性,lock接口主要负责的任务就是为线程上锁来实现线程安全。说到这里我产生了一点疑问,lock的职责描述看起来很像是之前学到过的一个知识点synchronized。两者同为线程安全问题的解决方案,既然已经有了synchronized,为什么还要使用Lock对象呢?怀着这种疑问,我去查询了一下JDK官方文档希望可以得出答案:
在这里插入图片描述
果不其然,JDK特意为我们解释了为什么在已有synchronized的情况下还要使用Lock对象,原因我们可以总结如下:

  1. Lock拥有比synchronized更加广泛地API功能,允许更加灵活地编码结构。
  2. synchronized必须按照与其调用顺序相反的顺序来释放锁,而Lock对象支持更加灵活的锁释放
  3. synchronized不支持功能更丰富的锁粒度,如实现读锁与写锁分离等。

在这里我们看出,Lock对象可以看做是对synchronized的补强类,主要还是为了多线程编程下的业务处理逻辑简化和性能优化。为了证实这个观点,我们首先需要验证下synchronized的应用场景可不可以被Lock对象所替代?

二、Lock对象的应用

1.Lock对象的同步性验证

1)Lock接口的实现类

由于Lock是一个接口对象,我们想要创建Lock的时候需要调用他的实现类,JDK目前为止已知的Lock实现类有三个:

  1. ReentrantLock
  2. ReentrantReadWriteLock.ReadLock
  3. ReentrantReadWriteLock.WriteLock

2)Lock对象实现同步

这里我们先写一个Demo验证下:

public class LockTest_01 {

	// 创建一个lock对象
	private Lock lock = new ReentrantLock();

	public void sayHello() {
		lock.lock();
		System.out.println(Thread.currentThread().getName() + " say hello! time=" + System.currentTimeMillis());
		try {
			// 休眠一秒 观察其他线程是否实现了同步
			Thread.sleep(1_000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}

	}

	public static void main(String[] args) {
		// 创建测试对象
		LockTest_01 lockTest = new LockTest_01();
		Stream.of("测试线程一", "测试线程二", "测试线程三").forEach(s -> {
			new Thread(() -> {
				lockTest.sayHello();
			}, s).start();
		});

	}
}

测试结果:
在这里插入图片描述
这里可以观察到测试线程每隔1s依次执行,符合我们预期的同步效果,证明了Lock对象配合lock()和unlock()方法可以实现对指定逻辑代码实现加锁的效果。

3)多代码块之间的同步性

这里我们还要测试一下,如果多个方法应用了同一个lock对象,是否能够实现多代码块之间的同步,将上面的代码新增sayGoodbye方法:

public class LockTest_01 {

	// 创建一个lock对象
	private Lock lock = new ReentrantLock();

	public void sayHello() {
		lock.lock();
		System.out.println(Thread.currentThread().getName() + " say hello! time=" + System.currentTimeMillis());
		try {
			// 休眠一秒 观察其他线程是否实现了同步
			Thread.sleep(1_000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}

	}
	// 新增saygoodbye
	public void sayGoodbye() {
		lock.lock();
		System.out.println(Thread.currentThread().getName() + " say goodbye! time=" + System.currentTimeMillis());
		try {
			// 休眠一秒 观察其他线程是否实现了同步
			Thread.sleep(1_000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		// 创建测试对象
		LockTest_01 lockTest = new LockTest_01();
		Stream.of("测试线程一", "测试线程二", "测试线程三").forEach(s -> {
			new Thread(() -> {
				lockTest.sayHello();
				lockTest.sayGoodbye();
			}, s).start();
		});

	}
}

观察结果:
在这里插入图片描述
这里我们可以观察到多个代码块之间也可以通过使用同一个Lock对象实现同步。

4)unlock的使用

需要注意的是,lock方法上锁后我们需要配合unlock方法一起使用,但是在这里我们要避免因为程序异常造成的unlock方法无法被有效调用,所以建议配合try/catch一起使用,将unlock放进finally 代码块中。

2. Condition与多路通知

1)condition实现wait/notify通知模型

之前在多线程通信的时候,我们通过wait/notify的搭配使用实现了一个简单的生产/消费的多路通知模型,那么我们能不能通过Lock对象也实现多路通知的功能呢?这还用问,当然是可以的。JDK为了协助Lock对象,提供了一个Condition对象来实现实现多路通知功能,它主要是通过两个API:awaitsignal配合使用达到模拟wait/notify的效果。这里我们先简单的实现一个测试代码:

public class ConditionTest_01 {
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	public void waitMethod() {
		try {
			lock.lock();
			System.out
					.println(Thread.currentThread().getName() + "线程waitMethod方法执行    时间:" + System.currentTimeMillis());
			condition.await();
			System.out.println(Thread.currentThread().getName() + "线程经过暂停后继续恢复执行    时间:" + System.currentTimeMillis());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void signalMethod() {
		try {
			lock.lock();
			System.out
					.println(Thread.currentThread().getName() + "线程signalMethod方法执行  时间:" + System.currentTimeMillis());
			condition.signal();
		} finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ConditionTest_01 conditionTest_01 = new ConditionTest_01();
		new Thread(() -> {
			conditionTest_01.waitMethod();
		}, "测试线程一").start();
		// 通过休眠 模拟延时唤醒
		Thread.sleep(5_000);
		new Thread(() -> {
			conditionTest_01.signalMethod();
		}, "测试线程二").start();
	}

}

运行结果:
在这里插入图片描述
这里我们观察到condition.await方法运行的效果等同于Object.wait方法而condition.signal方法等同于Object.notify,需要注意的是在调用await方法时,同样要求当前线程需要持有锁,否则将会抛出java.lang.IllegalMonitorStateException异常!

2)选择性通知

我们也可以通过创建多个condition对象来实现线程的选择性唤醒:

public class ConditionTest_02 {
	private Lock lock = new ReentrantLock();
	private Condition conditionForApple = lock.newCondition();
	private Condition conditionForHuawei = lock.newCondition();

	public void waitMethod() {
		try {
			lock.lock();
			System.out.println(Thread.currentThread().getName() + "线程执行等待方法 时间:" + System.currentTimeMillis());
			if ("Apple".equals(Thread.currentThread().getName())) {
				conditionForApple.await();

			} else {
				conditionForHuawei.await();
			}
			System.out.println(Thread.currentThread().getName() + "线程被唤醒 时间:" + System.currentTimeMillis());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void notifyApple() {
		try {
			lock.lock();
			conditionForApple.signal();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ConditionTest_02 conditionTest_02 = new ConditionTest_02();
		new Thread(() -> {
			conditionTest_02.waitMethod();
		}, "Apple").start();
		new Thread(() -> {
			conditionTest_02.waitMethod();
		}, "Huawei").start();
		Thread.sleep(5_000);
		conditionTest_02.notifyApple();
	}
}

观察结果:
在这里插入图片描述
这里由于huawei线程一直等待,所以控制台右上角中断按钮一直是亮着的。

3)Condition对象的总结

这里我们可以简单地总结下Condition对象的使用

  1. 通过Condition对象的await/signal方法可以实现wait/notify的通知模型,这里await方法底层实际是通过jvm提供的本地方法**part()**实现的,在调用await/signal时和wait/notify一样,当前线程需要持有锁对象。
  2. 我们也可以通过创建不同的Condition对象实现选择性的线程通知
  3. 当我们想唤醒所有线程的时候可以通过signalAll方法实现

4) Condition对象的面试题

某企鹅面试题:

通过A,B,C三个线程交替输出1到100的数字。 

我在这里写了个简单的实现,如果有更好的方案请评论留言我,谢谢

public class AlternatePrinting {
	private Lock lock = new ReentrantLock();
	private volatile Integer count = 0;
	private final static Integer MAX = 100;
	private Condition condition = lock.newCondition();

	public void print() {
		try {
			lock.lock();
			// 确保线程全部创建成功
			Thread.sleep(2l);
			while (count < MAX) {
				count++;
				System.out.println(Thread.currentThread().getName() + "线程打印了数字: " + count);
				//这里利用了signal的有序唤醒
				condition.signal();
				condition.await();
			}
			condition.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) {
		AlternatePrinting alternatePrinting = new AlternatePrinting();
		Stream.of("A", "B", "C").forEach(s -> {
			try {
				new Thread(() -> {
					alternatePrinting.print();
				}, s).start();
				Thread.sleep(1l);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
	}

}

在上面的Demo中,有的同学看到后会疑问我是如何确保A,B,C按照顺序拿到锁的?这里就要引出今天最后一个知识点了。

三、Lock对象的锁特性

1.公平锁与非公平锁

首先我们要理解下什么是公平锁,什么是非公平锁:

  1. 公平锁是指线程获取锁采用先到先得的方式,每次获取锁之前都会检查等待队列中有没有排队等待的线程,如果有则会将当前线程增加到等待队列中,若没有则尝试获取锁。
  2. 非公平锁就是指线程在获取锁的时候允许插队,一个线程获取锁之前不会判断等待队列的状态而是直接去尝试获得锁。

这里我们可以通过new ReentrantLock(boolean); 构造函数在创建lock对象时指定是否持有公平锁,ReentrantLock对象默认是持有非公平锁的。
那么既然ReentrantLock默认是持有非公平锁,那么证明刚才我们写的排序Demo并不是通过ReentrantLock本身的公平锁实现的,而是通过signal具有的公平唤醒特性。这里我们可以打开signal的源码:

    public final void signal()
    {
      if (!isHeldExclusively()) {
        throw new IllegalMonitorStateException();
      }
      AbstractQueuedLongSynchronizer.Node localNode = firstWaiter;
      if (localNode != null) {
        doSignal(localNode);
      }
    }

发现signal每次都是唤醒等待队列中排序第一个的线程,也就是signal的唤醒是公平的。

2.读写锁(共享锁与排它锁)

ReentrantLock具有完全排他功能,同一个时间只能够由同一个线程获得锁来处理任务,这样虽然保证了同步实例变量后的线程安全性,但是效率非常的低,所以JDK提供了一个新的Lock接口的实现类来解决这个问题:ReentrantReadWriteLock,他可以对我们日常读取数据与修改数据实现不同加锁来控制代码是否需要同步运行。

  1. 与读取操作有关,不涉及数据修改的时候,我们可以使用读锁(共享锁)。
    Lock lock = new ReentrantReadWriteLock().readLock()
  2. 与数据修改有关,我们需要使用写锁(排他锁)。
    Lock lock = new ReentrantReadWriteLock().writeLock()

这里需要注意读写锁之间是否能够同步的规律为:
如果没有排它锁,则所有线程可以异步运行,如果存在排它锁,则所有线程需要与排它锁同步执行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓龙oba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值