线程/线程池/锁知识点介绍

本文深入探讨了多线程、ThreadLocal、AQS、线程池及锁等并发编程核心技术,覆盖了实现方式、原理、应用场景等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、多线程

1.多线程实现方式

(1)继承Thread
(2)实现Runnable接口
(3)实现Callable接口,有返回值,需要借助FutureTask类
(4)使用线程池

2.线程安全

(1)线程安全概念:无论单线程还是多线程,调用一个对象的行为都能得到正确结果。
(2)线程安全实现
①将对象设置成不可变的,比如声明成private final 并不设置set访问器;
②消除同一资源这个概念,让每一个资源都有一份数据的拷贝;
③使用互斥锁,同一时间只有一个线程在使用共享资源;
④使用乐观锁,处理数据的时候判断数据是否被篡改,如果被篡改可以回滚然后重试

3.JUC开发框架

介绍:JUC(java.util.concurrent包)是Java5.0开始提供的一组专门实现多线程并发处理的开发框架。
JUC核心类:
(1)Executor:具体Runnable任务的执行者
(2)ExecutorService:一个线程池管理者,能把Runnable,Callable提交到池中进行调度
(3)Semaphore:一个计数信号量
(4)ReentrantLock:一个可重入的互斥锁定Lock,功能类似synchronized,但要强大很多
(5)Future:是与Runnable,Callable进行交互的接口,比如一个线程执行结束取返回的结果等等,还提供了cancel终止线程
(6)BlockingQueue:阻塞队列
(7)CompletionService:ExecutorService的扩展,可以获得线程执行结果
(8)CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等
(9)Callable:用于创建线程的Callable接口,与Runnable相比有返回值
(10)CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到达到某个公共屏障点
(11)ScheduledExecutorService:一个ExecutorService,可安排在给定的延迟后运行或定期执行的命令

4.volatile关键字

从并发编程三要素方面分析 :
a.可见性(最重要的特性)。对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
b.原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类型于a++/ a = b这种复合操作不具有原子性,AtomicInteger(volatile和CAS结合)可以保证原子性。
c.有序性,也就是防止指令重排序。

volatile关键字通过“内存屏障”来防止指令被重排序
内存屏障插入策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。
  其中StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。

Sychronized、lock可以解决可见性、原子性和有序性的问题,volatile可以解决可见性和有序性atomic可以解决原子性问题。

5.CAS

CAS: Compare And Swap , CAS(V,E,N) ,其中v表示要更新的变量,E表示预期值,N表示新值。当期望值E与当前线程的变量值V相同时,说明还没线程修改该值,当前线程可以将内存中值修改成N,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作。
过程是原子的。有个ABA问题:比如有个变量值为1,A读了1之后挂起,B改成了0,然后又改成了1,A唤醒之后发现值和预期的相同,通过CAS修改成功了,对于某些场景看重过程的就有问题了。可以添加版本号解决,即使数据一样,版本号不一样也不允许修改。

synchronized、volatile、CAS比较:
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile提供多线程共享变量可见性和禁止指令重排序优化。
CAS是基于冲突检测的乐观锁(非阻塞)

LongAdder与AtomicLong(CAS):LongAdder一般应用于高并发计数场景,在并发度不高的情况下直接对base进行累加,高的时候采用了分段CAS的机制,将核心数据分离成一个Cell数组,每个cell独立维护内部的值,最终的值由每个cell和base组成,将热点数据分离来提高并发度。缺点是并发更新可能会有误差。

二、Threadlocal

1.ThreadLocal介绍

用于解决多线程访问同一个共享变量的时候出现并发问题。

2.ThreadLocal实现原理

set时将值放入ThreadLocalMap中。每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。

3.ThreadLocal如何避免内存泄漏

ThreadLocalMap中的key的是一个Entry数据是一个弱引用(extends WeekReference)可以被回收,如果数据初始化好之后,一直不调用get、set等方法,value却存在一条从Current Thread过来的强引用链,这就会导致内存泄漏,可以在使用完后调用remove方法来避免。(但是实际上在ThreadLocalMap中的set/getEntry方法中,会对key为null进行判断,如果为null的话,那么是会对value置为null的)

4.ThreadLocal使用场景

①可以用于保存用户登录信息:第一次登录的时候保存在session中,然后放到Threadlocal中。
②可以用于解决SimpleDateFormat线程不安全问题:如format底层有个setTime方法,多线程下可能A设置的值会给B。

5.NamedInheritableThreadLocal

