第2章 线程安全性

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

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


竞态条件

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区

  • eg.1 一个计数器类Counter 来源
public class Counter {
	protected long count = 0;
	public void add(long value){
		this.count = this.count + value;   
	}
}

this.count = this.count+value;
包含了3个独立的动作:读取-修改-写入

JVM执行顺序

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存

线程A和B交错执行

this.count = 0;
A:	读取 this.count 到一个寄存器 (0)
B:	读取 this.count 到一个寄存器 (0)
B: 	将寄存器的值加2
B:	回写寄存器值(2)到内存. this.count 现在等于 2
A:	将寄存器的值加3
A:	回写寄存器值(3)到内存. this.count 现在等于 3

两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料


  • eg.2 错误的单例模式代码(延迟初始化的竞态条件)
public class LazyInitRace {
    private ExpensiveObject instance = null;

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

class ExpensiveObject { }

LazyInitRace包含了竞态条件,可能会破坏类的正确性.
A、B线程同时执行getInstance,A判空,创建新实例;同样,B判空,创建新实例;那么两次调用就会得到不同的结果

做到线程安全的几种策略

确保原子操作,使用线程安全的类

上面的例子都包含一组需要以原子方式执行的操作,简而言之就是"先检查后执行" <=> 读取-修改-写入 这一系列统筹为复合操作;必须以原子方式执行的操作才能确保其安全性.

  • eg.1改造 使用AtomicLong原子类
public class Counter {
    private final AtomicLong count = new AtomicLong(0);
    public void add(long value) {
        this.count.set(this.count.get() + value);
    }
}

使用AtomicLong能够确保所有对count变量的访问操作都是原子的

加锁机制

eg.3 按照上面的建议使用线程安全的类

public class UnsafeCachingFactorizer extends GenericServlet 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);
        }
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

使用原子引用的情况下,lastNumber 和 lastFactors的set操作是原子的,没有问题;A线程读取的过程中,B线程可能修改了他们,这样A线程不变性条件被破坏了.

所以: 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量,这样就引入了加锁机制.

内置锁

每个java对象都可以用作一个实现同步的锁,这些锁称之为内置锁或监视器锁。线程在进入同步代码块之前自动获取锁,退出时自动释放锁。获取内置锁唯一途径:进入由这个锁保护的同步代码块或方法

synchronized (lock){
    // TODO
}
  • synchronized修饰方法表示:在同一时刻,只有一个线程可以执行该方法
  • A、B两个线程,A获得锁的同时,B只能等、
重入

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

eg.4 子类继承重写父类的doSth方法

public class Father {

    public synchronized void doSth() {
        System.out.println("father.doSth()");
    }

    public static class Son extends Father {
        @Override
        public synchronized void doSth() {
            System.out.println("son.doSth()");
            doAnotherThing();

        }

        private synchronized void doAnotherThing() {
            super.doSth();
            System.out.println("son doAnotherThing()");
        }
    }

    public static void main(String[] args) {
        (new Son()).doSth();

    }
}
// 输出
// son.doSth()
// father.doSth()
// son doAnotherThing()

synchronized是可重入的,不然上面的代码没法执行;
这里的对象锁只有一个,就是Son的对象锁;

  • 1.son.doSth 获取son对象锁
  • 2.doAnotherThing再次请求son对象锁
  • 3.执行super.doSth 获取son对象锁
  • 4.如果不是重入锁,后面两个操作会死锁执行失败

ps:由于super只是一个标记符,告诉jvm调用invoke父类的doSth方法(父类该方法将被继承到子类对象的方法表中),此时并未创建父类对象,所以这个父类方法的锁应该还是son对象的内部锁(唯一),由于可重入,所以持有内部锁后还可以再持有一次而不会陷入死锁

用锁来保护状态

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

  • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护
  • synchronized能保证单个操作的原子性,对于多个操作合并为一个复合操作的情况,需要额外的加锁机制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值