1_基础知识_chapter02_线程安全性_1_线程安全性

  • 对象的状态

    (1) 对象的状态是指 类的实例或静态变量

    (2) 也包括其他依赖对象的域

    例如某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中

  • Java的同步机制

    (1) synchronized

    (2) volatile

    (3) 显示锁

    (4) 原子变量: package java.util.concurrent.atomic

  • 将线程不安全的类修改为线程安全的类的方法

    (1) 不在线程之间共享状态变量

    (2) 将状态变量修改为不可变变量

    (3) 在访问状态变量时使用同步

  • 程序状态的封装性越好, 越容易实现程序的线程安全性

  • 线程安全性

    (1) 定义

    当多个线程访问某个类时,不论运行时环境采用何种调度方式, 或者线程如何调度执行, 在主调代码中不需要任何额外的同步, 这个类总能表现出正确的行为

    (2) 无状态的类一定是线程安全的

    即,类中没有任何静态变量和实例变量,类只提供方法,所有变量都是局部变量。

    没有状态变量,自然也就没有线程同步的问题

  • 竞态条件

    (1) 定义

    由于执行时序的不同,造成不正确的结果称为竞态条件

    (2) 常见的竞态条件情形

    先检查后执行

    示例

      if (!flag) {
          ....
      }
    

    读取-修改-写入

    示例

      count += 1
    

    (因为count+=1这一条语句事实上包含了读取-修改-写入三个步骤,不是原子操作)

    (3) 当在无状态的类中添加一个状态变量时,如果该状态完全由线程安全的对象来管理,那么这个类一定是线程安全的

    实际情况中,应尽可能使用线程安全的类作为状态变量,例如java.util.concurrent.atomic中的类

    示例

    线程不安全

      @NotThreadSafe
      public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    
          private long count = 0;
    
          public long getCount() {
    
              return count;
          }
    
          public void service(ServletRequest req, ServletResponse resp) {
    
              ...
    
              ++count;
    
              ...
          }
      }
    

    线程安全

      @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) {
    
              ...
    
              count.incrementAndGet();
    
              encodeIntoResponse(resp, factors);
          }
      }
    

    其中,count.get()和count.incrementAndGet();都是AtomicLong类提供的实例方法

    (3) 状态变量有多个时,对每个变量的更新都使用原子操作,结果未必是原子操作

    正确的做法是:

    当更新某一个状态变量时, 如果各个状态变量之间不是彼此独立的,那么需要在同一个原子操作中, 对其他变量同时进行更新

  • 内置锁与synchronized

    (1) 每个Java对象都可以用作一个实现同步的锁,称为内置锁(Intrinsic Lock)或监控器锁(Monitor Lock),用作synchronized(...)

    (2) 在synchronized块中,无论程序正常运行结束,还是抛出异常,都会在退出同步代码块时自动释放锁

    (3) 重入

    如果一个线程试图获得一个已经由它自己持有的锁, 那么这个请求一定会成功, 不会引发死锁

    例如

    递归函数 如果加上了synchronized, 每次调用自己时不会被阻塞

    2° __子类覆盖了父类的synchronized方法, 且在方法中又显示调用父类的方法时(super.xxx()),不会被阻塞

一种实现原理是, 为每个锁关联一个计数值和一个所有者线程,如果请求锁时发现占有锁的是所有者线程本身,那么只会增加计数值,不会阻塞
  • (1) 某个线程获得对象的锁以后, 只能阻止其他线程获得同一个锁, 不能阻止其他线程获取其他的锁

    因此, 每个共享的和可变的变量都应该只由一个锁来保护

    (2) 一种常见的加锁约定

    将所有的可变、共享的状态变量都封装在对象内部, 并通过对象本身(this)的内置锁对所有访问可变状态的代码路径进行同步

  • 同步代码块的大小

    (1) 理论上, 可以将所有方法都尽可能加上synchronized,但是一来滥用会导致性能问题,二来每个方法都是原子操作,不代表整体就是原子操作

    (2) 同步代码块的合理大小取决于三个因素

    安全性: 必须达到

    简单性: 同步的部分设计不能过于复杂

    性能: 同步代码块范围过大会导致性能问题

    (3) 安全性必须达到简单性和性能常常是矛盾的

    当实现某个同步策略时,一定不能盲目的为了性能而牺牲简单性;

    当执行时间较长的计算和IO操作时,一定不要继续持有锁。

    (4) 一个设计合理的示例

      @ThreadSafe
      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);
          }
    
          private void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
          }
    
          private BigInteger extractFromRequest(ServletRequest req) {
    
              return new BigInteger("7");
          }
    
          private BigInteger[] factor(BigInteger i) {
    
              return new BigInteger[]{i};
          }
      }
    

    这个示例首先满足了安全性; 其次将同步代码块的范围尽可能缩小, 满足了性能要求; 再次没有将同步代码块拆的很零碎,满足了简单性要求。所以是一个设计良好的类。

    对单个变量实现原子操作来说, 原子变量(AtomicXXX)很有效;但是由于多个原子操作的叠加未必是一个原子操作,所以当状态变量很多时, 仍有考虑所有状态变量的同步更新访问问题,即便它们都使用了原子变量。

    在这个示例中,对hits和cacheHits这两个状态变量的更新和访问使用了synchronized机制,没有必要引入原子变量,增大设计的复杂程度

  • 总结

    (1) 无状态的类一定是线程安全的

    (2) 当在无状态的类中添加一个状态变量时,如果该状态完全由线程安全的对象来管理,那么这个类一定是线程安全的

    例如只添加一个状态变量时,添加一个原子变量类型AtomicXXX就很合适

    (3) 当更新某一个状态变量时, 如果各个状态变量之间不是彼此独立的,那么需要在同一个原子操作中, 对其他变量同时进行更新

    一种常见的约定是: 将所有的可变、共享的状态变量都封装在对象内部, 并通过对象本身(this)的内置锁对所有访问可变状态的代码路径进行同步

    (4) 同步代码块的大小要合理,必须满足安全性,且在简单性性能方面权衡合适

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值