符合死锁的四个条件:

  1. 互斥条件:一个时刻一个线程一个资源

  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:线程已获得的资源,在未用完之前,不能被其他线程剥夺。

  4. 循环等待条件:若干线程形成头尾相接的循环等待资源关系。

如何预防和避免线程死锁:

  1. 破坏请求与保持条件:一次性申请所有资源
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

避免死锁:

​ 避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态

安全状态:指的是系统能够按照某种线程推进顺序(P1,P2,P3...Pn)来为每个线程分配资源。知道每个线程对资源的最大需求,是每个线程都iiu可以顺利完成。称< P1,P2...Pn>序列为安全序列。

sleep()方法没有释放锁,wait()方法释放了锁。

可以直接调用Thread类的run方法:

​ 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

​ run方法可以创建一个线程,但是相当于同步的方式,没有多线程的存在。

只有调用start方法才是交给jvm管理,才是多线程。

为什么需要内存模型JMM(Java Memory Model)

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

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

简化多线程编程和增强程序可移植性的

happen-before原则

​ happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。


volatile 的作用

  1. 保证变量的内存可见性
  2. 禁止指令重排序

volatile 关键字能保证数据的可见性,但不能保证数据的原子性synchronized 关键字两者都能保证。

乐观锁和悲观锁

悲观锁

​ 像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

​ 版本号机制和CAS算法。

CAS算法:

​ Compare And Swap

​ 函数公式:CAS(V,E,N)V:表示要更新的变量E:表示预期值N:表示新值。

CAS算法详解 - 知乎 (zhihu.com)

版本号机制:

image-20230913103827343

ABA问题

image-20230913104139016

LongAdder:

image-20230913103432278

image-20230913103422314

乐观锁总结

image-20230913105001248

哈哈总结

理论上来说:

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)

(AbstractQueuedSynchronizer)

​ 主要用来构造锁和同步器。比如:ReentrantLock ,Semaphore。

AQS核心思想

  • ​ AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。
  • CLH锁: 其实是一个虚拟的双向队列(虚拟即不存在实例,仅存在节点之间的关联关系)。暂时获取不到锁得线程被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
  • AQS使用int成员变量state表示同步状态,通过内置得FIFO线程等待/等待线程来完成获取资源现成的排队工作。

AQS资源共享方式

  • 独占Exclusive(只有一个线程能执行,如ReentrantLock)

  • 共享Share(多个线程可以给同时执行,如Semaphore/CountDownLatch)

常见同步工具类

Semaphore(信号量)

synchronizedReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量

Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。

原理

​ Semaphore是共享锁的一种实现,默认构造的AQS的state值为permits。

​ 以无参 acquire 方法为例,调用semaphore.acquire() ,线程尝试获取许可证,如果 state > 0 的话,则表示可以获取成功,如果 state <= 0 的话,则表示许可证数量不足,获取失败。

​ 如果可以获取成功的话(state > 0 ),会尝试使用 CAS 操作去修改 state 的值 state=state-1。如果获取失败则会创建一个 Node 节点加入等待队列,挂起当前线程。


ReentrantLock 与synchronized的区别(增加了那些高级功能)

​ 等待可中断:lock.lockInterruptibly()。synchronized不可中断。

​ 可实现公平锁。

​ 可实现选择性通知(锁可以绑定多个条件) synchronized=>wait(),notify()/notifyAll(),等待通知,ReentrantLocak=>Condtiton。使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现选择性通知使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”

ReentrantReadWriteLock

​ 其实是两把锁:读锁(共享锁(一把锁可以被多个线程同时获得))和写锁(独占锁)

​ 底层也是AQS。

读锁拿到,能不能拿到写锁(不能);相反分情况。

​ 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

​ 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)

读锁为什么不能升级为写锁

​ 可降级不可升级。

​ 因为升级会导致,引发线程的争夺(会导致死锁);并且写锁是独占锁,回影响性能

StampedLock

​ jdk1.8引入,读写锁;不可重入;不支持条件变量Condition

​ 不同于一般Lock类,不是直接实现Lock或ReadWriteLock接口,基于CLH锁(AQS也是基于这个)独立实现的。

