多线程--线程安全
Posted Kirl z
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程--线程安全相关的知识,希望对你有一定的参考价值。
多线程--线程安全
1. java 进程如何运行
java 类名
运行一个java进程 (系统分配java进程的内存空间),
加载class字节码到java进程的内存, java程序 (java虚拟机) 执行字节码指令 (一行一行的翻译为机器码, CPU执行机器码)
会启动main线程执行main主函数
内存 (栈, 堆, 方法区)
- 栈 (线程私有) : 包含局部变量表 (局部变量 + 基础数据类型的值)
- 堆 (线程共享) : 对象和数组 (虽然是共享, 但是线程能不能用, 取决于线程是否能获取到对象的引用)
- 方法区 (线程共享) : 字符串常量池 + 静态变量 jdk1.6及之前在方法区, 1.7在堆
2. 线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
多线程是同时并发, 并行执行多行代码指令, 要考虑所有的情况都符合期望, 才是安全的
3. 线程不安全的原因
3.1 代码层面
多个线程对同一个变量的操作 (读, 写), 有一个写操作, 就有线程安全问题
3.2 原理层面
3.2.1 原子性
原子性: 多行代码执行, 执行时, 是一组不可再分的最小执行单位
多个线程同时并发并行的执行代码指令, 可能在一个线程操作一个共享变量时, 是有前后依赖关系, 指令之间有其他线程的操作, 就会导致线程不安全
表达出一组操作需要具有不可拆分(其他线程并发并行在中间插入指令执行)
(1) n++, n- -, ++n, - -n
分解为三步:
- 从主存读取变量值(int tmp = 从主存读取的值)
- 修改 (tmp = tmp + 1)
- 写回主存
CPU 执行线程中的指令, 考虑效率问题 (内存比 CPU 高速缓存慢很多), 把内存变量的值复制到 CPU高速缓存
(2) Object o = new Object();
分解为三步:
- 分配对象的内存空间(java进程)
- 对象初始化(成员变量, 实例化代码, 构造方法, 这些会编译为初始化指令)
- 赋值给变量
(3) 了解
long 等等 64位变量, 不具有原子性
3.2.2 可见性
主存: 线程都使用的共享区域, 对其中变量/对象的操作
工作内存; 线程之间互相不可见, CPU执行线程中的代码指令, 是从主存复制到CPU高速缓存
3.2.3 有序性
代码重排序: java 代码书序是固定的, 但是 jvm 执行字节码或者CPU执行机器码, 都是可能重排序指令顺序, 目的是提高运行效率
一段代码是这样的:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问
题,可以少跑一次前台。
重排序, 是会考虑指令前后的依赖关系的
4. 如何解决线程安全问题?
- 一组代码, 如果存在多线程对共享变量的操作, 都需要考虑线程安全问题
- 一般把多线程操作的共享变量, 成为临界资源
- 一组代码, 称为临界区
锁, 在java层面, 也是基于对象来设计为锁的, 要是用同一把锁 (同一个锁对象, 才能解决线程安全)
思路:
把临界区加锁, 多个线程执行临界区代码, 最终表现, 就是一个线程申请加锁, 执行代码, 释放锁, 其他线程申请失败, 需要等待 (不一定是阻塞态, 也可以是运行态)
一个线程一个线程依次的执行临界区代码
5. 解决多线程安全问题
5.1 synchronized 关键字
同步的关键字: 依次执行某段代码
使用:
(1) 静态方法上: 加锁整个方法, 锁对象为当前的类对象 (Class <当前类型>, 类对象)
private synchronized static void t1() {
// SynchronizedThread.class 类对象
}
(2) 实例方法上: 加锁整个方法, 多对象为 this
synchronized (this) {
// 那个对象调用该实例方法, this就是指谁
}
(3) 同步代码块:
synchronized (锁对象) {
}
作用: 对同一对象加锁的线程, 造成同步互斥作用 (多个线程依次执行临界区代码)
注意: 如果不是使用同一个对象加锁, 意味着能同时执行 (并发并行)
synchronized 加锁的原理(部分):
- 本质上是对对象头进行加锁, 对同一个对象加锁的线程, 是同步互斥 (一个对象有一个线程加锁后, 必须释放锁, 其他线程才能获取到)
- 底层原理是基于操作系统的锁实现
5.2 volatile 关键字
volatile 是修饰一个变量的关键字
作用:
- 保证变量的可见性 (分解为字节码指令后的, 有变量的指令行, 变量有可见性)
- 建立一个内存屏障, 禁止指令重排序
使用场景:
(1) 多线程对共享变量的操作, 如果代码行本身保证了原子性, 就可以不加锁, 只使用volatile保证可见性, 该共享变量的操作也是线程安全的
- 读是原子性 ;
- 写 (修改, 赋值) 操作: 值不依赖共享变量 (n++, n = n + 1), 比如是一个常量, 就是原子性
private static boolean isStop = false;
while (!isStop) {
//
}
以上保证线程安全的代码, 不需要加锁, 多线程可以并发并行的执行, 效率是非常高的
(2) 多线程代码设计目标: 线程安全的情况下, 尽可能的提高效率
具体的参考实现方式, 参考:
临界区代码越多, 多线程对临界区加同一个锁, 同步互斥时, 执行临界区代码效率越低
优化方案:
- 锁的细粒度化 (临界区代码行越少) (共享变量的写操作的代码行)
- 哪些代码不加锁, 也能保证线程安全 (读的共享变量使用 volatile, 写一般都会依赖其他变量, 暂时不考虑)
以上总体可以称为读写分离 (读读并发, 读写并发, 写写互斥)
以上是关于多线程--线程安全的主要内容,如果未能解决你的问题,请参考以下文章