多线程的宏观和微观视角

首先我们在做并发编程的的时候会考虑到原子性丶可见性和有序性,在宏观上会考虑到安全性丶活跃性和性能;

微观视角

  • 可见性: 一个线程对共享变量的修改,另外一个线程能够立刻感知到,我们称为可见性;
  • 原子性: 一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性;
  • 有序性: 就是我们代码的执行顺序,依赖等。(指令重排导致顺序被打乱);

  线程工作内存: 是指 Cpu 的 '寄存器''高速缓存',线程的 工作内存/本地内存 是指cpu的寄存器和高速缓存的抽象描述,数据读取顺序优先级 是:寄存器->高速缓存->内存


宏观视角

  • 安全性: 安全性我认为其实是包含了原子性丶可见性和有序性的,是一个总的概念,在程序开发的时候首先要注重安全性,会在下面详细解释这三点;
  • 活跃性 活跃性告诉我们的是要避免死锁,饥饿和活锁; --- 死锁:这个都不陌生,线程A持有1锁,等待获取2锁,线程B持有2锁,等待获取1锁,这就是个典型的死锁,就就是阻塞了。 --- 饥饿:饥饿当多线程获取锁都得时候,总有线程没有机会获取到锁,出现饥饿的三中情况:1-高优先级的线程吞噬了低优先级线程的CPU使用权 2-线程被一直阻塞(比如Thread.Sleep) 3-等待线程永远不被唤醒,也可以理解为锁的优先级,我们常用的synchronized就是非公平锁,例如线程A,B,C按顺序获取锁1,首先是A获取到了锁,执行完临界区代码释放了锁,这是线程D来了直接获取到了锁,这就是非公平锁;ReentrantLock()默认是非公平锁,可以在ReentractLock(true)创建公平锁; --- 活锁:在生活中A和B同时进入左手门,为了不发生碰撞,A和B互相礼让同时进入了右手门,为了不发生碰撞又进入了左手们会一直循环下去,实例代码找到适用的场景在增加
  • 性能 1:延迟: 延迟就是指一个请求调用到返回所使用的时间,时间越短,程序的处理的就越快,性能也就会高; 2:吞吐量: 吞吐量就是值在单位时间内(秒)处理的请求数量,吞吐量越大,程序处理的请求就越多,性能也越好;

可见性:线程工作空间导致可见性问题

  例如:线程A在主存中年将变量age=0拉去到自己的工作内存中,然后做了age = 5,当然这个操作是在cpu的寄存器中进行的,然后写会高速缓存中,这时线程A的高速缓存还未执行同步主内存的操作,线程B又将age=0从主存拉取到了线程B的工作内存中,导致A线程已经更新但是B线程看不到的可见性问题;

原子性:线程切换导致原子性问题 ++count

  例如:当线程A从主内存中将共享变量Count加载到线程A的工作内存后,发生了线程切换,这个时候线程B也将共享变量Count从主内存加载到了线程B的工作内存,这时线程A和B的工作内存中count都是0,线程B执行了Count = Count + 1,然后写回到主内存,这时候线程切换完成,回到了线程A再次执行 Count = Count + 1,再将线程A工作内存计算过的count写回主内存,现在我们得到的主内存呢中Count值是1而不是2。

有序性:指令重排导致有序性问题;

在这里讲一个例子,就是获取单例双重检查锁(double-checked locking)判断:

/**
 * @Auther: lantao
 * @Date: 2019-03-28 14:32
 * @Company: 随行付支付有限公司
 * @maill: [email protected]
 * @Description: TODO
 */
public class Test1 {
    
    private DoMain doMain;
    
    public DoMain getDoMain(){
        if(doMain == null){
            synchronized (this.getClass()){
                if(doMain == null){
                    doMain = new DoMain("");
                }
                return doMain;
            }
        }else{
            return doMain;
        }
    }
}

复制代码

  在上边的代码中在synchronized内和外都有一个if判断,判断doMain是否为null操作,有很多人对synchronized中的if null判断不理解,其实可以这样想,线程A和线程B都执行到了synchronized这里进行竞争锁,结果A得到锁,判断if null,结果还未实例化,继续进行实例化,然后return对象并释放锁,这时线程B获取到了锁进入if null判断,发现doMain已经被线程A实例化过了,直接返回实例即可,第二个if null的作用就在这里;

看上去上边的代码是完美的,但是new的操作上我们理解是:

  • 创建内存M
  • 在内存M上初始化doMain对象
  • 将内存M的地址指向变量doMain

但是实际上优化后(指令重排)的执行路径可能是这样的:

  • 创建内存M
  • 将内存M的地址指向变量doMain
  • 将内存M的地址指向变量doMain

%E6%9C%89%E5%BA%8F%E6%80%A7%E9%97%AE%E9%A2%98.png

博客地址:lantaoblog.site

猜你喜欢

转载自juejin.im/post/5d53d08751882579675ed330