ThreadLocal, volatile, synchronized, map, epoll, AQS简单总结
阅读原文时间:2023年07月13日阅读:3
  • ThreadLocal

ThreadLocal主要是为了解决内存泄漏的问题,它是一种弱引用:

引用总共有四种,,我简单列一下:

  • 强引用(Strong Reference):正常引用,根据垃圾回收算法,当这个引用存在时,就无法对引用对象进行 GC(如果根可达的话)
  • 软引用(Soft Reference):能够获取到引用对象,当发生 FGC 时,堆内存不够时,软引用会回收引用对象,应用在缓存等。
  • 弱引用(Weak Reference):能够获取到引用对象,当发生 GC 时,会回收引用对象,应用在 ThreadLocal 等。
  • 虚引用(Phantom Reference):不能获取到引用对象,作用是当引用对象被 GC 时,虚引用会获得一个系统通知,应用场景跟一般的代码无关。

ThreadLocal在set 时会:

  1. 每个Thread线程中的field创建一个 threadLocals Map,key为ThreadLocal。

    public void set(T value) {  
        Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);  
        if (map != null)  
            map.set(this, value);  
        else  
            createMap(t, value);  
    }
  2. 将ThreadLocal 和value封装的Entry,放在ThreadLocalMap的变量table Entry数组

    static class ThreadLocalMap {

    /\*\*  
     \* The entries in this hash map extend WeakReference, using  
     \* its main ref field as the key (which is always a  
     \* ThreadLocal object).  Note that null keys (i.e. entry.get()  
     \* == null) mean that the key is no longer referenced, so the  
     \* entry can be expunged from table.  Such entries are referred to  
     \* as "stale entries" in the code that follows.  
     \*/  
    static class Entry extends WeakReference<ThreadLocal<?>> {  
        /\*\* The value associated with this ThreadLocal. \*/  
        Object value;
    Entry(ThreadLocal&lt;?&gt; k, Object v) {  
        super(k);  
        value = v;  
    }  
    } /\*\* \* The initial capacity -- MUST be a power of two. \*/ private static final int INITIAL\_CAPACITY = 16; /\*\* \* The table, resized as necessary. \* table.length MUST always be a power of two. \*/ private Entry\[\] table; private void set(ThreadLocal<?> key, Object value) {
        // We don't use a fast path as with get() because it is at  
        // least as common to use set() to create new entries as  
        // it is to replace existing ones, in which case, a fast  
        // path would fail more often than not.
    
        Entry\[\] tab = table;  
        int len = tab.length;  
        int i = key.threadLocalHashCode &amp; (len-1);
    
        for (Entry e = tab\[i\];  
             e != null;  
             e = tab\[i = nextIndex(i, len)\]) {  
            ThreadLocal&lt;?&gt; k = e.get();
    
            if (k == key) {  
                e.value = value;  
                return;  
            }
    
            if (k == null) {  
                replaceStaleEntry(key, value, i);  
                return;  
            }  
        }
    
        tab\[i\] = new Entry(key, value);  
        int sz = ++size;  
        if (!cleanSomeSlots(i, sz) &amp;&amp; sz &gt;= threshold)  
            rehash();  
    }</code></pre></li>

同理,get时:

  1. 先取当前线程的ThreadLocalMap

  2. ThreadLocalMap中以threadlocal为key(其中会以key的hash定位在Entry数组中的位置)

  3. 返回Entry的value

    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }

如何解决内存泄漏问题的:
Ps: 弱引用,gc即回收,reference 弱用指向Person

    WeakReference reference = new WeakReference(new Person("weak"));  
    System.out.println("before:"+reference.get());  
    System.gc();  
    System.out.println("after:"+reference.get());

output:
before:Person{name='weak'}
after:null

 ThreadLocal tl = new ThreadLocal();

tl.set(new Object());
tl=null;
如下图,set时,ThreadLocal被tl引用,并且被localmap中Entry弱引用(Entry 继承了WeakReference)。

_当 tl=null 时,ThreadLocal不被tl所引用,当仍然被_Entry引用。
如果是用强引用的话,线程不结束,ThreadLocal永远都不会被回收,用弱引用的话,当发生gc时,ThreadLocal即被回收,避免内存泄漏

参考:

