JUC并发编程底层原理

JUC并发编程底层原理

1. 并发的三大特性

  • 可见性
  • 有序性
  • 原子性

2. 可见性

2.1 并发并行

并发是一个处理器处理多个任务,会分配时间片给多个任务,看起来是多个任务一起执行

并行是多个处理器处理多个任务

2.2 JMM模型

2.2.1 工作流程
1. jmm用的内存共享模型是,每个线程都有一个本地内存,会缓存一些全局变量。
2. 如果线程对某个全局变量做了修改,其实就是对主内存和这个线程的本地内存做了修改,但不会更新到其他线程的本地内存;由于其他线程会从自己的本地内存读取(本地就相当于每个线程自己的缓存),所以其他线程可能会发生脏读,也就是可见性的问题
3. 可用使用内存屏障保证可见性
2.2.2 线程结束把值刷回主存,还是立马刷回主存

不是立马刷回主存,使用内存屏障回立马刷回主存

2.2.3 本地内存什么时候过期

跟redis一样,都有缓存的过期策略;本地内存过期就从主存拿数据

2.2.4 可见性的两种方式
2.2.4.1 内存屏障
  • 原理:linux系统 x86版本,jvm底层实现它其实不会去调硬件的内存屏障,而且通过c++语言会去执行一个lock的前缀指令,然后会立马刷新内存(“lock; addl $0,0(%%rsp)” : : : “cc”, “memory”)
1 inline void OrderAccess::storeload() { fence(); }
2 inline void OrderAccess::fence() {
3 if (os::is_MP()) {
4 // always use locked addl since mfence is sometimes expensive
5 #ifdef AMD64
6 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
7 #else
8 __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
9 #endif
10 }
  • 如何使用:像lock锁、synchronized锁和volatile它们的底层都会去调jvm层面的内存屏障
2.2.4.2 线程上下文切换
  • 原理:上下文切换会有几十毫秒的等待时间,当切换回来时,线程重新载入数据时,本地内存可能存在已过期的情况
2.2.4.3 CPU缓存架构&缓存一致性协议
  • 总线窥探(MESI)

    当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。

    Write-invalidate,值被修改,过期其他缓存(这种cpu用的比较多)

    Write-update,值被修改,更新其他缓存

    mesi,从主内存加载值,标记为独享状态,其他线程也从主内存加载这个值,标记为共享状态;c线程修改这个值,c线程的状态为修改状态,其他线程的本地内存状态为无效状态,下次加载时,会从主内存读取。

  • 总线仲裁机制
    类似于串行化的原子性操作,用的不多了解即可

3. 有序性

3.1指令重排序

不管是jvm还是汇编层面,都会在不影响程序结果的情况下,对代码执行顺序进行优化,此过程叫指令的重排序。(内存屏障可以禁止重排序)

3.2 原理

因为有指令重排序的情况存在,在单线程执行下不影响流程,多线程则会因为执行顺序的变化影响流程

例:
Order order = new Order();
代码顺序:
1. 在jvm堆中开辟一个内存给new Order对象
2. 在内存中创建order对象
3. order变量指向 内存地址
重排序后的执行顺序:
1. 在jvm堆中开辟一个内存给new Order对象
2. order变量指向 内存地址
3. 在内存中创建order对象

结论:因为第三步和第二步的顺序调换,如果此时有其他线程判断order == null,因为重排序的原因,代码执行完第二步,order == null 会为true,但其实这个内存中还没有创建这个对象,只是有了这个地址;如果禁止重排序,那么必须等第三步执行完,order == null 才会为true。

4. 集合

4.1 ArrayList

元素有放入顺序,元素可重复 。底层采用数组来实现的,需要维护下标,增删慢(尾部增删也快)查询快。

4.2 LinkedList

元素有放入顺序,元素可重复 。底层采用双向链表来实现的,有指针指向,增删快查询慢。

4.3 HashSet(Set)

元素无放入顺序,元素不可重复;底层采用HashMap来实现,用hashmap的keys做列表。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    private static final Object PRESENT = new Object();
    
    public boolean add(E e) {
         // add方法直接用的hashmap的put。key为add的值  value是一个常量不用关注。
        return map.put(e, PRESENT)==null;
    }
    }