三种读写控制模式:读锁,写锁,乐观锁。

  • 写锁:独占锁(一把锁只能被一个线程获取)。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。

  • 读锁(悲观锁):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。

  • 乐观读:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

    StampedLock实现了不仅多个读不互相阻塞,同时在读操作时不会阻塞写操作能够达到这种效果,它的核心思想在于,**在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。

    这种操作方式决定了StampedLock在读线程非常多而写线程非常少的场景下非常适用,同时还避免了写饥饿情况的发生。

    StampedLock不可重入

    tampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。


tampedLockReentrantReadWriteLock 一样,StampedLock 同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock的替代品,性能更好。

StampedLock 适合什么场景?

java实现多线程的几种方式

1继承Thread类:

​ 1.创建多个线程,调用start方法,启动线程的时候,并不是调用线程类的run方法,而是调用了线程类的start方法。那么我们能不能调用run方法呢?答案是肯定的,因为run方法是一个public声明的方法,因此我们是可以调用的,但是如果我们调用了run方法,那么这个方法将会作为一个普通的方法被调用,并不会开启线程。这里实际上是采用了设计模式中的模板方法模式,Thread类作为模板,而run方法是在变化的,因此放到子类来实现。(能调用,但是就不算开启线程了)
​ 2。指定线程名称,不指定的话,系统会默认指定线程名,命名规则是Thread-N的形式。但是为了排查问题方便,建议在创建线程的时候指定一个合理的线程名字。下面的代码是不使用线程名的样子。

2实现runnable接口:

​ 1.将低程序的耦合度

​ 2.Runnabl接口中只定义一个方法run方法。

​ 3.利用线程任务和线程的控制分离,实现了线程的解耦。我们要实现一个线程,可以借助Thread类,Thread类要执行的任务就可以由实现了Runnable接口的类来处理

​ 4.1)创建线程任务

​ 2)创建可运行类:

	线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。这是很有用的,比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承Thread类的方式来创建线程的方式,实现Runnable接口是更为常用的。

​ 3)lamba方式创建线程任务:简化内部类的编写。

3使用内部类的方式

​ 线程只想执行一次的时候,就可以使用内部类,可以少定义一个类。就分为两种情况:

​ 1)继承Thread类

​ 2)实现runnable接口。

当两者同时内部实现时,忽略runnable的方式

代码如下:

 // 基于子类的方式
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    printThreadInfo();
                }
            }
        }.start();
    // 基于接口的实现
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                printThreadInfo();
            }
        }
    }).start();
}

lambda方式改造:

// 使用lambda的形式
new Thread(() -> {
    while (true) {
        printThreadInfo();
    }
}).start();

// 对比不使用lambda的形式
new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            printThreadInfo();
        }
    }
}).start();

4定时器

​ 定时器可以说是一种基于线程的一个工具类,可以定时的来执行某个任务。在应用中经常需要定期执行一些操作,比如要在凌晨的时候汇总一些数据,比如要每隔10分钟抓取一次某个网站上的数据等等,总之计时器无处不在。

​ 1.指定时间点执行

import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;
public class CreateThreadDemo9_Timer {

    private static final SimpleDateFormat format =
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws Exception {
        ```
        // 创建定时器
        Timer timer = new Timer();

        // 提交计划任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了...");
            }
        }, format.parse("2017-10-11 22:00:00"));
        ```
    }
}

​ 2.间隔时间重复执行

		// 提交计划任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了...");
            }
        },
   		new Date(), 1000);

5带返回值的线程实现方式(Callable接口)

  •  imort java.util.concurrent.Callable;
     import java.util.concurrent.FutureTask;
     
     /**
     
      * 带返回值的方式
        */
        public class CreateThreadDemo11_Callable {
     
        public static void main(String[] args) throws Exception {
     
            // 创建线程任务
            Callable<Integer> call = () -> {
                System.out.println("线程任务开始执行了....");
                Thread.sleep(2000);
                return 1;
            };
            
            // 将任务封装为FutureTask
            FutureTask<Integer> task = new FutureTask<>(call);
            
            // 开启线程,执行线程任务
            new Thread(task).start();
            
            // ====================
            // 这里是在线程启动之后,线程结果返回之前
            System.out.println("这里可以为所欲为....");
            // ====================
            
            // 为所欲为完毕之后,拿到线程的执行结果
            Integer result = task.get();
            System.out.println("主线程中拿到异步任务执行的结果为:" + result);
     
        }
        }
    

