synchronized
两个案例
请看如下代码:
1 | private void count(int newValue) { |
x 与 y 的值可能是不相等的。开启两个线程执行 count 方法,如果值不相等则打印出来:
1 | public void runTest() { |
由于线程抢夺时间片轮流工作,在 count 方法内,x = 1,y = 1;x = 2,y = 2;x = 3,y = 3…… 依次执行,当 x = 4 时,切换线程,而此时 y = 3。并不是一个方法执行完毕才切换线程,两行代码执行之间也可能切换线程。在 count 之前增加 synchronized 关键字,代码如下所示:
1 | private synchronized void count(int newValue) { |
count 合成了一个原子操作,该方法不会同时被两个线程调用,不会被打断。
1 | 原子操作即CPU级别的操作,不可能被别的线程拆开的操作。 |
再看如下代码:
1 | private int x = 0; |
两个线程都执行 x++ 操作1000000次,但是最后有很小的几率其中一个线程会打印出2000000。原因是 x++ 并不是原子性操作,x++ 是 x = x+1,会被拆成如下两步:
1 | int temp = x + 1; |
线程切换时有可能在第一行代码之后。假设第一次 x = 100,线程1执行,temp 为101,还未给 x 赋值,切换线程,线程2执行,x = 100,temp 同样为101,再执行 x 赋值,此时 x = 101。此时切换回线程1,线程1继续执行 x = temp 操作,temp 之前计算的出 101,于是 x 赋值为101。重复赋值。执行2000000次,有很多像这样的重复赋值,所以实际打印出的值不会是2000000。
增加 synchronized ,如下:
1 | private synchronized void count() { |
则其中一个线程会打印出2000000。
synchronized 的本质
保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个
Monitor 监视的代码,最多只能有⼀个线程在访问。
所有加了 synchronized 的方法都会被同一个 Monitor 监视。像下面代码:
1 | private synchronized void count(int newValue) { |
Monitor 保护的是 count 和 setName 两个方法,线程1访问 count,不希望别的线程干扰,而线程2访问 setName 同样是无法访问的。但这样设计是有用的,synchronized 方法,保护的事实上是资源(变量、数据等)。假如是如下两个方法:
1 | private synchronized void count(int newValue) { |
一个线程在访问 count 时,确实不希望其它线程再访问 minus 方法。关注的是资源而不是方法。
synchronized 方法,是一个快捷的方式,可以保护整个方法内部的所有代码,还有一个更加通用和自由的方式,如下:
1 | private void count(int newValue) { |
synchronized 代码块的方式需要传入一个对象,此处是 this,是一个 Monitor,这样做的好处是可以指定具体的 Monitor。事实上 synchronized 方法,Monitor 是帮我们指定的,这个 Monitor 就是当前的对象。如下代码:
1 | public class SynchronizedDemo { |
代码中 count 和 minus 方法也是互斥的,它们被同一个 Monitor 监视了,都是当前的对象即 SynchronizedDemo,或者说它们加了同一个锁。而 setName 方法和其余两个方法不是互斥的,因为加了不同的锁,使用了不同的 Monitor。
保证线程之间对监视资源的数据同步。即任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。
从共享内存中复制数据到⾃⼰的缓存,这是为性能考虑,在缓存中操作数据比较快。
由于静态方法可以直接拿类来调用而非对象,static 静态方法加 synchronized,可以使用类名,如下代码:
1 | static SingleMan newInstance() { |
在静态方法上面加 synchronized 也是可以的,此时 Monitor 帮我们指定的就是当前的类。
volatile
保证加了 volatile 关键字的字段的操作具有原⼦性和同步性,其中原⼦性相当于实现了针对单⼀字段的线程间互斥访问。因此 volatile 可以看做是简化版的 synchronized。
volatile 只对基本类型 (byte、 char、 short、 int、 long、 float、 double、 boolean) 的赋值操作和对象的引⽤赋值操作有效。
1 | 加了 volatile 关键字依然不能保证类似 a++ 为原子性操作,因为 a++ 只是一种简便写法,其本身是分两步实现的。 |
下面代码也是无法保证原子性操作的:
1 | private volatile User user; |
针对一个对象内部的一些属性操作,是不会受 volatile 影响的,volatile 只能保护对象本身的赋值操作,类似如下代码:
1 | private volatile User user; |
类似“user.name = “wy””的操作还需使用 synchronized。
java.util.concurrent.atomic 包
volatile 无法保证 a++ 为原子性操作,但是 atomic 可以。这个包下⾯有 AtomicInteger、AtomicBoolean 等类,作⽤和 volatile 基本⼀致,可以看做是通⽤版的 volatile。像 Java 多线程的使用一文中,ThreadFactory 中使用的 count 增加的方法。
Lock / ReentrantReadWriteLock
同样是“加锁”机制。但使⽤⽅式更灵活,同时也更麻烦⼀些。代码如下:
1 | private Lock lock = new ReentrantLock(); |
可以保证锁控制的中间代码不被其它调用。这样的写法和 synchronized 没有区别。如果锁控制的代码之间有异常抛出,锁将无法被释放,所以需要写成如下类似代码:
1 | Lock lock = new ReentrantLock(); |
1 | finally 保证在⽅法提前结束或出现 Exception 的时候,依然能正常释放锁。 |
⼀般并不会只是使⽤ Lock ,⽽是会使⽤更复杂的锁,例如 ReadWriteLock:
1 | ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); |
增加读写锁可以做到一个线程在写的时候其它线程既不能读也不能写,在读的时候其它线程不能写但是可以一起读。读写锁比 synchronized 代码块要更加精细。
本质
线程安全问题的本质
在多个线程访问共同的资源时,在某⼀个线程对资源进⾏写操作的中途(写⼊已经开始,但还没结束),其它线程对这个写了⼀半的资源进⾏了读操作,或者基于这个写了⼀半的资源进⾏了写操作,导致出现数据错误。
锁机制的本质
通过对共享资源进⾏访问限制,让同⼀时间只有⼀个线程可以访问资源,保证了数据的准确性。
1 | 不论是线程安全问题,还是针对线程安全问题所衍⽣出的锁机制,它们的核⼼都在于共享的资源,⽽不是某个⽅法或者某⼏⾏代码。 |
参考资料:
腾讯课堂 HenCoder