Java帝国危机,线程罢工了!

简介: Java帝国危机,线程罢工了!

Java 帝国发生了一场危机,各个线程正在闹罢工。。。


「发生了什么事,听说各个线程最近正在闹罢工」国王老虚说道


「报告国王,最近各个线程反应创建对象太难了,要求王国进行变革」线程大臣启奏道


「创建对象有什么难的,我们不是用了 bump the pointer 机制吗,new 一下对象不就创建了吗」老虚大惑不解,「我们知道对象一般来说都是先分配在堆上的 Eden 区的,那么在堆上怎样才能快速地给对象分配空间呢?假设堆是内存是绝对规整的,用过的放一边,空闲的放另一边,中间放一个指针作为分界点,那么在分配对象时只需要将指针移动到与对象大小相等的距离即可,这样创建对象只要不断地移动指针就行啦。这就是我们所说说的 bump the pointer(指针碰撞)」老虚边说边画出了以下图示



网络异常,图片无法展示
|


「指针碰撞我们当然知道,如果是单线程这样轻轻移动指针分配对象的方式当然很快,但如果是多线程呢,会产生严重的锁竞争呀」


网络异常,图片无法展示
|


「这确实是个问题,锁在多线程下确实会产生比较严重的问题,虽然这里用的是 CAS 乐观锁,但在多线程对象分配上由于锁竞争关系也会有较严重的性能问题」老虚沉思道


TLAB



「能否这样,我们知道对象一般是在 Eden 区分配的,为每个线程创建一块单独的区域,每个线程分配对象时只在自己的区域里分配,在自己的区域分配时也采用 bump the pointer 的方式来分配,这样既可以用 bump the pointer 的方式来加速了对象的创建,又避免了创建对象时的锁竞争,可谓一举双得!」线程大臣说道


「妙啊,我们给这块区域取个名字吧,就叫它 Thread Local Allocation Buffer(即线程本地分配缓存区),这块是线程专用的内存分配区域」老虚道


网络异常,图片无法展示
|


「还有一个问题,这块区域该分配多大呢,如果分配太大,可能一个线程根本就没有分配对象的需求或者分配对象很少,造成了空间的浪费,如果分配太小,则可能某些线程比较活跃,分配的对象比较多,那么就要重新分配一个 TLAB,或者直接在 Eden 上分配,这样频繁分配 TLAB 或者在 Eden 分配会造成资源与性能的浪费」不愧是国王,一眼看出问题的本质


「是的,TLAB 大小主要和两个因素有关:每个 gc 内需要对象分配的线程个数以及线程每次 gc 分配的内存,这两项指标显然也与历史值有关,所以我们需要根据历史值来算出当前应该分配的 TLAB 大小,有一种算法指数平均数算法(EMA)可以干这事」线程大臣也不赖,一眼就抓住了问题的关键


「如果 TLAB 满了咋办」老虚困惑道


「满了就针对此线程创建一个 TLAB,或者直接丢到 Eden 区呗,另外需要说明的是 TLAB 比较适用于小对象的分配,大对象一般直接分配到 Eden 区哦」线程大臣解释道


逃逸分析与标量替换



老虚采纳了线程大臣的建议实现了 TLAB,由于采用了 TLAB 机制,各个线程的工作效率瞬间提升,老虚笑开了花,可是好景不长,新的问题又出现了。。。


「老虚啊,我发现采用 TLAB 之后线程的工作效率确实提升了很多,但一些线程反映由于 GC 时的 STW(stop the word),导致他们啥也干不了,这个问题自 Java 帝国诞生起就出现了,能否解决一下」


「这没办法,STW 是必须的,总不能一边清理垃圾一边扔垃圾吧,那垃圾还怎么收拾地干净」


「STW 确实不能避免,但能否减少 GC 次数呢,GC 次数少了,STW 自然也少了,GC 发生在堆中,那只要对象不分配在堆中,GC 次数不就自然而然少了吗」线臣大臣说到

「难不成要把它分配在栈上?」老虚一听能减少 GC 次数,顿时来了精神


「没错,就是要把它分配在栈上!这样线程在调用栈销毁后对象也就销毁了」线程大臣看起来胸有成竹「但它首先必须满足一个条件:逃逸分析」


「什么是逃逸分析」老虚x疑惑道


逃逸分析是指分析指针动态范围的方法,分析在程序的哪些地方可以访问到指针。当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。我们就说这个对象「逃逸」了,否则就说对象未逃逸,未逃逸的对象是可以分配在堆栈上的(采用标量替换的形式)的。


「Talk is cheap, show me your code,举几个例子来吧」老虚道


public class EscapeTest {
    public static Object globalVariableObject;
    public Object instanceObject;
    public void globalVariableEscape(){
        globalVariableObject = new Object(); // 1.静态变量,外部线程可见,发生逃逸
    }
    public void instanceObjectEscape(){
        instanceObject = new Object(); // 2.赋值给堆中实例字段,外部线程可见,发生逃逸
    }
    public Object returnObjectEscape(){
        return new Object();  // 3.返回实例,外部线程可见,发生逃逸
    }
    public void noEscape(){
        //仅创建线程可见,对象无逃逸
        Object noEscape = new Object();  //4. 仅创建线程可见,对象无逃逸
    }
}
复制代码


我们可以看到,当对象符合以下两种条件时我们就说它逃逸了


  1. 被赋值给了对象的字段或类的变量,因为很显然对象分配在堆中,是线程共享的,其他线程可能对其进行修改
  2. 对象被传进了不确定的代码中去运行,比如返回给上一个调用栈赋值给其他对象的属性等


只有那种满足条件 4 的仅创建线程可见的对象,才能被判断为无逃逸,才能将对象分配到堆上


「未逃逸的对象怎样才能被分配在栈上呢?」老虚还是有点困惑


「我们先了解两个名词:标量聚合量,标量就是不可进一步分解的量,像 Java 的基本类型如 int 等基本类型以及 reference 类型就是标量,聚合量就简单了,就是各个标量的组合,对象其实就是聚合量,所以让对象分配在栈上其实很简单,将其替换为各个标量即可」线程大臣顿了顿,给出了标量替换的 demo


网络异常,图片无法展示
|


「妙啊,通过将对象打散为多个标量,由于标量是直接在栈上分配的,就避免了对象在堆中的分配」这个思路确实给力!老虚立即下令实行


锁消除



「老虚啊,我无意中发现未逃逸的对象还有锁消除功能」线程大臣兴奋地说


「啥是锁消除」老虚挺兴奋的


我们先来看看 StringBuffer 的 append 方法:


@Override
public synchronized StringBuffer append(Object obj) {
  toStringCache = null;
  super.append(String.valueOf(obj));
  return this;
}
复制代码


你看看是不是有个 synchronized 锁,那如果 StringBuffer 不是逃逸对象,比如下面这样


public void test() {
  StringBuffer sb = new StringBuffer()
  sb.append(s1).append(s2)
  return sb.toString();
}
复制代码


那 append 方法的 Synchronized 锁就可以消除了对不对


「可以可以」老虚兴奋极了,完成之后 JVM 帝国的生产力又提升了一个新台阶。。。

相关文章
|
12天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
21天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
8天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
28 9
|
11天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
8天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
11天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
25 3
|
10天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
22 1
|
11天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
11天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
40 1