JUC 笔记

并发三要素

可见性,原子性,有序性

并发问题产生的原因

可见性:CPU多级缓存的一致性问题

原子性:线程调度(CPU 时间片,上下文切换)

有序性:指令重排序


缓存一致性

高速缓存Cache

缓存行

根据空间局部性原理(位置相邻的数据常常会在相近的时间内被访问),为提高缓存命中率将缓存行作为 Cache 与内存交互的最小单位,在 x86 架构中为 64 个字节。

总线锁&缓存锁

处理器发出 LOCK 指令锁定总线,使其他处理器的请求阻塞,该处理器独占共享内存。缺点是 CPU 利用率低下

缓存锁对CPU的缓存中的缓存行进行锁定,通过缓存一致性协议保证确保修改操作的一致性

缓存一致性协议

图解 MESI:https://zhuanlan.zhihu.com/p/467782159


指令重排序

as-if-serial 语义

保证单线程下指令重排序后执行结果不变

重排序类型

编译器重排序:编译器对代码进行优化的过程中可能会打乱代码顺序

指令集并行(ILP):指令之间不存在相关时,它们在流水线中可以重叠起来并行执行。这种指令序列中存在潜在并行性称为指令级并行

内存重排:store buffer 延迟写入,在多线程下无法保证可见性

曹大谈内存重排 - Stefno - 博客园


解决并发问题:JMM

并发编程需要解决的两个问题

同步:不同线程操作发生顺序的控制

通信:线程间如何交换信息

并发编程的两个模型

内存共享:JMM

消息传递:Actor

[译] 你想知道的关于 actor 模型但可能不敢问的所有信息 | 敖小剑的博客

JMM是什么

是一种抽象的模型。主要用来定义多线程中变量的访问规则,用来解决变量的可见性、有序性和原子性问题,确保在并发环境中安全地访问共享变量。

JMM 如何保证线程安全

定义 happens-before 规则

  • 程序顺序规则
  • volatile 变量规则:volatile 域的写发生于 volatile 域的读之前
  • monitor 锁规则:解锁发生在加锁前
  • start & join 规则:启动操作发生于被启动线程的所有操作之前;线程中的所有操作都发生于对此线程的终止检测之前
  • 传递性

volatile

功能

  • 防止重排序
  • 保证可见性

原理

volatile 通过内存屏障保证可见性和有序性

CPU 内存一致性模型

不同 CPU 为提升性能对一致性要求不同,常见架构有 SC, TSO, PSO, RMO

Java 内存屏障

Java 为屏蔽不同 CPU 一致性模型的差异,抽象出了四种内存屏障:

LoadLoad Barriers

LoadStore Barriers

StoreLoad Barriers

StroreStore Barriers

什么时候插入哪种内存屏障遵循 JSR 133规范:

第一步 / 第二步volatile 读volatile 写
LoadStore
StoreStore
volatile 读LoadLoadStoreLoadLoadLoadLoadStore
volatile 写StoreLoadStoreStore

总结为以下三大原则:

  • volatile 读写之间相互序列化
  • volatile 读之后不能乱序
  • volatile 写之前不能乱序

深入理解java内存屏障(volatile实现原理)_java内存屏障原理-CSDN博客


Synchronized

功能

对象锁:

  • 代码块,指定锁对象为 this 或自定义对象
  • 普通方法锁(this)

类锁:

  • 代码块,指定锁对象为 xxx.class
  • 静态方法锁(Class)

原理

Monitor(管程)

Java 使用抽象程度更高的管程来实现并发编程的同步和互斥,它简化了信号量复杂的 PV 操作配对,AQS 和 ObjectMonitor 是对管程机制的不同实现。

https://www.bilibili.com/video/BV1cWUDYgERn/

信号量 VS 管程

https://www.cnblogs.com/binarylei/p/12544002.html

ObjectMonitor

  • ObjectMonitor 关键属性
    _recursions   = 0;   // synchronized是一个重入锁,这个变量记录锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 调用wait阻塞的线程:等待线程组成的双向循环链表,_WaitSet是第一个节点
    _cxq          = NULL ; // 有线程在执行,新进入的线程会进入这个队列:多线程竞争锁会先存到这个单向链表中 (FILO栈结构:非公平!)
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)

  • ObjectMonitor 等待唤醒机制 

Monitor对象全解析 - 张高峰的博客 | Peakiz Blog

Java并发基石——所谓“阻塞”:Object Monitor和AQS(1)_on object monitor-CSDN博客

Java 对象结构

