Loading

线程一些问题

date: 2020-09-28 11:08:00
updated: 2020-09-21 18:15:00

线程一些问题

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. 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
  3. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  4. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递需要通过主内存来完成。

原理:
线程1对共享变量的修改要想被线程2及时看到

  1. 把工作内存1中更新过的共享变量刷新到主内存中
  2. 将主内存中最新的共享变量的值更新到工作内存2中

实现方式:

  1. synchronized
  2. volatile

pic1

2.1 synchronized 实现可见性以及原子性

JMM关于synchronized的两条规定:

  1. 线程解锁前(退出synchronized代码块之前),必须把共享变量的最新值刷新到主内存中,也就是说线程退出synchronized代码块值后,主内存中保存的共享变量的值已经是最新的了
  2. 线程加锁时(进入synchronized代码块之后),将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)

两者结合:线程解锁前对共享变量的修改在下次加锁时对其他线程可见

根据以上推出线程执行互斥代码的过程:
1.获得互斥锁(进入synchronized代码块)
2.清空工作内存
3.从主内存拷贝变量的最新副本到工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存
6.释放互斥锁(退出synchronized代码块)

2.2 volatile 实现可见性

volatile 特性:

  1. 能够保证volatile变量的可见性
  2. 不能保证volatile变量复合操作的原子性

如何实现内存的可见性?

  1. 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 => 写后存(store)
  2. 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 => 读前读(load)

通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

为什么不能保证volatile变量复合操作的原子性?
通俗的讲,volatile不能加锁,对number++;的操作会被多个线程交叉执行,导致出现不同的结果

2.4 synchronized 和 volatile 区别

  1. volatile 不需要加锁,更轻量级
  2. volatile 只保证可见性,不保证原子性

3. 线程的 as-if-serial

这个主要针对的是单线程。多线程肯定不能保证顺序。

单线程 as-if-serial 语义 -- 单线程两条语句,并不一定按顺序执行。但是重排序必须保证最后执行结果的一致性。

原因:有的指令执行不占用cpu,但是耗时长;有的指令占用cpu,耗时短。

3.1 指令重排序与内存屏障

代码书写的顺序与实际执行的顺序不同,原因是编译器或处理器为了提高程序性能而做的优化。

编译器重排
// 优化前
int x = 1;
int y = 2;
int a1 = x * 1;
int b1 = y * 1;
int a2 = x * 2;
int b2 = y * 2;

// 优化后
int x = 1;
int y = 2;
int a1 = x * 1;
int a2 = x * 2;
int b1 = y * 1;
int b2 = y * 2;
CPU只读一次的x和y值。不需反复读取寄存器来交替x和y值。
处理器重排
// 初始化
int a = 0;
int b = 0;
int x = 0;
int y = 0;

// 处理器A执行
a = 1;
x = b;
// 处理器B执行
b = 2;
y = a;

pic2

变量全部存储在主内存。a=1 这个操作在A处理器本地缓存中处理,此时a变量最新的值没有刷新到主内存中,此时B处理器从主内存中读a的值就会返回0。同理y也可能被赋值为0。

针对这种情况,处理器提供四种内存屏障 -- LoadLoadBarrier、StoreStoreBarrier、LoadStoreBarrier、StoreLoadBarrier。Load是读,Store是写。屏障前的指令必须全部执行完,才能执行屏障后的指令。

4. 对象的创建过程

class T{
    int m = 8;
}
T t = new T();

对应的汇编码

0 new #2 <T>
3 dup
4 invokespecial #3 <T.<init>>
7 astore_1
8 return

0 new #2 <T> 负责在内存中申请一块内存,此时成员变量都是默认值,即m=0
4 invokespecial #3 <T.<inti>> 执行初始化,此时m=8
7 astore_1 建立t和T的关联关系

4. DCL 单例模式需要加 volatile 吗

public class LHan {
    private static volatile LHan instance; // 必须要加 volatile
    private LHan(){}
    public static LHan getInstance(){
        if(instance == null){ // 这一层判断是为了提高效率。锁竞争很耗费时间和效率,避免多个线程每一次getInstance都要进入到同步代码块
            synchronized(this){
                if(instance == null){
                    instance = new LHan();
                }
            }
        }
        return instance;
    }
}

4.1 对象的创建过程

class T{
    int m = 8;
}
T t = new T();

对应的汇编码

0 new #2 <T>
3 dup
4 invokespecial #3 <T.<init>>
7 astore_1
8 return

0 new #2 <T> 负责在内存中申请一块内存,此时成员变量都是默认值,即m=0
4 invokespecial #3 <T.<inti>> 执行初始化,此时m=8
7 astore_1 建立t和T的关联关系

4.2 必须要加 volatile 的原因

A -> getInstance -> new 一个对象,在创建过程中,如果指令发生重排,即先执行了 0 new #2 <T>,然后 7 astore_14 invokespecial #3 <T.<inti>> 发生重排,此时 instance 不为null,但是变量均为默认值

B -> getInstance -> 不为null,直接返回,获取到的变量值为默认值,会发生错误

posted @ 2020-10-22 12:01  猫熊小才天  阅读(137)  评论(0编辑  收藏  举报