4.4 HashMap

存储结构: jdk1.7用数组、链表。1.8用数组、链表、红黑树。

put原理: 首先会对put的key值进行hash运算然后取模,得到下标,如果这个下标的数据没有值就直接赋值,如果出现hash冲突,就以链表的方式存储(1.8尾插。1.7头插),如果数组容量超过阈值就进行扩容,扩容会重新计算hash;1.8如果链表长度超过8,就会变成红黑树来存储。

为什么多线程put会导致cpu百分百: 1.7的头插法,会导致最后进来的值在链表的头部,即ab两个值,a先进b后进。链表的顺序就是b-》a,如果扩容重新计算hash,hashmap用的是单向链表,只能从链表头部遍历拿值,重新计算后的链表顺序可能为a-》b,顺序发生了颠倒;多线程的场景下,线程1put的时候在扩容之前链表的顺序b-》a,同时间线程2put扩容完成后顺序变成了a-》b,导致循环指向。1.8的尾插法,扩容不会发生链表顺序的调换,所以不会出现这种场景。

为什么数组长度一定是2的次幂:为了&运算,增加hash运算的散列性和效率,方便数据迁移,旧数据不需要重新计算,要么是原下标,要么是原下标加上扩充的长度就等于扩容后的下标。

思维拓展:库分表不是经常会根据某个id进行hash运算取模得出库或者表的下标,如果下标不够用了需要扩容,就需要数据迁移,有点类似hashmap底层一样,那是不是可以模仿hashmap底层和扩容方式,用2的n次方作下标的数量,用&计算代替取模,这样下次数据迁移就不用计算下标了,要么是原来的下标,要么是原来的下标+n(原来的下标数量),这样下次分库分表扩容的时候 数据迁移会方便很多 不用重新计算下标

4.5 ConcurrentHashMap

特点:并发安全的HashMap ,比Hashtable效率更高;Hashtable是直接把整个put方法加上synchronized;

1.7原理:对比Hashtable,ConcurrentHashMap的优点是锁的粒度比较小,采用是的分段锁,数据结构对比hashmap也有所改变,把每个数组对应的链表,换成了Segment(就是一个hashmap);在put的时候,先会算出下标,确定put到哪个Segment对象,如果这个Segment对象为null,就初始化;然后对这个Segment对象的put方法加锁,扩容也只会对Segment对象的数组扩容,ConcurrentHashMap的数组不会扩容。
思维拓展:

1.不管是1.7还是1.8的锁,只要是非阻塞,在未获取到锁的时候;总会让线程去做点别的事情,比如帮助扩容,比如1.7的提前计算下标,提前遍历链表看有没有重复的k(如果k重复就直接更换value就行),比如提前创建一些对象,那么我们在写业务代码的时候,如果有个接口用到了非阻塞锁+循环获取锁的情况下。在没有获取到锁的情况下可以提前去做一些别的事情,new对象或者查数据库或者一些其他的计算和判断,在不影响逻辑的情况在,提前去做,等获取到锁了,就可以省略这些步骤,提高接口整体性能。

2.分段锁的思想可以好好利用,可以减少锁竞争。不管是1.7的Segment还是1.8的addcount都是采用把一个任务分成很多个小任务,同时不影响正常的业务逻辑。

1.8原理:1.8采用的是cas+synchronized锁,首先一进来就是一个死循环,判断有没有初始化,没有的就去初始化,然后计算k的数组下标,如果这个下标的数组为空,就用cas的赋值,如果不为空,就用synchronized锁住,然后判断这个节点是红黑树还是链表,然后在put进去。然后还会判断这个map是不是在扩容,如果是就会帮助扩容;还会判断是否需要树化等;