Mark Word

内置锁状态

57bit4bit1bit2bit
biased_locklock
无锁unused(25bit)indentity_hashCode(31bit)unused(1bit)age001
轻量级锁ptr_to_lock_record 指向方法栈帧中锁记录的指针 (62bit)00
重量级锁ptr_to_heavyweight_monitor 指向重量级锁监视器的指针 (62bit)10
GC 标记空 (62bit)11
偏向锁线程 ID(54bit)epoch(2bit)unused(1bit)age101

lock:锁标志位

biased:是否启用偏向锁

age:对象分代年龄。在 GC 中,每次被复制到 survivor 区年龄都 + 1,达到阈值 15 进入老年代(4bit 最大 15)

identity_hashcode:对象标识 HashCode。当调用 Object.hashCode() 方法或 System.identityHashCode() 方法时将计算结果写入对象头。

优化 Synchronized 

锁消除

JVM 通过逃逸分析省略同步,如果检测到某段代码不被共享或参与竞争就去除同步锁。 

锁粗化

一连串操作对同一个对象加锁解锁时,JVM 将锁的范围扩展到这一连串操作。

锁升级

无锁 👉 偏向锁 👉 轻量级锁 👉 重量级锁

偏向锁:无竞争就不同步

同一个线程多次获取锁和释放锁是资源的浪费,如果一个线程长时间持有该锁,只需要获取一次即可。

获取偏向锁流程

 轻量级锁:错时竞争就是无竞争

如果持锁线程进入同步块的时间短,无需阻塞等待锁的线程,通过自旋等待锁的释放。

注:自旋——消耗 CPU,让线程循环等待 

 重量级锁:mutex

线程切换需要保存上下文,需要进入内核态,内核态与用户态的转换非常耗时。


Java

AQS

数据结构

技术依赖关系

重要变量和方法

AQS 有两种资源共享模式,Exclusive(独占)或者 Share;

compareAndSetState方法,在独占模式下进行锁请求时经常使用——因为是独占模式所以肯定不允许多个线程同时更改State的状态。

private volatile int state; // 管理共享资源状态或计数,具体含义由同步器实现

//返回同步状态的当前值
protected final int getState() {  
        return state;
}

 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}

//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

Node 内部类

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        volatile int waitStatus;
        
        volatile Node prev;

        volatile Node next;

        Node nextWaiter;

ReentrantLock

特点是公平与非公平锁,可重入锁

内部类

构造函数

默认创建非公平锁,转入参数 fair 决定 new 公平锁还是非公平锁

final void lock()

lock会调用内部类 sync.lock(),无论是 FairSync 还是 NonfairSync 都是调用父类 AQS 的 acquire 方法;其中 tryAcquire(arg) 由 reentrantLock 实现,若当前线程成功获取独占操作权则直接返回;

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire 失败后,将该线程加入阻塞队列 addWaiter(Node.EXCLUSIVE),Node.EXCULSIVE 表示该节点在独占模式下等待;

加入队列后还要设置它的状态——acquireQueued(final Node node, int arg):

shouldParkAfterFailedAcquire(pred, node) 用于判断是否阻塞当前节点。只有当前节点的前驱节点状态是 SIGNAL 才阻塞当前节点的线程,因此队列中头节点不是 SIGNAL 表示当前节点的线程还未完成阻塞。

lock.unlock()

​​​​​​​public void unlock() {
    sync.release(1);
}

调用 sync 实现的 tryRelease( arg ),成功且头节点的后继节点被阻塞则释放后继节点:


    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentrantLock 作为可重入锁,只有释放全部的锁才能退出独占操作:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

与 Synchronized 对比

  • 需要手动释放锁,编程麻烦但更灵活,还可以通过 Condition 选择获得锁的线程
  • 可以实现公平与非公平锁

BlockingQueue

 方法

抛出异常返回特定值阻塞限时阻塞
入队add(o)offer(o)put(o)offer(o, timeout, timeunit)
出队remove(o)poll()take()poll(o, timeout, timeunit)
检查element()peek()

接口实现类使用场景

ArrayBlockingQueue

  • 数组实现,容量固定,内存占用确定
  • 存取共用一把锁,存取操作无法并行

不适用于要求高并发和吞吐量的场景,可以处理生产者-消费者问题

LinkedBlockingQueue

  • 链表实现,数据新增移除比数组快,队列容量默认Integer.MAX_VALUE,存储线程基本不会阻塞,但是消费线程速度慢会造成内存占用过多
  • 存取操作分别拥有独立的锁,可实现存取并行

