多线程与高并发day07

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

个人学习笔记,仅供参考!欢迎指正!

一、ReentrantLock源码

可以对以下代码,进行Debug跟踪,来阅读源码。

public class Day07{
    private static volatile int i = 0;

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        i++;
        lock.unlock();
    }
}

在这里插入图片描述

在这里插入图片描述

lock.lock()方法调用的是sync的lock()方法,可以看到sync是一个内部类继承自AQS,NonfairSync又继承自Sync
在这里插入图片描述
在这里插入图片描述

由此可以画出这样一个关系图:
在这里插入图片描述

  1. sync.lock();继续跟进,jdk1.8中则是调用了compareAndSetState(0,1)或者acquire(1);
    在这里插入图片描述

compareAndSetState(0,1)方法调用的又是AQS中的compareAndSetState(0,1)方法,调用的最根本的方法是unsafe类中的compareAndSwapInt(0,1),使用的是CAS的原理在这里插入图片描述

  1. jdk11中则是调用了tryAcquire(1);
    在这里插入图片描述

tryAcquire(arg)中跟进去调用的是NonfairSync的tryAcquire,则说明是子类重写了父类方法。
在这里插入图片描述

由此也可画出泳道图:
在这里插入图片描述

跟进去以后可以看到,有个getState()方法,且使用了compareAndSetState()方法。
在这里插入图片描述

跟进getState()方法后会发现调用的是AQS的方法,state是一个volatile修饰的int类型变量。
在这里插入图片描述在这里插入图片描述

state是AQS一个非常核心的数值,它的意义一般由子类来定。就像我们的例子中使用的ReentrantLock,其中state数值为1或0。当变为1时则表示当前线程获得了这把锁,什么时候释放了就从1又变为0,这里1和0代表加锁和解锁。(如果是CountDownLatch呢?可以自己读一下。)在state值基础上,跟着一个队列,是AQS自己维护的队列。

在这里插入图片描述

AQS的队列又被称为CLH队列,他是一个双向链表,链表中的节点是一个线程节点,这个Node类是AQS的一个静态内部类。这个队列也是个线程队列,它既有前驱又有后继所以是一个双向链表。所以AQS的核心就是一个state以及监控这个state的一个双向链表,链表中的节点装的是线程。哪个线程要获得这把锁、要等待都要进入这个队列。当其中某个node拿到这把锁,成功修改state之后,就说明里面这个线程持有这把锁,所以当acquire时期待值为0就直接修改为1,就拿到了这把锁,没拿到的就进入队列。
在这里插入图片描述
在这里插入图片描述

所以这里来看他的执行流程:
因为是非公平,所以上来就抢(执行tryAcquire()方法),如果没抢到则执行acquireQueued()方法(进入队列等待),
在这里插入图片描述

怎么算是抢到了?
先获取当前线程current,拿到state,如果state==0,就使用CAS的方式期望值为零把它改成1,如果成功返回true,则把独占的当前线程设置为current(说明得到这把锁,且是互斥的别人得不到);
如果当前线程已是独占线程(current == getExclusiveOwnerThread),则往后加(nextc = C+acquires),表示可重入了。

在这里插入图片描述

来看看没抢到后的执行:
在这里插入图片描述

addWaiter()方法:
EXCLUSIVE意指排他的,如果是SHARED就是共享的。首先new一个Node,以死循环表示不干成誓不罢休。把队尾先存入oldTail,将node的前置节点设置为oldTail,以CAS的方式把新节点node设置为队尾,成功则把oldTail的next设为现任队尾node。
为什么使用CAS呢?
因为要加锁,如果使用synchronized则对这整个链表加了锁,而CAS的方式相当于只是使用乐观锁,锁了队尾,粒度要细很多。
在这里插入图片描述

来谈一点细节:跟入node.setPrevRelaxed(oldTail);方法
在这里插入图片描述
在这里插入图片描述

VarHandle是JDK1.9之后才有的。例如:Object o=new Object();VarHandle就是o指向Object的引用。也就是说VarHandle也指向Object。Varhandle就是指向某个变量的引用。
在这里插入图片描述

下面是个关于VarHandle的小demo:
让Varhandle 指向T01_HelloVarHandle.classz中名为x的int类型的变量

