了解线程安全性

说明

本文基于《java并发编程实战》进行总结

正文

      要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)可变的(Mutable)状态的访问。“共享”意味着变量可以右多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。

什么是线程安全性

      在线程安全性地定义中,最核心的概念就是正确性
      正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及各种后验条件(Postcondition)来描述对象操作的结果。我们可以将单线程的正确性定义为“所见即所知(we know it when see it)”。

      线程的安全性可以定义为:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就成这个类是线程安全的

无状态的对象

一个类既不包含任何域,也不包含任何对其他类中于的引用,这个类的对象即为无状态对象

//一个无状态的简单因式分解Servlet
public class StatelessFactorizer implements Servlet {
   public void service(ServletRequest req, ServletResponse resp) {
      BigInteger i = extractFromRequest(req);
      BigInteger[] factors = factor[i];
      encodeIntoResponse(resp, factors);
   }
}

在计算过程中的临时变量状态仅存在于线程栈上的局部变量中,并且只能有正在执行的线程访问,线程之间不会相互影响,因为没有共享状态。
由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的

      在Servelt框架中,大多数的Servlet都是无状态的,极大地降低了实现线程安全性的复杂性。只有当Servlet在处理请求时需要保存一些信息,线程安全性才会成为一个问题

原子性

原子性的特征:一个不可分割的操作

例如:++count, –count 这类操作,看上去只是一个操作,但这个操作并非原子的。它包含了三个独立的操作:读取count的值,将值加1或减1,然后将计算结果写入count。这是一个“读取–修改–写入”的操作序列,并且其结果状态依赖于之前的状态

这类操作,在并发环境下,是非线程安全的,在没有同步的情况下,不同线程对同一个值进行操作可能会发生错误,这是因为它们的执行顺序可能会发生错误

在并发编程中,这种由于不恰当的执行时序而出现不正确的结果。这种情况称为 —- 竞态条件(Race Condition)

竞态条件

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

示例:延迟初始化的竞态条件

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

A和B两个线程同时执行getInstace,可能会返回两个不同的实例。因为,在线程A判断instance为空到创建实例之间,线程B可能也会判断到instance为空,也会创建一个实例,此时就会创建两个不同的实例对象

竞态条件并不总是产生错误,还需要某种不恰当的执行时序

加锁机制

     “先检查后执行”及“读取-修改-写入”等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保安全性。java中使用锁来确保原子性,锁机制是java的一种内置机制

内置锁

      java使用同步代码块(Synchronized Block)来支持原子性,包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

      每个java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Instrinsic Lock)或监视器锁(Monitor Lock),内置锁是一种互斥锁,最多只有一个线程能持有这种锁。当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞

重入

      内置锁是可重入的,即如果某个线程试图3获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。

      重入的一种实现方法是,为每一个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放

用锁来保护状态

      如果使用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问这个变量的所有位置上都要使用同一个锁

      对象的内置锁与其状态之间没有内在的关联。对象的域不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,线程获取对象的锁,只能阻止其他线程获取同一个锁

      当类的不变性条件涉及多个状态变量时,在不变性条件中的每个变量都必须由同一个锁来保护。

      有种情况是:每个操作都不可分割,确保原子性,但合并成一个复合操作时,还需要额外的加锁机制。
如:“如果不存在则添加(put-if-absent)”

if( !vector.contains(element))
   vector.add(element);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值