并发问题系统学习(更新中)

进程、线程

  • 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。可以理解为一个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的数值。

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

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

Java并发编程三大特性

原子性:加锁

synchronized或LOCK

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

synchronized、volatile(推荐)、LOCK

有序性

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

AQS

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

AQS常见的实现类:

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

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

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

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

ReentrantLock

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

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

相关推荐

  1. 并发系统面临的问题 及 解决方案

    2024-05-10 21:34:09       4 阅读
  2. 算法系统学习(持续更新

    2024-05-10 21:34:09       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-05-10 21:34:09       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-05-10 21:34:09       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-05-10 21:34:09       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-05-10 21:34:09       20 阅读

热门阅读

  1. 洛谷P10397题解

    2024-05-10 21:34:09       13 阅读
  2. CF988D题解

    2024-05-10 21:34:09       11 阅读
  3. React 第二十八章 前端框架

    2024-05-10 21:34:09       9 阅读
  4. 按键精灵写的有点失败了

    2024-05-10 21:34:09       9 阅读
  5. 关于学习与智慧

    2024-05-10 21:34:09       8 阅读
  6. 说说SpringBoot自动配置原理

    2024-05-10 21:34:09       13 阅读
  7. thinkphp5 中路由常见的使用方法

    2024-05-10 21:34:09       12 阅读