https://www.bilibili.com/video/BV117411g7ib?from=search&seid=2919186627810339242

https://zhuanlan.zhihu.com/p/155961030

  • volatile

volatile 五层实现:

https://www.bilibili.com/video/BV1wT4y137PS?from=search&seid=15046204320406766119

各层实现方式:

Java 层加volatile关键字

字节码加上ACC_volatile

JvM加内存屏障,实现4条规范

HOtspot实现 汇编 lock add指令

CPU采用缓存一致性,锁总线

基础知识:

存储器层次结构:

从各层取数据时间:

多核CPU缓存结构:

一颗CPU多核公用L3,多颗 CPU公用内存

从内存读数据到cpu是按照一块块读(缓存行,64个字节),提高效率,局部性原理(一般读到的数据,旁边的数据一般也会马上用到)。

数据一致性,缓存对齐:

public class TestDemo1 {
private static long[] arr = new long[2];

public static void main(String\[\] args) throws InterruptedException {

    Thread t1 = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (long i = 0; i < 100\_000\_0000; i++) {  
                arr\[0\]=i;  
            }  
        }  
    });

    Thread t2 = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (long i = 0; i < 100\_000\_0000; i++) {  
                arr\[1\]=i;  
            }  
        }  
    });  
    final long start = System.nanoTime();  
    t1.start();  
    t2.start();  
    t1.join();  
    t2.join();  
    System.out.println((System.nanoTime()-start)/1000000);

}  

}
输出:4556

public class TestDemo2 {
private static long[] arr = new long[16];

public static void main(String\[\] args) throws InterruptedException {

    Thread t1 = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (long i = 0; i < 100\_000\_0000; i++) {  
                arr\[0\] = i;  
            }  
        }  
    });

    Thread t2 = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (long i = 0; i < 100\_000\_0000; i++) {  
                arr\[8\] = i;  
            }  
        }  
    });  
    final long start = System.nanoTime();  
    t1.start();  
    t2.start();  
    t1.join();  
    t2.join();  
    System.out.println((System.nanoTime() - start) / 1000000);  
}  

}
输出:3115

为何demo2会比demo1 快呢?

这是因为缓存一致性原理:demo1线程每次读取64个字节,2个线程每次读取缓存行都会把arr读入缓存中,之后对arr进行赋值后通知其他的线程有改变,需要重新读取缓存行;

而demo2线程之间读取不是同一缓存行,改变后不需要重新读取值:

缓存一致性协议(MESI 协议只是其中一种,intel的):缓存行数据改变,需要通知其他读取这一缓存行数据的线程重新进行读取,是CPU级别支持的,和是否volatile无关。

Cpu为了提高效率,会乱序执行:

但有时不能乱序执行,这就涉及到了8种不能乱序的原则(jvm的规范)happens-before:

为了使指令1和2不能乱序,需要在1,2间加一堵墙,这就是内存屏障,volatile具有禁止指令重排序

  • synchronized

参考:https://www.bilibili.com/video/BV1EC4y1x74v?from=search&seid=11191200429719978919

基础知识:

CAS:compare and swap, 在底层是调用cmpxchg(CAS)指令实现, 但这条指令在回写还是可能被其他线程修改,所以还在前加lock指令

所以CAS实现原子操作时是lock cmpxchg 指令实现的。

用户态和内核态:用户态只能访问用户空间的操作指令(想要访问内核指令需要通过操作系统),内核态能访问所以指令。

synchronized早期是重量级锁,因为申请锁必须通过kernel, 系统调用。

现在synchronized已经优化了,有一个锁升级的过程。synchronized在汇编层面有monitorenter(synchronized开始),monitorexit(synchronized结束)

Markword:

查看markword工具:JOL=Java Object Layer

    <dependency>  
        <groupId>org.openjdk.jol</groupId>  
        <artifactId>jol-core</artifactId>  
        <version>0.8</version>  
    </dependency>

public class TestJavaObjectLayer {

public static void main(String\[\] args) {  
    Object obj = new Object();  
    System.out.println("查看对象内部信息=======");  
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

}  

}

output:
查看对象内部信息=======

WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

对象的内存布局:

对象在内存的布局可以分为3快区域:对象头(header)、实例数据(Instance data)和对齐填充(Padding)