1.NamedInheritableThreadLocal是ThreadLocal的一个子类,它允许子线程继承父线程中的ThreadLocal变量。在多线程环境下,每个线程都可以拥有自己独立的变量副本,而InheritableThreadLocal则提供了一种机制,使得子线程可以获取到父线程中定义的ThreadLocal变量的值。
2.NamedInheritableThreadLocal在Spring框架中主要用于解决跨线程的本地变量传递问题。例如,在Web开发中,可以使用NamedInheritableThreadLocal来传递HTTP请求相关的上下文信息,如用户身份、会话信息等。这些信息在请求处理过程中需要在多个线程之间共享,而NamedInheritableThreadLocal提供了一种简单而有效的方式来实现这一点。

三、AQS

1.AQS介绍及应用

(1)介绍:AQS 是 AbustactQueuedSynchronizer 的简称, 它是一个 Java 提高的底层同步工具 类, 用一个 int 类型的变量表示同步状态, 并提供了一系列的 CAS 操作来管理这个同步 状态。
(2)应用
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大 量的同步器,主要使用继承的方式来使用(设计模式为模板模式), 比如我们提到的 ReentrantLock, Semaphore, 其他的诸如 ReentrantReadWriteLock, SynchronousQueue, FutureTask 等等皆是基于AQS 的。
在这里插入图片描述

2.AQS实现方式

AQS支持两种同步方式:1、独占式 2、共享式
这样方便使用者实现不同类型的同步组件, 独占式如 ReentrantLock, 共享式如 Semaphore,CountDownLatch,组合式的如 ReentrantReadWriteLock。总之, AQS 为使用提供了底层支撑, 如何组装实现, 使用者可以自由发挥。

3.如何设计一个互斥的lock

(前三步为加锁,后两步为释放锁)
1.互斥性:int state=0;==0表示空闲,>0表示被占用
2.存储没有抢占到锁的线程 用数组、集合、链表(reentrantLock)、队列等存储Thread对象。
3.阻塞没有抢占到锁的对象 wait();LockSupport.park()(reentrantLock)
4.释放锁 state–
5.唤醒阻塞的线程 notify()/notifyAll()/LockSupport.unpark()

4.ReentrantLock

可重入锁:就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。常见的有Synchronized、ReentrantLock。主要解决的问题是避免线程死锁的问题。如果一个锁不可重入,一个已经获得同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相当于会出现自己要等待自己释放锁。

ReentrantLock 和 Synchronized的对比:
ReentrantLock通过AQS实现,Synchronized通过监视器模式;
ReentrantLock通过使用lock、unlock加锁解锁,更灵活;
ReentrantLock有公平模式和非公平模式,Synchronized是非公平锁;
ReentrantLock可以关联多个条件队列,Synchronized只能关联一个条件队列。

5.读写锁ReadWriteLock

有公平策略和非公平策略(默认)。用全局变量state来表示同步状态,前16位表示读锁状态,读锁成功则加1,释放则减一,后16位表示写锁状态。读锁是一个支持重进入的共享锁,写锁状态为0时,读锁总会成功获取,如果被其他线程获取写锁则会等待(防止写锁被饿死);写锁是一个支持重进入的排它锁。
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

6.CountDownLactch

(1)实现原理:①CountDownLatch是一个同步计数器,他允许一个或者多个线程在另外一组线程执行完成之前一直等待,基于AQS共享模式实现的
②是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作来。
(2)CountDownLatch 的一个非常典型的应用场景是: 有一个任务想要往下执行, 但必须要 等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务 调用一个 CountDownLatch 对象的 await() 方法, 其他的任务执行完自己的任务后调用 同一个 CountDownLatch 对象上的 countDown()方法, 这个调用await()方法的任务将 一直阻塞等待, 直到这个 CountDownLatch 对象的计数值减到 0 为止。
(3)CountDownLatch与join
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
(4)CountDownLatch和CyclicBarrier
CyclicBarrier 可以重复使用, 而 CountdownLatch 不能重复使用。

7.CyclicBarrier

介绍:一个同步辅助类, 它允许一组线程互相等待, 直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互 相等待, 此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用, 所以 称它为循环 的 barrier。

四、线程池

1.线程池简述

线程池中,当需要使用线程时,会从线程池中获取一个空闲线程,线程完成工作时,不会直接关闭线程,而是将这个线程退回到池子,方便其它人使用。
优点:降低资源消耗;提高响应速度;提高线程的可管理性。

2.线程池参数

(1)线程池创建方式有两种:executors创建方式(不推荐,里面参数配置的不太好,容易内存溢出,后面会提到)和ThreadPoolExecutor创建方式,下面介绍ThreadPoolExecutor创建方式
(2)ThreadPoolExecutor线程池参数
①corePoolSize 指定了线程池里的线程数量,核心线程池大小
②maximumPoolSize 指定了线程池里的最大线程数量
③keepAliveTime 当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁。
④unit 时间单位。TimeUnit
⑤workQueue 任务队列,用于存放提交但是尚未被执行的任务。
队列选择:ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作,反之亦然。
PriorityBlockingQueue:具有优先级别的阻塞队列。
⑥threadFactory 线程工厂,用于创建线程,一般可以用默认的
⑦handler 拒绝策略,所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。
  什么时候拒绝?当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务,该任务会交给RejectedExecutionHandler(自定义策略的时候需要实现该策略) 处理。
  线程池提供了四种拒绝策略,也可以自定义拒绝策略:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
选择拒绝策略时,需要考虑以下几点:
‌业务需求‌:关键业务推荐使用AbortPolicy,及时反馈程序运行状态;无关紧要业务推荐使用DiscardPolicy。
‌系统性能‌:CallerRunsPolicy适合可以容忍任务在调用者线程中执行的业务场景,但需注意调用者线程的性能。
‌任务重要性‌:DiscardOldestPolicy适合允许丢弃老任务的场景,需根据实际业务衡量。

3.线程池实现机制(线程池原理)

(1)线程池小于核心线程数时,新提交任务将创建一个新线程执行;
(2)达到到核心线程数时,新任务提交至任务队列workqueue中;
(3)任务队列已满时,若最大线程数小于核心线程数,新任务会创建新线程执行任务;
(4)任务数超过最大线程数时,新任务提交有拒绝策略处理
(5)线程销毁机制:当设置allowCoreThreadTimeOut(true)时,关闭达到KeepAliveTime(TimeOut)的线程,可以销毁至0; 设置为false时,超过核心线程数且有线程TimeOut,会销毁空闲线程,最低可达到核心线程数之后则不再销毁。超过最大线程数且没有TimeOut时,会销毁线程,只保留最大线程数数量的线程。

在这里插入图片描述
注:
(1)放入队列中的线程是非核心线程,不被运行
(2)实际场景中核心线程数和最大线程数经常是设置成一样的(固定大小线程池)

4.线程池配置方案及调优

(1)线程数设置:
CPU密集(解密、压缩、计算)
CPU核心数获取:Runtime.getRuntime().availableProcessors();
理论上为CPU核心数,一般为CPU核心数+1(多一个可以用于切换已使用完成的CPU资源)
IO密集(数据库、文件读写、网络通信)
公式1:CPU核心数*(1-阻塞系数),阻塞系数一般取0.8-0.9 也就是核心数乘以5-10
公式2:CPU核心数*(1+IO耗时/CPU耗时)
公式3:CPU核心数*2+1
(2)Executor自带的几种线程池(一般不用这个,通过这几种自带的给你提供下怎么设计线程池的思路):
new ThreadPoolExecutor:参数依次为核心线程数、最大线程数、线程存活时间、时间单位、队列
可以看到下面的参数设置有队列无限大,最大线程数无限大的情况,所以容易内存溢出。

可缓冲线程池:(0,Integer.max,60L,TimeUtils.SECONDS,new SynchronousQueue())
核心线程数为0,说明默认任务放入队列;最大线程数为Integer.max,工作线程的创建数量几乎没有限制。SynchronousQueue是blockingqueue的一种实现,队列内部只含一个元素,元素空了才能存入,存在才能取出。空闲线程存活时间为60秒,可灵活回收空闲线程(核心线程数为0说明线程可销毁至0个)。
使用场景为很多短期异步的(最大线程数几乎没限制) 小程序或负载较轻的服务器(不如容易内存溢出)

固定大小线程池:(num,num,0,TimeUtils.MILLISECONDS,new BlockingQueue())
核心线程和最大线程数相同,线程池大小达到最大值时,新提交的任务放入无界阻塞队列中,
线程空闲时,再从队列中取出任务执行。
使用场景为长期的任务。

定时任务线程池:(num,Integer.max,0,NANOSECONDS,new DelayWorkQueue()):
支持定时或周期性任务执行,DelayWorkQueue是一种按照延时时间排列的队列,线程都繁忙时,会将延时时间最短的放在前面。每次队列中取出的都是超期的任务。
使用场景为周期性执行的任务

单个线程池:(1,1,0,TimeUtils.MILLISECONDS,new BlockingQueue())
单一线程化的线程池,所有任务会按照制定顺序执行。在线程池内部是单线程,但在主线程提交任务的时候,没有阻塞,仍是异步的。(至少提交任务时主线程不阻塞)
场景:一个任务一个任务执行的场景。

