第三篇:并发编程

目录

一. 线程

1、线程的生命周期(5种状态)

2、创建线程的方式

3. 用户线程和守护线程

4. todo 如何查看线程的CPU利用率

二. 多线程

1、创建线程池

2、工作队列有哪些?

3、拒绝策略有哪些?

4、提交任务到线城池

5、关闭线程池

三. 线程安全

1、并发编程三大特性

2、如何解决线程安全问题?

3、死锁?

4. synchronize

5. volatile

四. 线程间的通信

1. 多线程如何通信?

2. wait和sleep有什么区别?

3. 为什么wait、notify定义在Object类中?

4. yield的作用?

5. java内存模型

五. JUC(java.util.concurrent)

(一)atomic(核心CAS)

1. concurrent包中有哪些原子类?

2. atomic原子类的原理?

(二)Lock(AQS、核心ReentrantLock)

1. AQS(AbstractQueenSynchronizer)

2. Synchronize和ReentrantLock的区别?

(三)阻塞队列(BlockingQueue)

(四)并发容器

1. ConcurrentHashMap

2. CopyOnWriteArrayList

3 ThreadLocal


一. 线程

1、线程的生命周期(5种状态)

        在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态。这是因为时间片非常短,线程切换极快,区分这两个无意义。

    线程上下文切换指的是:比如CPU时间片耗尽导致线程切换时,需要保存当前线程的信息(比如程序计数器,栈信息等),这也需要消耗CPU和内存资源,频繁切换就会导致效率低下。

2、创建线程的方式

  • 继承Thread类,重写run()方法
  • 实现runnable接口,重写run()方法
  • 实现callable接口,重写call方法
  • 用线程池Executors工具类创建

runnable接口和callable接口有什么不同?

callable有返回值,runnable没有。

start()和run()方法方法有什么区别?

start()用于启动线程,只能执行一次;run()用于执行线程中的方法,可以执行很多次。

3. 用户线程和守护线程

  • 用户线程:运行在前台,执行具体任务。(如:主线程,连接网络的子线程)
  • 守护线程:运行在后台,为其他线程服务。一旦所有用户线程运行结束,守护线程会随JVM一起结束工作。(如:垃圾回收线程)

用户线程结束,JVM退出。守护线程不会影响JVM退出。

  • 进程:操作系统最小执行单位
  • 线程:CPU最小执行单位
  • 协程:轻量级线程,由用户控制,在用户态执行

一个线程中可以有多个协程。线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行。

4. 如何查看线程的CPU利用率

  • top命令查看所有的进程
  • top -H -p [进程号] 查看进程的所有线程(这里就可以查看CPU利用率)
  • jstack [进程号] | grep "[线程号的16进制表示]" 查看线程堆栈

二. 多线程

1、创建线程池

(1)使用Executors创建(不建议使用)

  • newFixThreadPool(最常用,效率高):创建固定大小的线程池。来新任务,就创建新线程,直到达到最大线程数。
  • newCacheThreadPool:创建缓存线程池,不规定大小,使用的少,就回收一部分,使用的多,就多创建。
  • newScheduleThreadPool:创建无限大小的线程池,可用于执行周期性任务。
  • newSingleThreadExcutor:创建单线程线程池。只有一个线程在运行,该线程异常时才有新的线程来代替他。

阿里的规范:

(2)使用ThreadPoolExecutor方式创建

【ThreadPoolExecutor 线程池参数】

三个非常重要的参数:

  • corePoolSize:核心线程数,线程池中长期存活的线程数。
  • maxinumPool:最大线程数,线程池允许工作的最大线程。
  • workQueue:当有新任务来临,但是核心线程数已满,就会放入队列中。

其他参数:

  • allowCoreThreadTimeOut:允许核心线程超时。
  • keepAliveTime:空闲线程存活时间,线程空闲时间超过keepAliveTime,就会退出,直到线程数量=核心线程数;当allowCoreThreadTimeOut=true,则会直到线程数量=0。
  • unit:等待时间单位。
  • handler:拒绝策略。当线程池中线程已达到最大线程数,就会按拒绝策略丢弃线程。
