线程一些问题
1. 概述
单线程和多线程 表象化的一种理解:线程就是程序执行的路径,单线程就是只有一条路径,多线程就是多条路径
进程:一个程序装载到内存里,分配好资源如网络等等。是分配资源的最基本单位
线程:程序开始运行,将一条条指令放到cpu里执行。是调度执行的最基本单位
CPU:ALU(算术逻辑单元,负责计算) + Registers(寄存器,存储数据) + PC(计数器,记录执行到哪一条指令,记录指令地址)
一个核在同一时刻只能运行一个线程。
但是目前也存在线程撕裂者,即超线程:由于ALU速度大于Registers,所以如果一个ALU搭配一套Registers+PC即是一个线程,如果一个ALU搭配两套Registers+PC就是超线程,省略了线程上下文切换的时间。
线程切换(上下文切换):运行T1线程时,会把T1的数据放入寄存器中计算,如果时间到了需要切换时,会把T1的数据地址等等放入到缓存中,把T2的数据放入到寄存器中
线程数并不是越大越好,因为CPU要保证所有的线程都有执行的机会,会浪费很多时间在线程切换上。
哪怕是单核CPU设置多线程也有意义。多线程是为了提高CPU利用效率,一个程序并不是全部时刻都在占用CPU。
2. 线程的可见性
一个线程对共享变量值的修改,能够及时的被其他线程看到。
JVM中关于各种变量(线程共享变量)的访问规则
- 所有的变量都存储在主内存中。
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递需要通过主内存来完成。
原理:
线程1对共享变量的修改要想被线程2及时看到
- 把工作内存1中更新过的共享变量刷新到主内存中
- 将主内存中最新的共享变量的值更新到工作内存2中
实现方式:
- synchronized
- volatile
2.1 synchronized 实现可见性以及原子性
JMM关于synchronized的两条规定:
- 线程解锁前(退出synchronized代码块之前),必须把共享变量的最新值刷新到主内存中,也就是说线程退出synchronized代码块值后,主内存中保存的共享变量的值已经是最新的了
- 线程加锁时(进入synchronized代码块之后),将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)
两者结合:线程解锁前对共享变量的修改在下次加锁时对其他线程可见
根据以上推出线程执行互斥代码的过程:
1.获得互斥锁(进入synchronized代码块)
2.清空工作内存
3.从主内存拷贝变量的最新副本到工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存
6.释放互斥锁(退出synchronized代码块)
2.2 volatile 实现可见性
volatile 特性:
- 能够保证volatile变量的可见性
- 不能保证volatile变量复合操作的原子性
如何实现内存的可见性?
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 => 写后存(store)
- 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 => 读前读(load)
通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。
为什么不能保证volatile变量复合操作的原子性?
通俗的讲,volatile不能加锁,对number++;的操作会被多个线程交叉执行,导致出现不同的结果
2.4 synchronized 和 volatile 区别
- volatile 不需要加锁,更轻量级
- volatile 只保证可见性,不保证原子性
3. 线程的 as-if-serial
这个主要针对的是单线程。多线程肯定不能保证顺序。
单线程 as-if-serial 语义 – 单线程两条语句,并不一定按顺序执行。但是重排序必须保证最后执行结果的一致性。
原因:有的指令执行不占用cpu,但是耗时长;有的指令占用cpu,耗时短。
3.1 指令重排序与内存屏障
代码书写的顺序与实际执行的顺序不同,原因是编译器或处理器为了提高程序性能而做的优化。
1 | 编译器重排 |
1 | 处理器重排 |
变量全部存储在主内存。a=1 这个操作在A处理器本地缓存中处理,此时a变量最新的值没有刷新到主内存中,此时B处理器从主内存中读a的值就会返回0。同理y也可能被赋值为0。
针对这种情况,处理器提供四种内存屏障 – LoadLoadBarrier、StoreStoreBarrier、LoadStoreBarrier、StoreLoadBarrier。Load是读,Store是写。屏障前的指令必须全部执行完,才能执行屏障后的指令。
4. 对象的创建过程
1 | class T{ |
对应的汇编码
1 | 0 new #2 <T> |
0 new #2 <T>
负责在内存中申请一块内存,此时成员变量都是默认值,即m=04 invokespecial #3 <T.<inti>>
执行初始化,此时m=87 astore_1
建立t和T的关联关系
4. DCL 单例模式需要加 volatile 吗
1 | public class LHan { |
4.1 对象的创建过程
1 | class T{ |
对应的汇编码
1 | 0 new #2 <T> |
0 new #2 <T>
负责在内存中申请一块内存,此时成员变量都是默认值,即m=04 invokespecial #3 <T.<inti>>
执行初始化,此时m=87 astore_1
建立t和T的关联关系
4.2 必须要加 volatile 的原因
A -> getInstance -> new 一个对象,在创建过程中,如果指令发生重排,即先执行了 0 new #2 <T>
,然后 7 astore_1
和 4 invokespecial #3 <T.<inti>>
发生重排,此时 instance 不为null,但是变量均为默认值
B -> getInstance -> 不为null,直接返回,获取到的变量值为默认值,会发生错误