IO模型-同步异步、阻塞非阻塞

同步和异步

同步和异步描述的是消息通信的机制。

同步

当一个request发送出去以后,会得到一个response,这整个过程就是一个同步调用的过程。哪怕response为空,或者response的返回特别快,但是针对这一次请求而言就是一个同步的调用。

异步

当一个request发送出去以后,没有得到想要的response,而是通过后面的callback、状态或者通知的方式获得结果。

可以这么理解,对于异步请求分两步:

  • a. 调用方发送request没有返回对应的response(可能是一个空的response)

  • b. 服务提供方将response处理完成以后通过callback的方式通知调用方。

对于a而言是同步操作(调用方请求服务方),对于b而言也是同步操作(服务方回掉调用方)。从请求的目的(调用方发送一个request,希望获得对应的response)来看,这两个步骤拆分开来没有任何意义,需要结合起来看,而这整个过程就是一次异步请求。异步请求有一个最典型的特点:需要callback、状态或者通知的方式来告知调用方结果。

阻塞和非阻塞

阻塞和非阻塞描述的是程序在等待调用结果(消息,返回值)时的状态。

阻塞

阻塞调用是指调用方发出request的线程因为某种原因(如:等待系统资源)被服务方挂起,当服务方得到response后就唤醒挂起线程,并将response返回给调用方。

非阻塞

非阻塞调用是指调用方发出request的线程在没有等到结果时不会被挂起,直到得到response后才返回。

阻塞和非阻塞最大的区别就是看调用方线程是否会被挂起。

图示

同步阻塞
异步阻塞
同步非阻塞
异步非阻塞

举例

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 老张把水壶放到火上,立等水开。(同步阻塞)
    老张觉得自己有点傻
  2. 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
    老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~的噪音。
  3. 老张把响水壶放到火上,立等水开。(异步阻塞)
    老张觉得这样傻等意义不大
  4. 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
    老张觉得自己聪明了

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。

情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。


JUC-ThreadLocal

基本介绍

ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 TLAB

ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的

ThreadLocal 作用:

  • 线程并发:应用在多线程并发的场景下

  • 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度

  • 线程隔离:每个线程的变量都是独立的,不会互相影响

对比 synchronized:

synchronized ThreadLocal
原理 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问 ThreadLocal 采用以空间换时间的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

常用方法

方法 描述
ThreadLocal<>() 创建 ThreadLocal 对象
protected T initialValue() 返回当前线程局部变量的初始值
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyDemo {

private static ThreadLocal<String> tl = new ThreadLocal<>();

private String content;

private String getContent() {
// 获取当前线程绑定的变量
return tl.get();
}

private void setContent(String content) {
// 变量content绑定到当前线程
tl.set(content);
}

public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 设置数据
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}

应用场景

ThreadLocal 适用于下面两种场景:

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

ThreadLocal 方案有两个突出的优势:

  1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
  2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

ThreadLocal 用于数据连接的事务管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JdbcUtils {
// ThreadLocal对象,将connection绑定在当前线程中
private static final ThreadLocal<Connection> tl = new ThreadLocal();
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
public static Connection getConnection() throws SQLException {
//取出当前线程绑定的connection对象
Connection conn = tl.get();
if (conn == null) {
//如果没有,则从连接池中取出
conn = ds.getConnection();
//再将connection对象绑定到当前线程中,非常重要的操作
tl.set(conn);
}
return conn;
}
// ...
}

用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalDateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}

public static String format(Date date) {
return threadLocal.get().format(date);
}
}

底层结构

JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据

JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值

  • 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
  • Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value)
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰

JDK8 前后对比:

  • 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量
  • 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,防止内存泄露

成员变量

  • Thread 类的相关属性:每一个线程持有一个 ThreadLocalMap 对象,存放由 ThreadLocal 和数据组成的 Entry 键值对

    1
    ThreadLocal.ThreadLocalMap threadLocals = null
  • 计算 ThreadLocal 对象的哈希值:

    1
    private final int threadLocalHashCode = nextHashCode()

    使用 threadLocalHashCode & (table.length - 1) 计算当前 entry 需要存放的位置

  • 每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象:

    1
    private static AtomicInteger nextHashCode = new AtomicInteger()
  • 斐波那契数也叫黄金分割数,hash 的增量就是这个数字,带来的好处是 hash 分布非常均匀:

    1
    private static final int HASH_INCREMENT = 0x61c88647