1. 对象头:Hotspot虚拟机的对象头包括2个部分:markword和类型指针(classpoint)

  • markword:用于存储对象自身的运行时数据,如哈希码)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据长度在32位和64位虚拟机(未开启压缩指针)中分别为4字节和8字节,如下:
  • 类型指针:即对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,4个字节。

2. 实例数据 :存储对象真正的有效信息,即定义的各种字段内容,无论是从父类继承下来的,还是在子类中定义的,都要记录下来。

3. 对齐填充:这部分不是必然存在的,也没特别的含义,hotspot要求自动内存管理系统要求对象起始地址必须是8字节的整数倍,当对象大小不是8的倍数时,需补齐。

synchronized加锁可以通过JOL查看,只是修改了markword的值。

synchronized的锁升级过程(偏向锁未启动的过程):

  1. new 一个对象,此时偏向锁标志0,锁标志位01,此时是无锁态
  2. 当对对象加上synchronized关键字时,此时为偏向锁标志为1,此时为偏向锁状态(此时没有竞争)
  3. 当竞争轻度加剧时,偏向锁标志会撤掉,会变为自旋锁。过程:每个线程栈会有Lock Record,竞争后markword有一个指向得到锁线程的Lock Record,没有得到锁的线程继续CAS自旋。
  4. 当竞争再加剧,会向操作系统申请升级为重量级锁。这时markword指向的是一个objectmonitor。

偏向锁和自旋锁是用户态的锁:

细节:

  • synchronized是可重入锁,偏向锁状态是每进入一次在线程栈增加记录一个Lock Record,出一次则弹出一个Lock Record

等待队列在monitor即waitset中:

打开偏向锁时间可以通过参数设置,默认4秒:

拓展与之相关的wait与notify

https://www.jianshu.com/p/99f73827c616

https://www.jianshu.com/p/ffc0c755fd8d

  • map

https://m.toutiao.com/is/JxLMw7A/

https://m.toutiao.com/is/JxL2ocx/

基础IO测试Linux 中一切程序皆文件

在Linux启动下面程序,并且用命令抓取这个程序的所有子线程:

1. root@hecs-x-xlarge-2-linux-20201111105933:~/io# strace -ff -o ./ooxx java TestIOServer

  -ff 表示抓取子线程

-o输出到ooxx

