复习-JavaGuide-并发

Atomic预备知识

Java实现CAS的原理[非javaguide] #

i是非线程安全的,因为**i不是原子操作;可以使用synchronized和CAS实现加锁**

synchronized是悲观锁,一旦获得锁,其他线程进入后就会阻塞等待锁;而CAS是乐观锁,执行时不会加锁,假设没有冲突,如果因为冲突失败了就重试,直到成功

  • 乐观锁和悲观锁

    • 这是一种分类方式
    • 悲观锁,总是认为每次访问共享资源会发生冲突,所以必须对每次数据操作加锁,以保证临界区的程序同一时间只能有一个线程在执行
    • 乐观锁,又称**“无锁”**,假设对共享资源访问没有冲突,线程可以不停的执行,无需加锁无需等待;一旦发生冲突,通常是使用一种称为CAS的技术保证线程执行安全
      • 无锁没有锁的存在,因此不可能发生死锁,即乐观锁天生免疫死锁
      • 乐观锁用于**“读多写少”的环境,避免加锁频繁影响性能;悲观锁用于“写多读少”,避免频繁失败及重试**影响性能
  • CAS概念,即CompareAndSwap ,比较和交换,CAS中,有三个值(概念上)
    V:要更新的变量(var);E:期望值(expected);N:新值(new) 判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。 一般来说,预期值E本质上指的是“旧值”(判断是否修改了)

    1. 如果有一个多个线程共享的变量i原本等于5,我现在在线程A中,想把它设置为新的值6;
    2. 我们使用CAS来做这个事情;
    3. (首先要把原来的值5在线程中保存起来)
    4. 接下来是原子操作:首先我们用(现在的i)去与5对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,i的值被设置成了6;
    5. 如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍然为2。

其中i为V,5为E,6为N

CAS是一种原子操作,它是一种系统原语,是一条CPU原子指令,从CPU层面保证它的原子性(不可能出现说,判断了对比了i为5之后,正准备更新它的值,此时该值被其他线程改了

多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

  • Java实现CAS的原理 - Unsafe类

    • 在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现

    • Java中有一个Unsafe类,在sun.misc包中,里面有一些native方法,其中包括:

      boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
      boolean compareAndSwapInt(Object o, long offset,int expected,int x);
      boolean compareAndSwapLong(Object o, long offset,long expected,long x);
      
      
      //------>AtomicInteger.class
      
      public class AtomicInteger extends Number implements java.io.Serializable {
      private static final long serialVersionUID = 6214790243416807050L;
      
      // setup to use Unsafe.compareAndSwapInt for updates
      private static final Unsafe unsafe = Unsafe.getUnsafe();
      private static final long valueOffset;
      
      static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
      }
      
      private volatile int value;
      public final int getAndIncrement() {
      	return unsafe.getAndAddInt(this, valueOffset, 1);
      
    }
    

    }

    ...

memory-area

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

如果没有特殊说明,针对的都是HotSpot虚拟机

前言 #

  • 对于Java程序员,虚拟机自动管理机制,不需要像C/C++程序员为每一个new 操作去写对应的delete/free 操作,不容易出现内存泄漏内存溢出问题
  • 但由于内存控制权交给Java虚拟机,一旦出现内存泄漏和溢出方面问题,如果不了解虚拟机是怎么样使用内存,那么很难排查任务

运行时数据区域 #

Java虚拟机在执行Java程序的过程中,会把它管理的内存,划分成若干个不同的数据区域

JDK1.8之前:

  1. 线程共享 堆,方法区【永久代】(包括运行时常量池)
  2. 线程私有 虚拟机栈、本地方法栈、程序计数器
  3. 本地内存(包括直接内存)

ly-20241212141952681

JDK1.8之后:
Java 运行时数据区域(JDK1.8 之后) 1.8之后整个永久代改名叫"元空间",且移到了本地内存中

规范(概括):
线程私有:程序计数器,虚拟机栈,本地方法栈

线程共享:堆,方法区,直接内存(非运行时数据区的一部分)

Java虚拟机规范对于运行时数据区域的规定是相当宽松的,以堆为例:

  1. 堆可以是连续,也可以不连续
  2. 大小可以固定,也可以运行时按需扩展
  3. 虚拟机实现者可以使用任何垃圾回收算法管理堆,设置不进行垃圾收集

