首页 > 代码库 > 基础知识
基础知识
线程或者锁在并发变成在并发编程中的作用,类似于铆钉和工字梁在土木工程中的作用。
java中主要的同步机制是关键字synchroinzed,它提供了一种独占的加锁方式,但“同步”这个术语还包括voliatile类型的变量,显式锁(Explicit Lock)以及原子变量。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的
一个无状态的Servlet:
@ThreadSafe public class StatelessFactorizer implements Servlet { public void service(ServletRequest req, ServletResponse resp){ BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp, factors); } }
与大多数Servlet相同,StatelessFactorizer是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈中的局部变量中,并且只能由正在执行的线程访问。访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程计算结果,因为这两个线程并没有共享状态,好像它们都在访问不同的实例。
无状态对象一定是线程安全的。
原子性
往无状态对象中增加一个状态
@NotThreadSafe public class UnsafeCountingFactorizer 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; encodeIntoResponse(resp, factors); } }
UnsafeCountingFactorizer并非线程安全的,尽管它在单线程环境中能正确运行。虽然递增操作++是一种紧凑的语法,使其看起来是一个操作,但是这个操作是并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作:读取count的值,将值 + 1,然后将计算结果写入count。
你可能认为,基于web的服务中,命中计数器的少量偏差或许是可以接受的,一般情况下是可以的,但是被用作序列等标志的时候,可能就有影响了。并发变成中,这种由于不恰当的执行时序出现不正确的情况是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)
竞态条件Race Condition
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。
使用”先检查后执行“的一种常见情况就是延迟初始化。例如:
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance(){ if(instance == null){ instance = new ExpensiveObject(); } return instance; } }
另一种竞态条件。“读取-修改-写入”,前面的有状态例子中就是这样。
复合操作
LazyInitRace和UnsafeCountingFactorizer都包含一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量。
原子方式的操作:将设两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指对于访问同一个状态的所有操作(包括操作本身)来说,这个操作是一个以原子方式执行的操作。
我们使用不加锁的方式修复前面的CountingFactorizer问题:
@ThreadSafe public class CountingFactorizer 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)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态以及状态转换情况更为容易,从而也更容易维护和验证线程安全性。
加锁机制
当在Servlet中添加一个状态变量时,可以通过多线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。但是如果想在Servlet中添加更多的状态,那么是否只需要添加更多的线程安全状态变量就足够了?
例如:希望将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无需重新计算。
@NotThreadSafe public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServlerRequest req, ServletResponse resp){ BigInteger i = extractFormRequest(req); if(i.equals(lastNumber.get())){ encodeIntoResponse(resp, lastFactors.get()); }else{ BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } }
这样并不安全,虽然这些原子引用本身都是现成安全的,但在UnsafeCachingFactorizer中存在着竞态条件,这可能产生错误的结果。
内置锁
java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
同步代码块包括两个部分:一个作为锁的对象引用,一个座位由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
synchronized (lock) {
//访问或修改由锁保护的共享状态
}
每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Instrinsic Lock)或监视器(Monitor Lock).线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。无论何种方式退出,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
java的内置所相当于一种互斥体(或互斥锁),这意味着最多只能由一个县城能持有这种锁。
由于每次只能由一个县城执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行。
这种同步机制使得确保因数分解Servlet的线程安全性变得更简单。然而却过于极端,因为多个客户端无法同时使用因数分解Servlet,服务的响应性非常低,无法令人接受。
@ThreadSafe public class SynchronizedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp){ BigInteger i = extractFormRequest(req); if(i.equals(lastNumber)){ encodeIntoResponse(resp, lastFactors); }else{ BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } }
重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是"线程",而不是“调用”。
重入的一种实现方法为:为每个锁关联一个获取计数器和一个所有者线程。当计数值为0时,这个所被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取的计数值置为1.如果同一个线程再次获取这个所,计数值将递增,而当线程退出同步代码块时,计数器会对应的递减。当计数器的值为0时,这个锁将被释放。
如果内置锁不是可重入的,那么这段代码将发生死锁:
public class Widget{ public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }
用锁来保护状态
由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独立访问。
对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,单对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式样的创建锁对象。你需要自行后遭加锁协议或者同步协议来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。
当类的不变性条件涉及多个状态变量时,那么还有另外一个需求:在不变性条件中的每一个变量都必须由同一个锁来保护。
活跃性与性能
在UnsafeCachingFactorizer中,我们通过在因数分解Servlet中引入缓存机制来提升性能。在缓存中需要使用共享状态,因此需要通过同步来维护状态的完整性。然而,如果使用SynchronizedFactorizer中的同步方式,那么代码的执行性能将非常糟糕。
我们将这种Web应用程序称之为不良并发(Poor Concurrency)应用程序:同时可调用的数量,不仅搜到可用处理资源的限制,还搜到应用本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,我们很容易做到既确保Servlet的并发性,同时又维护线程安全性。要确保同步代码块不要过小,并且不要讲本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态切执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。
修改后代码:
@ThreadSafe public class CachedFactorizer 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 = extractFormRequest(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); } }
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁
基础知识