​ 最后就是addcount了,对map的长度+1,ConcurrentHashMap是有两种计数器,一个是basecount,一个是cellcount,cellcount是很多个,放在一个数组里面,ConcurrentHashMap的size方法就是把这两种计数器相加,就得到长度,addcount首先是所有线程都用cas的方式对basecount+1,如果没抢到锁,就计算下标,去竞争这个下标的cellcount,对cellcount+1,也是cas的方式;有点像1.7的分段锁;

​ 如果到达一定长度就会进行扩容,ConcurrentHashMap的扩容的多线程的,而且是一个下标一个下标的进行数据迁移,如果对5这个下标进行扩容,首先会在这个下标创建一个ForwardingNode对象,代表这个下标正在扩容,其他线程帮助扩容的时候就会跳过这个下标,然后在数据迁移的时候会用synchronized锁锁住这个下标,其他线程无法往这个下标put值。

5. 线程池

入参:核心线程数、最大线程数、超时时间、队列、拒绝策略

原理:每个任务都会交给核心线程去处理,如果核心线程满了,就放队列里面,队列满了就继续创建线程;如果达到最大线程数,剩下的任务就交给拒绝策略;如果任务核心线程能够处理完成,那剩下的线程达到一定的过期时间还没任务处理,就自动销毁。jdk提供的拒绝策略有4种,放弃剩下任务、放弃任务并抛异常、交给主线程处理、放弃旧任务处理新任务、自定义拒绝策略

CPU密集型任务: 务最佳的线程数为 CPU 核心数的 1~2 倍

IO密集型任务: 线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间),具体线程数根据实际场景不断压测调整。

5.1 ArrayBlockingQueue

基于数组的有界阻塞队列,生产者和消费者用了同一把ReentrantLock锁,用了不同的条件队列来阻塞生产者消费者。采用了环形数组的方式,当消费者取值时,不会删除掉该元素,而是直接设置为null。

5.2 LinkedBlockingQueue

基于链表结构的阻塞队列,不设置容量大小时;为无界队列,生产者和消费者用了不同ReentrantLock锁,用了不同的条件队列来阻塞生产者消费者。因为存取用了不同的锁,效率会高于ArrayBlockingQueue队列。

5.3 SynchronousQueue

采用了cas+自旋的操作,提供了公平和非公平链表(先进先出和先进后出),队列容量是0,用于传递性场景

5.4 PriorityBlockingQueue

无界队列,生产者和消费者用了同一把ReentrantLock锁,优先级高的先出队,用于有高低优先级的场景。

5.5 DelayQueue

入队不阻塞,出队为空时阻塞,用于延迟的场景。

6. cas

原理:会传一个原值和修改的值,首先会判断这个原值和内存中对应的值是否相等,如果不相等那就返回内存中的值,如果相等就把内存中的值修改成要更新的值;这个读和写的操作是原子性的,并且多个线程去cas操作,只有一个线程会成功,同时其他失败的线程不会是阻塞状态;一般失败的线程会自旋操作,直到cas成功。

jdk原理:汇编层面就是调用了原子指令来进行cas操作,虽然满足了原子性,但是有序性和可见性不能保证;所以在原子指令前面还加了一个lock前缀指令,也就是内存屏障来保证可见性和有序性,同时jdk是直接回返的Boolean值。

缺点

  1. 自旋操作比较消耗cpu性能
  2. aba问题,可以使用版本号解决
  3. 只能保证一个共享变量原子操作

7.synchronized

7.1 对象内存布局

对象头: 有Mark Word和klass类型指针组成,Mark Word包含锁状态和偏向线程id和hashcode码;klass类型指针虚拟机通过这个指 针来确定这个对象是哪个类的实例。

实例数据:存放类的属性数据信息,包括父类的属性信息。

对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。

7.2 偏向锁

在没有其他线程竞争,只有一个线程获取锁的时候,就是偏向锁状态

7.3 轻量锁

