线程安全性

扫码查看

1.线程安全性

  • 定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

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

  1. 原子性

    • 有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作

    • 竟态条件

      • 某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气[2]。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。

      • 竞态条件这个术语很容易与另一个相关术语“数据竞争(Data Race)”相混淆。数据竞争是指,如果在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争

      • 延迟初始化的竟态条件

        @NotThreadSafe

        public class LazyInitRace{

        private ExpensiveObject instance=null;

        public ExpensiveObject getInstance(){

        if(instance==null) //a线程和b线程同时执行

        instance=new ExpensiveObject();

        return instance;

        }

        }
        • 复合操作

          • 要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中.

            @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)

            }

            }

            在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。[1]由于Servlet的状态就是计数器的状态,并且计数器是线程安全的,因此这里的Servlet也是线程安全的

            在实际情况中,应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性

  1. 加锁机制

    1. 当在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(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)

    }

    }

    }

    然而,这种方法并不正确。尽管这些原子引用本身都是线程安全的,但在UnsafeCaching Factorizer中存在着竞态条件,这可能产生错误的结果.

    在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。只有确保了这个不变性条件不被破坏,上面的Servlet才是正确的

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

  • 内置锁

    • Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)

      synchronized(lock){

      //访问或修改由锁保护的共享状态

      }

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

    Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等下去。

    这种同步机制使得要确保因数分解Servlet的线程安全性变得更简单。在程序清单2-6中使用了关键字synchronized来修饰service方法,因此在同一时刻只有一个线程可以执行service方法。现在的SynchronizedFactorizer是线程安全的。然而,这种方法却过于极端,因为多个客户端无法同时使用因数分解Servlet,服务的响应性非常低,无法令人接受。这是一个性能问题,而不是线程安全问题.

程序清单2-6 这个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=extractFromRequest(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();

      }
  • 用锁来保护状态

    • 由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性

    •  

12-18 08:06
查看更多