代码示例:

         ThreadPoolExecutor executor= new ThreadPoolExecutor(
                 5,                               //核心线程池大小,CPU密集型一般设置为n+1,IO密集型设置为2n
                 5,                     //最大线程数一般设置和核心线程数一致,避免了队列已满时创建和销毁线程的开销
                 60,                               //超时了没有人调用就会释放,默认值是60秒
                 TimeUnit.SECONDS,                 //超时单位
                 new LinkedBlockingDeque<>(3),     //阻塞队列
                 Executors.defaultThreadFactory(),               //线程工厂,创建线程的,一般不用动
                 new ThreadPoolExecutor.DiscardOldestPolicy());  //队列满了,丢弃最早未执行的任务

线城池执行流程:

2、工作队列有哪些?

  • ArrayBlockingQueue:基于数组的有界阻塞队列,适合读写性能高、容量固定的场景
  • LinkedBlockingQueue:基于链表的阻塞队列,适合增删性能高、长度可变的场景。最大长度默认是Integer.max_value,即2 的31 次方- 1
  • PriorityBlokingQueue:有优先级的阻塞队列

3、拒绝策略有哪些?

  • abortPolicy:丢弃任务并抛异常。(默认)
  • discardPolicy:直接丢弃任务。
  • discardOldestPolicy:丢弃最早未执行的任务。
  • callerRunPolicy:调用者执行该任务。(会导致效率非常低)。

4、提交任务到线城池

  • execute:提交不需要返回值的任务。
  • submit:提交需要返回值的任务。

submit提交任务:

会返回Future对象,并且可以通过Future的get()获取返回值,get()方法会阻塞当前线程直到任务完成。

而使用get(long timeout, TimeUnit unit)则会阻塞当前线程一段时间后立即返回,这时候线程可能没有执行结束。

//execute()用于提交Runnable任务
executor.execute(new RunnableTask());

//submit()用于提交Callable任务
Future<Result> future = executor.submit(new CallableTask());

5、关闭线程池

可以调用用shutdown()或shutdownNow()来关闭线程池。

  • shutdown():把线程池状态设置为SHUTDOWN,正在执行的任务会继续执行,没执行的不再执行。
  • shutdownNow():把线程池状态设置为STOP,正在执行的任务被停止,没执行的不再执行。

三. 线程安全

1、并发编程三大特性

特性含义产生原因解决办法
原子性一个或多个操作,要么同时成功,要么同时失败CPU执行任务时切换线程导致的Atomic开头的原子类可以、synchronize、Lock可以解决原子性问题
可见性访问共享变量时,一个线程修改对另一个线程可见各个线程访问的是当前CPU的缓存信息synchronize、Lock、volatile可以解决可见性问题
有序性程序按照代码先后顺序执行CPU指令重排序导致的Happen-Before原则可以解决有序性问题

什么是Happen-Before原则?

        存在依赖关系的不允许重排序。

2、如何解决线程安全问题?

  • 使用JUC包下的Atomic原子类,如AtomicInteger
  • synchronize
  • Lock

Q:什么时候会出现线程安全问题?

A:有共享变量时才会出现线程安全问题。

Q:servlet线程安全吗?

A:不安全。servlet是单例的,多线程访问共享资源时,必然导致线程安全问题。但是一般servlet是无状态的,即没有共享数据,所以某种意义上可以认为是线程安全的。

3、死锁?

死锁是指两个或两个以上线程因为竞争资源而造成的阻塞现象。

3.1 死锁的产生条件?
  • 互斥:一个锁只能被一个线程持有
  • 请求并保持:线程一直保持持有锁
  • 不剥夺:其他线程不能强行剥夺锁,除非持有锁的线程主动释放
  • 循环等待:等待锁的线程形成了环路,造成永久阻塞
3.2 如何避免死锁?

只需破坏四个条件中的一个。

  • 破坏互斥:不能破坏,我们本身就希望锁是互斥的
  • 破坏请求与保持:一次申请所有资源
  • 破坏不剥夺:占有一部分资源的线程进一步申请其他资源时,如果申请不到,就主动释放它占有的资源
  • 破坏循环等待:按照顺序申请资源
// thread1先获取锁1再获取锁2,thread2先获取锁2再获取锁1
// 如果thread1获取锁1等待锁2,thread2获取锁2等待锁1,就会造成死锁
public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for resource 2...");
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 and resource 2...");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for resource 1...");
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 2 and resource 1...");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

死锁检测:可以用jconsole或jVisualVM进行分析检测,有死锁的话有响应的提示,点到对应的线程里就能看到线程在wait lock monitor

死锁检测的常用3种方法-CSDN博客

数据库死锁:

