JUC并发编程

  1. 多线程相关

1.多线程的生命周期

2.Java创建多线程的4种方式

1、继承Thread类 2、实现Runnable接口 3、实现Callable接口 4、使用线程池

推荐使用线程池。如果不使用线程池,推荐使用Runnable,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用.

3.Sleep方法和Wait方法的区别

1.sleep()是Thread类中的方法,而wait()则是Object类中的方法。

2.最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3.使用范围:wait、notify和notifyAll只能在同步控制方法或者同步控制块里面使用,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;而sleep可以在任何地方使用。

4.相同点:sleep必须捕获异常,wait,notify和notifyAll同样需要捕获异常

4.sychornized与volatile的区别

1.sychornized:修饰代码块或方法;是一种线程同步机制,也可被称为可重入锁(悲观锁)

2.volatile:修饰变量;在Java并发编程中常用于保持内存可见性和防止指令重排序;volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,不能保证原子性。是比sychornized更轻量级的同步机制。

3.它们是jdk初始版本就存在的关键字。


public class VolatoleAtomicityDemo {
    public volatile static int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                for (int j = 0; j < 500; j++) {
                    volatoleAtomicityDemo.increase();
                }
            });
        }
        // 等待1.5秒,保证上面程序执行完成
        Thread.sleep(1500);
        System.out.println(inc);
        threadPool.shutdown();
    }
}

正常情况下,运行上面的代码理应输出 2500。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 2500。

为什么会出现这种情况呢?不是说好了,volatile 可以保证变量的可见性嘛!

也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5*500=2500。

很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:

1.读取 inc 的值。 2.对 inc 加 1。 3.将 inc 的值写回内存。

volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:

  1. 线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。

  1. 线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。

这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。

其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock或者AtomicInteger都可以

使用synchronized


public synchronized void increase() {
    inc++;
}

5.什么是Threadlocal?

ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。

Threadlocal 适用于在多线程的情况下,可以实现传递数据,实现线程隔离

ThreadLocal 提供给我们每个线程缓存局部变量。

6.Threadlocal实现原理

Treadlocal的set,get方法源码:

1. 在每个线程中都有自己独立的 ThreadLocalMap 对象,中 Entry 对象。

2. 如果当前线程对应的的 ThreadLocalMap 对象为空的情况下,则创建该 ThreadLocalMap对象,并且赋值键值对。

Key 为 this(调用set方法的ThreadLocal对象),value 就是为 object 变量值。

一个线程可以有多个ThreadLocal对象,一个ThreadLocal对象只能缓存一个变量值。

7.Threadlocal内存泄漏

内存泄漏问题

内存泄漏 表示就是我们程序员申请了内存,但是该内存一直无法释放;

内存溢出问题:

申请内存时,发现申请内存不足,就会报错 内存溢出的问题;

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用

所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,ThreadLocalMap的key (key就是ThreadLocal对象)会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

8.Threadlocal 采用弱引用而不是强引用

ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。

2、线程池相关,

1.线程池的作用

核心点:复用机制 提前创建好固定的线程一直在运行状态 实现复用 限制线程创建数量。

1.降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

2.提高响应速度:任务到达时,无需等待线程创建即可立即执行。

3.提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

4.提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行

2.创建线程池方式

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


class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}
 
class NumberThread1 implements Callable<Integer> {
    @Override
    public Integer call() {
        for (int i = 0; i <= 10; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
        return 100;
    }
}
 
public class ThreadPool {
    public static void main(String[] args) throws Exception {
        //1. 提供指定线程数量的线程池
        ExecutorService service = new ThreadPoolExecutor(5, //线程池的核心线程数
                10, //最大线程数
                0L, //多余空闲线程的存活时间
                TimeUnit.MILLISECONDS, //存活时间的单位
                new LinkedBlockingQueue<Runnable>(), //阻塞队列
                Executors.defaultThreadFactory(), //线程工厂,用于创建线程
                new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        Future<Integer> submit = service.submit(new NumberThread1());//适合使用于Callable
        System.out.println(submit.get());
        //3.关闭连接池
        service.shutdown();
    }
}

2.通过 Executor 框架的工具类 Executors 来创建(不推荐)

我们可以创建多种类型的 ThreadPoolExecutor:

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

  • ScheduledThreadPool :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

3.为什么不建议使用 Executors创建线程池?

因为默认的 Executors 线程池底层是基于 ThreadPoolExecutor构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生内存溢出(OOM),会导致我们最大线程数会失效。

4.线程池底层是如何实现复用的

本质思想:创建一个线程,不会立马停止或者销毁而是一直实现复用。

1. 提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗 cpu 的资源)

2. 当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行

拒绝策略;

3. 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;

5.线程池创建的线程会一直在运行状态吗?

不会,核心线程会一直运行,(最大线程-核心线程)的这些线程如果创建后,在存活时间内一直没有任务直接就会停止该线程。

例如:配置核心线程数 corePoolSize 为 2 、最大线程数 maximumPoolSize 为 5我们可以通过配置超出 corePoolSize 核心线程数后创建的线程的存活时间例如为 60s,在 60s 内没有核心线程一直没有任务执行,则会停止该线程。

6.线程池的核心参数

corePoolSize:核心线程数量 一直正在保持运行的线程

maximumPoolSize:最大线程数,线程池允许创建的最大线程数。

keepAliveTime:超出 corePoolSize 后创建的线程的存活时间。

unit:keepAliveTime 的时间单位。

workQueue:任务队列,用于保存待执行的任务。新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

threadFactory:线程池内部创建线程所用的工厂。

handler:任务无法执行时的处理器(拒绝执行策略)

7.线程池底层 ThreadPoolExecutor 底层实现原理

1.当线程数小于核心线程数时,创建核心线程

2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列

3.当线程数大于等于核心线程数,且任务队列已满

3.1 若线程数小于最大线程数,创建线程

3.2 若线程数等于最大线程数,抛出异常,拒绝任务

8.线程池的拒绝执行策略有哪些

1.AbortPolicy 丢弃任务,抛运行时异常

2.CallerRunsPolicy 执行任务

3.DiscardPolicy 忽视,什么都不会发生

4.DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务

5.实现 RejectedExecutionHandler 接口,可自定义处理器

9.线程池队列满了,任务会丢失吗

如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略

可以自定义异拒绝异常,将该任务缓存到 redis、本地文件、mysql 中后期项目启动实现补偿。实现 RejectedExecutionHandler 接口,可自定义处理器。

3、锁相关

1.乐观锁和悲观锁

        什么是悲观锁?使用场景是什么?

        悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

        像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

        悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。

        什么是乐观锁?使用场景是什么?

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制CAS 算法)。

乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。

如何实现乐观锁?

2.乐观锁的实现方式

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。乐观锁比较消耗cpu的资源。

1、版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

mybatis plus实现自动填充字段、乐观锁机制以及逻辑删除_mybatis锁机制-CSDN博客

2、CAS 算法

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

CAS锁(自旋锁)没有获取到锁的线程是不会阻塞的,通过循环控制一直不断的获取锁

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

CAS 涉及到三个操作数:

  • V :要更新的变量值(Var) 线程共享变量

  • E :预期值(Expected) 每个线程中都会缓存副本V的值

  • N :拟写入的新值(New)

CAS成功需要 V(共享变量值)=E(线程中缓存的值) 就会将N赋值给V

因为CAS 是通过硬件指令,保证原子性,所以不会有两个线程同时判断E=V

优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制重试。

缺点:

  1. 通过死循环控制,消耗 cpu 资源比较高,需要控制循次数,避免 cpu 飙高问题;

  2. 只能保证一个共享变量的原子操作

  3. ABA问题

什么是ABA问题?

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值(例如B),然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的"ABA"问题。

解决方案:变量前面追加上版本号或者时间戳。每次修改对比版本号或者时间戳。

3.Java 有哪些锁的分类

1. 悲观与乐观锁

2. 公平锁与非公平锁

3. 自旋锁/重入锁

4. 重量级锁与轻量级锁

5. 独占锁与共享锁

4.公平锁和非公平锁

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放 类似于吃饭排队。

公平锁底层实现:队列---底层实现方式---数组或者链表实现

非公平锁:不是据请求的顺序排列, 通过争抢的方式获取锁。

非公平锁效率比公平锁效率要高,Synchronized 是非公平锁

New ReentramtLock()(true)---公平锁 New ReentramtLock()(false)---非公平锁

底层基于 aqs 实现

5.独占锁与共享锁之间的区别

独占锁:在多线程中,只允许有一个线程获取到锁,其他线程都会等待。

共享锁:多个线程可以同时持有锁,例如 ReentrantLock 读写锁。读读可以共享、写写互斥、读写互斥、写读互斥。

6.什么是锁的可重入性

什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。例如

在同一个线程中锁可以不断传递的,可以直接获取。

可重入锁有

  • synchronized

  • ReentrantLock

7.redis如何实现分布式锁

应用场景:系统A和系统B是两个部署在不同节点的相同应用(集群部署),这时客户端请求传来,两个系统都受到了请求,并且该请求是对数据表进行插入操作,如果这个时候不加锁来控制,可能会导致数据库新增两条记录,这时系统也不能允许的,由于是在不同应用内,在单个应用内加JVM级别的锁,另一个应用是感知不到的,这时需要用到分布式锁。

setnx命令:set if not exists,当且仅当 key 不存在时,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。

  • 返回1,说明该进程获得锁,将 key 的值设为 value

  • 返回0,说明其他进程已经获得了锁,进程不能进入临界区。

命令格式:setnx lockKey lockValue

1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁

2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁

存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。

解决方案:设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间。

8.synchronized锁底层实现

底层是由c++写的

重量级锁(悲观锁、非公平锁):synchronized,但是synchronized目前已经优化,即锁升级操作

synchronized锁升级过程:

不要适用String常量、Integer、Long等基础类型作为synchronized锁对象。synchronized只能保证原子性和可见性,不能保证重排序。volatile只能保证可见性和有序性,不能保证原子性

偏向锁->轻量级锁->重量级锁

什么是偏向锁:偏向第一个持有该锁的线程,也就是如果没有竞争,只有一个线程A的场景下,该锁归属当前线程A。当出现另外一个线程B参与竞争,则升级为轻量级锁

偏向锁和轻量级锁:在用户态完成 重量级锁:在内核态完成

9.lock锁实现原理

Lock 常用子类 可重入锁ReentrantLock 有两种模式, 公平锁模式、非公平锁模式 。默认是非公平模式。如果要使用公平锁,创建的时候传入true。

底层基于AQS+Cas+LockSupport锁实现.

10.AQS底层怎么实现的?

AQS,是一个抽象类的队列式同步器,它的内部通过维护一个状态AtomicInteger state(共享资源)和双向链表实现的线程等待队列来实现同步功能。

Lock锁原理基于java AQS类封装,在获取锁的时候AQS类中有一个状态state+1,当前线程不断重入的时候都会不断1+,当在释放锁的时候state-1;最终state为0 该锁没有被任何线程获取到,没有抢到锁的线程,会存在一个双向的链表中

AQS的加锁方式本质上就是多个线程在竞争state,当state为0时代表线程可以竞争锁,不为0(一般为1;不为1的情况:如果是同一个线程多次获取锁--可重入,每获取一次就会+1,解锁就-1)时代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个双向链表中中,这些线程会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒

如果是公平锁,当线程释放锁之后,会根据链表向后查找,唤醒后一个线程去获取锁,根据队列的顺序机进行获取锁操作。

如果是非公平锁,当前线程释放锁之后,会唤醒队列中所有的锁一起竞争锁。

11.CountDownLatch的用法和实现

什么是CountDownLatch?

CountDownLatch是一个同步工具类,它通过一个计数器来实现的,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程就可以恢复执行任务。

应用场景:

1. 某个线程需要在其他n个线程执行完毕后再向下执行

2. 多个线程并行执行同一个任务,提高响应速度

底层实现:

AQS实现


CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
    try {
        System.out.println("t1开始执行..");
        countDownLatch.await();
        System.out.println("t1结束执行..");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "t1").start();
//每执行一次countDownLatch-1   到0时线程唤醒,开始执行
countDownLatch.countDown();
countDownLatch.countDown();

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Rk..

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

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

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

打赏作者

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

抵扣说明:

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

余额充值