public class TestIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8090);
System.out.println("step1: serverSocket new ServerSocket(8090)");
while (true) {
Socket client = serverSocket.accept();
System.out.println("client accpet:"+client.getPort());
new Thread(new Runnable() {
@Override
public void run() {

                try {  
                    InputStream input =  client.getInputStream();  
                    BufferedReader bf = new BufferedReader(new InputStreamReader(input));  
                    while (true) {  
                        System.out.println(bf.readLine());  
                    }

                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  
    }  
}  

}

可以看到有这个进程的所有线程:

oot@hecs-x-xlarge-2-linux-20201111105933:~/io# ll
total 4488
drwxr-xr-x 2 root root 4096 Nov 21 17:05 ./
drwx------ 9 root root 4096 Nov 21 17:05 ../
-rw-r--r-- 1 root root 16538 Nov 21 16:57 ooxx.3364
-rw-r--r-- 1 root root 183875 Nov 21 16:57 ooxx.3365
-rw-r--r-- 1 root root 901 Nov 21 16:57 ooxx.3366
-rw-r--r-- 1 root root 814 Nov 21 16:57 ooxx.3367
-rw-r--r-- 1 root root 779 Nov 21 16:57 ooxx.3368
-rw-r--r-- 1 root root 814 Nov 21 16:57 ooxx.3369
-rw-r--r-- 1 root root 198778 Nov 21 17:17 ooxx.3370
-rw-r--r-- 1 root root 1119 Nov 21 16:57 ooxx.3371
-rw-r--r-- 1 root root 1084 Nov 21 16:57 ooxx.3372
-rw-r--r-- 1 root root 1218 Nov 21 16:57 ooxx.3373
-rw-r--r-- 1 root root 43653 Nov 21 17:17 ooxx.3374
-rw-r--r-- 1 root root 42964 Nov 21 17:17 ooxx.3375
-rw-r--r-- 1 root root 42211 Nov 21 17:17 ooxx.3376
-rw-r--r-- 1 root root 1015 Nov 21 16:57 ooxx.3377
-rw-r--r-- 1 root root 3952124 Nov 21 17:17 ooxx.3378
-rw-r--r-- 1 root root 1044 Nov 21 16:54 'TestIOServer$1.class'
-rw-r--r-- 1 root root 1123 Nov 21 16:54 TestIOServer.class
-rw-r--r-- 1 root root 1191 Nov 21 16:54 TestIOServer.java

2. 通过grep 查找哪个线程输出 grep 'step1' ./*

root@hecs-x-xlarge-2-linux-20201111105933:~/io# grep 'step1' ./*
./ooxx.3365:write(1, "step1: serverSocket new ServerSo"…, 42) = 42
Binary file ./TestIOServer.class matches
./TestIOServer.java: System.out.println("step1: serverSocket new ServerSocket(8090)");

可以看出3365这个线程写的。可以 vi ooxx.3365 找到相应的行

3. 通过jps查看Java进程

root@hecs-x-xlarge-2-linux-20201111105933:~/io# jps
3364 TestIOServer
9496 Jps
1101 WrapperSimpleApp

3.1. 切换到3364进程目录: cd /proc/3364/, 里面有两个重要文件夹 fd 和 task

root@hecs-x-xlarge-2-linux-20201111105933:/proc/3364/fd# ll
total 0
dr-x------ 2 root root 0 Nov 21 16:57 ./
dr-xr-xr-x 9 root root 0 Nov 21 16:57 ../
lrwx------ 1 root root 64 Nov 21 16:57 0 -> /dev/pts/1 --标准输入
lrwx------ 1 root root 64 Nov 21 16:57 1 -> /dev/pts/1 ---标准输出
lrwx------ 1 root root 64 Nov 21 16:57 2 -> /dev/pts/1 ---标准错误输出
lr-x------ 1 root root 64 Nov 21 16:57 3 -> /usr/local/java/jdk1.8/jre/lib/rt.jar
lrwx------ 1 root root 64 Nov 21 16:57 4 -> 'socket:[9133699]'
lrwx------ 1 root root 64 Nov 21 16:57 5 -> 'socket:[9133701]'

有2个socket,一个IPv4, 一个IPv6。

3.2。 切换到3364进程目录task, 有这个进程下的所有线程(一一对应 步骤1 下的文件)

root@hecs-x-xlarge-2-linux-20201111105933:/proc/3364/task# ll
total 0
dr-xr-xr-x 17 root root 0 Nov 21 17:08 ./
dr-xr-xr-x 9 root root 0 Nov 21 16:57 ../
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3364/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3365/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3366/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3367/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3368/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3369/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3370/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3371/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3372/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3373/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3374/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3375/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3376/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3377/
dr-xr-xr-x 7 root root 0 Nov 21 17:08 3378/

4. netstat -natp 查看网络连接状态 3364 处于监听状态,只有服务端有监听状态

root@hecs-x-xlarge-2-linux-20201111105933:~# netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 638/systemd-resolve
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1313/sshd
tcp 0 0 192.168.0.114:22 121.36.59.153:10431 ESTABLISHED 4507/sshd: root@pts
tcp6 0 0 :::22 :::* LISTEN 1313/sshd
tcp6 0 0 :::8090 :::* LISTEN 3364/java

5. 模拟客户端连接服务端:nc localhost 8090,此时 netstat 后会多出13391/nc 和连通状态的3364/java(每建立一个连接会多出2条记录)

tcp6 0 0 :::8090 :::* LISTEN 3364/java
tcp6 0 0 :::443 :::* LISTEN 28767/docker-proxy
tcp6 0 0 :::222 :::* LISTEN 28781/docker-proxy
tcp6 0 0 ::1:8090 ::1:39230 ESTABLISHED 3364/java
tcp6 0 0 ::1:39230 ::1:8090 ESTABLISHED 13391/nc

6. 切换到/proc/3364/fd,看到多了一个socket, 同理task目录下面也会多一个文件,即新建立连接的线程

root@hecs-x-xlarge-2-linux-20201111105933:/proc/3364/fd# ll
total 0
dr-x------ 2 root root 0 Nov 21 16:57 ./
dr-xr-xr-x 9 root root 0 Nov 21 16:57 ../
lrwx------ 1 root root 64 Nov 21 16:57 0 -> /dev/pts/1
lrwx------ 1 root root 64 Nov 21 16:57 1 -> /dev/pts/1
lrwx------ 1 root root 64 Nov 21 16:57 2 -> /dev/pts/1
lr-x------ 1 root root 64 Nov 21 16:57 3 -> /usr/local/java/jdk1.8/jre/lib/rt.jar
lrwx------ 1 root root 64 Nov 21 16:57 4 -> 'socket:[9133699]'
lrwx------ 1 root root 64 Nov 21 16:57 5 -> 'socket:[9133701]'
lrwx------ 1 root root 64 Nov 21 17:48 6 -> 'socket:[9173039]'

从以上可以看出程序即文件。

  • BIO:上面的程序可以用以下来解释,

  1. 服务端程序启动: 生成一个socket,这个socket文件描述符fd5,绑定8090端口,监听8090,accept阻塞(此时没有客户端连接)
  2. 客户端c1连接上来,消除阻塞,抛出一个新线程,描述符为fd6,此线程会recefrom fd6,接收c1客户端的输入,同理 ,客户端 c2 抛出fd6, 接收c2的输入

这里的accept/recefrom是Blocking的,如果并且为每个连接建立一个新线程(如果不建立新线程,别的客户端会连接不上),这里有问题:

1.太多线程,建立一个线程就会走软中断,发生系统调用,消耗资源,根本问题是blocking,只能建立新线程

  • NIO:非阻塞IO

由于BIO有太多线程,又有了NIO

这里的 accept 和recefrom都是非阻塞的,只要单线程即可,过程如下:

  1. 服务端起来,绑定8090,并且监听
  2. 客户端C1连接,返回fd6,recefrom fd6,客户端C2连接上返回fd7, recefrom fd7
  3. 单线程循环执行fd6, fd7 recefrom

这里也有问题如果有1万客户端连接,单线程遍历1万次recefrom系统调用,如果只有2条有数据,剩下的都是浪费的系统调用。

  • 多路复用(select)

由于NIO太多无用的系统调用,出现了select。

select多路复用的执行过程如下:

  1. 多个客户端建立连接后,通过一次select系统调用(一次传入多个文件描述符号,调用是阻塞的),可以得到多个可用的文件描述符
  2. recefrom遍历步骤1可用的描述符号,同步读取相应数据

这种模式虽然减少了系统调用,但是每次都需要传递多个文件描述符到内核空间,然后内核空间需要遍历太多的描述符

  • epoll

由于select传递太多文件描述符到内核空间,还要遍历传进的描述符,才有epoll出现:

Epoll 的执行过程如下:

  1. 程序启动创建socket, 文件描述符fd5, 绑定端口号,并且进行监听
  2. 调用epoll_create系统调用,创建一个文件描述符fd8,指向8的内核一块空间(绿色部分)。
  3. 调用epoll_ctl系统调用,将fd5添加到fd8指向的那块内核空间,并且传入fd5的事件类型为accept
  4. 调用epoll_wait系统调用,这是一个指向fd8的阻塞的调用
  5. 一个客户端连上fd5,产生一个fd6的文件描述符代表这个连接, 由于fd5感兴趣的事件类型accept,所以fd5会挪一份到另外一个内核空间(蓝色部分)
  6. 调用epoll_ctl系统调用(每个客户端生命周期至少有一次这个调用),将fd6添加到fd8指向的内核空间,事件类型为读写 w/r,多个连接则重复4,5,6。
  7. 当fd6发生读写事件时,绿色空间的fd6感兴趣的事件类型是读写,所以会挪一份到蓝色的内核空间
  8. 之后程序会不停的从蓝色内核空间读取已经准备好数据的文件描述符。避免了select内核遍历的过程。

上面从绿色的内核空间是怎么触发挪一份到蓝色的内核空间的呢?

是通过硬中断来实现的,所以epoll的事件驱动(上图中的?号)是通过中断来实现的,能够充分发挥硬件,尽量不浪费CPU。

epoll应用:

其中netty是可以配置的,用epoll或Nio

为什么redis的epoll是轮训,nginx是阻塞?

因为redis是单线程的,还要LRU LFU RDB(fork子进程) AOF,而nginx只需要来一个连接处理一个连接,不需要干其他的活