编写线程安全的代码,核心是对于状态的访问操作进行管理。共享的和可变的状态的访问。
非正式意义上来说,对象的状态是指存储在状态变量中的数据(实例或静态域)中的数据。对象的状态可能包含其他依赖对象的域。(HashMap 状态也存储在Map.Entry中)
“共享”意味着变量可以由多个线程同时访问了,而“可变”意味着变量的值在其生命周期内可以发生改变。一个对象是否需要是线程安全的,取决于它是否被多个线程访问,采取同步机制。
如果多个线程访问同一个可变的状态变量时没有合适的同步,程序会报错,三种修复方法:
1 不在线程间共享该状态变量
2 将状态变量修改为不可变的变量
3 在访问状态变量时使用同步
如果开始没有考虑到这些问题,后期修改可能非常复杂,因为找出多线程在那些位置将访问同一个变量是非常复杂的。
当然有些时候需要牺牲一些良好的设计原则,换区性能或者对遗留代码的向后兼容。正确的编程方法是:首先代码正确运行,然后提高代码速度。
什么是线程安全性
线程安全性给出一个确切的定义是十分复杂的。
最核心的概念就是:正确性。 含义是:某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及各种后验条件(Postcondition)来描述对象操作的结果。
我们无法知道类是否正确。所以不能编写详细的规范,但是只要能够确信“类的代码能工作”。这种“代码可信性”很接近我们对于正确性的理解。因此将单线程的正确性近似定义为“所见即所知”。Then,我们可以定义线程安全性:当多个线程访问某个类时,这个类能始终表现出正确的行为,就成为是线程安全的。
无状态的对象一定是线程安全的。 因为线程之间没有共享状态,所以就相当于访问不同的实例。
原子性
递增并不是原子操作: 包括,读取,修改和写入。三个步骤。
竞态条件
当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
竞态条件(race condition):由于不恰当的执行顺序出现不确定的结果
单例模式!!!
竞态条件不总是会导致错误,还需要某种不恰当的实行时序。
但是如果多次返回不同实例,就会丢失部分信息。
复合操作
递增操作,为了保证线程安全性,必须先检查后执行。通过加锁确保原子操作。
AtomicLong 可以确保对于计数器的操作都是原子的。
加锁机制
这个程序虽然原子引用本身是安全的,但是存在竞态条件,因为无法同时更新lastNumber 和lastFactor的值。
内置锁
JAVA 有内置锁机制支持原子性,同步代码块(synchronized block) 包含两个部分:一个作为锁的对象的引用,一个作为由这个锁包保护的代码块。
关键字synchronized 来修饰的方法就是一种跨越整个方法体的同步代码块。静态的synchronized方法义Class对象作为锁。
每个JAVA 对象都可以用作一个实现同步的锁,内置锁(intrinsic lock)或 监视锁(Monitor Lock)。 线程在进入代码块之前都会自动获取锁。并且退出时自动释放锁。
获取内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。JAVA 的内置锁相当于一个互斥体。这意味着只有一个线程能够持有这种锁。
重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞。
然后,由于内置锁是课冲入的。如果线程请求已经由自己持有的锁,请求会成功。"重入" 意味着 获取锁的操作的粒度是“线程”,而不是“调用”。
重入的一种实现方法:为每一个锁关联一个获取计数值和一个所有者线程。 计数0 时,认为不被使用。当请求一个未被占用的锁,JVM 记下锁的持有者,并且计数置为1。 如果同一个线程再次获取,计数++。当线程退出代码块,--。当减为0 时,锁被释放。
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。
如果内置锁是不能重置的,doSomething 方法在获取Widgt上的锁的时候就会发生死锁。
用锁来保护状态
由于锁能保护代码路径串行的形式来访问。因此可以通过锁,来构造一些协议以实现对于共享状态的独占访问。
访问共享状态的复合操作,如果在复合操作的执行过程中有一个锁,那么复合操作也能够成为原子操作。如果仅仅放入同步代码块是不够的,如果需要协调对某个变量的访问(需要协调的域变到了变量上,而不是同步代码块),那么这个变量的所有位置都需要同步。
常见的错误是:在写入共享变量的时候才同步,然而事实并非如此。对于多个可能被多个线程访问的可变状态变量,访问它时,都需要进行加锁。
在获取对象关联的锁时,并不能阻止其他线程访问该对象。当某个线程获取到对象的锁之后,只能阻止其他线程获取同一个锁。内置锁目的是为了免去显示创建锁对象。但是需要自行构造加锁的协议或者同步策略来保证共享对象的安全访问。
每个共享和可变的变量都应该是只有一个锁来保护,从而使维护人员知道是哪一个锁。
常见约定:将所有的可变状态都封装在对象内部。并通过对象的内置锁对所有访问可变对象的代码进行同步。例如,Vector和其他的同步集合类
如果每个包含多个变量的不变形条件,其中设计的所有变量都需要由同一个锁来保护
活跃性与性能
如果缓存中需要使用共享状态,因此需要通过同步来维护状态的完整性。
如果对整个service使用了synchronized方法,每次就只能由一个线程可以执行,就违反了Serlvet的框架初衷。Serlvet 需要能够处理多个请求。
通过将代码分为两个独立的同步代码块,每个同步代码块只包含一小段代码,其中一个代码块负责保护,一个负责保护判断是否需要返回缓存结果,另一个负责对于缓存的数值和进行因数分解的结果进行的更新。
实现了简单性和并发性的平衡。 如果将同步代码块分的过细,会有多次释放锁的过程。
通常简单性和性能之间存在着制约因素。
当执行时间较长的计算或者可能无法完成操作时(网络I/O 或控制台I/O),一定不要持有锁
原文:https://www.cnblogs.com/frank-QAQ/p/9797148.html