Java 并发编程实践 读书笔记二

基础

线程安全

并发编程不会涉及过多的线程或锁。

编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的,可变的状态

一个对象的状态就是它的数据,存储在状态变量中,比如:实例域或静态域。

所谓共享,是指一个变量可以被多个线程访问。

所谓可变,是指变量的值在生命周期内可以改变。我们真正要做的,是在不可控制的并发访问中保护数据。

无论如何,只要又多于一个的线程来访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。

Java中同步机制

  • synchronized 提供了独占锁
  • volatile 变量,显示锁和原子变量的使用

一开始就将一个类设计成是线程安全的,比在后期重新修复它更容易。

对程序的状态封装的越好,你的程序就越容易实现线程安全,同时有助于维护者保持这种线程安全性。

设计线程安全的类时,优秀的面向对象技术—封装,不可变性以及明确的不可变约束—会给你提供诸多的帮助。

什么是线程安全性

一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。

当多个线程访问同一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为依然是正确的,那么称这个类是线程安全的。

  • 实例:一个无状态的stateless 的 servlet
    • 线程访问无状态对象的行为,不会影响其他线程访问该对象时的正确性,所以无状态对象是线程安全的。
    • 无状态对象永远是线程安全的。

原子性

Long count = 0;
long getCount(){
	return count;
}
++count;

无状态代码中进行++操作,容易遗失更新,并且他并非原子操作

在一些偶发时段里,出现错误结果的可能性对于并发程序而言非常重要,以致于专门用一个名词来描述他们:竞争条件

  1. 竞争条件:当计算的正确性依赖于运行时相关的时序或者多线程的交替时,会产生竞争条件。

    为获取期望结果,需要依赖相关的事件的分时。

    大多数竞争条件的特点:使用潜在的过期观测值来做决策或执行计算。这种竞争条件被称作检查再运行

  • 实例:惰性初始化中的竞争条件。

    • 检查再运行的常见用法是惰性初始化。
    • 惰性初始化的目的是延迟对象的初始化,直到对象真正使用它,同时确保它只初始化一次。
  • 在多线程环境下,容易返回不同的实例对象。

  1. 复合操作

    为了避免竞争条件,必须阻止其他线程访问我们正在修改的变量,让我们确保:当其他线程想要查看或修改一个状态时,必须在我们的线程开始之前或者完成之后,而不能在操作过程中。这样称为原子操作。

    原子操作指:该操作对于所有的操作,包括他自己,都满足前面描述的状态。

    AtomicLong count = new AtomicLong(0);
    long getCount(){
    	return count.get();
    }
    count.incrementAndGet();
    

    使用原子类型变量定义初始化变量,已达到自增的安全性。

当一个不变约束涉及多个变量时,变量间不是彼此独立的:某个变量的值会制约其他几个变量的值。因此,更新一个变量的时候,要在同一个原子操作中更新其他几个。

为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。

  1. 内部锁

    强制原子性的内置锁机制:synchronized块。

    两个部分:

    • 锁对象的引用

    • 锁保护的代码块

      synchronized方法是对跨越了整个方法体的synchronized块的简短描述,至于synchronized方法的锁,就是该方法所在的对象本身。(静态synchronized方法从class对象上获取锁)

      每个Java对象可以隐式扮演一个用于同步的锁的角色:这些内置的锁被称作内部锁或者监视器。

      内部锁在Java中扮演了互斥锁的角色。若线程A尝试请求被线程B占有锁时,线程A必须等待或阻塞,直到B释放它,如果B不释放,A将永远等下去。

  2. 重进入

    当一个线程请求其他线程已占有的锁时,请求线程将被阻塞,然而内部锁是可重进入的,因此线程在试图获得他自己占有的锁时,会请求成功。

    重进入的实现是通过为每一个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求数置为1,如果同一线程再次请求这个锁,计数将递增,每次占有线程退出同步块,计数器值递减,直到为0,锁被释放。

    重进入方便了锁行为的封装,简化了面向对象并发代码的开发。

用锁来保护状态

仅使用synchronized来包装复合操作是不够的,如果用同步来协调访问变量,每次访问变量时都需要同步,进一步讲,用锁来协调访问变量时,每次访问变量都需要用同一个锁。

对于每个可被多线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

每个共享的可变变量都需要由唯一一个确定的锁保护,而维护者应该清楚这个锁。

对于每一个涉及多个变量的不变约束,需要同一个i锁保护其所有的变量。

活跃度与性能

在SynchronizedFactorizer中,Servlet在我们的同步下运行,性能变得很糟糕。这个方式通过同步整个Service方法实现的,虽然使我们重获安全性,但是代价高昂。

觉得synchronized块的大小需要权衡各自设计需求,包括安全性,简单性和性能。有时简单性和性能会彼此冲突,然而通常都能从中找到一个合理的平衡。

通常简单性与性能之间是互相牵制的,实现一个同步策略时,不要过早为了性能而牺牲简单性。

使用锁时,应清楚代码块中功能,以及它执行过程是否耗时。(有些耗时计算或操作,执行期间不要占有锁。)

如果线程长时间占有锁,就会引起活跃度与性能风险的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值