苏州网站搭建公司世界网站制作

当前位置: 首页 > news >正文

苏州网站搭建公司,世界网站制作,输入关键词搜索,洛阳网站建设汉狮怎么样目录 1、什么是线程安全性#xff1f; 2、操作的原子性#xff1a;避免竞态条件 3、锁机制#xff1a;内置锁和可重入 4、如何用锁来保护状态#xff1f; 5、同步机制中的活跃性与性能问题 编写线程安全的代码#xff0c;其核心在于对状态访问操作进行管理#xff0…目录 1、什么是线程安全性 2、操作的原子性避免竞态条件 3、锁机制内置锁和可重入 4、如何用锁来保护状态 5、同步机制中的活跃性与性能问题 编写线程安全的代码其核心在于对状态访问操作进行管理特别是对共享的(Shared)和可变的(Mutable)状态的访问。//核心对共享并且可变状态进行管理 对象的状态是指存储在状态变量中的数据。状态变量可以是类的实例或成员变量。 一个对象是否需要是线程安全的取决于它是否被多个线程访问。要使得对象是线程安全的需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同那么可能会导致数据破坏以及其他不该出现的结果。//通过同步实现对象的线程安全 Java 中的主要同步机制是关键字 synchronized它提供了一种独占的加锁方式此外还包括 volatile 类型的变量显式锁 (Explicit Lock)以及原子变量等。//解决同步中的可见性和原子性顺序 1、什么是线程安全性 当多个线程访问某个类时这个类始终都能表现出正确的行为那么就称这个类是线程安全的。//所见即所知 public class StatelessFactorizer extends GenericServlet implements Servlet {public void service(ServletRequest req, ServletResponse resp) {//1-从req中获取值BigInteger i extractFromRequest(req);BigInteger[] factors factor(i);//2-编码并响应encodeIntoResponse(resp, factors);} } 上述 StatelessFactorizer 是无状态的它既不包含任何成员变量也不包含任何对其他类中成员变量的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中并且只能由正在执行的线程访向。由于线程访问无状态对象的行为并不会影响该对象在其他线程中操作的正确性因此无状态对象是线程安全的。//不进行共享就不存在线程安全问题 2、操作的原子性避免竞态条件 假设我们想增加一个“命中计数器”(Hit Counter) 来统计所处理的请求数量。最简单的方法是在 Servlet 中增加一个 long 类型的成员变量并且每处理一个请求就将这个值加 1代码如下 //存在线程安全问题 public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {//计数器private long count 0;public long getCount() {return count;}public void service(ServletRequest req, ServletResponse resp) {//1-从req中获取值BigInteger i extractFromRequest(req);BigInteger[] factors factor(i);count;//2-编码并响应encodeIntoResponse(resp, factors);} } 我们都知道虽然递增操作 count 看上去只是一个操作但这个操作并非原子的。实际上它包含了三个独立的操作读取 count 的值将值加 1然后将计算结果写入 count。这是一个 “读取 - 修改 - 写” 的操作序列并且其结果状态依赖于之前的状态。//指令是非原子性的如果步骤乱了结果也会乱 在上述 UnsafeCountingFactorizer 中存在多个竞态条件从而使结果变得不可靠。最常见的竞态条件就是 “先检查后执行 (Check-Then-Act)” 操作即通过一个可能失效的观测结果来决定下一步的动作。//如果前提条件是错误的那么论证的结果一般也是错误的 比如首先观察到某个条件为真(例如文件 X 不在)然后根据这个观察结果执行用应的动作(创建文件X)但事实上在你观察到这个结果以及开始创建文件之间观察结果可能变得无效(另一个线程在这期间创建了文件 X)从而导致各种问题(未预期的异常数据被覆盖、文件被破等)。//单例问题 为了确保线程安全性避免竞态条件“先检查后执行”和“读取 - 修改 - 写入”等操作必须是原子的。 public class CountingFactorizer extends GenericServlet implements Servlet {//使用原子类private final AtomicLong count new AtomicLong(0);public long getCount() { return count.get(); }public void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);BigInteger[] factors factor(i);count.incrementAndGet();encodeIntoResponse(resp, factors);} } 在实际情况中应尽可能地使用现有的线程安全对象(例如 AcomicLong)来管理类的状态。与非线程安全的对象相比判断线程安全对象的状态及其状态转换情况要更加容易从而也更容易维护和验证线程安全性。//仅对单个变量的安全性有效 3、锁机制内置锁和可重入 面对多个变量时原子类并不能保证同步机制有效 //存在线程安全问题 public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {//原子类变量1private final AtomicReferenceBigInteger lastNumber new AtomicReferenceBigInteger();//原子类变量2private final AtomicReferenceBigInteger[] lastFactors new AtomicReferenceBigInteger;public void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);//两个变量不能保证同时获取或者同时设置if (i.equals(lastNumber.get())) //获取变量1的值encodeIntoResponse(resp, lastFactors.get()); //获取变量2的值else {BigInteger[] factors factor(i);lastNumber.set(i); //设置变量1的值lastFactors.set(factors); //设置变量2的值encodeIntoResponse(resp, factors);}} } 此时就需要引入锁机制来确保线程的同步。 Java 提供了一种内置的锁机制来支持原子性同步代码块(Synchronized Block)。每个Java 对象都可以用做一个实现同步的锁这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。//就是所谓的管程Synchronized 太常见就不过多介绍了 Synchronized 的问题使用同步代码块很容易对代码进行过于极端的保护虽然解决了安全问题但带来了性能问题。//锁的粗粒度和细粒度问题 内置锁是可重入的因此如果某个线程试图获得一个已经由它自己持有的锁那么这个请求就会成功。“重人”意味着获取锁的操作的粒度是“线”而不是“调用”。//不可重入会造成自己阻塞自己的问题 重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程。当计数值为 0 时这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时JVM 将记下锁的持有者并且将获取计数值置为 1。如果同一个线程再次获取这个锁计数值将递增而当线程退出同步代码块时计数器会相应地递减。当计数值为 0 时这个锁将被释放。//重入锁的实现原理 4、如何用锁来保护状态 锁能使其保护的代码以串行形式来访问因此可以通过锁来实现对共享状态的独占访问。 下边是一些正确使用锁的建议 1如果用同步来协调对某个变量的访问那么在访问和操作这个变量的所有位置上都需要使用同步。而且在访问和操作变量的所有位置上都要使用同一个锁。//对共享变量的读写都要上锁 之所以每个对象都有一个内置锁只是为了免去显式地创建锁对象。你可以自行构造加锁协议或者同步策略来实现对共享状态的安全访问并且在程序中自始至终地使用它们。 2每个共享的和可变的变量都应该只由一个锁来保护从而使维护人员知道是哪一个锁。 3对于每个包含多个变量的不变性条件其中涉及的所有变量都需要由同一个锁来保护。 5、同步机制中的活跃性与性能问题 试想如果同步可以避免竞态条件问题那么为什么不在每个方法声明时都使用关键字 synchronized 呢? 事实上如果不加区别地用 synchronized可能导致程序中出现过多的同步。此外如果只是将每个方法都作为同步方法例如 Vector那么并不足以确保 Vector 上复合操作都是原子的 //非原子操作 if (!vector.contains(element))vector.add(element); 此外将每个方法都作为同步方法还可能导致活跃性问题(Liveness)或性能问题(Performance)。 如下代码如果使用 SynchronizedFactorizer 中的同步方式那么代码的执行性能将非常糟糕。//不能直接把方法一锁了之虽然实现了线程安全但付出了太大性能代价 //线程安全 public class SynchronizedFactorizer extends GenericServlet implements Servlet {//成员变量private BigInteger lastNumber;private BigInteger[] lastFactors;//直接锁方法存在性能问题public synchronized void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);if (i.equals(lastNumber))encodeIntoResponse(resp, lastFactors);else {BigInteger[] factors factor(i);lastNumber i;lastFactors factors;encodeIntoResponse(resp, factors);}} } 锁优化思路缩小同步代码块的作用范围做到既确保 Servlet 的并发性同时又维护线程安全性。要确保同步代码块不要过小并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去从而在这些操作的执行过程中其他线程可以访问共享状态。//将粗粒度的锁尽量缩小将执行时间长的代码进行剥离 重新构造后的 CachedFactorizer 实现了在简单性与并发性 之间的平衡。代码如下 //线程安全 public class CachedFactorizer extends GenericServlet implements Servlet {//共享变量private BigInteger lastNumber;private BigInteger[] lastFactors;//命中计数器private long hits;//cache命中计数器private long cacheHits;public synchronized long getHits() {return hits;}public synchronized double getCacheHitRatio() {return (double) cacheHits / (double) hits;}public void service(ServletRequest req, ServletResponse resp) {//1-从req获取值BigInteger i extractFromRequest(req);BigInteger[] factors null; synchronized (this) { //同步代码块1对变量进行操作hits;if (i.equals(lastNumber)) {cacheHits;factors lastFactors.clone();}}if (factors null) {factors factor(i); //局部变量不需要进行同步synchronized (this) { //同步代码块2对变量进行操作lastNumber i;lastFactors factors.clone();}}//2-响应把执行时间长的代码进行剥离encodeIntoResponse(resp, factors);}void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {}BigInteger extractFromRequest(ServletRequest req) {return new BigInteger(7);}BigInteger[] factor(BigInteger i) {// Doesnt really factorreturn new BigInteger[]{i};} } 在 CachedFactorizer 中不再使用 AtomicLong 类型的命中计数器而是使用了一个 long 类型的变量。当然也可以使用 AtomicLong 类型对在单个变量上实现原子操作来说原子变量非常有用。但此处由于我们已经使用了同步代码块来构造原子操作而使用两种不同的同步机制不仅会带来混乱也不会在性能或安全性上带来任何好处因此在这里不使用原子变量。//同一个类中应该只使用一种同步机制让代码简单易懂。 要判断同步代码块的合理大小需要在各种设计需求之间进行权衡包括安全性(必须满足)、简单性和性能。有时候在简单性与性能之间会发生冲突但在二者之间通常能找到某种合理的平衡。通常在简单性与性能之间存在着相互制约因素。当实现某个同步策略时一定不要盲目地为了性能而牺牲简单性 (这可能会破坏安全性)。//努力做到安全性和性能的平衡 无论是执行计算密集的操作还是在执行某个可能阻塞的操作如果持有锁的时间过长那么都会带来活跃性或性能问题。所以当执行时间较长的计算或者可能无法快速完成的操作时(例如网络I/O 或控制台I/O)一定不要持有锁。//执行时间较长的代码不要持有锁 至此全文到此结束。