您的位置:首页 > 编程语言 > Java开发

java并发编程基础之线程安全

2016-03-14 17:04 405 查看
# java并发编程基础之线程安全


标签(空格分隔): 多线程

2原子性
21竞态条件

22延迟初始化的竞态条件

23复合条件

3加锁机制
31内置锁

32重入

4用锁来保护状态

5活跃性和性能

杂项

线程上线文的开销是不菲的。

对象的状态:存储在状态变量的数据

状态变量:实力变量,静态域,依赖其他对象的域

无状态对象一定是线程安全的

活跃性:发生某个操作无法继续执行的情况。例如死锁,饥饿,活锁。

2.2原子性

@NotThreadSafe
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;
encodeIntoResponse(resp, factors);
}
}


++count看似是一个独立的操作其实这是一个“读取-修改-写入”的操作。并不是一个原子操作。

并且由于多线程执行的时候“读取-修改-写入”的操作有肯呢个会交替执行最后就会导致错误的结果。交替执行情况如下:

A value=0 ———-> 0+1=1 ———–> value=1

B value=0 ———–> 0+1=1———–>value=1

在并发编程中由于这种不恰当的执行顺序而出现不正确的结果。这种情况叫做静态条件

2.2.1竞态条件:

竞态条件:多个线程对同一个资源进行操作的时候,由于执行顺序的不同,可能导致错误的结果。

常见的竞态条件就是“先检查后执行”(check then act) 即:通过一个可能失效的观测结果来决定下一步的动作

2.2.2延迟初始化的竞态条件

@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;

public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}


这段代码中就有一个“先检查后执行”的静态条件。当多个线程同时访问这段代码。

当线程A进来判断 Instance 不等于 null 于是执行 new ExpensiveObject();此时如果B进来也需要判断Instance是否为Null取决于线程的调度情况和A初始化Instance所需要的时间。instance有可能会初始化两次,就有可能产生以下错误情况。

2.2.3复合条件

为了避免竞态条件,我们需要一种以原子方式执行的操作。即:当一个线程操作某个变量的过程中不允许其他线程对其操作。

比如之前提到的“先检查后执行”和“读取-修改-写入” 统称为符合操作:包含了一组必须以原子方式执行的操作。

下一节我们可以用加锁的方式去解决,现在先用java提供的线程安全的类,这个类提供原子操作去解决问题

@ThreadSafe
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);
}
}


这个线程安全的类提供了incrementAndGet()这个原子操作。来保证线程安全。此时这个servlet得状态变量已经是线程安全的了。所以这个servlet也编程了线程安全

2.3加锁机制

当servlet只有一个状态变量的时候。我们用线程安全类AtomicLong这种方式来管理状态变量可以让servlet线程安全。但是如果servlet有多个状态变量的时候仅仅添加线程安全类是不够的。

eg:我们希望提升servlet的性能,将最近计算的结果缓存起来。当两个连续的请求对数值进行因数分解的时候,可以直接使用上一次的计算接果尔不需要重复计算。所以我们需要两个状态变量分别保存,最近执行的被因数分解的值,和分解结果。程序如下

@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
private final AtomicReference<BigInteger> lastNumber//最近执行的因数分解的值
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> l
4000
astFactors//因数分解的结果
= new AtomicReference<BigInteger[]>();

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);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}


尽管对于每一个状态变量的单独操作是原子性的。但是两个状态变量本身应该保持一种“不变性条件”即:lastFactor中的因数的积应该等于lastNumber。只有保证这个“不变性条件”不被破坏,这个servlet才是线程安全的。

因此当更新某一个变量的时候,需要在一个原子操作下同时更新其他相关变量。

在这个类的执行过程中就有可能会破坏这个“不变性条件”,尽管每次操作都是原子的。但是在更新一个变量的时候不能同时更新另一个变量。多线程访问的时候。就可能发生“不变性条件”被破坏了

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

2.3.1内置锁

java提供一种锁机制来实现原子操作 同步代码块。synchronized 修饰的普通方法的锁就是调用方法所在的对象

synchronized 修饰的静态方法的锁就是这个类的Class对象

public synchronized void show() {
//
}
public static synchronized void show() {
//
}
public void show() {
//
synchronized (this) {
//
}
}


利用这种锁机制可以解决上面因数分解程序的线程安全问题。解决代码如下:

@ThreadSafe
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);
}
}
}


现在每次一有一个线程可以执行servlet保证了线程安全,但是性能令人无法接受,我们将在之后解决这个问题。

2.3.2重入

当一个线程访问另一个线程持有的锁的时候,这个访问的线程会被阻塞。然而内置锁是可以重入的,一个线程可以成功获得他已经持有的锁。实现方式就是JVM将会记录下锁的持有者,和一个计数器初始值为0。当这个锁第一次被获的时候,记录下锁的持有者 然后count++ 变为1。当发生重入的时候 count会加1。当线程退出同步代码块的时候 count 会递减。知道count=0的时候,这锁就可以被其他线程获取。

下面看一段代码

public class Widget {
public synchronized void doSomething(){
//...
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
System.out.println(toString() + "call doSomething");
super.doSometing();
}
}


如果内置锁不可重入,这段代码将会发生死锁。

2.4用锁来保护状态。

由于锁可以让受期保护的代码块串行访问,所以可以用锁来构造一些协议来实现对共享变量的独占访问,从而确保一致性。

一些访问共享变量的复合操作需要加锁编程原子操作从而避免产生竞态条件。并且仅仅将符合操作封装到一个同步代码块中是不够的,其他访问这个共享变量的操作都需要使用同步,并且这些同步操作需要使用同一个锁

对于可能被多个线程同时访问的可变的状态变量,在访问它的同时都需要持有一个锁,那么我们称这个状态变量是被锁保护的

每个共享变量都应该只由一个锁保护,从而使维护人员知道是哪一个锁。

对于每个包含多个变量的不变性条件,其中涉及所有的变量都需要同一个锁来保护

注意 : 虽然synchronized可以避免静态条件,但是不加区分的滥用synchronized可能导致程序出现过多的同步,同时尽管每个方法都加上了synchronized 例如Vector类 但是并不能保证 这个类的符合操作就是原子的。

if(!vector.contains(element))
vector.add(element);


尽管每一个操作都是原子的。但是这段代码还是存在“如果不存在 则添加”这样的竞态条件。多个操作产生了一个复合操作,还是需要额外的同步。此外将每个方法都设置成同步还会导致活跃性问题和性能问题。

2.5活跃性和性能

在之前的SynchronizedFactorizer整个service都是同步的,在并发情况下性能很低。在原来的基础上我们增加了一些功能,并且缩小了同步代码块的范围代码如下:

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);
}
}


我们划分成了两个同步代码块

第一个同步代码块负责保护 是否需要直接返回缓存结果的“先检查后执行”的复合操作。

第二个同步代码块负责保护 对缓存的数值和因式分解的结果进行同步的更新。

增加了命中计数器和缓存命中计数器,属于共享变量中的一部分。所以在第一个同步代码块将这两个变量进行更新

在这段代码中我们实现了简单性和并发性,之间的平衡。在设计上要保持同步代码块足够短和简单。有时候简单性和性能之间会存在这冲突,但是通常二者之间能找到一种平衡 就像CachedFactorizer 的实现。

通常在简单性和性能之间存在着相互制约的因素。当实现某个同步策略的时候。一定不要盲目的为了性能而牺牲简单性(因为这可能会破坏安全性)

当执行时间较长的计算或者可能无法快速完成的操作(例如网络IO 控制台IO) 一定要持有锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息