并发问题系统学习

进程、线程

  • 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。可以理解为一个java应用。

  • 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。但会导致内存泄漏上下⽂切换死锁。

多线程和锁的关系

只有拿到锁的线程才能访问共享资源,多线程之间的通信和协作,通常使用锁和等待/通知机制来实现。

线程死锁

线程 A 持有资源 2 ,线程 B 持有资源 1 ,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack

并发、并行

简单说,轮流做是并发,一起做是并行。

线程创建方式

  • 继承Thread类,重写run()方法,调用start()方法启动线程

一个例子

这段代码输出结果可能是ab或者ba。

  • 实现 Runnable 接口,重写run()方法

上面两种都是没有返回值的。

  • 实现Callable接口,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值

JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。直接执行run方法就相当于执行一个普通的方法。直接执行thread中的run方法也是相当于顺序执行run方法。

线程等待、休眠与通知

等待

休眠

等待和休眠区别

通知

实例

执行顺序

停止线程

  1. 使用退出标志,使线程正常退出。
    volatile boolean flag = false ;t1.flag = true ;
  2. stop强行终止t1.stop();
  3. interrupt
    Thread t2 = new Thread(()->{
    while(true) {
    Thread current = Thread.currentThread();
    boolean interrupted = current.isInterrupted();
    if(interrupted) {
    System.out.println("打断状态:"+interrupted);
    break;
    }
    }
    }, "t2");
    t2.start();
    Thread.sleep(500);

线程上下文切换

使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU来应付多个线程。

为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。

守护线程

Java中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。

在JVM 启动时会调用 main 函数,main函数所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。

那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。

线程间有哪些通信方式(操作系统)

volatile 的作用主要

  • 保证可见性: 当一个变量被声明为 volatile 后,对该变量的写操作会立即被其他线程所看到,保证了多个线程之间对该变量的可见性。换句话说,一个线程对 volatile 变量的修改对其他线程是可见的,不会出现线程间的数据不一致问题。例:

static volatile boolean stop = false;

  • 禁止指令重排序: volatile 变量的读写操作会插入内存屏障,防止编译器和处理器对其进行指令重排序优化,保证了代码执行的顺序性。这样可以确保对 volatile 变量的写操作先于后续的读操作,避免出现因指令重排序导致的意外结果。

写变量让volatile修饰的变量的在代码最后位置
读变量让volatile修饰的变量的在代码最开始位置

总的来说,volatile 主要用于在多线程环境中确保变量的可见性和一致性,它适用于一种场景:变量被多个线程共享,并且这些线程可能会同时读写这个变量。通过使用 volatile 关键字,可以有效地避免由于线程间数据不一致导致的并发问题。

并发锁

synchronized【对象锁】关键字的使用

synchronized 关键字解决的是多个线程之间访问资源的同步性, synchronized 关键字可以保证
被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
1. 修饰代码块 synchronized (this) 和 synchronized (lock)
指定加锁对象,对给定对象 / 类加锁。 synchronized(this/object) 表示进⼊同步代码库前要获得给定对象的锁 synchronized(类 .class) 表示进⼊同步代码前要获得 当前 class 的锁。
  1. synchronized (this):这种方式是在非静态方法中使用的,它将当前对象(即调用该方法的对象)作为同步锁。只有在同一对象实例上获取锁的线程才能进入同步代码块,其他线程需要等待当前对象锁释放后才能继续执行。因此,同一对象的不同方法之间也是同步的。

  2. synchronized (lock):这种方式是在静态方法或者普通代码块中使用的,它将指定的对象作为同步锁。通常情况下,会使用一个静态对象作为锁。多个线程在获取到相同的锁对象时才能进入同步代码块,其他线程需要等待锁释放后才能执行。因此,不同对象实例上的同步代码块之间是独立的,不会相互影响。

2. 修饰实例⽅法 : 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
3. 修饰静态⽅法 : 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得
class 的锁 。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个
静态资源,不管 new 了多少个对象,只有⼀份 )。所以,如果⼀个线程 A 调⽤⼀个实例对象的
⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,
是允许的,不会发⽣互斥现象, 因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访
问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁
总结:
  • synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class
类上锁。
  • synchronized 关键字加到实例⽅法上是给对象实例上锁。
  • 尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

对象单例模式

1.懒汉模式

2.饿汉模式

synchronized底层实现原理

  • synchronized 关键字底层原理属于 JVM 层⾯。
synchronized 同步语句块的实现使⽤的是 monitorenter monitorexit 指令,其中
monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位
置。
  • 当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
  • monitor内部有三个属性,分别是owner、entrylist、waitset。其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
  • 在执⾏ monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1
  • 在执⾏ monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