6基于线程池的方式实现

	我们知道,线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。当然了,线程池也不需要我们来实现,jdk的官方也给我们提供了API。

​~~~java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CreateThreadDemo12_ThreadPool {
public static void main(String[] args) throws Exception {
```
// 创建固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);

while (true) {
    // 提交多个线程任务,并执行
    threadPool.execute(new Runnable() {
        @Override
        public void run() {
            printThreadInfo();
        }
    });
}
```
}

/**
 * 输出当前线程的信息
   */
   private static void printThreadInfo() {
   System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
   try {
       Thread.sleep(1000);
   } catch (Exception e) {
       throw new RuntimeException(e);
   }
}
}
​~~~

ThreadLocal

ThreadLocal类主要解决的就是让每个线程绑定自己的值。 访问一个threadLocal变量,则每一个线程都会有这个变量的本地副本。

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

TheadLocal原理:

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

image-20230911091251266

ThreadLocal内存泄漏问题:

image-20230911091303773


线程池

什么线程池

为什么使用线程池

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

​ 线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

image-20230909100145045

创建线程池的方式

1通过ThreadPoolExecutor构造函数来创建(推荐)。

2通过 Executor 框架的工具类 Executors 来创建。

image-20230909101358784

不推荐内置线程池方法来创建线程池。

image-20230909101708017

线程池常见参数