-- 事务1
begin;
-- SQL1更新id为1的
update user set age = 1 where id = 1;
-- SQL2更新id为2的
update user set age = 2 where id = 2;
commit;
-- 事务2
begin;
-- SQL1更新id为2的
update user set age = 3 where id = 2;
-- SQL2更新id为1的
update user set age = 4 where id = 1;
commit;

4. synchronize

        synchronize取得的都是对象锁或类锁,而不是把某段代码或某个方法作为锁,并且必须是同一把锁才能锁住。

synchronize修饰非静态方法的时候是对象锁,修饰静态方法是类锁

对象锁的范围是同一个对象,类锁它的范围是类的所有对象

为什么叫对象锁,因为锁的范围是同一个对象

4.1 说一下synchronize的原理?

        synchronize底层是通过对象监视器monitor实现的,monitor内部维护了一个锁计数器,如果为0,说明处于空闲状态,可以获取锁,否则获取不到。

        任何java对象都有一个monitor与之关联,当monitor被持有后,对象就处于锁定状态。

  • 如果是synchronize代码块,底层具体是monitorenter和monitorexit两个指令实现的
  • 如果是synchronize修饰方法,就用ACC_SYNCHRONIZED标识这是一个同步方法
4.2 可重入锁、自旋锁、偏向锁、轻量级锁、重量级锁、共享锁、排他锁、可中断锁的概念?
  • 可重入锁:已经获取锁的线程可以再次获取锁。原理是底层维护了一个计数器,当线程获取锁则加一,再次进入时,判断是同一线程则继续加一,释放锁时减一。
  • 自旋锁:让一个线程在获取锁时忙循环一段时间,如果短时间内能获取锁,就避免进入阻塞状态。
  • 偏向锁:顾名思义,偏向锁会偏向第一个获取锁的线程。如果运行过程中,同步锁只有一个线程访问,则给线程加偏向锁,在对象头中记录线程ID,当线程下次再想获取锁,不需要重新申请锁可直接进入同步代码;如果多个线程抢占,则升级为轻量级锁。
  • 轻量级锁:轻量级锁是一种乐观锁,它认为锁的竞争较小,使用CAS来获取锁。
  • 重量级锁:可以认为是java中的监视器锁(monitor),使用互斥量来获取锁。
  • 共享锁:也叫读锁。共享锁就是多个事务可以对同一数据共享同一把锁,都能访问到数据,但是只能读不能写。
  • 排它锁:也叫写锁。排它锁不能和其他锁共存,但是可读可写。
  • 可中断锁:获取锁的过程可以中断,当一个线程在等待锁时,如果另一个线程对其调用interrupt()方法就会中断获取锁。ReentranLock是可中断锁,Synchronize是不可中断锁
  • 公平锁:先来后到,每个线程获取锁的顺序按照线程请求锁的先后顺序
  • 非公平锁:随机,每个线程获取锁的顺序是随机的。
4.3 锁升级的原理?

锁状态:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。

  • 当有一个线程访问到同步代码块时,升级为偏向锁,并在对象头中存储threadId为当前线程id;
  • 再有线程访问时,比较当前线程id和对象头中threadId是否一致,如果一致可再次进入,如果不一致则升级为轻量级锁
  • 再有锁竞争时,通过CAS修改对象头中的锁标志位。如果锁标志位是“释放”,则修改为“锁定”,此时该线程获取到锁。没有获取到锁的线程进行自旋,十次自旋之后还没获取到锁则升级为重量级锁

4.4 说一下Java中的对象头

        对象头主要包括了mark word(标记字段) 和 类型指针(用于确定这个对象是哪个类的实例)。

Mark Word存储的信息根据锁状态不同而有区别,主要是:对象hashcode、GC标记、锁标记位

5. volatile

        volatile用于解决可见性问题。有两个作用:

  • 保证可见性(对该变量的写操作会立即同步到主内存,对该变量的读操作会从主内存获取最新的值)
  • 禁止指令重排序(编译器不会对volatile修饰的关键字进行指令进行指令重排序,保证有序性)

volatile规定:如果多个线程操作volatile修饰的变量,那么这个变量的写操作必须在读操作之前完成,禁止指令重排序。

volatile是如何解决可见性问题的?

        volatile通过内存屏障来解决可见性问题。内存屏障是一条CPU指令,用于保证指令的执行顺序。

