读书可以直面你的灵魂,并对其发起拷问。
以下内容为 Java 并发编程实战 中第一部分的总结,可以帮你快速了解多线程编程的基础知识,在后续学习中可以更快入门多线程编程。
本期主题:线程安全性
抛开锁与同步,我们从抽象层面了解一下线程与线程安全。
线程安全性是什么
线程安全性是对某个类来说的,我们一般说某个类具有线程安全性,就代表在多线程操作的环境下,该类的状态可以被正确的修改和读取。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Share)和可变的(Mutable)状态的访问。
状态,可以是对象中的成员变量,或者类变量(静态变量),一个类或对象可以有多个状态,每个状态都可以看做线程安全性的一个“威胁”。一个类或对象的状态越少,这个类或对象的线程安全性就越能得到保证,这一点可以与避免过度设计进行联想,毕竟很难做到完全不依赖状态进行多线程编程,所以要合理设计状态。
如果从一开始就设计一个线程安全的类,那么比在以后再讲这个类修改为线程安全的类要容易许多。当然,如果一个类的对象不需要被多个线程进行访问,那它就不需要是线程安全的,避免过度设计!!!
两个重要概念:线程安全的程序-线程安全的类
线程安全的程序是在多线程环境运行下不会出现错误的程序,线程安全的类是指具有线程安全性的类,也就是在多线程操作的环境下状态可以被正确读取的类。那么,线程安全的程序是否完全由线程安全的类构成?
并不一定,完全线程安全的类构成的程序也有可能线程不安全,而线程安全的类可以由非线程安全的类组成。在任何情况下,只有当类中仅包含自己的状态时,线程安全类才是有意义的。
线程安全性的核心概念-正确性
正确性是指,某个类的行为与其规范完全一致。这种完全一致是在任何环境下都生效的,比如多个线程同时调用某个类的对象方法,互换这些线程的调用顺序,最终结果不会发生改变。正确性在很大程度上接近线程安全性,你可以将正确性理解为线程安全性。
原子性
如果你进行过数据库方向的学习,你应该会很清楚这个概念。某些操作具有原子性是指,这些操作是连续执行的。不存在其他操作插队的现象。这种原子性在单线程环境下很好保证,但在多线程环境下,对于这些操作,同一时刻可能有很多线程在同步执行,这时就会有别的线程插队操作导致当前线程的操作结果失败,这也叫做发生了竞态条件,竞态条件的概念下面会讲,这里举个例子看看原子性究竟是什么。
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
- 1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
- 2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。如果你有兴趣,可以假设多线程状态下的插队操作情况,这种情况导致A账户金额在每次交易时都不一致,混乱甚至不合法。
竞态条件
当某个计算的正确性却决于多个线程的交替执行顺序时,就会发生竞态条件,换句话说,程序的运行结果要看运气如何。常见的竞态条件类型就是“检查-执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。
想要避免竞态条件,就必须在某个线程修改该变量时,通过某种方式阻止其他线程使用这个变量,保证状态的一致性,我们常见的方法就是锁与同步机制。
总结
线程安全性在脱离锁与同步机制后很难保证,从抽象的层面来讲,掌握上述概念对于后面更多多线程编程的知识学习很有必要,因为多线程编程本质上是一种访问安全思想的体现。接下来的文章会涉及到Java编程语言中具体的某种锁或者同步机制,有了抽象层面的基础,学习具象知识也就变得不难了。