成员方法

方法都是线程安全的,因为 ThreadLocal 属于一个线程的,ThreadLocal 中的方法,逻辑都是获取当前线程维护的 ThreadLocalMap 对象,然后进行数据的增删改查,没有指定初始值的 threadlcoal 对象默认赋值为 null

  • initialValue():返回该线程局部变量的初始值

    • 延迟调用的方法,在执行 get 方法时才执行
    • 该方法缺省(默认)实现直接返回一个 null
    • 如果想要一个初始值,可以重写此方法, 该方法是一个 protected 的方法,为了让子类覆盖而设计的
    1
    2
    3
    protected T initialValue() {
    return null;
    }
  • nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为斐波那契散列,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中

    1
    2
    3
    4
    private static int nextHashCode() {
    // 哈希值自增一个 HASH_INCREMENT 数值
    return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
  • set():修改当前线程与当前 threadlocal 对象相关联的线程局部变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否存在
    if (map != null)
    // 调用 threadLocalMap.set 方法进行重写或者添加
    map.set(this, value);
    else
    // map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量
    createMap(t, value);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 获取当前线程 Thread 对应维护的 ThreadLocalMap 
    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }
    // 创建当前线程Thread对应维护的ThreadLocalMap
    void createMap(Thread t, T firstValue) {
    // 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • get():获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
    // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
    ThreadLocalMap.Entry e = map.getEntry(this);
    // 对 e 进行判空
    if (e != null) {
    // 获取存储实体 e 对应的 value值
    T result = (T)e.value;
    return result;
    }
    }
    /*有两种情况有执行当前代码
    第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
    第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */
    // 初始化当前线程与当前 threadLocal 对象相关联的 value
    return setInitialValue();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private T setInitialValue() {
    // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判断 map 是否初始化过
    if (map != null)
    // 存在则调用 map.set 设置此实体 entry,value 是默认的值
    map.set(this, value);
    else
    // 调用 createMap 进行 ThreadLocalMap 对象的初始化中
    createMap(t, value);
    // 返回线程与当前 threadLocal 关联的局部变量
    return value;
    }
  • remove():移除当前线程与当前 threadLocal 对象相关联的线程局部变量

    1
    2
    3
    4
    5
    6
    7
    public void remove() {
    // 获取当前线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    // map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体
    m.remove(this);
    }

相关博客:https://blog.csdn.net/zhiyikeji/article/details/125473692

内存泄漏分析:https://cloud.tencent.com/developer/article/2185709


JVM-执行流程示例

深度剖析:https://z.itpub.net/article/detail/A323358F7E35E224FD288ABBB1FD9BAE

原始 Java 代码:

1
2
3
4
5
6
7
8
public class Demo {	
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

javap -v Demo.class:省略

  • 常量池载入运行时常量池

  • 方法区字节码载入方法区

  • main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4)

  • 执行引擎开始执行字节码

    bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令

    • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    • ldc 将一个 int 压入操作数栈
    • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
    • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

    istore_1:将操作数栈顶数据弹出,存入局部变量表的 slot 1

    ldc #3:从常量池加载 #3 数据到操作数栈
    Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成

    istore_2:将操作数栈顶数据弹出,存入局部变量表的 slot 2

    iload_1:将局部变量表的 slot 1 数据弹出,放入操作数栈栈顶

    iload_2:将局部变量表的 slot 2 数据弹出,放入操作数栈栈顶

    iadd:执行相加操作

    istore_3:将操作数栈顶数据弹出,存入局部变量表的 slot 3

    getstatic #4:获取静态字段

    iload_3

    invokevirtual #5

    • 找到常量池 #5 项
    • 定位到方法区 java/io/PrintStream.println:(I)V 方法
    • 生成新的栈帧(分配 locals、stack等)
    • 传递参数,执行新栈帧中的字节码
    • 执行完毕,弹出栈帧
    • 清除 main 操作数栈内容

    return:完成 main 方法调用,弹出 main 栈帧,程序结束