写内存屏障:在写指令后插入store barrier指令,强制写入本地内存的数据立即更新到主内存中,使其他线程可见。并通过cpu总线通知其他线程的本地缓存失效。

读内存屏障:缓存中的数据失效,从主内存中读取最新数据。

四. 线程间的通信

1. 多线程如何通信?

  • synchronize加锁的线程用:Object的 wait/nofity/notifyAll 方法
  • reentrantLock类加锁的线程用:Condition的 await/signal/signalAll 方法

多线程如何进行交换数据?

通过管道进行线程间通信。(1)字节流 (2)字符流

2. wait和sleep有什么区别?

  • wait用于线程间的通信,sleep用于停止执行;
  • wait释放锁,sleep不释放锁;
  • wait必须被唤醒(notify,notifyAll),sleep自然醒。

3. 为什么wait、notify定义在Object类中?

因为任意对象都可以作为锁,任意对象都能使用的方法一定在Object类中。

为什么不把wait、notify定义在Thread类中?

因为一个线程可以持有多个锁,如果定义在Thread类中,释放锁时就不知道释放的是哪个锁

为什么sleep()定义在Thread类中?

因为sleep()是让当前线程停止执行,并不涉及到对象锁

4. yield的作用?join的作用?

t1.join():作用是当前线程等待被调用的线程(t1)执行完毕后再执行

Thread.yield:暂时释放cpu执行权,线程状态由运行状态转为就绪状态。

LockSupport.park()作用是当前线程进入等待状态,直到其他线程调用unpark

//主线程要等t1线程执行完毕才能执行
public static void main(String[] args){
    Thread t1 = new Thread(new Worker("thread-1"));
    t1.start();
    t1.join();
    System.out.println("main end");
}

5. java内存模型

        java内存模型定义了本地内存和共享内存之间的交互规则和行为,以及保证多线程之间的内存原子性、可见性、有序性

        共享变量存储在共享内存中,工作内存中存储线程私有变量和共享变量的副本。线程操作共享变量时,先复制一份到本地内存中,待操作完毕,再把最新值放入共享内存。

CPU高级缓存

        CPU缓存是位于CPU内部的高速存储器,用于存储CPU从内存读取的数据以及频繁使用的指令。CPU通过缓存减少对内存的访问次数,以提高CPU访问数据的速度和效率。

CPU三级缓存:CPU的高速缓存有三层,L1、L2、L3。

  • 一级缓存访问速度最快和访问延迟最低,主要用于暂存 CPU 频繁访问的数据和指令;
  • 二级缓存容量较大,速度较快,主要用于提高一级缓存的命中率,一级缓存找不到的就到二级缓存找;
  • 三级缓存容量最大,速度相对较慢,主要用于多个 CPU 核心之间的数据共享和协作,三级缓存通常是所有 CPU 核心共享的,能够提高系统的并发性能。

线程本地内存用于存储线程私有数据以及线程栈空间,CPU高速缓存用于加速对数据的访问

CPU高速缓存造成的问题

        高速缓存和主存之间容易数据不一致,操作系统(windows和Linux)都通过内存模型解决了这个问题。Java也有自己的内存模型,java当然也可以复用操作系统提供的内存模型,但是这会导致换一套系统代码就无法运行了,所以java也提供了自己的内存模型

五. JUC(java.util.concurrent)

(一)atomic(核心CAS)

1. concurrent包中有哪些原子类?

原子类:atomicInteger,atomicLong,atomicBoolean,atomicReference

原子数据:atomicIntegerArray,atomicLongArray

解决ABA问题的原子类

atomicMarkableReference:通过引入Boolean变量来反应中间有没有变过

atomicStampedReference:通过引入int来累加反应中间有没有变过

2. atomic原子类的原理?

主要利用CAS(compare and swap) 、volatile 和 native方法 来保证原子操作。

CAS的原理:

CAS包含三个参数,valueOffset、expect、update。

  • valueOffset是要修改的变量的内存地址;
  • expect是期望值;
  • update是新值。

拿valueOffset内存地址中的值和expect比较,如果相等,则把valueOffset中的值更新为update

(二)Lock(ReentrantLock、AQS)

1. ReentrantLock

        ReentrantLock是java中的独占锁,它实现了Lock接口,和Synchronize的功能类似,但是比Synchronize功能更强大和灵活。ReentrantLock内部实现了AQS,用于实现锁的获取和释放机制。

