0. 写在前面
Java 多线程编程中 Lock 的概念和synchronized 、volatile 关键字是经常遇到的内容或者说知识点,本文是我最近学习多线程的个人总结,会说明这三者涉及的一些基础概念,所以它们不是很全面,跳动性会有点大,参考资料会在文章结尾给出。
前提条件
- 最基本的,我觉得你需要了解线程的概念,这是最低要求了🤣。
1. 线程安全
线程安全是我们最常听到的名词,我直接拿出《Concurrency in Java》中对其的定义:
A class is thread‐safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code. 当多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
你会发现,线程安全被限制在类级别(class level),我们也常听到某某某类不是线程安全的。举个例子,假使现在我们有一个ArrayList 对象,当多个线程同时访问/操作该对象时,很有可能出问题,比如都调用其add 方法,list 对象中的元素很有可能出现不一致的情况,当然还有许许多多其它的场景展示非线程安全。如果某类是线程安全的,那么你就可以放心地去使用它,而无需花费额外的精力保证线程安全。
补充:文章中多次提到的访问(access),比如访问某对象,这其中包含调用该对象的方法,在此,你可以理解为读写行为都会有。
当我们使用非线程安全的类时,为了保证正确的结果,就需要增加一些额外的操作,比如加锁,上面提到的协同(coordination),我现在还未涉及到,暂不做讨论。看起来,加锁似乎是万能的,因为当许多事情并行/异步运行时,加锁使得我们访问某个对象可以是串行/同步的。但是,从哲学方面考虑,事物总是有两面性,当在某些方面突出时,它肯定牺牲了其它的资源。加锁虽然可以解决我们遇到的绝大部分非线程安全的问题,但也带来了更大的开销,效率更低,所以我们需要一些更轻量的方式来满足不同的场景。
来到正题,synchronized 使lock()/unlock() 更为方便,volatile 仅仅保证可见性,并不保证原子性。我们在后面做更详细的介绍,换句话说,此时你可以把这篇博客关掉了,因为文章中心句就是前面那一句,你已经得到了你想要的🤪。
2. 锁 Lock
此时,你可以找到Lock 接口的源码,它在java.util.concurrent.locks 包下,使用它非常简单:
Lock l = ...;
l.lock();
try {
} finally {
l.unlock();
}
比较常用的锁有ReentrantLock 和ReentrantReadWriteLock ,对于前者,使用方式和👆🏻相同,当某个线程中的ReentrantLock (可重入锁)对象请求锁成功后,它可以再次调用其lock() 方法请求锁,成功后,锁的被拥有数(hold count)加1,同时,你还可以配合Condition 一起使用,在此不做深入。对于后者,它基本支持前者的语义,场景更定制化,效率有时会更高,它包含两个内部类:ReadLock 和WriteLock ,使用方式依然和👆🏻相同,其中写锁和其它锁互斥,读锁和读锁之间共享,即不会出现阻塞的情况。
3. synchronized
每个对象内部都有一个隐式的可重入锁,使用synchronized ,我们不必再去创建锁(因为已经有一个可以用了),也不必去lock() 和unlock() ,你可以使用synchronized 包裹一个方法或是一段代码块:
void method() {
synchronized (a) {
}
}
class A {
synchronized void method() {
...
}
}
void method() {
synchronized (a) {
}
}
你注意到synchronized 后会跟一个参数,它可以是某个对象,此时会请求该对象的锁;也可以是类名,像是synchronized (A.class) ,此时请求的是该类的锁。需要注意的是,如果参数改变,这可能会是对象改变,这样同步就会失效,因为对象不是同一个,请求的对象的锁自然也不是同一个,看一下下面例子:
String str = "watman";
...
synchronized (str) {
}
str = "badman";
synchronized (str) {
}
4. volatile
因为线程会对涉及的变量做本地拷贝或者本地副本(local copy),这样数据可能会与其他线程中保存的数据不同。多个线程访问相同对象时,可能出现数据不一致的情况,volatile 是域修饰符(field modifer),比如
class Menagerie {
public volatile Integer count;
}
volatile 会使其修饰的属性在必要的时候与主内存中的值一致,我们称其为可见性,有些时候,满足可见性就能使我们的类线程安全,当然,有些时候不能。volatile 不保证原子性,它的意思是执行volatile 修饰的属性的某个操作时,属性的值会在执行的过程中被改变。所以,对于需要原子性的场景,你还需要同步。你可能会问,同步也需要保证可见性呀!事实是,同步同时满足可见性和原子性,同步块内的代码,或者锁包裹内的代码也会在必要的时候保证其中的值一致。
5. 小结
本文对 Java 多线程编程中常用的Lock 、synchronized 和volatile 做了一个大致的介绍,包含一些使用方式和注意事项。
参考资料
- 《Java多线程编程核心技术》:非常不推荐,自己非常后悔买的一本书,因为它真的很水,你可以在豆瓣上看一下它的评价,以及仔细的看一下它的目录;
- 《Java核心技术卷一》并发部分
- 《深入理解JVM虚拟机》并发部分
- 一些博客
|