/**
 * 用给定的初始参数创建一个新的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;
}

image-20230909102423778

线程池的饱和策略

​ 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

image-20230909152509729

线程池常用的阻塞队列

image-20230911094120836

应为当队列无界是,既无法达到队列的最大值,所以不需要扩容核心线程数,所以最大创建的线程为核心线程数。

线程池处理任务流程

image-20230911095311909

如何设定线程池大小

​ 多线程这个场景来说主要是增加了上下文切换成本

image-20230912214251667

image-20230912214615293

image-20230912214728382

如何动态创建线程池参数(美团的解决方案)

线程池参数动态化。

image-20230912220304390

image-20230912220916392

动态化的原理

image-20230912221120577

经过前面两个方法的分析,我们知道了最大线程数核心线程数可以动态调整。

如何动态指定等待队列的长度

image-20230912221522520

ResizableCapacityLinkedBlockIngQueue 的队列:

定义一个队列,让其可以对 Capacity 参数进行修改即可。操作起来也非常方便,把 LinkedBlockingQueue 粘贴一份出来,修改个名字,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。

image-20230912221651770

Future类

作用:

​ 具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。

​ 其实就是多线程中经典的 Future 模式,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。

image-20230912223357498

image-20230912223411012

Callable与Futrue的关系:

FutureTask 提供了 Future 接口的基本实现,常用来封装 CallableRunnable,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。ExecutorService.submit() 方法返回的其实就是 Future 的实现类 FutureTask

<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task); 

FutureTask 有两个构造函数,可传入 Callable 或者 Runnable 对象。实际上,传入 Runnable 对象也会在方法内部转换为Callable 对象。

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
    // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;
}

FutureTask相当于对Callable 进行了封装,管理着任务执行的情况,存储了 Callablecall 方法的任务执行结果

CompletableFuture 类的作用

image-20230912224201734

AQS(AbstractQueuedSynchronizer)

抽象队列同步器java.util.concurrent.locks 包下面。

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

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

AQS的原理

​ 1.如果被请求的资源空闲,则当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为共享状态。

​ 2.如果请求得资源被占用时,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制AQS就是用CLH队列实现的,即将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列。一个线程相当于一个节点,一个节点保存着1)线程的引用 ;2)当前节点在队列中的状态,3)前驱节点,4)后驱节点。

image-20230913084538695

img

AQS使用int成员变量state表示同步状态,通过内置的线程等待队列来完成获取资源线程的排队工作。

state由volatile关键字修饰,用于展示当前临界资源的获锁情况。

// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;

image-20230913085728033

以CountDownLatch为例:

image-20230913085742750

Semaphore的用处

​ synchronized和ReentrantLock都是一次只允许一个线程访问某个资源。

​ semaphore可以用来控制同时访问特定资源的线程数量。

image-20230913091337554

当初始资源个数为1时,Semaphore退化为排他锁。

两种模式

​ 1.公平模式:调用acquire方法的顺序就是获取许可证的顺序,遵循FIFO。

​ 2.非公平模式:抢占式的。

image-20230913091940545

Semaphore的原理:

​ 共享锁的一种实现,默认构造AQS的state的值为permits,可以将permits的值理解为许可证的数量,只有拿到许可证的线程才能执行。

调用semaphore。acquire(),尝试获取许可证,

​ 如果state>=0,表示可以获取成功。

​ 获取成功的话,使用CAS操作去修改state的值使得state=state-1。、

​ 如果state<0,则表示许可证数量不足。此时加入阻塞队列,挂起当前线程。

image-20230913092739056

CountDownLatch的用处

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。

CountDownLatch的原理

​ 共享锁的一种实现。默认构造AQS的state值为count。image-20230913094116372

CountDownLatch的场景:

image-20230913094741572

伪代码如下:

public class CountDownLatchExample1 {
    // 处理文件的数量
    private static final int threadCount = 6;
    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建)
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {
                try {
                    //处理文件的业务操作
                    //......
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //表示一个文件已经被完成
                    countDownLatch.countDown();
                }

            });
        }
        countDownLatch.await();
        threadPool.shutdown();
        System.out.println("finish");
    }
}

CyclicBarrier的作用

可循环的屏障image-20230913095923137

CyclicBarrier的原理

​ CyclicBarrier内部通过一个count变量作为计数器,count的初始值为parties属性为初始值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

image-20230913102044703

image-20230913102148537

await的使用

在Java中,"await"方法通常与多线程编程和并发控制相关。这个方法通常用于等待某个条件或事件的发生,然后暂停当前线程的执行,直到条件满足或事件发生后再继续执行。"await"方法通常与信号量、条件变量、锁或线程池等并发控制机制一起使用,以实现线程的同步和协作。 在Java中,常见的使用"await"方法的类和接口包括:

  1. CountDownLatch(倒计数门闩):使用await()方法等待计数器归零。

  2. CyclicBarrier(循环屏障):使用await()方法等待到达指定的屏障位置。

  3. Phaser(阶段器):使用awaitAdvance()方法等待其他参与者到达相同的阶段。

  4. Condition(条件):在使用ReentrantLock或ReentrantReadWriteLock时,可以使用await()方法等待条件满足。

    需要注意的是,"await"方法必须在某种线程同步机制的作用域内使用,并且通常需要正确设置条件或事件的触发条件,以避免线程永远等待或出现死锁等并发问题。

JMM(Java内存模型)

CPU缓存模型

​ 解决CPU处理速度和内存不匹配的问题。

​ CPUCache工作方式可能会导致内存缓存不一致性的问题。image-20230913105637563

​ 为了解决这个问题:通过制定缓存一致协议(MESI协议),或其他手段。image-20230913105926856

Java线程池详解

监控线程池

​ SpringBoot 中的 Actuator 组件。自己写一个监控线程池的HealthContributor,(https://www.codenong.com/cs110918477/)

image-20230914093705754

建议不同类别的业务用不同的线程池

一个线程池执行父子任务(父等子的回应,子等父的释放资源)容易发生死锁。

image-20230914094335603

线程池和ThreadLocal共用的坑

image-20230914094901185

Java常见并发容器总结

基本都在java.util.concurren包里。

image-20230914095305775

ConcurrentHashMap

1.7版本Segment数组+HashEntry数组+链表

image-20230914100412071

1.8版本Node数组+链表+红黑树

image-20230914100449918

CopyOnWriteArrayList

Java1.5版本使用的是vetor,但是该容器增删改查基本都加了synchornized.相当于给vector加了一把大锁,性能非常低下。

JUC中唯一的线程安全Lisy就是这个CopyOnWriteArrayList.

写时复制(Copy-On-Write)的策略(只有写写互斥);

image-20230914101216031

ConcurrentLinkedQueue(非阻塞队列)(无锁)

阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。

主要使用CAS非阻塞算法来实现线程安全。其使用链表作为数据结构。

BlockingQueue(阻塞队列)(是一个接口)(以后好好看看

ArrayBlockingQueue(实现类)(有界)

LinkedBlockingQueue(实现类无界)(单向链表)

PriorityBlockingQueue(实现类无界,支持优先级)image-20230914102624707

ConcurrentSkipListMap

跳表(时间复杂度O(Logn))

​ 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 2级索引跳表

上图是一个2级索引跳表

image-20230914103230214

跳表是一种利用空间换时间的算法。

image-20230914103314105

Atomic原子类总结

​ 在java.util.concurrent.atmoic包下面

image-20230914104243897

原子类可分为4类。

image-20230914105633623

image-20230914105640828

一般都是通过CAS操作,比synchornized的操作开销小多了。

ThreadLocal详解

img

具体使用

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

需要理解的是:

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

​ 当我们创建一个ThreadLocal对象时,我们可以使用withInitial方法为其指定一个供应商函数(Supplier Function)。供应商函数在需要的时候创建初始值,返回一个新的对象作为线程的局部变量。 在这个例子中,ThreadLocalTest.holder是一个ThreadLocal<ThreadLocalTest>对象,它的泛型参数是ThreadLocalTest,表示每个线程都将拥有一个ThreadLocalTest对象作为其局部变量ThreadLocalTest.holder使用了withInitial方法,并传入了一个方法引用ThreadLocalTest::new作为供应商函数。这意味着当一个线程首次访问ThreadLocalTest.holderget()方法时,会调用ThreadLocalTest::new方法创建一个新的ThreadLocalTest对象作为该线程的局部变量。 换句话说,每个线程都有自己的ThreadLocalTest对象,通过ThreadLocalTest.holder.get()获取。如果线程首次访问holder时还没有创建过ThreadLocalTest对象,withInitial指定的供应商函数会被调用来创建一个新的对象。而后续的访问会返回该线程已经创建的那个对象。 这种方式保证了每个线程都拥有独立的对象实例,而不会相互影响。每个线程都可以向自己的ThreadLocalTest对象的messages列表中添加消息,而不会影响其他线程的列表。 希望这样解释更详细一些,如果还有疑问,请随时问我!😊

ThreadLocal的数据结构

image-20230919161828361

GC 之后 key 是否为 null?

https://blog.csdn.net/thewindkee/article/details/103726942

ThreadLocal的内存泄漏:image-20230920085323031

ThreadLocal的Hash算法

int i = key.threadLocalHashCode & (len-1);

image-20230920090359185

ThreadLocalHash冲突

img

ThreadLocalMap过期Key的探测式清理流程:

​ 共有两种过期Key的数据清理方式:探测式清理启发式清理

探测式清理:

​ 离得更近,变位置。

image-20230920093603460

启发式清理:

image-20230921083356837

image-20230921083617885

ThreadLocal扩容机制

在ThreadLocal.set()的最后,如果执行完清理式工作后,未清理到任何数据,且当前散列数据中Entry的数量达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑。

image-20230921084412889

rehash()的阈值为threshold*2/3

扩容的阈值:只是通过image-20230921084645433

扩容后的大小为oldLen*2.

image-20230921085523348

Get方法

第一种情况:通过key值查找出散列表中slot的位置,然后通过判断slot位置的key是否与查找的key值相同。相同则返回

第二种情况:slot位置的Entry.key和查找的key不一致:

​ 不一致时,往后迭代查找。当Entry.key=null,触发一次探测式数回收操作。

InheritableThreadLocal

posted on 2023-09-21 09:40  一个痴迷于技术的码农  阅读(10)  评论(0编辑  收藏  举报