毕业设计购物网站怎么做,佛山网红打卡景点大全排名榜,中国搜索引擎网站排名,蒙自市建设局网站【0】README 0.0#xff09;本文部分文字描述转自#xff1a;“java并发编程实战”#xff0c; 旨在学习“java并发编程实践(2)线程安全性” 的相关知识#xff1b;0.1#xff09;几个术语#xff08;terms#xff09;t1#xff09;对象的状态#xff1a;是指存储在状…【0】README
0.0本文部分文字描述转自“java并发编程实战” 旨在学习“java并发编程实践(2)线程安全性” 的相关知识0.1几个术语termst1对象的状态是指存储在状态变量中的数据 t2共享意味着变量可以有多个线程同时访问 t3可变意味着变量的值在生命周期内可以发送变化 Attention我们将像讨论代码那样来讨论线程安全性但更侧重于如何防止在数据上发送不受控的并发访问0.2一个对象是否需要是线程安全的取决于它是否被多个线程访问 要使得对象是线程安全的需要采用同步机制来协同对对象可变状态的访问0.3当多个线程访问某个状态变量并且其中有一个线程执行写入操作时必须采用同步机制来协同这些线程对变量的访问java中的主要同步机制是关键字synchronized它提供了一种独占的加锁方式但“同步”术语还包括volatile类型的变量显式锁以及原子变量Conclusion同步术语有4种 synchronized关键字volatile类型的变量显式锁原子变量干货——同步术语有4种0.4如果当多个线程访问同一个可变的状态变量时没有使用合适的同步那么程序就会出现错误。有三种方式可以修改这个问题waysway1不在线程之间共享该状态变量 way2将状态变量修改为不可变的变量 way3在访问状态变量时使用同步 【1】什么是线程安全性1在线程安全性的定义中最核心的概率是正确性2正确性定义某个类的行为与其规范完全一致我们将现场的正确性近似定义为所见即所知3线程安全性定义当多个线程访问某个类时这个类始终都能表现出正确的行为那么就称这个类是线程安全的 4无状态对象该对象既不包含任何域也不包含任何对其他类中域的引用计算过程中的临时状态仅存储在线程栈上的局部变量中并且只能由正在执行的线程访问干货——无状态对象是线程安全的 【2】原子性看个荔枝计数器public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {private long count 0;public long getCount() {return count;}public void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);BigInteger[] factors factor(i);count; // highlight line.encodeIntoResponse(resp, factors);}
}对以上代码的分析Analysis A1它包含三个独立的操作 读取count的值将值加1然后将计算结果写入count干货——这是一个读取——修改——写入的操作序列并且其结果依赖于以前的状态 A2图1.1给出了两个线程在没有同步的case下同时对一个计数器执行递增操作时发生的情况这不是线程安全的 【2.1】竞态条件1intro当某个计算的正确性取决于多个线程的交替执行时序时那么就会发生竞态条件换句话说就是正确的结果要取决于运气。最常见的竞态条件类型是“先检查后执行check-then-act”操作即通过一个可能失效的观测结果来决定下一步的动作干货——竞态条件它是一个条件当...的时候当某个计算的正确性取决于多个线程的交替执行时序时就产生了竞态条件2先检查后执行的概念竞态条件的本质——基于一种可能失效的观察结果来做出判断或者执行某个计算。这种类型的竞态条件称为“先检查后执行”首先观察到某个条件为真如文件X不存在然后根据这个观察结果采用相应的动作创建文件X但事实上在你观察到这个结果以及开始创建文件之间观察结果可能变得无效了另一个线程在这期间创建了文件X从而导致各种问题数据被覆盖文件被破坏等干货——先检查后执行的概念【2.2】实例延迟初始化种的竞态条件1使用先检查后执行的一种常见case 就是 延迟初始化延迟初始化的目的是将对象的初始化操作推迟到设计被使用时才进行同时要确保只被初始化一次干货——引入延迟初始化看个荔枝延迟初始化中的竞态条件不要这么做pre namecode classjavapublic class LazyInitRace {private ExpensiveObject instance null;public ExpensiveObject getInstance() {if (instance null)instance new ExpensiveObject();return instance;}
}
class ExpensiveObject { } 对以上代码的分析AnalysisA1在LazyInitRace 中包含了一个竞态条件它可能会破坏这个类的正确性 A2假设线程A 和 线程B 同时执行getInstace方法A看到instance为空 因此创建一个新的ExpensiveObject 实例B同样需要判断instance是否为空。此时的instance是否为空要取决于不可预测的时序包括线程的调度方式以及A需要花多长时间来初始化ExpensiveObject 并设置instance如果B检查到 instance为空 那么在两次调用getInstance方法时可能会得到不同的结果即使getInstance通常被认为是返回相同的实例 Attention一种竞态条件 读取——修改——写入这种操作如count 递增一个计数器【2.3】复合操作1LazyInitRace 类包含一组需要以原子方式执行的操作。要避免竞态条件问题就必须在某个线程修改该变量时通过某个方式防止其他线程使用这个变量从而确保其他线程只能在修改操作完成之前或之后读取和修改状态而不是在修改状态的 过程中干货——如何避免竞态条件问题Attention原子操作定义假定有两个操作O1 和 O2如果从执行操作O1 的线程T1来看当另一个线程T2执行操作O2时要么将操作O2全部执行完要么完全不执行操作O2那么操作O1 和 操作O2 对彼此来说是原子的。原子操作是指对于访问同一个状态的所有操作包括该操作本身来说 这个操作是一个以原子方式执行的操作干货——原子操作定义2复合操作我们将“先检查后修改”以及“读取——修改——写入”等操作统称为复合操作包含了一组必须以原子方式执行的操作以确保线程安全性3使用一个现有的线程安全类来修改 UnsafeCountingFactorizer 得到 CountingFactorizer public class CountingFactorizer extends GenericServlet implements Servlet { // code2.2.3private final AtomicLong count new AtomicLong(0); //highlight line. safe thread class.public long getCount() { return count.get(); }public void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);BigInteger[] factors factor(i);count.incrementAndGet(); // highlight line.encodeIntoResponse(resp, factors);}
}对以上代码的分析Analysis A1在 java.util.concurrent.atomic包中包含了一些原子变量类用于实现在数值和对象引用上的原子状态转换 A2通过用AtomicLong 来代替long类型的计数器能够确保所有对计数器状态的访问操作都是原子性的 A3由于servlet的状态就是计数器的状态并且计数器是线程安全的因此这里的servlet也是线程安全的 Attention在实际case中应该尽可能使用现有的线程安全对象如AcomicLong来管理类的状态【3】加锁机制java中用于确保原子性的内置机制1requirement假设我们想提升servlet的性能将最近的计算结果缓存起来当两个连续的请求对相同的数值进行因式分解时可以直接使用上一次的计算结果而无须重新计算。要实现该缓存策略需要保存两个状态最近执行因式分解的数值以及分解结果2代码2.3 通过AtomicLong以线程安全的方式来管理计数器的状态那么在这里是否也可以使用类似的 AtomicReference来管理最近执行因式分解的数值及其分解结果吗public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {private final AtomicReferenceBigInteger lastNumber new AtomicReferenceBigInteger(); //被分解的数值private final AtomicReferenceBigInteger[] lastFactors new AtomicReferenceBigInteger[](); //分解后的因子public void service(ServletRequest req, ServletResponse resp) {BigInteger i extractFromRequest(req);if (i.equals(lastNumber.get()))encodeIntoResponse(resp, lastFactors.get());else {BigInteger[] factors factor(i);lastNumber.set(i); //highlight line.lastFactors.set(factors); //highlight line.encodeIntoResponse(resp, factors);}}
}对以上代码的分析非安全的AnalysisA1在线程安全性的定义中要求多个线程之间的操作无论采用何种执行时序或交替方式都要保证不变性条件不被破坏 A2UnsafeCachingFactorizer 的不变性条件之一是在 lastFactors 中缓存的因数之积应该等于在 lastNumber 中缓存的数值所以当更新某个变量时需要在同一个原子操作中对其他变量同时进行更新如第1次请求分解12 而第2次请求分解20第3次请求分解20当请求分解20的时候lastNumber变了这就会引起lastFactors 改变 A3在使用原子引用AtomicReference的case下尽管对set方法的每次调用都是原子的但仍然无法同时更新lastNumber 和 lastFactors。如果只修改了其中一个变量那么在这两次修改操作之间其他线程将发现不变性条件被破坏了 A4而且我们也不能保证会同时获取两个值在线程A获取这两个值的过程中线程B 可能修改了它们这样线程A 也会发现不变性条件被破坏了 Attention
要保持状态的一致性就需要在单个原子操作中更新所有相关的状态变量干货——要保持状态的一致性就需要在单个原子操作中更新所有相关的状态变量【3.1】内置锁1intro to 同步代码块java提供了一种内置的锁机制来支持原子性——同步代码块2同步代码块分为两部分一个是作为锁的对象引用一个是作为由这个锁保护的代码块3以关键字synchronized来修饰的方法就是一种横跨方法体的同步代码块其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁干货——同步代码块和锁的定义synchronized(lock) {
// 访问或修改由锁保护的共享状态
}4每个java对象都可以用作一个实现同步的锁这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁并且在退出同步代码块时自动释放锁而无论是通过正常的控制路径退出还是通过从代码块抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法干货——获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法且每次只有一个线程执行内置锁保护的代码块5并发环境中的原子性与事务应用程序中的原子性有着相同的含义一组语句作为一个不可分割的单元被执行任何一个执行同步代码块的线程都不可能看到有其他线程正在执行由同一个锁保护的同步代码块6下面是UnsafeCachingFactorizer 引入同步代码块synchronized关键字后的SynchronizedFactorizer 代码public class SynchronizedFactorizer extends GenericServlet implements Servlet {GuardedBy(this) private BigInteger lastNumber;GuardedBy(this) 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);}}对以上代码的分析AnalysisA1用关键字synchronized来修饰方法service()方法因此在同一时刻只有一个线程可以执行service方法这种方法过于极端了因为多个clients 无法同时使用因式分解服务的响应性能降低 A2所以在 synchronized关键字修改service方法之后这就变成一个性能问题而不是线程安全问题了干货——非线程安全转为线程安全但却带来了性能问题 【3.2】重入内置锁是可重入的1当某个线程请求一个由其他线程持有的锁时发出请求的线程就会阻塞。然而由于内置锁是可以重入的因此如果某个线程试图获取一个已经由它持有的锁那么这个请求就会成功2重入的概念“重入”意味着获取锁的操作的粒度是线程而不是调用重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程。。当计数值为0时这个锁就被认为是没有被任何线程所持有的。当线程请求一个未被持有的锁时JVM 将记下锁的持有者并且将获取计数值设置为1.如果同一个线程再次获取这个锁计数值将递增而当线程退出同步代码块时计数器会相应地递减。当计数值减为0时这个锁将被释放干货——重入的原理3重入进一步提升了加锁行为的封装性看个荔枝子类改写了父类的 synchronized方法然后调用父类的方法此时如果没有可重入的锁那么这段代码将产生死锁3.1产生死锁的原因因为每个doSth方法在执行前都会获得 Widget上的锁。然而如果内置锁不是可重入的那么在调用 super.doSth时将无法获得 Widget上的锁因为这个锁已经被持有从而线程将永远停顿下去等待一个永远也无法获取的锁。重入则避免了这种死锁case的发生public class Widget {public synchronized void doSth(){...}
}
public class LoggineWidget extends Widget {public synchronized void doSth() {super.doSth();}
}【4】用锁来保护状态1状态变量是由这个锁保护的对于可能被多个线程同时访问的可变状态变量在访问它时都需要持有同一个锁在这种case下称状态变量是由这个锁保护的2当某个变量由锁来保护时意味着在每次访问这个变量时都需要首先获得锁这样就确保在同一时刻只有一个线程可以访问这个变量。当类的不变性条件涉及多个状态变量时那么还有另外一个需求在不变性条件中的每个变量都必须由同一个锁来保护因此可以在单个原子操作中访问或更新这些变量从而确保不变性条件不被破坏Attention对于每个包含多个变量的不变形条件其中涉及的所有变量都需要由同一个锁来保护3如果同步可以避免竞态条件的问题为什么不在每个方法声明时都使用关键字synchronized 事实上如果不加区别地滥用 synchronized可能导致程序中出现过多的同步将每个方法都作为同步方法还可能导致活跃性问题或性能问题【5】活跃性与性能1参见“3.1”中的SynchronizedFactorizer 该类的service方法是一个synchronized方法因此每次只有一个线程可以执行。这就背离了Servlet框架的初衷即servlet需要能同时处理多个请求这在负载过高的case下 将给用户带来糟糕的体验2下图给出了当多个请求同时到达 因式分解时发生的case 这些请求将排队等待处理。我们将这种web应用程序称为“不良并发程序”因为可同时调用的数量不仅受到可用处理资源的限制还受到应用程序本身结构的限制3幸运的是通过缩小同步代码块的作用范围我们很容易做到既确保servlet的并发性同时又维护线程安全性应该尽量将不影响共享状态且执行时间过长的操作从同步代码块中分离出去从而在这些操作的执行过程中其他线程可以访问共享状态4看个荔枝将SynchronizedFactorizer修改为 CachedFactorizer该代码使用两个独立的同步代码块每个同步代码块都只包含一小段代码。其中一个同步代码块负责保护判断是否只需要返回缓存结果的“先检查后执行”操作序列另一个同步代码块则负责确保对 缓存的数值和因式分解结果进行同步更新干货——同步代码块包括synchronized代码块和synchronized修饰的方法public class CachedFactorizer extends GenericServlet implements Servlet {GuardedBy(this) private BigInteger lastNumber;GuardedBy(this) private BigInteger[] lastFactors;GuardedBy(this) private long hits;GuardedBy(this) 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) {BigInteger i extractFromRequest(req);BigInteger[] factors null;synchronized (this) {hits;if (i.equals(lastNumber)) {cacheHits;factors lastFactors.clone();}}if (factors null) {factors factor(i);synchronized (this) {lastNumber i;lastFactors factors.clone();}}encodeIntoResponse(resp, factors);}AttentionA1通常在简单性与性能之间存在着相互制约因素。当实现某个同步策略时一定不要盲目地为了性能而牺牲简单性这可能会破坏安全性 A2当执行时间较长的计算或可能无法快速完成的操作时例如网络IO或控制台 IO一定不要持有锁