ReentrantLock使用 

        ReentrantLock lock = new ReentrantLock(); 以下都用 lock.

  • 获取锁 lock()  该方法阻塞,如果锁被其他线程持有,则当前线程会被阻塞,直到获取锁
  • 尝试获取锁 tryLock() 返回值为true或false,该方法非阻塞,获取不到锁就返回false
  • 释放锁  unlock()
  • 可中断的获取锁 lockInterruptibly() 将来可以调用 interrupt()中断锁

2. AQS

        AQS(AbstractQueenSynchronizer)是一个同步器。ReentrantLock的同步器就继承了AQS。AQS的核心思想主要是两个方面:同步状态和等待队列

  • 同步状态:AQS使用一个整型来表示锁的状态。比如0代表锁空闲,1代表被某个线程持有。
  • 等待队列(CLH队列):AQS使用一个双向链表来管理等待获取同步状态的线程。当一个线程获取同步状态失败,就加入等待队列,并进入阻塞状态,直到获取同步状态为止。

AQS主要包含两个核心方法:

  • acquire(int arg):尝试获取同步状态,获取失败则加入等待队列,并阻塞当前线程,直到获取同步状态为止
  • release(int arg):释放同步状态,并唤醒等待队列中某个线程让其有机会获取同步状态

AQS的模板方法主要有:

  • 独占式 获取和释放同步状态,如tryAcquire() tryRelease()
  • 共享式 获取和释放同步状态,如tryAcquireShared() 和 tryReleaseShared()
  • 获取 等待在同步队列中的线程集合,如getQueueThreads()

ReentranLock是公平锁还是非公平锁?

        ReentranLock默认就是非公平锁,除非在构造函数中传true。

        ReentranLock实现非公平锁的原理是:在调用lock()方法时先试用CAS获取锁,获取不到才调用acquire(),再获取不到才加入等待队列。

3. Synchronize和ReentrantLock的区别?

  • Synchronize是关键字,ReentrantLock是类;
  • Synchronize自动加锁释放锁,ReentrantLock需要手动;
  • Synchronize可以修饰类、方法、代码块,ReentrantLock只适用于代码块;
  • Synchronize底层是用对象监视器monitor实现锁,ReentrantLock底层是用AQS加锁和释放锁
  • Synchronize只能是非共平锁,ReentranLock可以指定是公平锁还是非共平锁
  • Synchronize是不可中断锁,ReentrantLock可以选择中断锁或不可中断锁

项目中用的是Synchronize还是Lock?

        Lock多一点,首先Lock可以手动加锁和释放锁,更加灵活,但是一定要注意在finally中释放锁。其次,Lock提供了tryLock方法,一定时间内获取不到锁就放弃,不会一直阻塞。

(三)阻塞队列(BlockingQueue)

1. 什么是阻塞队列?常见阻塞队列有哪些?

阻塞队列是支持两个附加操作的队列。当队列为空时,获取操作被阻塞,等待队列变为非空;当队列已满时,存储操作被阻塞,等待队列可用。

常见阻塞队列:

  • ArrayBlockingQueue:基于数组的有界阻塞队列,适合读写性能高、容量固定的场景
  • LinkedBlockingQueue:基于链表的阻塞队列,适合增删性能高、长度可变的场景。最大长度默认是Integer.max_value,即2 的31 次方- 1
  • PriorityBlokingQueue:有优先级的阻塞队列

