并发编程实战-线程安全性

1.线程未同步的三种修复方式

当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误.有三种方式可以修复这个问题.

  1. 不在线程之间共享该状态变量.
  2. 将状态变量修改为不可变的变量.
  3. 在访问状态变量时使用同步.

2.性能优化的限制

  • 首先使代码正确运行,然后再提高代码的速度.
  • 只有性能测试结果告诉你必须提高性能,以及测量结果表明这种优化在实际环境中确实能带来性能提升时,才进行优化.

3.线程安全的定义

当多个线程访问某个类时,不管运行时程序采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现正确的行为,那么就称这个类是线程安全的.
在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施.

无状态 对象一定是线程安全的 如Servlet框架

public class StatelessFactorizer implements Servlet {


    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        //do something
    }
......
}
    

4.原子性

如果增加一个全局变量,那么StatelessFactorizer 就不是线程安全的了.多线程访问count++而出现数据偏差的情况,称为 竞态条件

静态条件的两种表现形式:

public class UnsafeCountingFactorizer implements Servlet {
	private long count = 0;

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    	++count;
        //do something
    }
......
}
    
public class LazyInitRace {
	private ExpensiveObject instance = null;
	public ExpensiveObject getInstance() {
		if (instance == null) {
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

避免竞态条件的方式: 当某个线程修改该变量时,阻止其它线程使用这个变量,即保证原子性.

可用线程安全类来避免竞态条件的产生:

public class UnsafeCountingFactorizer implements Servlet {
	private final AtomicLong count = new AtomicLong(0);

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    	count.incrementAndGet();
        //do something
    }
......
}
    

5.加锁机制

下例虽然使用了原子引用,但是并不是线程安全类

public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger> lastFactors = new AtomicReference<>();

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = BigInteger.valueOf(xxx);
        if (i.equals(lastNumber.get())) {
            BigInteger f = lastFactors.get();
            //do something f
        }else {
            BigInteger factors = factor(i);
            //set方法虽然是独立的,但是无法同时更新lastNumber和lastFactors,因此线程不安全
            lastNumber.set(i);
            lastFactors.set(factors);
            //do something
        }
    }
}

原因是虽然单个引用保证了原子性,但是其中lastNumberlastFactors之间并不是彼此独立的,而是某个变量的值会对另一个产生约束.

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

5.1 内置锁

  • java每个对象都可以做一个实现同步的锁,这些锁被称为内置锁
  • 内置锁相当于一个互斥锁
public class SynchronizedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;

    @Override
    public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = BigInteger.valueOf(xxx);
        if (i.equals(lastNumber )) {
            //do something f
        }else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            //do something
        }
    }
}

虽然synchronized保证了线程安全,但是这种方法并不可取,性能太低(并不是synchronized本身性能低,而是用其修饰方法,导致性能低)

5.2 重入

如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功.
实现方式:

  • 每个锁关联一个获取计数值和一个所有者线程.计数为0时,认为没有被任何线程持有,当同一个线程重复请求一个未被持有的锁,计数累加,JVM也会记下锁持有者.
public class Widget {
    public synchronized void doSomething() {
        //do something
    }
}

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

上面例子中,如果内置锁不可重入,那么super.doSomething();就会无法获取Widget上的锁(已经被线程持有了),产生死锁,线程会阻塞等待一个永远也无法获取的锁.

6.用锁来保护状态

通过锁来构造一些协议以实现对共享状态的独占访问

对于可能被多个线程同时访问的可变状态变量,访问它时需要持有同一个锁,这个变量是由这个锁保护的.
每个共享的和可变的变量都应该只由一个锁来保护

加锁约定:

  1. 将所有可变状态封装到对象内部,通过内置锁对所有访问可变状态的代码进行同步(Vector或其它同步集合类中使用该方式)
  2. 对于每个包含多个变量的不变性条件,其中设计的所有变量都需要由同一个锁来保护.
  3. 尽量不要将每个方法都作为同步方法,性能较低

7.活跃性与性能

上面的SynchronizedFactorizer 类虽然保证了线程安全,但是效率却非常低.
为保证效率,应遵循:

  1. 不要将本应是原子的操作拆分到多个同步代码块中.
  2. 应尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去.
  3. 不要将同步代码块分解的过细,因为加锁和释放锁也需要开销

@ThreadSafe
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 this.hits;}
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
        BigInteger i = BigInteger.valueOf(111);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factors(i);
            synchronized (this) {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        //doSomething
    }

    private BigInteger[] factors(BigInteger i) {
        return new BigInteger[0];
    }
...
}

本例中,++hits;操作无需自己的同步代码块,虽然这么做不会破坏原子性,但是也会稍微增加性能开销.

  • 通常,简单性(直接同步方法)和性能(同步代码块)之间存在相互制约因素.当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性(安全性始终保证在第一位)
  • 执行时间较长的计算或者可能无法快速完成的操作时,一定不要持有锁.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值