采用cas自旋的方式去获取锁,在用户态执行。

7.4 重量锁

阻塞其他线程的方式,在内核态执行。

7.5 锁升级的过程

new一个对象的时候,如果此时偏向锁没有开启,那么是无锁状态,如果开启则是偏向锁状态,只不过是匿名偏向,还没有线程id。如果只有一个线程获取锁,那么此时是线程id是有记录的,如果发生如果线程竞争,同时线程数量很多或者等待时间太长,会之间从偏向锁升级成重量锁;反之,会升级成轻量锁,同过cas的方式去自旋获取锁,当自旋到达一定次数;升级成重量级锁,阻塞其他线程,放入等待队列。

8.AQS

组成

  1. 有一个voliate修饰state的,标志是否持有锁(0 无锁,大于0有锁)
  2. 同步队列,双向链表,获取锁失败后放入这个队列
  3. Condition条件队列,单向链表,线程等待时进入条件队列并且释放锁;如果被唤醒,则进入等待队列去竞争锁

特性

  • 阻塞等待队列

  • 共享/独占

  • 公平/非公平

  • 可重入

  • 允许中断

原理:AQS是一个抽象同步框架, Lock、 CountDownLatch、Semaphore 等都用到了 AQS,采用模板设计模式,把独占锁/共享锁的加锁解锁交给子类实现;

独占锁-加锁/解锁后逻辑 :首先判断加锁是否成功,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态封装成一个Node节点,通过cas+死循环的方式加入到同步队列中,采用的尾插,同时阻塞该线程,当同步状态释放时,会把头节点唤醒,使其再次尝试获取锁。

共享锁-加锁/解锁后逻辑 :加锁逻辑和独占锁一样,释放锁时,会把头节点唤醒,使其再次尝试获取锁。如果获取锁成功,就会唤醒下一个线程,直到后续线程获取锁失败,继续阻塞。

await: 在Condition接口中,首先把这个线程添加到条件队列中(不存在就创建队列,没有用cas的方式,因为await方法必须在独占锁中使用,是线程安全的),然后释放掉这个线程的独占锁,并阻塞该线程。

signalAll:在Condition接口中,把条件队列的节点,全部迁移到等待队列中。

8.1 ReentrantLock

加锁

1. NonfairSyncReentrantLock的内部类
    // NonfairSync 非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // cas的方式修改State的值(也就是用cas的方式获取锁)
            if (compareAndSetState(0, 1))
                // 获取锁成功的线程设置为当前线程 
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 这个方法是aqs的获取锁的方法,里面调用了tryAcquire方法
                acquire(1);
        }

        // 重写aqs的独占锁获取锁的方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
2.  nonfairTryAcquire方法具体实现
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取aqs的State值(等于0代表无锁)
            int c = getState();
            if (c == 0) {
                // 无锁 cas一次,如果改成功则获取锁成功
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    
          /**
           * 1.current(当前线程) 是否等于 getExclusiveOwnerThread(获取锁的线程)
           * 2. 如果相等那么是重入锁状态,State+1写入
           * 3. 由此可以看出ReentrantLock中,state等于0代表无锁,state大于0代表有锁
           * state的次数代表重入的次数
           */
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
3. 非公平锁实现
// 其他逻辑和公平锁一样,获取锁的时候多了一个hasQueuedPredecessors()方法,判断同时是头节点(或者等待队列没有线程的时候)的情况下 才会取尝试获取锁
if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }

解锁

// 1. 直接把State设置为0   2. 把当前获取锁的线程标记 设置为null     
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

原理总结 :ReentrantLock重写了aqs独占锁的加锁解锁方法,采用cas的方式获取锁,同时支持可重入锁;state等于0代表无锁,state大于0代表有锁, state的次数代表重入的次数;公平锁就是只取等待队列的头部线程去获取锁(或者队列没有线程),非公平锁,会进行两次cas去获取锁;第二次获取失败,add到等待队列;解锁就是把state设置为无锁,把当前获取锁的标记设置为null。

