ThreadLocal主要是为了解决内存泄漏的问题,它是一种弱引用:
引用总共有四种,,我简单列一下:
ThreadLocal在set 时会:
每个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);
}
将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<?> 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 & (len-1);
for (Entry e = tab\[i\];
e != null;
e = tab\[i = nextIndex(i, len)\]) {
ThreadLocal<?> 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) && sz >= threshold)
rehash();
}</code></pre></li>
同理,get时:
先取当前线程的ThreadLocalMap
ThreadLocalMap中以threadlocal为key(其中会以key的hash定位在Entry数组中的位置)
返回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 五层实现:
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具有禁止指令重排序
参考: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:
查看对象内部信息=======
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)
2. 实例数据 :存储对象真正的有效信息,即定义的各种字段内容,无论是从父类继承下来的,还是在子类中定义的,都要记录下来。
3. 对齐填充:这部分不是必然存在的,也没特别的含义,hotspot要求自动内存管理系统要求对象起始地址必须是8字节的整数倍,当对象大小不是8的倍数时,需补齐。
synchronized加锁可以通过JOL查看,只是修改了markword的值。
synchronized的锁升级过程(偏向锁未启动的过程):
偏向锁和自旋锁是用户态的锁:
细节:
等待队列在monitor即waitset中:
打开偏向锁时间可以通过参数设置,默认4秒:
拓展与之相关的wait与notify
https://www.jianshu.com/p/99f73827c616
https://www.jianshu.com/p/ffc0c755fd8d
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]'
从以上可以看出程序即文件。
这里的accept/recefrom是Blocking的,如果并且为每个连接建立一个新线程(如果不建立新线程,别的客户端会连接不上),这里有问题:
1.太多线程,建立一个线程就会走软中断,发生系统调用,消耗资源,根本问题是blocking,只能建立新线程
由于BIO有太多线程,又有了NIO
这里的 accept 和recefrom都是非阻塞的,只要单线程即可,过程如下:
这里也有问题如果有1万客户端连接,单线程遍历1万次recefrom系统调用,如果只有2条有数据,剩下的都是浪费的系统调用。
由于NIO太多无用的系统调用,出现了select。
select多路复用的执行过程如下:
这种模式虽然减少了系统调用,但是每次都需要传递多个文件描述符到内核空间,然后内核空间需要遍历太多的描述符
由于select传递太多文件描述符到内核空间,还要遍历传进的描述符,才有epoll出现:
Epoll 的执行过程如下:
上面从绿色的内核空间是怎么触发挪一份到蓝色的内核空间的呢?
是通过硬中断来实现的,所以epoll的事件驱动(上图中的?号)是通过中断来实现的,能够充分发挥硬件,尽量不浪费CPU。
epoll应用:
其中netty是可以配置的,用epoll或Nio
为什么redis的epoll是轮训,nginx是阻塞?
因为redis是单线程的,还要LRU LFU RDB(fork子进程) AOF,而nginx只需要来一个连接处理一个连接,不需要干其他的活
手机扫一扫
移动阅读更方便
你可能感兴趣的文章