适用于消费者和生产者速度相当的核心业务,如订单完成的短信/邮件通知


FutureTask

类的关系

实现了 runnable,callable 和 future

 使用方法

线程池提交

        FutureTask<Integer> futureTask = new FutureTask<>(()->{
            Thread.sleep(3000);
            return 11111;
        });
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(futureTask); // 线程池启动法

线程启动

        new Thread(futureTask).start(); // 线程启动法

构造函数

参数为 callable 的构造函数


    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

 参数为 runnable 的构造函数,采用适配器模式将 runnable 和 result 组合成 callable

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

RunnableAdapter 是适配器 

    public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }

RunnableAdapter 持有一个 Runnable 接口 task 和泛型 result,通过构造函数进行赋值;调用 call() 方法实际是调用 task.run() 并返回 result。

    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

核心成员

    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes
    /** The thread running the callable; CASed during run() */
    private volatile Thread runner;
    /** Treiber stack of waiting threads */
    private volatile WaitNode waiters;

outcome:get() 方法返回的结果或异常原因

runner:运行 callable 任务的线程

waiters:Treiber栈,存放等待线程,通过 CAS 保证线程安全(如多个线程同时调用 get() 方法被阻塞需要同时入栈),底层是单链表

WaitNode 持有线程和指向下一个 node 的 next 指针:


    /**
     * Simple linked list nodes to record waiting threads in a Treiber
     * stack.  See other classes such as Phaser and SynchronousQueue
     * for more detailed explanation.
     */
    static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

FutureTask 状态转换

  • NEW:新建的任务
  • COMPLETING:运行中的任务(异常原因或运行结果还没有到 outcome)
  • INTERRUPTING:正在响应用户调用的 cancel(boolean mayInterruptIfRunning) 方法
  • NORMAL:任务执行完成,结果在 outcome
  • EXCEPTIONAL:任务执行异常,异常原因在 outcome
  • CANCELLED:用户调用了 cancel(false) 方法取消任务且不中断任务执行线程
  • INTERRUPTED:用户调用了 cancel(true) ,该方法使用 runner.interrupt() 中断任务执行线程,线程已中断

线程安全

volatile & UNSAFE


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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

corePoolSize:核心线程数

maximumPoolSize:最大线程数

keepAliveTime:阻塞队列中线程空闲的最长时间

 unit:keepAliveTime 的单位

workQueue:阻塞队列,可选 BlockingQueue 接口的各种实现类

threadFactory:默认 DefaultThreadFactory,自定义工厂可以继承 ThreadFactory 接口,在构造函数中传入自定义字符串修改 namePrefix

handler:拒绝策略,线程池提供了四种策略——

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:提交任务的线程自己运行
  • DiscardOldestPolicy:丢弃队列中等待最久的线程
  • DiscardPolicy:直接丢弃任务

线程池执行流程

检验一下你是否看懂了

1. 10 个线程在核心运行,20个任务在队列等待

2. 第 1001 个任务触发拒绝策略

为什么不允许用 Executors 创建线程池?

通过ThreadPoolExecutor的方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

newFixedThreadPool & newSingleThreadExecutor:LinkedBlockingQueue 是无界队列,处理任务过慢会导致任务无限堆积,造成 OOM

newCachedThreadPool & newScheduledThreadPool:最大线程数是 Integer.MAX_VALUE,任务过多会创建过多线程,造成 OOM

配置线程池的考虑

如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。


ThreadLocal

方法

 .withInitial()

ThreadLocal 设置初始值的方法

    // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

 Set(T value)

获取当前运行线程的 ThreadLocalMap,不为空则插入,为空则新建一个 map

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

 createMap:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get()

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();
    }

ThreadLocalMap

构造函数

ThreadLocalMap 构造函数,新建一个 Entry 数组,并插入初始值;

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalMap 本质上是 HashMap。

内存泄露问题

Key 是弱引用,value 是强引用

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

key 弱引用是为了防止内存泄露。线程不结束运行,ThreadLocal 静态变量不能被释放,但是创建过多线程从而增加的 ThreadLocal 中不再使用的却没有被释放。将 key 设为弱引用,发生 GC 时自动回收 key,而 ThreadLocalMap 的 expungeStaleEntry、replaceStaleEntry、cleanSomeSlots 和 expungeStaleEntries 方法会清理 key 为 null 的元素。

开放地址法

ThreadLocalMap 使用开放地址法解决 Hash 冲突

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();
        }

线性探测:di = i

        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值