线程安全之锁总结

1.0 Synchronized 和 ReentrantLock的区别

  • Synchronized是Java内建的同步机制,所以也有人称其为Intrinsic Lock,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
    在Java5之前,Synchronized是仅有的同步手段,在代码中Synchronized可以用来修饰方法,代码块等,本质上,Synchronized方法等同于吧方法全部语句用Synchronized块包起来。
  • ReentrantLock,通常翻译为再入锁,是Java5提供的锁实现,它的语义和Synchronized基本相同。再入锁通过代码直接调用lock()方法进行获取,代码书写更加灵活。与此同时ReentrantLock提供了很多实用的方法,能够实现很多Synchronized无法做到的细节控制,比如可以控制fairness,也就是公平性,或利用定义条件等。但是编码中也需要注意,必须要明确调用unlock方法释放,不然就会一直持有该锁。
  • Synchronized和ReentrantLock的性能不能一概而论,早期版本的Synchronized在很多场景下性能差异较大,在后续版本进行了较多改进,在底竞争的场景下性能表现可能优于ReentrantLock

2.0 需要掌握的知识点

  • 理解什么是线程安全
  • synchronized 和 ReentrantLock 的基本使用与案例
  • 掌握 Synchronized 和 ReentrantLock的底层实现;理解锁膨胀,降级;理解偏向锁、自旋锁、轻量级锁、重量级锁等概念
  • 掌握并发包中java.util.concurrent.lock 的各种不同实现和案例分析。

3.0 什么是线程安全

线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反应在程序中可以看做是数据。
换个角度,如果线程不是共享的,或者是不可修改的,也就不存在线程安全问题,进而可以推断出保证线程安全的两个方法:

  1. 封装:通过封装我们可以将对象内部状态隐藏、保护起来。
  2. 不可变:final 和 Immutable就是这个道理,但是JAVA语言目前还没有真正意义上的原生不可变,但未来可能会引用。
    线程安全需要保证几个基本特性:
  • 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现
  • 可见性:是一个线程修改了某个共享变量,其状态能够立即被其他线程知道,通常被解释为将线程本地状态反应到主内存上,volatile就是负责保证可见性。
  • 有序性:保证线程内,串行语义,避免指令重排序。

4.0 锁类型

在这里插入图片描述

Synchronized 分析及应用

ReentrantLock 分析及应用

其他锁(ReadWriteLock、StampedLock)

我们发现锁并不都是实现了Lock接口,ReadWriteLock是一个单独的接口,它通常是代表了一对锁,分别对应了只读和写操作,标准类库中提供了再入版本的读写锁实现(ReentrantReadWriteLock),对应语义和ReentrantLock比较相似。
StampedeLock也是一个单独的类型,从类图中可以看出它是不支持再入性的语义的,也就是说它不支持以持有锁的线程为单位。

为什么我们需要ReadWriteLock(读写锁)等其他锁?

这是因为虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限性,通俗点说就是太霸道,要么不占,要么独占。实际场景中,有的时候不需要大量竞争的写操作,而是以并发读取为主,如何进一步优化并发操作的粒度呢?
Java并发提供的读写锁等扩展了锁的能力,它所基于的原理是多个读操作是不需要互斥的,因为读操作并不需要更改数据,所以不存在互相干扰,而写操作则会导致并发一致性的问题,所以写线程之间、读写线程之间需要精心设计各种互斥逻辑。
下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多,并发写少的情况下,能够比纯同步版凸显出优势。

public class RWSample {
	private fnal Map<String, String> m = new TreeMap<>();
	private fnal ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	private fnal Lock r = rwl.readLock();
	private fnal Lock w = rwl.writeLock();
	public String get(String key) {
		r.lock();
		Sysem.out.println("读锁锁定!");
		try {
			return m.get(key);
		} fnally {
			r.unlock();
		}
	}
	public String put(String key, String entry) {
		w.lock();
		Sysem.out.println("写锁锁定!");
		try {
			return m.put(key, entry);
		} fnally {
			w.unlock();
		}
	}
	// …
}

在运行中,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
读写锁看起来比Synchronized的粒度更细一些,但是在实际应用中表现也不进人如意,主要因为相对比较大的开销。

StampedLock

所以JDK后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
参考下面样例代码:

public class StampedSample {
	private fnal StampedLock sl = new StampedLock();
	void mutate() {
		long samp = sl.writeLock();
		try {
			write();
		} fnally {
			sl.unlockWrite(samp);
		}
	}
	Data access() {
		long samp = sl.tryOptimisicRead();
		Data data = read();
		if (!sl.validate(samp)) {
			samp = sl.readLock();
			try {
				data = read();
			} fnally {
				sl.unlockRead(samp);
			}
		}
	return data;
	}
// …
}

注意:这里的WriteLock 和 UnLockWrite一定要保证成对调用
并发包内的各种同步工具,不仅仅是各种Lock,其他如Semaphore、CountDownLatch,甚至早期的FutureTask等,都是基于AQS框架的。
AQS框架详述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值