使用场景: 用于单线程执行同时支持可重入。

8.2 Semaphore

加锁

      // 非公平锁 初始化的时候需要传一个信号量
      NonfairSync(int permits) {
            super(permits);
        }

        // 重写aqs的共享锁 方法
        protected int tryAcquireShared(int acquires) {
            // 小于0获取锁失败 
            return nonfairTryAcquireShared(acquires);
        }
 
 
 final int nonfairTryAcquireShared(int acquires) {
     // 一进来就是死循环
            for (;;) { 
                // 获取 State
                int available = getState();
                // State-acquires
                int remaining = available - acquires;
                // remaining小于0 代表没有资源 或者 cas成功 退出循环
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

// 公平锁的实现
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                // 其他逻辑一样 多了一个判断是否头节点
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

解锁

        protected final boolean tryReleaseShared(int releases) {
            // 死循环的方式+releases 然后cas的方式设置到内存
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

原理总结 :Semaphore重写了aqs共享锁的加锁解锁方法,初始化的时候传一个资源量,获取锁的时候通过死循环+cas的对资源量减少,直到cas成功或者资源小于0,小于0获取锁失败;大于0获取成功。解锁的时候对资源量进行回加操作。

使用场景: 用于限流等场景。

8.2 CountDownLatch

await

  public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

------------------------------------------------------------------------------------------------------------
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 如果State != 0 就放入阻塞队列
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

------------------------------------------------------------------------------------------------------------
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

countDown

    public void countDown() {
        sync.releaseShared(1);
    }

------------------------------------------------------------------------------------------------------------
       public final boolean releaseShared(int arg) {
    // cas+自旋的方式 State-- 如果State cas后等于0 就唤醒等待的线程线程
        if (tryReleaseShared(arg)) {
            //  唤醒线程
            doReleaseShared();
            return true;
        }
        return false;
    } 

------------------------------------------------------------------------------------------------------------
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

原理总结 :CountDownLatch用的共享锁,调用await方法,如果State不等于0,阻塞该线程,其他线程用过countDown方法,对State自减,自减后等于0,就唤醒等待的线程线程

使用场景: 用于一个线程等待其他线程完成操作。

8.3 CyclicBarrier

await

 private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
          // 里面使用了ReentrantLock
        final ReentrantLock lock = this.lock;
         // 加锁
        lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            // 对count-1
            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        // 如果count==0 那么唤醒所有等待的线程 放入等待队列
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        // 阻塞线程 并放入条件队列
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    private void nextGeneration() {
        // 唤醒所有等待线程  条件队列 转到等待队列
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

原理总结 :用了ReentrantLock和条件队列,先用ReentrantLock加锁,并对计数器减减操作,如果计数器等于0 就唤醒所有线程(吧把条件队列的节点转到同步队列),并把计数器重置,如果不等于0 就把这个线程阻塞放入条件队列,并且释放锁。

使用场景: 用于多个线程阻塞,当阻塞数量到达指定值时就放行。

8.4 ReentrantReadWriteLock

// 写锁 加锁
        protected final boolean tryAcquire(int acquires) {
            
            Thread current = Thread.currentThread();
            // 获取读写锁的状态
            int c = getState();
            // 获取写锁的状态
            int w = exclusiveCount(c);
            
            if (c != 0) {
                // 写锁未加锁 || 当前线程不等于 获取写锁的线程 (因为写写 和写读互斥,所以 只要State被上锁, 如果不是重入写锁的情况下都默认失败。)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 重入锁 设置重入次数
                setState(c + acquires);
                return true;
            }
            // 公平锁需要先入等待队列 || cas操作尝试获取写锁
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
           
            setExclusiveOwnerThread(current);
            return true;
        }
// 写锁 解锁
      protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                // 获取写锁的线程 设置null
                setExclusiveOwnerThread(null);
      // 设置写锁的状态
            setState(nextc);
            return free;
        }
// 读锁 加锁
      protected final int tryAcquireShared(int unused) {
          
            Thread current = Thread.currentThread();
            int c = getState();
          // 写锁已被加锁 && 获取写锁的线程不是当前线程
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
          // 获取读锁状态
            int r = sharedCount(c);
          // 公平锁先入等待队列 && 读锁小于 读锁最大次数 && cas上读锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // r==0代表首次上读锁
                if (r == 0) {
                    // 设置 第一个读锁的 线程和重入次数
                    firstReader = current;
                    firstReaderHoldCount = 1;
                    // 首次读锁记录 == 当前线程
                } else if (firstReader == current) {
                    // 首次读锁重入次数+1
                    firstReaderHoldCount++;
                } else {
                    // 不是首次的读锁 用ThreadLocal记录读锁的重入次数
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
          // cas上读锁失败的情况下 循环去获取读锁
            return fullTryAcquireShared(current);
        }
// 读锁 解锁
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            // 如果是首次的读锁 
            if (firstReader == current) {
                // 首次读锁重入次数==1
                if (firstReaderHoldCount == 1)
                    // 线程记录设置null
                    firstReader = null;
                else
                    // 重入次数-1
                    firstReaderHoldCount--;
            } else {
                // 非首个读锁 也是一样的逻辑 在ThreadLocal里处理重入次数 和线程记录
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            // 死循环用cas操作 释放读锁
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

原理总结 :写写互斥读写互斥 读读不互斥,利用State的高低位来记录写锁和读锁;读和写锁都支持可重入,读锁的重入次数用ThreadLocal存储,写锁采用的是独占锁,读锁是共享锁;写锁加锁时除开重入写锁,只要有读锁或者写锁都加锁失败。读锁加锁时除开重入写锁,只要有写锁就加锁失败。

使用场景: 读多写少的场景。

9. ThreadLocal

9.1 原理

public
class Thread implements Runnable {


    // 线程对象里面的ThreadLocalMap  所以说这个map不是共用放在ThreadLocal里,而是放在线程里面的。
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    
--------------------------------------------------------------------------------------
public class ThreadLocal<T> {
    // get方法
    public T get() {
       // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程里的ThreadLocalMap  (t.threadLocals;)
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // ThreadLocalMap的key是ThreadLocal对象 所以这里get方法直接get(this)就能拿到value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 为空就设置初始化的value
        return setInitialValue();
    }
    
    public void set(T value) {
          // 获取当前线程
        Thread t = Thread.currentThread();
         // 获取线程里的ThreadLocalMap  (t.threadLocals;)
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 设置value
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
}

用一个map装所存储的值,这个map就是ThreadLocalMap,这个map不是放在ThreadLocal里面,而是放在Thread中,作为一个属性和内部类,它的key就是ThreadLocal对象,是弱引用,value就是存的值,当调用ThreadLocal的get方法时,会先从Thread中拿到map,然后get(this)就可以拿到value值。

这样设计的好处就是一个Thread对应一个map,就不用所有的线程都用同一个map,从而产生线程不安全问题,然后加锁导致性能低。

9.2 为什么会内存泄漏

当ThreadLocal对象被回收,然后线程一直处于工作状态就会出现,因为线程不销毁,map就一直存在,然后ThreadLocal对象被回收就无法使用map又无法回收。可以使用remove方法来避免。

9.3 为什么要用弱引用

ThreadLocalMap的key使用了弱引用,而它的key是ThreadLocal,也就是说,就算ThreadLocal对象存在,但是它的value和线程不存在了,这个map也会被回收。

如果不使用弱引用,就算value和线程都被回收了,只要ThreadLocal对象存在,那么这个线程的ThreadLocalMap还是不会回收,造成资源浪费,因为ThreadLocal本来就是线程共用的,基本不会销毁,或者说我a线程的map不用了,为什么要等其他线程线程和ThreadLocal对象销毁,应该立马清除掉a线程的map。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值