单个线程定时任务线程池:上面的定时任务线程池,核心线程数设置为1
窃取线程池:会创建足够多线程的线程池,通过工作窃取的方式,使多核CPU不闲置
new ForkJoinPool(parallelism,ForkJoinPool.defaultForkJoinWorkerThreadFactory,null,ture)

(3)实践中用过的自定义线程池(有点像上面的固定大小线程池)
核心线程数和最大线程数相同,KeepAliveTime=0,使用Blockingqueue(定长,否则可能内存溢出),自定义拒绝策略:
xxxPolicy implements RejectedExecutionHandler (Runnable r,ThreadPoolExecutor exe){
Exe.getQueue().put®;//BlockingQueue的put方法–大于最大size时阻塞,因为任务不能丢
}

5.自定义线程池代码编写

1.创建一个服务类,一个线程类,在服务类中创建线程池:设置各种参数及拒绝策略;execute方法放入线程类(加任务);
2.在服务类中加入任务数c1、错误数c2等参数(volitile修饰),在加入任务后c1++,在线程类中执行完c–,报错时c2++,一旦报错抛出异常;
3.最后校验任务数是否为0判断任务是否执行完。
代码如下

package com.example.springb_web.utils.ThreadPool;

public class ThreadPoolTestMain {
    public static void main(String[] args) {
        new ThreadPoolService().calTask(5,10,20);
    }
}

package com.example.springb_web.utils.ThreadPool;




import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolService {
    public static volatile int job_count;
    public static volatile int error_count;
    public static AtomicInteger handle_count = new AtomicInteger();

    public boolean calTask(int poolSize,int batchCount,int taskCount){
        try {
            ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(poolSize,poolSize,0, TimeUnit.HOURS,new ArrayBlockingQueue<>(poolSize),new ForceQueuePolicy());
            CallTaskThread thread = new CallTaskThread();
            thread.setBatchCount(batchCount);
            for(int i = 0;i<taskCount;i++){
                poolExecutor.execute(thread);
                synchronized (this){
                    job_count ++;
                }
                if(error_count > 0){
                    error_count = 0;
                    return false;
                }
            }

            while (true){
                if(job_count == 0){
                    break;
                }
                Thread.sleep(10000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    public class ForceQueuePolicy implements RejectedExecutionHandler{

        @Override
        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
            try {
                threadPoolExecutor.getQueue().put(runnable);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doTask() throws Exception{
        System.out.println(Thread.currentThread().getName()+":done"+":"+handle_count.incrementAndGet());
        Thread.sleep(1000);
    }
}

package com.example.springb_web.utils.ThreadPool;

import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class CallTaskThread  implements Runnable{

    private int batchCount;


   /* @Autowired
    private ThreadPoolService service;*/


    @Override
    public void run() {
        try {
            for(int i = 0;i<batchCount ; i++){
                ThreadPoolService service = new ThreadPoolService();
                service.doTask();
            }
        } catch (Exception e) {
            ThreadPoolService.error_count ++;
            e.printStackTrace();
        }finally {
            ThreadPoolService.job_count --;
        }
    }

    public int getBatchCount() {
        return batchCount;
    }

    public void setBatchCount(int batchCount) {
        this.batchCount = batchCount;
    }
}

五、锁

1.锁的分类

在这里插入图片描述

可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。如sychronized/Reetrantlock。
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
排他锁:如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。所有线程都能得到资源,但是吞吐效率低,其他线程都阻塞了,唤醒的开销大
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。吞吐效率高但是有些线程获取不到资源可能饿死。

2.锁(sychronized)的优化

锁有四种状态-无锁、偏向锁、轻量级锁、重量级锁,他们会随竞争的激烈逐渐升级,不会降级。偏向锁、轻量级锁是乐观锁,重量级锁是悲观锁。
偏向锁:创建对象时,对象头有一个偏向锁标志,线程持有锁时会将这个标志置为一,表示获取到锁了(类似于可重入锁),两个线程来竞争锁时升级
轻量级锁:自旋获取锁,一定次数还没成功之后升级成重量级锁。
重量级锁:也是互斥锁、悲观锁,阻塞或唤醒线程需要从用户态转换到内核态,非常消耗资源。

适应性自旋锁:JDK1.6引入,自旋次数不在固定,更好的使用资源。如果线程上次自旋成功了,会认为他这次自旋还会成功,允许他自旋更多的次数,若某个锁很少获取成功就会减少自旋次数甚至直接阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值