2. 如何使用Condition创建阻塞队列?

        定义一个普通队列Queue、Lock、以及两个Condition(一个代表生产者,一个代表消费者)

  • put()方法中,加锁,判断队列是否已满,如果是则producer调用await()让线程等待。否则给队列中添加元素,并调用consumer的signal(),通知消费者队列中有数据可用,释放锁
  • get()方法中,加锁,判断队列是否为空,如果是则consumer调用await()让线程等待。否则从队列中获取元素,并调用producer的signal(),通知生产者队列中有空间可用,释放锁
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BlockingQueueExample<E> {
    private final Queue<E> queue;
    private final int capacity;

    private final Lock lock = new ReentrantLock();
    private final Condition producer = lock.newCondition();
    private final Condition consumer = lock.newCondition();

    public BlockingQueueExample(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
    }

    public void put(E element) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                System.out.println("Queue is full, producer is waiting...");
                producer.await();
            }
            queue.offer(element);
            System.out.println("Produced: " + element);
            consumer.signal(); // 通知消费者队列中有数据可用
        } finally {
            lock.unlock();
        }
    }

    public E get() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                System.out.println("Queue is empty, consumer is waiting...");
                consumer.await();
            }
            E element = queue.poll();
            System.out.println("Consumed: " + element);
            producer.signal(); // 通知生产者队列中有空间可用
            return element;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BlockingQueueExample<Integer> blockingQueue = new BlockingQueueExample<>(5);

        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    blockingQueue.put(i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    blockingQueue.get();
                    Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

Condition的作用?

        允许线程等待某个条件成立,或者通知其他线程某个条件已经满足

为什么Condition通常和Lock一起使用?

        因为条件等待操作通常涉及到共享资源的访问和修改,Lock来保证互斥,Condition解决线程的等待和唤醒。

Condition和Object的wait/notify()有什么区别?

  • Object内部只有一个线程等待队列,wait()把线程加入等待队列,notify()随机唤醒等待队列中的一个线程
  • 而一个Lock可以创造多个Condition,这意味着可以根据不同等待条件创造不同的Condition,从而独立控制多个等待队列

(四)并发容器

1. ConcurrentHashMap

        详见java基础篇

2. CopyOnWriteArrayList

CopyOnWriteArrayList是一个并发容器,多个线程并发遍历时不会抛并发修改异常。

在CopyOnWriteArrayList中,如果是写操作,会复制一份底层数据副本,写操作修改数据副本,读操作还读取原list,写操作完成之后把原list指针指向数据副本。

主要实现思想:读写分离;最终一致性;使用另外开辟空间的思路,来解决并发冲突。

3 ThreadLocal

ThreadLocal是什么?

        ThreadLocal是线程内部的局部变量,属于线程私有,不被其他线程共享。

ThreadLocal如何使用?有哪些应用场景?

使用:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

应用场景:

(1)Spring用ThreadLocal保证一个线程用的是同一个数据库连接池。(把connection放入ThreadLocal中)

(2)用ThreadLocal来解决日志串联的问题。比如把traceID放到ThreadLocal中,然后每次日志打印都加上traceID,这样微服务内部整个调用链路就能串联起来,快速定位问题。微服务之间传递traceID可以在请求头中传递。

ThreadLocal的原理?

        ThreadLocal是线程内部的一个变量,这个变量的类型是Map(ThreadLocalMap),ThreadLocal存储值的时候,是把自己本身(即ThreadLocal,此为弱引用)当做key,要set的值当做value,放入一个ThreadLocalMap中,ThreadLocalMap被线程持有。

源码:ThreadLocal主要是set、get、remove方法。

【set源码】

【get源码】

【remove源码】

为什么不调用remove方法会造成内存泄漏?

        因为ThreadLocalMap持有ThreadLocal弱引用和value强引用,JVM下一次GC就会回收ThreadLocal弱引用,map中就会出现,key为null,但value不为null的entry,导致value这个强引用无法被回收。而ThreadLocalMap和当前线程的生命周期一样长,如果当前线程是线程池中的线程,该线程后续又没有再调用get set remove方法,就会造成内存泄漏。

注:调用get set方法的时候,会清除map中key为null的值。

为什么Entry中的key要设为弱引用?

        因为设为弱引用能防止大多数情况的内存泄漏。如果设为强引用,不调用remove方法,key和value都无法被回收。但是设为弱引用,当key被回收,key为null时,下一次该线程调用get set方法,就能清除key为null的值了。

(五)并发工具

1. CountDownLatch(闭锁)

CountDownLunch底层维护了一个计数器,作用是一个线程会等待若干线程执行完毕后他才执行。

CountDownLatch的重要方法

//构造函数,参数count为计数值
public CountDownLatch(int count) {  };  
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   

//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; 
 
//将count值减1
public void countDown() { };  

//任务
public class CountDownLatchThread implements Runnable {
    private String threadName;
    private CountDownLatch latch;

    public CountDownLatchThread(String threadName, CountDownLatch latch) {
        this.threadName = threadName;
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("threadName = " + threadName + " running");
        latch.countDown();
    }
}

//使用
public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        Thread threadA = new Thread(new CountDownLatchThread("A", latch));
        Thread threadB = new Thread(new CountDownLatchThread("B", latch));
        threadA.start();
        threadB.start();

        //当前线程被挂起,直到count=0
        latch.await();
        System.out.println("主线程执行完毕,主线程继续执行");
    }
}

2. CyclicBarrier(栅栏)

        CyclicBarrier作用是等待其他线程全都到达某一个屏障点(Barrier),再一起执行。

//任务
public class CyclicBarrierThread extends Thread{
    CyclicBarrier cyclicBarrier;

    public CyclicBarrierThread(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "到达栅栏 A");
            cyclicBarrier.await(); // 用await()设置屏障点
            System.out.println(Thread.currentThread().getName() + "冲破栅栏 A");

            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "到达栅栏 B");
            cyclicBarrier.await(); // 用await()设置屏障点
            System.out.println(Thread.currentThread().getName() + "冲破栅栏 B");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//使用
public class CyclicBarrierTest {
    public static void main(String[] args) {
        int threadNum = 3;

        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum, new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "完成最后任务");
            }
        });

        for (int i = 0; i < threadNum; i++) {
            new CyclicBarrierThread(cyclicBarrier).start();
        }
    }
}

CountDownLatch和CyclicBarrier都维护了一个计数器,它们的区别是:

  • CountDownLatch是等其他线程执行完毕当前线程才回继续执行,CycliBarriar是等待其他线程全都变成某个状态,再一起执行。
  • 调用CountDownLatch的countDown()方法线程不会阻塞,CyclicBarriar调用await()方法线程会阻塞。
  • CountDownLatch不能复用,CyclicBarrier可以循环使用

3. Semaphore(信号量)

        Semaphore是信号量,作用是限制某个代码块的并发数。Semaphore的构造函数里传int值,表示最多可用 i 个线程可同时访问Semaphore。

重要方法:

//构造函数,permits表示许可线程的数量
public Semaphore(int permits)
//构造函数,fair表示是否是公平锁
public Semaphore(int permits, boolean fair)
//获取许可并阻塞
public void acquire() throws InterruptedException
//释放许可
public void release()
//任务线程
public class SemaphoreThread extends Thread {
    private Semaphore semaphore;

    public SemaphoreThread(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "acquire");

            Thread.sleep(1000);

            semaphore.release();
            System.out.println(Thread.currentThread().getName() + "release");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//使用
public class SemaphoreTest {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);

        for (int i=0; i<5; i++){
            new SemaphoreThread(semaphore).start();
        }
    }
}

4. Future和FutureTask有什么用?

Future和FutureTask都用于执行异步任务,Future是接口,FutureTask实现了future和runable接口

Future:线程池submit方法的返回值就是Future接口,submit方法里要传一个callable实现类

public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(5);
    Future<String> future = executor.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            return "hello";
        }
    });
    String result = future.get();
    executor.shutdown();
}

FutureTask:执行异步任务时,可以往里边传入callable的实现类,再通过线程或线程池执行,就能对这个异步任务的结果进行等待获取、判断是否完成、取消等。

public static void main(String[] args) throws Exception {
    FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "hello futureTask";
        }
    });

    // 方式一:直接用线程启动 futureTask
//  new Thread(futureTask).start();

    // 方式二:线程池启动
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(futureTask);

    futureTask.get(); //获取异步结果
    executor.shutdown();
}

CompletableFuture和Future的区别?

        CompletableFuture实现了Future接口。

  • Future的作用是获取异步计算的结果;
  • CompletableFuture还可以组合异步任务、在任务执行完毕设置回调函数等
public class FutureDemo {
    public static void main(String[] args) {
        // supplyAsync:用于创建异步任务
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 异步执行的任务,返回结果为字符串
            return "Hello, CompletableFuture!";
        });

        // thenApply:对结果进行转换处理。入参为function,返回值为CompletableFuture对象
        CompletableFuture<String> thenApplyFuture = future.thenApply(result -> {
            // 将字符串转换为长度
            return result.toUpperCase();
        });

        // thenAccept:对结果进行消费处理。入参为consumer,没有任何返回值
        future.thenAccept(result -> {
            // 打印结果
            System.out.println("thenAccept: " + result);
        });

        // thenRun:在异步任务完成后执行指定操作。入参为Runnable,Runnable不接收任何参数,也不返回任何结果
        future.thenRun(() -> {
            // 打印完成信息
            System.out.println("thenRun: Task completed.");
        });

        // 等待异步任务完成
        try {
            future.get(); // 阻塞等待异步任务完成
            thenApplyFuture.get(); // 阻塞等待thenApplyFuture完成
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值