synchronized和Lock区别

锁升级

Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

对象的内存结构

对象在内存中存储的布局:

MarkWord:

  • Monitor重量级锁:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联。

  • 轻量级锁:

  • 偏向锁

锁升级

CPU ⾼速缓存

CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。 CPU Cache 缓存的是内存数据⽤于解决 CPU 处理速度和内存不匹配的问题,内存缓存的 是硬盘数据⽤于解决硬盘访问速度过慢的问题。
先复制⼀份数据到 CPU Cache 中,当 CPU 需要⽤到的时候就可以直接从 CPU Cache 中读取数
据,当运算完成后,再将运算得到的数据写回 Main Memory 中。CPU 为了解决内存缓存不⼀致性问题可以通过制定缓存⼀致协议或者其他⼿段来解决。

JMM(Java 内存模型)(JVM)

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

CAS

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。

旧的预期值就是希望从哪个值开始变的,需要跟内存值做对比,如果一样的话就更新内存值为B的数值。

一般思路是通过自旋锁实现。

CAS操作适用于精细粒度的并发控制,可以避免使用传统的加锁机制带来的性能开销和线程阻塞。然而,CAS操作也存在一些限制和注意事项:

  1. ABA问题:CAS操作无法感知到对象值从A变为B又变回A的情况,可能会导致数据不一致。为了解决ABA问题,可以引入版本号或标记位等机制。
  2. 自旋开销:当CAS操作失败时,需要不断地进行重试,会占用CPU资源。如果重试次数过多或者线程争用激烈,可能会引起性能问题。
  3. 并发性限制:如果多个线程同时对同一内存地址进行CAS操作,只有一个线程的CAS操作会成功,其他线程需要重试或放弃操作。

在Java中,提供了相关的CAS操作支持,如AtomicInteger、AtomicLong、AtomicReference等类,可以实现基于CAS操作的线程安全操作。

并发程序出现问题的根本原因

Java并发编程三大特性

原子性:加锁

synchronized或LOCK

可见性:让一个线程对共享变量的修改对另一个线程可见

synchronized、volatile(推荐)、LOCK

有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保
证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终
执行结果和代码顺序执行的结果是一致的。volatile

AQS

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQS常见的实现类:

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待.

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
  • 比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

ReentrantLock

翻译过来是可重入锁,CAS+AQS队列实现。

可中断;可以设置超时时间;可以设置公平锁;支持多个条件变量;与synchronized一样,都支持重入.

exclusiveOwnerThread 是指 ReentrantLock(可重入锁)的独占锁所有者线程属性

ConcurrentHashMap

一种线程安全的高效map集合:将整个存储空间分成多个段(Segment),每个段都类似于一个小的 HashMap,拥有自己的锁。这种设计实现了对不同段的并发访问,每个线程在访问时只需要锁住相应的段,而不是整个 ConcurrentHashMap,从而提高了并发性能。

线程池

线程池的核心参数

  • corePoolSize 核心线程数目
  • maximumPoolSize 最大线程数目 = ( 核心线程 + 救急线程的最大数目 )
  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler 拒绝策略 - 当所有线程都在繁忙, workQueue 也放满时,会触发拒绝策略

线程池执行原理

1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略
拒绝策略:
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;

常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
比较常见的有4个,用的最多是ArrayBlockingQueue和LinkedBlockingQueue
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

LinkedBlockingQueue读和写各有一把锁,性能相对较好
ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些

如何确定核心线程数

IO密集型任务
一般来说:文件读写、DB读写、网络请求等
推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
CPU密集型任务
一般来说:计算型代码、Bitmap转换、Gson转换等
推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)

线程池的种类有哪些

CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成
倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await() 用来等待计数归零
  • countDown() 用来让计数减一

如何控制某个方法允许并发访问线程的数量

Semaphore [ˈsɛməˌfɔr] 信号量,是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。
Semaphore两个重要的方法
lsemaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
lsemaphore.release():释放一个信号量,此时信号量个数+1

ThreadLocal

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。

ThreadLocal基本使用

ThreadLocal-内存泄露问题

内存泄漏是指程序在运行过程中,因为某些原因导致无用的对象(即不再需要的对象)无法被垃圾回收器正确地回收,进而导致系统占用的内存空间持续增加,直至耗尽所有可用内存资源,最终导致程序崩溃或者系统变得非常缓慢。

四种引用情况

在使用ThreadLocal的时候,强烈建议:务必手动remove。

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mmm`

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值