`
he_wen
  • 浏览: 233966 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

线程安全

阅读更多

一、本文主要描述线程安全:

      1.1: 什么是线程安全

      1.2: 保持同步的原子性

      1.3: 锁机制

      1.4: 用锁保护状态

      1.5: 激活性和性能

 

二、线程安全的类一般是无状态的对象、或者类里面的变量是不可变的,下面举例说明什么是无状态类

 

public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}


 

 

跟线程模型有关系,因为每个线程都有自己的变量,并且变量放在自己的线程堆栈中(除了共享变量),所以说无论多个线程怎么访问,该线程类都是安全。

 

三、也许有人会问是什么样的操作是原子性的呢?

 

      那么举个例子  count++;

   这个操作时原子性的吗?好像单道程序中该操作时原子性,但在早多线程中该操作不是原子性;该操作分为读、修改、写入,因此该操作就会在多个线程中执行时会别切换,也就说明该操作会在执行过程中呗切换给下一个线程。

 

下面还是举上面的那个例子:

 

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

}


 

 

该例子有一个私有的但是多个线程共享的变量,而在service方法中该方法体里面执行了++count,所以该类不是线程安全的。

  类失去原子性操作的另一个典型是:检查在运行

下面就为大家举个单例模式中的懒加载问题:

 

public class LazyInitRace {
    private ExpensiveObject instance = null;

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

 

大家可能会一下子知道这个没有什么问题,但是在多线程中运行该getInstance方法时,有可能两个或以上的线程会判断instance为空。。。所以说要尽量在判断和新建对象的时候不要被线程切换,也就是说保持操作的原子性的时候那么该类就是线程安全。

 

下面为大家阐明上面出现的两个问题的解决方案:

 

   第一:要想解决原子性的操作就需要给这些操作加锁

  第二:要让每个线程知道,当有个线程修改对象或者修改变量时,其他线程都要可见,这个关系到JVM的类存模型

 

第一个问题read-modify-write解决方案就是:

    我们引用了JDK包中java.lang.concurrent.atomic 该包里面可以让变量保持原子性操作,具体类的简单介绍如:

 

 

AtomicBoolean 可以用原子方式更新的 boolean 值。
AtomicInteger 可以用原子方式更新的 int 值。
AtomicIntegerArray 可以用原子方式更新其元素的 int 数组。
AtomicIntegerFieldUpdater<T> 基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong 可以用原子方式更新的 long 值。
AtomicLongArray 可以用原子方式更新其元素的 long 数组。
AtomicLongFieldUpdater<T> 基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReference<V> AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference<V> 可以用原子方式更新的对象引用。
AtomicReferenceArray<E> 可以用原子方式更新其元素的对象引用数组。
AtomicReferenceFieldUpdater<T,V> 基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
AtomicStampedReference<V> AtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。

具体用法可参照文档的用法:

 

 

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

 

第二个问题解决方案就是加上关键字sychronized.

 

四、锁

 

下面为大家引入前面的第一个例子,然后又为该类添加一个变量(该变量用来缓存servlet最近计算的结果),而且该变量可以保持原子性操作,那么想一想两个变量都是保持原子性操作的,那么该类是否是线程安全的呢?

 

答案:否

 

这个例子的代码:

public class UnsafeCachingFactorizer implements Servlet {
     private final AtomicReference<BigInteger> lastNumber
         = new AtomicReference<BigInteger>();
     private final AtomicReference<BigInteger[]>  lastFactors
         = 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);
         }
     }
}

 看起来这个类是没有问题的,但是大家用多线程的思维看待这个类,就会发现该类存在着竞争条件:

   lastNumber.set(i);

   lastFactors.set(factors);

 这两个操作一定要是原子性操作,否则别的线程就会在执行这两个操作时,切换掉,以至于别的线程达不到想要的数据。

 

总结:一个类中虽然每个变量是线程安全的,但是如果组合起来就不是线程安全,以为变量之间会相互依赖,只有变量不互相依赖,那么这个类仍然是线程安全的。换一句话:为了保持类的状态的一致性,那么就要把更新的相关变量在一个原子操作中完成。

接下来说明两个锁:内部锁和可重入锁

 

内部锁用sychronized关键字,为了保持原子的一致性,在并发编程中要善于的用这个关键字,因为滥用的话:导致死锁

、竞争条件更加激烈、并发性并单一性更加慢。

 

可重入锁:

When a thread requests a lock that is already held by another thread, the requesting thread blocks. But because intrinsic locks are reentrant, if a thread tries to acquire a lock that it already holds, the request succeeds. Reentrancy means that locks are acquired on a per-thread rather than per-invocation basis. [7] Reentrancy is implemented by associating with each lock an acquisition count and an owning thread. When the count is zero, the lock is considered unheld. When a thread acquires a previously unheld lock, the JVM records the owner and sets the acquisition count to one. If that same thread acquires the lock again, the count is incremented, and when the owning thread exits the synchronized block, the count is decremented. When the count reaches zero, the lock is released

有高人可以解释一下这段话说的是什么,说实话这个不是很清楚

下面有一个例子:

 

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

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

 

如果没有可重入锁就会死锁。

 

五:用锁保护类的状态

 


人们通常犯的错误是认为:只有在写入共享变量的时候才会用到同步。下面总结一下:当多个线程访问每个可变的变量,访问整个变量时候一定要拥有同一个锁。

 

大家也许会问同步关键字能够解决所有的竞争条件那么为什么不用同步来全部解决呢?

 这是因为滥用同步会使得应用程序构建的同步代码块过多或者过少,所以要善于考虑这个问题。仅仅把同步关键字放在方法前面(同步集合时这么干的,同步集合扩大了同步代码,是的在并发性能上非常弱),那么也不会解决复合行为(put-if-absent)如:

 

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


六、激活性和性能

 

在解决servlet缓存原子性操作时加同步关键字,这样会引起很大的性能问题,如图所示:

 



 

 


因此就要考虑:尝试在程序中运行时间比较久的操作放在同步代码块外面但有不影响类的共享状态

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

 

在上面的类中四个变量都没有用到AtomicLong 类来保持原子一致性,这是因为方法中用了两个同步代码块,来协调保持变量的同步性。

那么大家肯定会想,在方法中加同步关键字和在方法里面分解出耗时且不影响类状态的改变的同步代码块,这两个解决方案如何选择呢?

那么必须要编程者清晰的知道类中的变量和竞争条件。所以编程者要切记在方法中添加关键字,虽然简单但是会影响并发性能,特别是在方法里面含有耗时的操作,如:I/O,网络连接、等等。

  • 大小: 15.9 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics