JUC核心考案 | 系统性学习 | 含源码分析及代码实验 | 无知的我费曼笔记(图文排版无水印)

无知的我正在复盘JUC。。。
笔记特点是

  • 重新整理了涉及资料的一些语言描述、排版而使用了自己更容易理解的描述。。
  • 提升了总结归纳性
  • 同样是回答了一些常见关键问题。。。

并发篇

并发-线程状态

六种状态及转换

在这里插入图片描述

  • 新建

    • 当一个线程对象被创建,但还未调用 start 方法时。处于新建状态
    • 此时未与操作系统底层线程关联;没有被分配内存;仅仅是一个JAVA对象
  • 可运行

    • 调用了 start 方法。就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度(交给CPU)执行
  • 终结

    • 线程内代码已经执行完毕。由可运行进入终结
    • 此时会取消与底层线程关联
  • 阻塞

    • 当获取锁失败后。由可运行进入 Monitor 的阻塞队列阻塞。此时不占用 cpu 时间
    • 当持锁线程释放锁时,会(按照一定规则)唤醒阻塞队列中的阻塞线程,阻塞线程进入下一轮锁竞争,并获取锁成功,线程进入可运行状态
  • 等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法。此时从可运行状态释放锁进入 Monitor 等待集合等待。同样不占用 cpu 时间
    • 当条件满足、其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程阻塞,线程进入下一轮锁竞争,并获取锁成功,恢复为可运行状态
  • 有时限等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法。此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
  • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁

    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
    • 还有一种情况是直接调用 sleep(long) 方法。这也会从可运行状态进入有时限等待状态。但与 Monitor 无关
  • 不需要主动唤醒,超时时间到自然恢复为可运行状态

其它情况(只需了解)

  • 可以用 interrupt() 方法打断等待有时限等待、sleep(long)的线程,让它们恢复为可运行状态
  • park,unpark 等方法也可以让线程等待和唤醒

五种状态

五种状态的说法来自于操作系统层面的划分

image-20210831092652602

  • 运行态:分到 cpu 时间,能真正执行线程内代码的
  • 就绪态:有资格分到 cpu 时间,但还未轮到它的
  • 阻塞态:没资格分到 cpu 时间的
    • 涵盖了 java 状态中提到的阻塞等待有时限等待
    • 多出了阻塞 I/O,指线程在调用阻塞 I/O 时,实际活由 I/O 设备(比如磁盘、网卡芯片)完成,此时线程无事可做,只能干等
  • 新建、终结态:与 java 中同名状态类似,不再啰嗦

并发-线程池核心参数

七大参数

corePoolSize 核心线程数

  • 就是线程池能保留的核心线程的最大数目

maximumPoolSize 最大线程数

  • 就是核心线程 + 救急线程的最大数目

KeepAliveTime 生存时间

  • 就是救急线程能够存在线程池的时间。在生存时间内,如果没有新任务,则会释放该线程

unit 时间单位

  • 救急线程的生存时间单位,如秒、毫秒等

workQueue 工作队列

  • 当没有空闲核心线程时,新来任务会加入到此队列排队;但是当队列满时,会创建救急线程执行任务

threadFactory 线程工厂

  • 定制线程对象的创建。例如设置线程名字、是否是守护线程等

handler 拒绝策略

  • 当所有线程都在繁忙 且 workQueue 也放满时,会触发拒绝策略,就是拒绝新任务加入。拒绝方式如下
  1. 抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
  2. 丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
  3. 丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
  4. 由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy

image-20210831093204388

  • 核心线程 始终保存在线程池中
  • 救急线程 当完成任务,该线程不会保留在线程池中

并发-线程池核心参数 代码演示

package day02;

import org.slf4j.Logger;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static day02.LoggerUtils.*;

// --add-opens java.base/java.util.concurrent=ALL-UNNAMED
public class TestThreadPoolExecutor {

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger c = new AtomicInteger(1);
        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);//工作队列
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,//核心线程数
                3,//最大线程数
                0,//救急线程生存时间
                TimeUnit.MILLISECONDS,//救急线程生存时间
                queue,//工作队列
                r -> new Thread(r, "myThread" + c.getAndIncrement()),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        showState(queue, threadPool);
        threadPool.submit(new MyTask("1", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("2", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("3"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("4"));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("5", 3600000));
        showState(queue, threadPool);
        threadPool.submit(new MyTask("6"));
        showState(queue, threadPool);
    }

    private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<Object> tasks = new ArrayList<>();
        for (Runnable runnable : queue) {
            try {
                Field callable = FutureTask.class.getDeclaredField("callable");
                callable.setAccessible(true);
                Object adapter = callable.get(runnable);
                Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
                Field task = clazz.getDeclaredField("task");
                task.setAccessible(true);
                Object o = task.get(adapter);
                tasks.add(o);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        main.debug("pool size: {}, queue: {}", threadPool.getPoolSize(), tasks);
    }

    static class MyTask implements Runnable {
        private final String name;
        private final long duration;

        public MyTask(String name) {
            this(name, 0);
        }

        public MyTask(String name, long duration) {
            this.name = name;
            this.duration = duration;
        }

        @Override
        public void run() {
            try {
                LoggerUtils.get("myThread").debug("running..." + this);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "MyTask(" + name + ")";
        }
    }
}

AbortPolicy()拒绝策略输出结果如下

image-20220511121020003

CallerRunsPolicy()由调用者(比如下面的主线程)运行

image-20220511121542225

DiscardPolicy()直接丢弃。没有报错信息

image-20220511121218643

DiscardOldestPolicy()把最先加入到任务队列的任务丢弃,把最新的任务加入进去

image-20220511121344280

并发-wait vs sleep

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同
    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同
    • sleep(long) 和 wait(long) 都会在等待相应毫秒后醒来。此外,wait(long) 可以被 notify 唤醒;sleep(long) 可以被interrupt() 方法打断。
    • wait() 可以被 notify 唤醒。但是如果不唤醒就一直等下去。
    • 它们都可以被打断唤醒。//比如使用interrupt方法
  • 锁特性不同(重点)
    • wait 方法的调用 必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后 会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 并不会释放对象锁——如果在 synchronized 代码块中执行的话(我放弃 cpu,你们也用不了)

并发-wait vs sleep 代码演示

报错操作 还没有获得锁就wait

image-20220509141647399

image-20220509141712964

解决办法是 先获得锁再wait

image-20220509141752108

问题 以下代码输出内容是什么——调用wait

调用wait的同时释放了锁

问题 以下代码输出内容是什么——调用sleep

image-20220509142308838

调用sleep同时不会释放锁,而是在休眠结束后,才释放锁

问题 以下代码输出内容是什么——调用sleep,打断睡眠

image-20220509142442349

image-20220509142511354

并发-lock vs synchronized

不同点

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现。//被称为内置锁,这是因为其是由语法实现的
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥(当一个线程运行时,其他线程都不能运行,陷入阻塞状态)、同步(当某线程需要另外线程的结果时,该线程会等待另外线程运行而得到结果,才会继续运行)、锁重入功能(能否对同一线程重复加锁)
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、两种公平锁(公平锁就是获取锁的机会公平——先来先得)、可打断、可超时、多条件变量(就是多个等待队列) //公平锁比非公平锁的吞吐量要低。在应用中,使用非公平锁即可
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

公平锁

  • 公平锁的公平体现
    • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
    • 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
    • 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
  • 公平锁会降低吞吐量,一般不用

条件变量

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

并发-lock 阻塞演示

重写MyReentrantLock继承自ReentrantLock

package day02;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static day02.LoggerUtils.*;

// --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
public class TestReentrantLock {
    static final MyReentrantLock LOCK = new MyReentrantLock(true);//参数是true代表公平锁;false代表非公平锁

    static Condition c1 = LOCK.newCondition("c1");
    static Condition c2 = LOCK.newCondition("c2");

    static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException, IOException {
        learnLock();
    }

    private static void learnLock() throws InterruptedException {
        System.out.println(LOCK);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t1").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t2").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t3").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
        }, "t4").start();
    }

    private static void fairVsUnfair() throws InterruptedException {
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t1").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t2").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t3").start();

        Thread.sleep(100);
        new MyThread(() -> {
            LOCK.lock();
            get("t").debug("acquire lock...");
            sleep1s();
            LOCK.unlock();
        }, "t4").start();

        get("t").debug("{}", LOCK);

        while (!stop) {
            new Thread(() -> {
                try {
                    boolean b = LOCK.tryLock(10, TimeUnit.MILLISECONDS);//@tryLock()如果10毫秒还没有获得锁头,直接返回false,而不是像Lock()一直等待
                    if (b) {
                        System.out.println(Thread.currentThread().getName() + " acquire lock...");
                        stop = true;
                        sleep1s();
                        LOCK.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    private static void sleep1s() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class MyReentrantLock extends ReentrantLock {
        private final Map<String, Condition> conditions = new HashMap<>();

        public MyReentrantLock(boolean fair) {
            super(fair);
        }

        public Condition newCondition(String name) {
            Condition condition = super.newCondition();
            conditions.put(name, condition);
            return condition;
        }

        @Override//打印 ReentrantLock 内部结构
        public String toString() {
            StringBuilder sb = new StringBuilder(512);
            String queuedInfo = getQueuedInfo();
            List<String> all = new ArrayList<>();
            all.add(String.format("| owner[%s] state[%s]", this.getOwner(), this.getState()));
            all.add(String.format("| blocked queue %s", queuedInfo));
            for (Map.Entry<String, Condition> entry : this.conditions.entrySet()) {
                String waitingInfo = getWaitingInfo(entry.getValue());
                all.add(String.format("| waiting queue [%s] %s", entry.getKey(), waitingInfo));
            }
            int maxLength = all.stream().map(String::length).max(Comparator.naturalOrder()).orElse(100);
            for (String s : all) {
                sb.append(s);
                String space = IntStream.range(0, maxLength - s.length() + 7).mapToObj(i -> " ").collect(Collectors.joining(""));
                sb.append(space).append("|\n");
            }
            sb.deleteCharAt(sb.length() - 1);
            String line1 = IntStream.range(0, maxLength ).mapToObj(i -> "-").collect(Collectors.joining(""));
            sb.insert(0, String.format("%n| Lock %s|%n", line1));
            maxLength += 6;
            String line3 = IntStream.range(0, maxLength).mapToObj(i -> "-").collect(Collectors.joining(""));
            sb.append(String.format("%n|%s|", line3));
            return sb.toString();
        }

        private Object getState() {
            try {
                Field syncField = ReentrantLock.class.getDeclaredField("sync");
                Class<?> aqsClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer");
                Field stateField = aqsClass.getDeclaredField("state");
                syncField.setAccessible(true);
                AbstractQueuedSynchronizer sync = (AbstractQueuedSynchronizer) syncField.get(this);
                stateField.setAccessible(true);
                return stateField.get(sync);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        private String getWaitingInfo(Condition condition) {
            List<String> result = new ArrayList<>();
            try {
                Field firstWaiterField = AbstractQueuedSynchronizer.ConditionObject.class.getDeclaredField("firstWaiter");
                Class<?> conditionNodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode");
                Class<?> nodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
                Field waiterField = nodeClass.getDeclaredField("waiter");
                Field statusField = nodeClass.getDeclaredField("status");
                Field nextWaiterField = conditionNodeClass.getDeclaredField("nextWaiter");
                firstWaiterField.setAccessible(true);
                waiterField.setAccessible(true);
                statusField.setAccessible(true);
                nextWaiterField.setAccessible(true);
                Object fistWaiter = firstWaiterField.get(condition);
                while (fistWaiter != null) {
                    Object waiter = waiterField.get(fistWaiter);
                    Object status = statusField.get(fistWaiter);
                    result.add(String.format("([%s]%s)", status, waiter));
                    fistWaiter = nextWaiterField.get(fistWaiter);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return String.join("->", result);
        }

        private String getQueuedInfo() {
            List<String> result = new ArrayList<>();
            try {
                Field syncField = ReentrantLock.class.getDeclaredField("sync");
                Field headField = AbstractQueuedSynchronizer.class.getDeclaredField("head");
                Class<?> nodeClass = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
                Field waiterField = nodeClass.getDeclaredField("waiter");
                Field statusField = nodeClass.getDeclaredField("status");
                Field nextField = nodeClass.getDeclaredField("next");
                syncField.setAccessible(true);
                AbstractQueuedSynchronizer sync = (AbstractQueuedSynchronizer) syncField.get(this);
                waiterField.setAccessible(true);
                statusField.setAccessible(true);
                nextField.setAccessible(true);
                headField.setAccessible(true);
                Object head = headField.get(sync);
                while (head != null) {
                    Object waiter = waiterField.get(head);
                    Object status = statusField.get(head);
                    result.add(String.format("({%s}%s)", status, waiter));
                    head = nextField.get(head);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return String.join("->", result);
        }
    }

    static class MyThread extends Thread {
        public MyThread(Runnable target, String name) {
            super(target, name);
        }

        @Override
        public String toString() {
            return this.getName();
        }
    }
}

当线程都没有占有锁头时,各个线程状态如下图

image-20220509143431814

当t1占有锁时,各个线程状态是

image-20220509143719445

@阻塞队列有头结点

释放t1而让t2持有锁时,各个线程状态是

image-20220509143933266

再给t2加一把锁后,state + 1

image-20220509144025818

//如果想要完全对其解锁,需要解两次锁

并发-lock 公平非公平演示

使用的代码和上面的一致

非公平锁头

image-20220509145220314

有线程比t2提前获得了锁,这就意味着不是线程等待时机越长的越快获得锁,这是不公平的

公平锁

image-20220509145414689

在源码中,两种tryLock()的区别

A)无参的——无论如何都是非公平

image-20220509145508710

B)有参的——根据传入的参数决定是否公平

image-20220509145557923

并发-volatile

线程安全要考虑三个方面

  1. 可见性 当一个线程对共享变量修改时,另一个线程能看到最新的结果
  2. 有序性 一个线程中的代码按照编写顺序执行
  3. 原子性 一个线程内多行代码以一个整体运行,期间不能有其他线程代码插队

volatile具有的安全性质

可见性、有序性、没有原子性

原子性

  • 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
  • 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性

可见性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

有序性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致

  • 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

  • 注意:

    • volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
    • volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
    • volatile 读写加入的屏障只能防止同一线程内的指令重排

并发-volatile 原子性演示

在java代码中,一行代码其实是多条指令

举例说明 如下是经过反编译后的java代码(balance + 5)

image-20220509150255265

反编译控制台命令

javap -p -v [类名]

**原子性——因指令执行交错,而导致线程不安全 演示代码 **

package day02.threadsafe;

import day02.LoggerUtils;

import java.util.concurrent.CountDownLatch;

// 原子性例子

/**

 t1 10
 0: getstatic
                 t2
                 0: getstatic 10
                 3: iconst_5
                 4: isub
                 5: putstatic
                    5
 3: iconst_5
 4: iadd
 5: putstatic
 15



 */
public class AddAndSubtract {

    static volatile int balance = 10;

    public static void subtract() {
        int b = balance;
        b -= 5;
        balance = b;
    }

    public static void add() {
        int b = balance;
        b += 5;
        balance = b;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            subtract();
            latch.countDown();
        }).start();
        new Thread(() -> {
            add();
            latch.countDown();
        }).start();
        latch.await();
        LoggerUtils.get().debug("{}", balance);
    }
}

并发-volatile 可见性演示

package day02.threadsafe;


import static day02.LoggerUtils.get;

// 可见性例子
// -Xint
public class ForeverLoop {
    static volatile boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            get().debug("modify stop to true...");
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        }).start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        get().debug("stopped... c:{}", i);
    }
}

image-20220509151702621

可以看见,当stop已经变为true,但是foo()中的while循环仍然在执行

可见性原理——有问题的表述

image-20220509151503222

举例说明上一个问题

让另外一个线程获取该值

可以看见结果是 另外一个线程的确获取到了最新的变量值,而while循环中的仍然是旧的数据

这是因为 Java中 JIT 对经常重复的代码作的优化

就是让该代码越过了每次的编译,而是每次直接使用已经编译好了的字节码,从而导致了无法通过编译获取最新数据。在这里插入图片描述

举例说明——让编译器只解释,不作JIT优化

设置JVM参数 -Xint

image-20220509152812848

举例说明——让线程提前苏醒而使得循环次数降低,从而达到JIT优化条件

(略)

解决办法 使用volatile修饰符来指定某行代码不适用JIT优化。如下

static volatile boolean stop = false;

并发-volatile 有序性演示

并发-悲观锁 vs 乐观锁

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
    • 为了解决上一个问题,实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
  • 乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数 //这是因为线程需要占用cpu才能运行

并发-悲观锁 vs 乐观锁 Unsafe_cas

使用Unsafe修改共享变量失败。演示如下

image-20220511114849269

image-20220511114911662

结果输出的false、100。这是因为Unsafe的机制是 先比较到当前获取到的变量值和最新的变量值是一致的,才会去修改变量值而返回true,否则返回不修改 且 返回false

实现Unsafe绝对修改共享变量成功。代码如下

image-20220511115201684

image-20220511115235026

并发-悲观锁 vs 乐观锁 代码实现对比

package day02;


import jdk.internal.misc.Unsafe;

// --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
public class SyncVsCas {
    static final Unsafe U = Unsafe.getUnsafe();
    static final long BALANCE = U.objectFieldOffset(Account.class, "balance");//获取要修改变量的偏移位置

    static class Account {
        volatile int balance = 10;//要修改的变量
    }

    private static void showResult(Account account, Thread t1, Thread t2) {
        try {
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            LoggerUtils.get().debug("{}", account.balance);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	//悲观锁
    public static void sync(Account account) {
        Thread t1 = new Thread(() -> {
            synchronized (account) {
                int old = account.balance;
                int n = old - 5;
                account.balance = n;
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            synchronized (account) {
                int o = account.balance;
                int n = o + 5;
                account.balance = n;
            }
        },"t2");

        showResult(account, t1, t2);
    }
	//乐观锁
    public static void cas(Account account) {
        Thread t1 = new Thread(() -> {
            while (true) {
                int o = account.balance;
                int n = o - 5;
                if (U.compareAndSetInt(account, BALANCE, o, n)) {
                    break;
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            while (true) {
                int o = account.balance;
                int n = o + 5;
                if (U.compareAndSetInt(account, BALANCE, o, n)) {
                    break;
                }
            }
        },"t2");

        showResult(account, t1, t2);
    }

    private static void basicCas(Account account) {
        while (true) {
            int o = account.balance;
            int n = o + 5;
            if(U.compareAndSetInt(account, BALANCE, o, n)){
                break;
            }
        }
        System.out.println(account.balance);
    }

    public static void main(String[] args) {
        Account account = new Account();
        cas(account);
    }


}

并发-Hashtable vs ConcurrentHashMap

Hashtable 对比 ConcurrentHashMap

  • 初始化不同。Hashtable 可以看作是饿汉式的;ConcurrentHashMap 可以看作是懒汉式的(这是因为ConcurrentHashMap 在添加元素时,才创建数据结构)
  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合 //他们的键和值都不能为空
  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

ConcurrentHashMap 1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
  • 索引计算
    • 假设大数组长度是 2 m 2^m 2m,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
    • 假设小数组长度是 2 n 2^n 2n,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
  • Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准

ConcurrentHashMap 1.8

  • 数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁(这意味着数组的容量有多大、锁就有多少个),如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
  • 扩容条件:Node 数组满 3/4 时就会扩容
  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
  • 扩容时并发 get
    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
    • 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
    • 如果链表最后几个元素扩容后索引不变,则节点无需复制
  • 扩容时并发 put
    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
    • 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
    • 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
  • 与 1.7 相比是懒惰初始化
  • capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 2 n 2^n 2n
  • loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
  • 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容

并发-Hashtable 演示

image-20220510153128440

第一次扩容需要多少个元素?

(11 * 3)/4 = 8.25,所以是第九个元素会扩容。

扩容后的数组容量是 11 * 2 + 1。如图

image-20220510153144981

为什么不需要二次哈希?

这是因为其数组容量是质数,分散性已经足够好了

说明不需要二次哈希-查看源码。如下图

image-20220510153329792

可以看见 没有进行二次哈希;至于为什么需要按位与?这是因为防止出现负数

并发-ConcurrentHashMap 1.7 演示并发度

clevel(即并发度)决定数组容量

capacity决定小数组容量。小数组容量 = capacity / clevel;如果capacity 比 clevel 还要小,则取规定好的最小值2。

factor(即扩容因子)如果值为0.75,决定了小数组元素等于小数组容量的3/4后,会发生扩容;但无法影响大数组容量,大数组容量一经确定无法改变

image-20220510155914372

并发-ConcurrentHashMap 1.7 演示 Segment 索引计算

Segment 索引如何计算-查看 Segment 源码

image-20220510160918054

Segment 索引如何计算-可视化图形 演示

Segment索引 = 二次哈希值的二进制数的高(数组容量的2的n次幂的n)位值

元素在小数组的索引 = 二次哈希值的二进制数的低(数组容量的2的n次幂的n)位值

举例说明。如下

image-20220510161856321

并发-ConcurrentHashMap 1.7 演示 扩容

扩容前

image-20220510162827007

往索引为0处再加一个元素后扩容。如图

image-20220510162906085

这是因为此时链表元素个数超过了数组容量的3/4(既0.75)

并发-ConcurrentHashMap 1.7 演示 Segment[0]

每次创建新的小数组的容量以Segment[0]为作为原形,即容量与其一致。如图

image-20220510163737261

并发-ConcurrentHashMap 1.8 演示

下面是ConcurrentHashMap 创建的数据结构,可以看见没有Segment

image-20220510235849996

当点击put后,因为刚好元素刚好达到了数组容量的3/4,所有立刻扩容。如下

image-20220510235800467

并发-ConcurrentHashMap 1.8 构造参数的含义

capacity的作用是 设置一个将要放入的元素总个数,而不是直接等于数组的容量

factor的作用是 决定了扩容的时机,但是只在初始化构建数组时生效。后续的扩容时机都是关于3/4的

数组容量大小的设置只要满足 能够装下假设的元素总个数、2的n次幂的条件即可

初始化构建数组 举例说明。如下图

image-20220511001757034

factor的作用 举例说明。如下图

下图中数组元素已经超过了数组容量的1/2,但是还没有扩容。

image-20220511001921907

当数组元素超过了数组容量的3/4才扩容。如下图

image-20220511001952546

并发-ConcurrentHashMap 1.8 演示并发put

只要操作的不是同一个链表头,就可以并发执行

A)当操作的是同一个链表头 举例说明

image-20220511002651908

可看见 同时操作,操作的是同一个链表头时,会发生阻塞现象

B)当操作的不是同一个链表头 举例说明

image-20220511002531297

可以看见 同时操作,但是操作的不是同一个链表头,结果是并发

并发-ConcurrentHashMap 1.8 演示扩容

扩容时的两个步骤是

  1. 为key重新计算索引并迁移
  2. 设置已经处理好的旧链表头为forwardingNode。其作用是 让正在访问链表头的线程,知道其是否已经被处理过

A)演示第一步

image-20220511003816351

迁移前后的是 同一个对象,但是只在链表只有一个对象的前提下。如果链表有多个对象,就是不同的对象

B)演示第二步

image-20220511003843280

并发-ConcurrentHashMap 1.8 扩容细节

问题 如果含有多个元素的单链表发生扩容迁移,假定迁移前后都是同一个对象,那么在迁移期间,进行get操作,在迁移完成之后,对该单链表进行查询操作,由于当时迁移未完成,获取的是旧的单链表,有可能导致找不到想要的下一个指向的结点的情况

所以如果迁移前后链表中的结点的相互位置没有发生改变就是同一个对象,如果不是则反之。

在迁移扩容时新来的线程的操作结果

如果线程想要操作当前正在迁移的链表,会发生阻塞

如果线程想要操作还没有迁移的链表,会顺利执行

如果线程想要操作已经迁移的链表,并且原本的线程还没有对它们进行扩容,那么新来的线程会协助它们扩容 //每个线程一次性可以迁移16个链表

并发-ThreadLocal

作用

  • 避免争用引发的线程安全问题。ThreadLocal 可以实现线程间的【资源对象】的线程隔离,让每个线程各用各的【资源对象】
  • ThreadLocal 同时实现了线程内的资源共享

原理

每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

  • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
  • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
  • 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocalMap 的一些特点

  • key 的 hash 值统一分配
  • 初始容量 16,扩容因子 2/3,扩容容量翻倍
  • key 索引冲突后用开放寻址法解决冲突

弱引用 key

ThreadLocalMap 中的 key 被设计为弱引用,原因如下

  • Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存

内存释放时机

  • 被动 GC 释放 key
    • 仅是让 key 的内存释放,关联 value 的内存并不会释放
  • 懒惰被动释放 value
    • get key 时,发现是 null key,则释放其 value 内存
    • set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
  • 主动 remove 释放 key,value
    • 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
    • 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收

以下代码实现线程内使用同一个连接对象;不同线程使用各自的连接对象

package day02;

import day01.sort.Utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestThreadLocal {
    public static void main(String[] args) {
        test2();
    }

    // 一个线程内调用, 得到的是同一个 Connection 对象
    private static void test2() {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                LoggerUtils.get("t").debug("{}", Utils.getConnection());
                LoggerUtils.get("t").debug("{}", Utils.getConnection());
                LoggerUtils.get("t").debug("{}", Utils.getConnection());
            }, "t" + (i + 1)).start();
        }
    }

    // 多个线程调用, 得到的是自己的 Connection 对象
    private static void test1() {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                LoggerUtils.get("t").debug("{}", Utils.getConnection());
            }, "t" + (i + 1)).start();
        }
    }

    static class Utils {
        private static final ThreadLocal<Connection> tl = new ThreadLocal<>();

        public static Connection getConnection() {
            Connection conn = tl.get(); // 到当前线程获取资源
            if (conn == null) {
                conn = innerGetConnection(); // 创建新的连接对象
                tl.set(conn); // 将资源存入当前线程
            }
            return conn;
        }

        private static Connection innerGetConnection() {
            try {
                return DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useSSL=false", "root", "root");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

并发-ThreadLocal 原理

线程隔离原理的是通过每个线程内的map集合实现的。如下

image-20220511112439462

索引值的计算是 从0开始,每次创建一个新的Threadlocal对象,为其分配一个hash值,以后新的Threadlocal对象的索引值是 在前面的基础上,再累加一个数字的数值。

解决冲突的方式是 开放寻址法(如果想要存放到的桶下标已经有元素,在相邻的桶下标存放),而不是拉链法。如下

image-20220511113125512

并发-ThreadLocal 内存释放时机_get

通过源码观察到对于值引用没有改变为弱引用,对于键引用则改变为弱引用了

image-20220511121737511

所以当因内存不足而垃圾回收时,会释放掉没有被引用的key所占用的内存,但是值不会。

当进行get操作时,发现key已经为null了,会顺便把其对应的值释放,并重新创建一个key。如下

image-20220511122617342

image-20220511122627429

并发-ThreadLocal 内存释放时机_set

当进行set操作时,使用启发式扫描(扫描范围与元素个数、是否发现full key有关)。发现key已经为null了,会顺便把其对应的值和临近的null key-值释放,然后放入自己的key。如下

image-20220511123101157

image-20220511123111142

并发-ThreadLocal 内存释放时机_remove

问题 观察源码发现,静态变量引用ThreadLocal 对象。这就意味着该对象一直对key进行强引用,即使key本身是弱引用,但是在GC时仍然无法回收key,所以get、set方法不可行。如下

image-20220511123429947

解决办法是 由程序员自己使用remove()决定释放哪个键值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值