程序计数器 #

  • 是一块较小内存空间,看作是当前线程所执行的字节码行号指示器

  • java程序流程
    ly-20241212141953092

  • 字节码解释器,工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令

    分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器

  • 而且,为了线程切换后恢复到正确执行位置,每条线程需要一个独立程序计数器,各线程计数器互不影响,独立存储,我们称这类内存区域为**“线程私有”**的内存

  • 总结,程序计数器的作用

    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
    • 多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切回来的时候能够知道该线程上次运行到哪

    程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随线程创建而创建,线程结束而死亡

Java虚拟机栈 #

  • Java虚拟机栈,简称"栈",也是线程私有的,生命周期和线程相同,随线程创建而创建,线程死亡而死亡
  • 除了Native方法调用的是通过本地方法栈实现的,其他所有的Java方法调用都是通过来实现的(需要和其他运行时数据区域比如程序计数器配合)
  • 方法调用的数据需要通过栈进行传递每一次方法调用都会有一个对应的栈帧被压入栈,每一个方法调用结束后,都会有一个栈帧被弹出
  • 栈由一个个栈帧组成,每个栈帧包括局部变量表操作数栈动态链接方法返回地址。 栈为先进后出,且只支持出栈入栈

Java 虚拟机栈

  • 局部变量表:存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置) 局部变量表

  • 操作数栈 作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。计算过程中产生的临时变量也放在操作数栈中

  • 动态链接 主要服务一个方法需要调用其他方法的场景。

    在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用

    ...

completablefuture-intro

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

Java8被引入的一个非常有用的用于异步编程的类【没看

简单介绍 #

CompletableFuture同时实现了FutureCompletionStage接口

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}

CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程的能力。

img

Future接口有5个方法:

  • boolean cancel(boolean mayInterruptIfRunning) :尝试取消执行任务。
  • boolean isCancelled() :判断任务是否被取消。
  • boolean isDone() : 判断任务是否已经被执行完成。
  • get() :等待任务执行完成并获取运算结果。
  • get(long timeout, TimeUnit unit) :多了一个超时时间。

img

CompletionStage<T> 接口中的方法比较多,CompoletableFuture的函数式能力就是这个接口赋予的,大量使用Java8引入的函数式编程

常见操作 #

创建CompletableFuture #

两种方法:new关键字或 CompletableFuture自带的静态工厂方法 runAysnc()supplyAsync()

  1. 通过new关键字 这个方式,可以看作是将CompletableFuture当作Future来使用,如下:

    我们通过创建了一个结果值类型为 RpcResponse<Object>CompletableFuture,你可以把 resultFuture 看作是异步运算结果的载体

    CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
    

    如果后面某个时刻,得到了最终结果,可以调用complete()方法传入结果,表示resultFuture已经被完成:

    // complete() 方法只能调用一次,后续调用将被忽略。
    resultFuture.complete(rpcResponse);
    

    通过isDone()检查是否完成:

    public boolean isDone() {
        return result != null;
    }
    

    获取异步结果,使用get() ,调用get()方法的线程会阻塞 直到CompletableFuture完成运算: rpcResponse = completableFuture.get();

    ...

ThreadLocal详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

本文来自一枝花算不算浪漫投稿, 原文地址: https://juejin.cn/post/6844904151567040519open in new window。 感谢作者!

思维导图
img

目录 #

ThreadLocal代码演示 #

简单使用

public class ThreadLocalTest {
    private List<String> messages = Lists.newArrayList();

    public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);

    public static void add(String message) {
        holder.get().messages.add(message);
    }

    public static List<String> clear() {
        List<String> messages = holder.get().messages;
        holder.remove();

        System.out.println("size: " + holder.get().messages.size());
        return messages;
    }

    public static void main(String[] args) {
        ThreadLocalTest.add("一枝花算不算浪漫");
        System.out.println(holder.get().messages);
        ThreadLocalTest.clear();
    }
}
/* 结果 
[一枝花算不算浪漫]
size: 0
*/

简单使用2

...

Atomic原子类介绍

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

文章开头先用例子介绍几种类型的api使用

package com.aqs;

import lombok.*;

