前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程-加锁机制

并发编程-加锁机制

作者头像
ImportSource
发布2018-04-03 16:35:43
1.1K0
发布2018-04-03 16:35:43
举报
文章被收录于专栏:ImportSource

本文翻译自《Java Concurrency ?In ?Practice》,定期放送 ,让你利用碎片时间悄悄的看了一本书!

我们的文章是系列的。所以先请允许我加上上篇文章的link:并发编程-原子性

好,现在假装你已经看了前面的内容,现在我们继续这期的内容:

我们可以给我们的servlet添加一个状态变量,通过使用一个线程安全的对象来管理servlet的整个状态。但是如果我们想要给我们的servlet添加更多的状态的时候,我们是否还是仅仅添加更多的线程安全的状态变量就够了吗?

假设我们现在想要改善我们的servlet的性能,通过把最近的计算结果缓存起来来提升性能,就是两个client发过来的请求都是要得到相同的数字的因数分解结果的情况下,我们就把上一次计算好的结果直接返回给客户端,而不用每次都重新计算。要想实现这样的策略,我们只要保存两个东西就够了:最近执行因数分解的数值以及分解的结果。(当然了,这里只是举个例子,这并不是什么有效的缓存策略)

我们之前使用AtomicLong来管理计算器的状态,通过这种线程安全的手段;那么我们是不是也可以使用类似的思维来解决上面的问题呢?比如AtomicReference,他们长得这么像。如果按照这思路就是下面这样实现的:

不幸的是,这种做法是有问题的。尽管这个原子引用(atomic? references)本身是线程安全的,但UnsafeCachingFactorizer存在竞态条件的问题,这可能会产生一个错误的结果。(有关竞态条件的内容,你可以看看这一文:并发编程-多线程带来的风险

线程安全的定义中要求无论多个线程之间的操作是怎样的执行顺序或交替方式,都要确保“不变”!就是怎么搞都不会改变。比较官方的说法:不变性不能被破坏!UnsafeCachingFactorizer要想确保不变性,一个前提就是在lastFactors中缓存的因数之积应该等于在?lastNumber中缓存的数值。只要我们能够保证这个不变性能一直hold住,那我们的servlet就是正确的。当多个变量都在一个不变性中的时候,他们都不是独立的:而是某个变量的值会约束其他变量的值。因此,当我们update一个变量时,必须顺带更新其他的变量,而且这些更新必须在同一个原子操作(atomic ?operation)中进行。

有种极端糟糕的情况,UnsafeCachingFactorizer会违背这种不变性。使用原子引用(atomic? references),我们不能同时更新lastNumber和lastFactors,尽管对set方法的每次调用都是原子的;依然会存在那么一段易受侵害的时间窗口,这个窗口期里一个已经被修改了,另外一个却还没被修改,在这个时候其他线程就会发现这种不变性没有在hold了。同样的,我们也不能保证可以同时获取到两个值:比如当线程A获取两个值的时候,线程B就有可能修改这两个值,这个时候,线程A 也会发现不变性被破坏了。

2.3.1.?Intrinsic? Locks? 内置锁 ?

Java语言提供了内置的锁机制来确保原子性(atomicity):就是synchronized块。一个synchronized块有两部分组成:一个将要作为锁对象的引用以及被这个锁包围起来的代码块,这两部分。带有synchronized关键字的方法就是一个横跨整体方法体的synchronized块(synchronized? block),这个方法的锁就是方法调用所在的对象。(静态的synchronized? 块方法是使用类对象作为锁的)。

每个Java对象都可作为一个锁,用作同步的锁:这些锁被称为内置锁( ?intrinsic ?locks)或者monitor lock。线程会在进入同步块之前会自动获得锁,并且在退出的时候自动的释放锁,不管正常的退出还是抛出异常退出都会自动释放锁。获得内置锁(intrinsic ?locks)的唯一方法就是去进入到一个同步块或者进入到被锁保护的方法中去。

Java的内置锁相当于一个互斥体(或叫互斥锁),这意味着最多只有一个线程能够持有这种锁。当线程A尝试获取一个由线程B持有的锁的时候, A 必须等待或者block,直到B释放掉锁。如果B永远不释放这个锁,那么A 将永远等待下去。

由于只有一个线程在同一时间内可以执行被指定锁保护起来的代码块,所以被这个锁保护起来的同步块,会以原子的方式执行,并且多个线程之间互不干扰,彼此都很尊重对方。在并发的环境中,原子性的意思和在应用程序的事务是一样的,都是数个语句组成一个group,然后这个group作为一个不可分割的单元来被执行。任何一个正在执行同步块的线程都不会看到还有其他线程也在执行被同一个锁保护起来的同步块。

这种同步机制让我们确保这个servlet的线程安全性变得容易了许多。在程序清单2.6中,我们把service方法变成了synchronized方法,这样的话,在同一时间,就只能有一个线程可以进入到service方法中。这样的话,SynchronizedFactorizer就是线程安全的了;然而,这种方法相当的极端,因为多个client无法同时使用servlet,服务的响应能力变得很低!

但这个问题是一个性能问题,而不是一个线程安全问题,我们将会在以后的内容中解决这一问题。

2.3.2.?Reentrancy? 重入

当一个线程去请求一个被其他线程hold住的锁,那么这个请求将会block,将会阻塞。但是由于内置锁是reentrant,就是可以重新进入的,所以如果一个线程想要获得一个它已经hold住的锁,那么这个是可以成功的。Reentrancy意味着这个锁的获取是面向线程的,而不是基于每次调用的。Reentrancy(重入)有一种实现方式就是,为每个锁关联一个计数器和一个所有者线程。就是每个锁有一个count以及一个owner属性。你可以这样理解。当这个count为0时,意味着这个锁没有被任何一个线程持有。当一个线程请求一个没有被hold的锁时,JVM会记录下owner并set这个count的值为1。如果同一个线程又来请求锁,那么JVM就把这个count加1,就是increment。当这个线程要离开synchronized块时,这个count就会递减。当count变为0时,这个锁就被释放掉了。

? Reentrancy(重入)机制进一步封装了锁的内部行为,这样让我们开发面向对象的并发代码的时候就更容易了。如果没有重入锁的这种机制,就像程序清单2.7这种代码,就是一个子类 override了一个synchronized方法,然后调用父类的方法,这种情况如果没有重入机制,那么就会死锁,deadlock!因为在Widget类的doSomething方法和LoggingWidget的doSomething都是synchronized的,因此每个doSomething在执行之前都会去尝试获得锁。但是如果内置锁是不可重入的,那么?super.doSomething的这个动作将永远不会获得锁,因为这个锁已经被持有,从而线程将会永远的等待下去,等待一个永远无法获得的锁。重入机制把我们从这种情况的死锁中救了出来!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2016-07-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档