「本文已参与「新人创作礼」活动,一起开启掘金创作之路。」
基础知识
要编写线程安全的代码,核心在于,要对状态访问操作尽心管理, 特别是对一些共享的和可变的状态 共享意味着变量可以同时被多个线程同时访问 可变意味着变量的值在生命周期内可以发生变化
一个对象是否需要线程安全,取决于是否有多个线程要同时访问 如果要让对象是线程安全的,需要采用同步机制来协同对对象的可变状态的访问 同步机制的关键字是synchronized 还包括 volatile类型的变量 显式锁(Explicit Lock)以及原子变量
多线程访问一个可变的状态变量时候没有使用同步,那么可以采取下面三个方法进行修复
- 不在多个线程之间共享该变量,这样的状态变量隔离开
- 状态变量修改为不可变的变量
- 访问的时候使用同步
完全由线程安全的类组成的程序不一定是线程安全的 线程安全类中也可以包含非线程安全的类 只有当类中仅包含自己的状态,线程安全类才有意义
怎么定义一个类是线程安全的:
当多个线程访问这个类的时候,这个类始终能够表现出正确性,就表示这个类是线程安全的 无状态变量都是线程安全的
竞态条件
由于不恰当的操纵而导致执行时序出现不正确的结果
先检查后执行的常见情况就是 延迟初始化 目的是将对象的初始化操作推迟到实际被使用才运行,而且要确保只有被初始化一次
要防止竞态条件 就必须在某个线程改变这个变量的时候 通过某种方式防止其他线程使用这个变量 从而保证其他线程在当前这个线程完成操作之后才能进行操作
先检查-后执行 以及 读取-修改-写入 统称为复合操作
现在先学到是 原子变量类
例如 AtomicLong
private static final AtomicLong count = new AtomicLong(0);
获取值 count.get();
自增 count.incrementAndGet();
用这样的类型去取代普通的long 确保所有对计数器状态的访问操作都是原子的, 所以是线程安全的
如果要添加更多的状态 那么是否只要添加更多的线程安全的例如上面说的那样的线程安全状态的变量?
不仅要添加,还要去同时更新和同时读取
java的内置的锁机制支持原子性,也就是 同步代码块
同步代码块分成两块:
1、作为锁的对象引用
2、由这个锁保护的代码块
实现同步的锁叫做内置锁 或者是 监视器锁 或者是 互斥锁
获得锁的唯一路径是进入这个锁保护的同步代码块或方法
特点是 最多只有一个线程可以持有这个锁,当a尝试获取b持有的锁的时候 a必须等待或者阻塞,直到b释放这个锁,b不释放,a永远等待
这个锁保护的同步代码是以原子方式执行的 但是这样的方法 过于极端 多个客户端无法同时使用 导致响应性很低
重入
在上述过程中,因为会出现等待导致阻塞,而内置锁是有重入机制的, 也就是一个线程在持有这个锁的时候再次进入是可以成功的,获取锁的操作的粒度是线程而不是调用
方法就是为每个锁关联记一个获取技术值和一个所有者线程,当计数器为0,表示没有被任何线程持有 一旦一个线程持有 记录下锁的持有者 并且计数+1 同一个线程再次进入 计数器在加1 当线程退出的时候 计数值相应递减 直到为0的时候 才是真正的释放
并非只有在写入共享变量时才需要使用同步
标注@GuardeBy表示对象是由内置锁来保护的
加锁约定
将所有的可变状态封装在对象里面, 通过内置锁对所有访问可变状态的代码进行同步, 使该对象不会发生并发访问
只有被多个线程同时访问的可变数据才需要通过锁进行保护
小知识点:当要让程序崩溃或者停止的时候,之前所执行的任务不再重复执行 而是从之前生成的快照片那边开始, 这样的快照功能 可以使用TimeTask 若干时间触发一次 将程序状态保存到一个文件
对象的共享
synchronized并不是只能用于实现原子性或者确定临界区的 还有一个 内存可见性 确保一个线程修改了对象状态后,其他线程能够看到状态的变化
当读操作和写操作在不同的线程中执行,那么就无法保证执行读的线程能适时看到其他线程写入的值 因为需要同步机 制
发布和逸出
所谓的发布就是将对象发布到代码作用域外的地方, 比如 将一个指向该对象的引用保存到其他代码可以访问的地方 或者 在某一个非私有的方法中返回该引用 或者 将引用传递到其他类的方法中
所谓的逸出就是不该被发布的对象被发布
发布对象最简单的就是 将对象的引用保存到一个公有的静态变量 当从对象的构造函数中发布对象时候,只是发布了一个尚未构造完成的对象 哪怕发布对象的语句在代码的最后一行也是一样的效果 如果this引用在构造的过程中逸出,那么这是属于不正确构造
ps:不要在构造过程中使用this引用
线程封闭(Ad-hoc的封闭技术过于脆弱,不使用!)
1.栈封闭
在栈封闭中只能通过局部变量才能访问对象,因为局部变量的固有属性就是封闭在执行线程中 位于执行线程的栈中,其他线程无法访问
如果在线程内部使用非线程安全的对象,那么该独享也仍然是线程安全的
2.ThreadLocal类
维持线程封闭性的一种更规范的用法是 使用 ThreadLocal 这个类可以使线程中的某个值和保存值的对象关联起来
还有get和set等方法 get总是返回当前执行线程在调用set时设置的最新值 用于防止对可变的单实例变量或者全局变量进行共享
例如:jdbc的连接保存到ThreadLocal对象中,每个线程访问的时候都拥有自己的连接 当某个频繁执行的操作需要一个临时对象,比如一个缓冲区,同时避免在每次执行的时候 都重新分配该临时对象,就可以使用ThreadLocal