import java.util.concurrent.atomic.*;

@Data
@Getter
@Setter
@AllArgsConstructor
@ToString
class User {
    private String name;
    //如果要为atomicReferenceFieldUpdater服务,必须加上volatile修饰
    public volatile Integer age;
}

public class AtomicTest {
    public static void main(String[] args) {
        System.out.println("原子更新数值---------------");
        AtomicInteger atomicInteger = new AtomicInteger();
        int i1 = atomicInteger.incrementAndGet();
        System.out.println("原子增加后为" + i1);
        System.out.println("原子更新数组---------------");

        int[] a = new int[3];
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(a);

        int i = atomicIntegerArray.addAndGet(1, 3);
        System.out.println("数组元素[" + 1 + "]增加后为" + i);
        System.out.println("数组为" + atomicIntegerArray);
        System.out.println("原子更新对象---------------");
        User user1 = new User("ly1", 10);
        User user2 = new User("ly2", 20);
        User user3 = new User("ly3", 30);
        AtomicReference<User> atomicReference = new AtomicReference<>(user1);
        boolean b = atomicReference.compareAndSet(user2, user3);
        System.out.println("更新" + (b ? "成功" : "失败"));
        System.out.println("引用里值为"+atomicReference.get());
        boolean b1 = atomicReference.compareAndSet(user1, user3);
        System.out.println("更新" + (b1 ? "成功" : "失败"));
        System.out.println("引用里值为"+atomicReference.get());
        System.out.println("原子更新对象属性---------------");
        User user4=new User("ly4",40);
        AtomicReferenceFieldUpdater<User, Integer> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
        boolean b2 = atomicReferenceFieldUpdater.compareAndSet(user4, 41, 400);
        System.out.println("更新"+(b2?"成功":"失败"));
        System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
        boolean b3 = atomicReferenceFieldUpdater.compareAndSet(user4, 40, 400);
        System.out.println("更新"+(b3?"成功":"失败"));
        System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
        System.out.println("其他使用---------------");
        User user5=new User("ly5",50);
        User user6=new User("ly6",60);
        User user7=new User("ly7",70);
        AtomicMarkableReference<User> userAtomicMarkableReference=new AtomicMarkableReference<>(user5,true);
        boolean b4 = userAtomicMarkableReference.weakCompareAndSet(user6, user7, true, false);
        System.out.println("更新"+(b4?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        boolean b5 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, false, true);
        System.out.println("更新"+(b5?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        boolean b6 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, true, false);
        System.out.println("更新"+(b6?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
        System.out.println("AtomicStampedReference使用---------------");
        User user80=new User("ly8",80);
        User user90=new User("ly9",90);
        User user100=new User("ly10",100);
        AtomicStampedReference<User> userAtomicStampedReference=new AtomicStampedReference<>(user80,80);//版本80
        //...每次更改stamp都加1
        //这里假设中途被改成81了
        boolean b7 = userAtomicStampedReference.compareAndSet(user80, user100,81,90);
        System.out.println("更新"+(b7?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicStampedReference.getReference());
        boolean b8 = userAtomicStampedReference.compareAndSet(user80, user100,80,90);
        System.out.println("更新"+(b8?"成功":"失败"));
        System.out.println("引用里值为"+userAtomicStampedReference.getReference());
    }
}
/*
原子更新数值---------------
原子增加后为1
原子更新数组---------------
数组元素[1]增加后为3
数组为[0, 3, 0]
原子更新对象---------------
更新失败
引用里值为User(name=ly1, age=10)
更新成功
引用里值为User(name=ly3, age=30)
原子更新对象属性---------------
更新失败
引用里user4值为40
更新成功
引用里user4值为400
其他使用---------------
更新失败
引用里值为User(name=ly5, age=50)
更新失败
引用里值为User(name=ly5, age=50)
更新成功
引用里值为User(name=ly7, age=70)
AtomicStampedReference使用---------------
更新失败
引用里值为User(name=ly8, age=80)
更新成功
引用里值为User(name=ly10, age=100)

Process finished with exit code 0

*/

原子类介绍 #

  • 在化学上,原子是构成一般物质的最小单位,化学反应中是不可分割的,Atomic指一个操作是不可中断的,即使在多个线程一起执行时,一个操作一旦开始就不会被其他线程干扰
  • 原子类–>具有原子/原子操作特征的类
  • 并发包java.util.concurrent 的原子类都放着java.util.concurrent.atomicly-20241212141944757
  • 根据操作的数据类型,可以将JUC包中的原子类分为4类(基本类型、数组类型、引用类型、对象的属性修改类型)
    • 基本类型 使用原子方式更新基本类型,包括AtomicInteger 整型原子类AtomicLong 长整型原子类,AtomicBoolean 布尔型原子类

      ...

aqs详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

Semaphore [ˈseməfɔː(r)]

  • 何为 AQS?AQS 原理了解吗?
  • CountDownLatchCyclicBarrier 了解吗?两者的区别是什么?
  • 用过 Semaphore 吗?应用场景了解吗?
  • ……

AQS简单介绍 #

AQS,AbstractQueueSyschronizer,即抽象队列同步器,这个类在java.util.concurrent.locks包下面

ly-20241212141944167

AQS是一个抽象类,主要用来构建同步器

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
} 

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueueFutureTask(jdk1.7) 等等皆是基于 AQS 的。

AQS原理 #

AQS核心思想 #

面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来

AQS 核心思想是,如果被请求的共享资源(AQS内部)空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
[ 搜索了一下,CLH好像是人名 ] 在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
CLH队列结构
ly-20241212141944445

  • AQS(AbstractQueuedSynchronized)原理图
    ly-20241212141944597

    AQS使用一个int成员变量来表示同步状态,通过内置的线程等待队列来获取资源线程的排队工作。
    state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

    ...

java常见并发容器

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

JDK提供的容器,大部分在java.util.concurrent包中

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnWriteArrayList:线程安全的List,在读多写少的场合性能非常好,远好于Vector
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看作一个线程安全的LinkedList,是一个非阻塞队列
  • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了该接口。表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcorrentSkipListMap:跳表的实现,是一个Map,使用跳表的数据结构进行快速查找

ConcurrentHashMap #

  • HashMap是线程不安全的,并发场景下要保证线程安全,可以使用Collections.synchronizedMap()方法来包装HashMap,但这是通过使用一个全局的锁同步不同线程间的并发访问,因此会带来性能问题
  • 建议使用ConcurrentHashMap,不论是读操作还是写操作都能保证高性能:读操作(几乎)不需要加锁,而写操作时通过锁分段(这里说的是JDK1.7?)技术,只对所操作的段加锁而不影响客户端对其他段的访问

CopyOnWriteArrayList #

//源码
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
 
  • 在很多应用场景中,读操作可能会远远大于写操作
  • 我们应该允许多个线程同时访问List内部数据(针对读)
  • 与ReentrantReadWriteLock读写锁思想非常类似,即读读共享写写互斥读写互斥写读互斥
  • 不一样的是,CopyOnWriteArrayList读取时完全不需要加锁,且写入也不会阻塞读取操作,只有写入和写入之间需要同步等待

CopyOnWriteArrayList是如何做到的 #

  • CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
  • CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是满足 CopyOnWrite
  • 在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存(注意,是指向,而不是重新拷贝★重要★),原来的内存就可以被回收掉了

CopyOnWriteArrayList 读取和写入源码简单分析 #

  • CopyOnWriteArrayList读取操作的实现 读取操作没有任何同步控制操作,理由就是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全

      /** The array, accessed only via getArray/setArray. */
        private transient volatile Object[] array;
        public E get(int index) {
            return get(getArray(), index);
        }
        @SuppressWarnings("unchecked")
        private E get(Object[] a, int index) {
            return (E) a[index];
        }
        final Object[] getArray() {
            return array;
        }
    
  • CopyOnWriteArrayList写入操作的实现 在添加集合的时候加了锁,保证同步,避免多线程写的时候会copy出多个副本

    ...

线程池最佳实践

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

线程池知识回顾 #

1. 为什么要使用线程池 #

  • 池化技术的思想,主要是为了减少每次获取资源(线程资源)的消耗,提高对资源的利用率
  • 线程池提供了一种限制管理资源(包括执行一个任务)的方法,每个线程池还维护一些基本统计信息,例如已完成任务的数量

好处:

  1. 降低资源消耗
  2. 提高响应速度
  3. 提高线程的可管理性

2. 线程池在实际项目的使用场景 #

线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行

ly-20241212141942375

3. 如何使用线程池 #

一般是通过 ThreadPoolExecutor 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。构造函数如下:

  /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

使用代码:

...

java线程池详解

转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!

一 使用线程池的好处 #

  • 池化技术:减少每次获取资源的消耗,提高对资源的利用率
  • 线程池提供一种限制管理资源(包括执行一个任务)的方式,每个线程池还维护一些基本统计信息,例如已完成任务的数量
  • 线程池的好处
    • 降低资源消耗(重复利用,降低线程创建和销毁造成的消耗)
    • 提高响应速度(任务到达直接执行,无需等待线程创建
    • 提高线程可管理性避免无休止创建,使用线程池统一分配调优监控

二 Executor框架 #

Java5之后,通过Executor启动线程,比使用Thread的start方法更好,更易于管理效率高,还能有助于避免this逃逸的问题

this逃逸,指的是构造函数返回之前其他线程就持有该对象的引用,会导致调用尚未构造完全的对象
例子:

public class ThisEscape { 
  public ThisEscape() { 
    new Thread(new EscapeRunnable()).start(); 
    // ... 
  } 
    
  private class EscapeRunnable implements Runnable { 
    @Override
    public void run() { 
      // 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸 
    } 
  } 
}

处理办法 //不要在构造函数中运行线程

public class ThisEscape { 
  private Thread t; 
  public ThisEscape() { 
    t = new Thread(new EscapeRunnable()); 
    // ... 
  } 
    
  public void init() { 
    //也就是说对象没有构造完成前,不要调用ThisEscape.this即可
    t.start(); 
  } 
    
  private class EscapeRunnable implements Runnable { 
    @Override
    public void run() { 
      // 通过ThisEscape.this就可以引用外围类对象, 此时可以保证外围类对象已经构造完成 
    } 
  } 
}

Executor框架不仅包括线程池的管理,提供线程工厂队列以及拒绝策略

...

java内存模型

引用自https://github.com/Snailclimb/JavaGuide

从CPU缓存模型说起 #

  • redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题,CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题

    我们把内存看作外存的高速缓存,程序运行时把外存的数据复制到内存,由于内存的处理速度远高于外存,这样提高了处理速度

  • 总结,CPU Cache缓存的是内存数据,用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题 CPU Cache示意图:

    ly-20241212141939038 CPU Cache通常分为三层,分别叫L1,L2,L3 Cache 工作方式: 先复制一份数据到CPUCache中,当CPU需要用的时候就可以从CPUCache中读取数据,运算完成后,将运算得到的数据,写回MainMemory中,此时,会出现内存缓存不一致的问题,例子:执行了i++,如果两个线程同时执行,假设两个线程从CPUCach中读取的i=1,两个线程做了1++运算完之后再写回MainMemory,此时i=2 而正确结果为3

  • CPU为了解决内存缓存不一致问题,可以通过制定缓存一致协议(比如MESI协议)或其他手段。这个缓存一致协议,指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范 ly-20241212141939314 操作系统,通过内存模型MemoryModel定义一系列规范来解决这个问题

Java内存模型 #

ly-20241212141939466

指令重排序 #

  • 什么是指令重排序? 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行

  • 指令重排有下面2种

    • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
    • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序

另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题

即Java源代码会经历 编译器优化重排—>指令并行重排—>内存系统重排,最终编程操作系统可执行的指令序列

极其重要★:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下指令重排可能导致一些问题

编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存清空无效队列,从而保障变量的可见性。

JMM(JavaMemoryMode) #

什么是 JMM?为什么需要 JMM? #

一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。

实际上,对于Java来说,可以把JMM看作是Java定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,还规定了从Java源代码到CPU可执行指令的转化过程要遵守哪些和并发相关的原则和规范,主要目的是为了简化多线程编程增强程序可移植性

为什么要遵守这些并发相关的原则和规范呢?因为在并发编程下,CPU多级缓存和指令重排这类设计会导致程序运行出问题,比如指令重排,为此JMM抽象了happens-before原则

JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理直接使用并发相关的一些关键字(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

...