在这里插入图片描述

可以看到,VarHandle可以进行原子操作,这才是VarHandle的意义所在。
在这里插入图片描述

再来看看accquireQueued()方法:在队列里面尝试获得锁。那他是怎么做的呢?
首先拿到node的前置节点
如果前置节点是头节点则尝试tryAcpuire()也成功了,将node设置为头节点,将之前的头节点引用指向空利于GC回收。

在这里插入图片描述

二、ThreadLocal

以下引用部分来自于:https://blog.csdn.net/u010445301/article/details/111322569 特此注明

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前> > 线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
1.因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
2.既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
下图可以增强理解:
在这里插入图片描述

ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本
,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
原文链接:https://blog.csdn.net/u010445301/article/details/111322569

来看一个简单的Demo:

第一个线程睡两秒后打印tl对象,第二个线程睡一秒后设置tl对象,最后打印结果为null,说明ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

public class Day07 {
    //volatile static Person p = new Person();
    static ThreadLocal<Person> tl = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(tl.get());
        }).start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
        }).start();
    }
    static class Person {
        String name = "zhangsan";
    }
}

ThreadLocal源码

来让我们看看他是如何做到的呢?
先来看set方法:

  1. 当set时先获取当前线程t
  2. 然后获取t的ThreadLocalMap存入变量map
  3. 不为空则设置变量,将变量存入map,key为这个ThreadLocal对象,value为我们要设置的变量
  4. 若map为空则创建一个map并将值设入
    在这里插入图片描述

getMap()方法返回的是参数线程的名为threadLocals的,ThreadLocal.ThreadLocalMap类型的变量.
在这里插入图片描述
在这里插入图片描述

所以其实设置的时候是设置到了当前线程的map中去了。

ThreadLocal的应用场景:声明式事务,保证同一个Connection。

其他的诸如get();remove();createMap();等方法有待再去跟一下,印象会更深刻。

强软弱虚

java的引用有四种,强、软、弱、虚。
普通的Object o = new Object();中的引用就是强引用

定义的一个M类来做一个小实验,当垃圾回收发生时,回收对象时会调用finalize()方法,我们可以通过打印的方式来观察对象是否被回收。

public class M {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}
public class T01_NormalReference {
    public static void main(String[] args) throws IOException {
        M m = new M();
        //m = null;
        System.gc(); //DisableExplicitGC
			 //需要阻塞住当前线程,否则主线程将结束
        System.in.read();
    }
}

可以发现,这种强引用指向对象时,垃圾回收不会回收被强引用所指向的对象。

软引用在内存不够时才会被回收:
在运行参数VM options设置-Xms20M -Xmx20M来控制一下内存大小,以便于实验。

public class T02_SoftReference {
    public static void main(String[] args) {
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
        //m = null;
        System.out.println(m.get());
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());

        //再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
        byte[] b = new byte[1024*1024*15];
        System.out.println(m.get());
    }
}

//软引用非常适合缓存使用,不过一般我们使用redis

弱引用遭到gc就会回收:

public class T03_WeakReference {
    public static void main(String[] args) {
        WeakReference<M> m = new WeakReference<>(new M());
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
        ThreadLocal<M> tl = new ThreadLocal<>();
        tl.set(new M());
        tl.remove();
    }
}

弱引用一般使用在容器里:
在这里插入图片描述

在ThreadLocal中的应用:
在这里插入图片描述

在这里插入图片描述

所以当ThreadLocal不用时,一定要记得remove();

虚引用:当对象被回收时,通过Queue可以检测到(被回收,会有值放入队列中,也就是说如果队列中有内容就说明有虚引用被回收了),然后清理堆外内存。
在这里插入图片描述

虚引用里面的值是get不到的:

public class T04_PhantomReference {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
    
    public static void main(String[] args) {
        PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
        new Thread(() -> {
            while (true) {
                LIST.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }).start();
        new Thread(() -> {
            while (true) {
                Reference<? extends M> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null, 弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。

jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存, 而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收, 相应的用户线程会收到通知并对直接内存进行清理工作。

事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

一个小细节:
现在java也可以对内存进行操作:
在这里插入图片描述
在这里插入图片描述


总结

留个作业,去读unlock()源码,WeakHashMap源码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值