多线程学习笔记

一、什么是CAS?

CAS的全称叫做:campare and swap。也就是比较并且交换。它可以在没有锁的情况下对一个值进行更新,
例如我们要对一个值(如该值为0)进行自增操作,过程如下:

  1. 我们先读取这个值(我们记这个值为E=0),如果然后对该值进行自增操作(V=E+1=1)。
  2. 然后再次读取原来的那个值,如果还是为0,则该值没有被其他人操作过,那么我们可以放心的进行改写操作(这里就会引申出ABA问题)。
  3. ABA问题:在检查操作的时候,有一个线程把值反复的修改(如0-1-0)看似值没有发生变化,但实际上却有过变化!
  4. 解决ABA问题的好办法就是使用版本号,在变量面前追加版本信息,每次变量更新的时候就把版本号+1。JDK中的Atomic包里提供的类AtomicStamptedReference就是用来解决该问题的。

CAS本质实现:
CAS在jdk的源码中我们能够发现都是带有 native 关键字的CAS操作。是需要到JVM上去运行的,我们可以再去找 unsafe.cpp 的代码的CAS的操作,深入查看最后跟踪到汇编语言,在汇编语言中有"cmpxchg" 这么一条,顾名思义,即为"compare and exchange",依旧是比较并交换。我们可以看到,CAS在底层也有对应的一条汇编指令,也就是说硬件级别直接支持。
在"cmpxchg"之前,还有一条指令"LOCK_IF_MP",全称"LOCK IF MUTIL PROCESSORS",这是对于现在的机器来讲多核,如果是多核的情况还是需要加锁。
细想一下,我们在改写的时候如果另一个线程将其进行改写,那岂不是就覆盖了?在CAS的过程中(在比较完之后)被人改写了,那岂不是无法保证原子性?是的,在底层 “cmpxchg” 这条汇编指令是无法保证原子性!所以在该指令之前有额外的一条指令 “lock” ,这条指令的意思是说在执行 “cmpxchg” 的时候其他CPU不能对他进行修改,不许打断。

lock cmpxchg

二、Object object = new Object()在内存中在几个字节?

由一道面试题引发的一些问题。我们对此来详细阐述一下。

  1. 对象在内存中的布局:
    我们首先来看普通对象

    - 对象头(包含以下两个部分)
    	- markword,存储的是synchronize等一些锁的信息。
    	- class pointer,类指针,指向属于哪个类。
    - Instance data,实例数据,例如int即占用4个字节。
    	- 
    

三、volatile关键字的理解。

  1. 保证可见性,如何理解这个可见性呢?
  • 一个很简单的实例就能够体现,当一个线程在不断的while循环,循环的条件是该值为True;主线程将其修改为False,如果没有加volatile关键字的话,无法保证可见性,使该循环停下来。
  1. 若要解释volatile的底层原理的话,其对应的汇编的语句。其实只利用lock的命令操作,但是因为lock在汇编中不能单独使用需要搭配,那为什么不用nop呢?规定不能这么用。所以用了add操作但是只加了个0相当于没有操作。

     lock; addl $0,0(%%rsp)
    

    lock在其中如何保证可见性。
    【1】锁住缓存行。两个线程在写同一个缓存行的时候,当一个线程改写了数据之后就写入内存,并告诉另一个线程我改写了,你重新去内存里读取一下吧。然后另一个线程就去内存里重新读取,然后改写。
    【2】若数据过大超过缓存行大小,锁不住缓存行,锁总线。

  2. 防止指令重排序。(典型应用,单例模式双重检查锁)
    如何理解指令重排?
    例如我们写这么一段程序。

    public class T {
        public static void main(String[] args) {
            Object o = new Object();
        }
    }
    

    我们查看其ByteCode可以看到其new操作对应了三条汇编码语句
    【1】new 分配内存空间。
    【2】invoke 调用特殊方法,其调用了初始化方法。
    【3】建立关联,指向内存空间。即和“o”建立了联系。

    NEW java/lang/Object
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1
    

    就拿单例模式中的来讲,有两个线程A、B,当A进入第一层if(xxx == null)判断的时候,B线程已经上锁进行new的操作了,而我们从上可知,new操作细分为三个指令,恰好【3】和【2】指令进行重排序,然后发现并不为null,因为变量已经指向了内存空间,但是没有被初始化!是一个半初始化的状态。然后A线程就把这个值的单例给返回了,是一个错误的值!

    现在又有一个新的疑问了?为什么volatile能保证指令重排序呢,他底层不是一个lock么?lock怎么保证指令重排序的。

    这就要提到 内存屏障了,在硬件层面,不同的CPU可能设计不同的指令来保证指令之间不发生重排序的一条屏障指令,如intel,有sfence(意思:storefence,写屏障)、lfence(意思:loadfence,读屏障)、mfence。而JVM(Hotspot比较懒)用了一句通用的lock来作为代替。我们看这个lock就是一个天然的屏障,在lock前的指令先执行,在lock后的指令后执行,很完美。
    JVM在看到volatile的时候通通会加上lock来保证被修饰的变量的内存读写。例如,想要读,则有如下操作:

    lock
    load
    lock
    

四、锁升级过程

锁升级
【1】无锁new --> 偏向锁。(将自己的ID往上一贴,表明属于我自己),在图中对应了线程指针。
【2】偏向锁 --> 轻量级锁(自旋锁)。只要发生线程的竞争就会进行锁升级,例如两个线程A、B,每个线程有一个线程栈,里面会记录一个LR(lock record),谁先将LR贴上去谁获得。这个过程是通过自旋进行的。当A获得后,B线程继续在进行自旋等待,直到A线程释放为止。

五、超线程

进程是CPU分配资源的基本单位,线程是 CPU执行的基本单位。
一个ALU对应多个PC | register(寄存器)。
因为一个线程对应一个寄存器,而所谓的四核八线程说的就是一个ALU能够搭配两套这样的,不需要进行上下文切换,提高效率。

六、线程池

线程池参数:
1、corePoolSize,核心数(可以理解为主要的处理线程的数量)
2、maximumPoolSize,最大核心数(开始阻塞了,这些备用的也被用起来)
3、keepAliveTime,保持存活时间
4、时间单位
5、阻塞队列
6、线程工厂
7、拒绝策略

七、synchronized实现过程

1、字节码层级:monitorenter monitorexit
2、在jvm中自动进行锁升级
3、汇编:lock cmpxchg

八、引用

强弱软虚。
重点讲讲和线程相关的弱引用
1、ThreadLocal作为Thread当中的ThreadLocalMap threadLocals的Key存储。
2、对于ThreadLocalMap则为ThreadLocal的内部类,其中键值对Entry实现继承了弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        } 

3、其中 super(k) 表明了,key是通过弱引用指向了ThreadLocal对象,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值