java八股文复习----java多线程---2024/03/12

在这里插入图片描述

1.并发编程的三要素是什么?

在这里插入图片描述

2.java如何创建线程

在这里插入图片描述
继承Thread类创建线程
实现Runnable接口创建线程
使用Callable和Future创建线程 有返回值
使用线程池创建线程

代码演示
import java.util.concurrent.*;
public class threadTest{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //继承thread
        ThreadClass thread = new ThreadClass();
        thread.start();
        Thread.sleep(100);
        System.out.println("#####################");
 
        //实现runnable
        RunnableClass runnable = new RunnableClass();
        new Thread(runnable).start();
        Thread.sleep(100);
        System.out.println("#####################");
 
        //实现callable
        FutureTask futureTask = new FutureTask(new CallableClass());
        futureTask.run();
        System.out.println("callable返回值:" + futureTask.get());
        Thread.sleep(100);
        System.out.println("#####################");
 
        //线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
        threadPoolExecutor.execute(thread);
        threadPoolExecutor.shutdown();
        Thread.sleep(100);
        System.out.println("#####################");
 
        //使用并发包Executors
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(thread);
        executorService.shutdown();
    }
}
 
class ThreadClass extends Thread{
    @Override
    public void run() {
        System.out.println("我是继承thread形式:" + Thread.currentThread().getName());
    }
}
 
class RunnableClass implements Runnable{
    @Override
    public void run(){
        System.out.println("我是实现runnable接口:" + Thread.currentThread().getName());
    }
}
 
class CallableClass  implements Callable<String> {
    @Override
    public String call(){
        System.out.println("我是实现callable接口:");
        return "我是返回值,可以通过get方法获取";
    }
}

————————————————

                        leader_song

原文链接:https://blog.csdn.net/leader_song/article/details/132094080
在这里插入图片描述

3.java常见的锁

在这里插入图片描述

4.synchronized的用法

在这里插入图片描述

5.synchronized 和 Lock 的区别

在这里插入图片描述

  • synchronized是可重入锁、不可中断锁,非公平锁、
    lock的ReentrantLock是可重入锁,可中断锁,可以是公平锁也可以是非公平锁
  • synchronized是JVM层次通过监视器实现的,Lock是通过AQS实现的

5.1什么是AQS锁?

AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。

AQS的原理是,AQS内部有三个核心组件,
一个是state代表加锁状态初始值为0
一个是获取到锁的线程
还有一个阻塞队列

当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的

可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程。

5.2有哪些常见的AQS锁

AQS分为独占锁和共享锁

  • ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队

  • Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。

应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票

  • CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。

应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行

  • CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值

应用场景:多线程计算数据,最后合并计算结果的应用场景

6.为什么使用线程池

在这里插入图片描述

7.线程池的核心属性

在这里插入图片描述

8.线程池的执行流程

在这里插入图片描述
在这里插入图片描述

9.线程池的拒绝策略

在这里插入图片描述

10.线程池的几种创建方式

在这里插入图片描述
在这里插入图片描述

11.volatile关键字

11.1说说你对volatile关键字的理解

在这里插入图片描述

什么是内存可见性,什么又是重排序呢?

在这里插入图片描述

单例模式双重校验锁变量为什么使用 volatile 修饰?
禁止 JVM 指令重排序,new Object()分为三个步骤:

为实例对象分配内存,
用构造器初始化成员变量,
将实例对象引用指向分配的内存;

实例对象在分配内存后实才不为null。如果分配内存后还未初始化就先将实例对象指向了内存,那么此时最外层的if会判断实例对象已经不等于null就直接将实例对象返回。而此时初始化还没有完成。
————————————————

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/w20001118/article/details/125724647

11.2volatile 与 synchronized 的区别?

在这里插入图片描述

12.ThreadLocal

12.1ThreadLocal 是什么?

在这里插入图片描述

12.2ThreadLocal 可以作用于哪些场景?

在这里插入图片描述

12.3为了解决什么问题?

在这里插入图片描述

解决了对象在被共享访问时带来的线程安全问题

通过 threadLocal.set() 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,
这样的话每个线程都使用自己的对象实例,彼此不会影响从而达到了隔离的作用。

12.4ThreadLocal 内存泄漏的原因?

在这里插入图片描述

13.线程的状态转换有什么(生命周期)

在这里插入图片描述

  • 新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。

  • 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

  • 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。

  • 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

  • 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
    ————————————————

                         leader_song
    

原文链接:https://blog.csdn.net/leader_song/article/details/132094080

14.进程和线程的区别,进程间如何通信

  • 进程:系统运行的基本单位,进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
  • 线程:独立运行的最小单位,一个进程包含多个线程且它们共享同一进程内的系统资源

进程间通过管道、 共享内存、信号量机制、消息队列通信

15.什么是死锁

死锁指多个线程在执行过程中,因争夺资源造成的一种相互等待的僵局

16.死锁的必要条件

  • 互斥条件:同一资源同时只能由一个线程读取

  • 不可抢占条件:不能强行剥夺线程占有的资源

  • 请求和保持条件:请求其他资源的同时对自己手中的资源保持不放

  • 循环等待条件:在相互等待资源的过程中,形成一个闭环

  • 在这里插入图片描述


  • 想要预防死锁,只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,还可以用银行家算法可以预防死锁

17.CAS


什么是CAS

CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。

在并发编程中,我们都知道i++操作是非线程安全的,这是因为 i++操作不是原子操作,我们之前在讲多线程带来了什么问题中有讲到,大家应该还记得吧?

如何保证原子性呢?

常见的做法就是加锁。

在 Java 中,我们可以使用 synchronized关键字 和 CAS(Compare-and-Swap)来实现加锁效果。

**synchronized 是悲观锁,**尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多(前面有细讲,戳链接回顾),但依然是悲观锁,线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。

CAS 是乐观锁,线程执行的时候不会加锁,它会假设此时没有冲突,然后完成某项操作;如果因为冲突失败了就重试,直到成功为止。

17.1乐观锁与悲观锁

锁可以从不同的角度来分类。比如我们在前面讲 synchronized 四种锁状态的时候,提到过偏向锁、轻量级锁、重量级锁,对吧?乐观锁和悲观锁也是一种分类方式。

悲观锁

对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁

乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。

由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。


乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;
悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。


17.2什么是 CAS

在 CAS 中,有这样三个值:

V:要更新的变量(var)
E:预期值(expected)
N:新值(new)

比较并交换的过程如下:

判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

这里的预期值 E 本质上指的是“旧值”。

我们以一个简单的例子来解释这个过程:

如果有一个多个线程共享的变量i原本等于 5,我现在在线程 A 中,想把它设置为新的值 6;
我们使用 CAS 来做这个事情;
首先我们用 i 去与 5 对比,发现它等于 5,说明没有被其它线程改过,那我就把它设置为新的值 6,此次 CAS 成功,i的值被设置成了 6;
如果不等于 5,说明i被其它线程改过了(比如现在i的值为 2),那么我就什么也不做,此次 CAS 失败,i的值仍然为 2。
在这个例子中,i就是 V,5 就是 E,6 就是 N。


CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是

  • (1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作

  • (2)CAS频繁失败导致CPU开销大

  • (3)ABS问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决

  • 那有没有可能我在判断了i为 5 之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

不会的。因为 CAS 是一种原子操作,它是一种系统原语,是一条 CPU 的原子指令,从 CPU 层面已经保证它的原子性。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

17.3 CAS的缺点

(1)ABA问题

如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。

  • 解决办法: 使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JUC包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
(2)循环时间长开销会比较大:

自旋重试时间,会给CPU带来非常大的执行开销

(3)只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作
  • 解决办法:
    从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,
    你可以把多个变量放在一个对象里来进行CAS操作

17.4 CAS使用注意事项

  • (1)CAS需要和volatile配合使用

CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性

  • (2)CAS适用于并发量不高、多核CPU的情况

CPU多核情况下可以同时执行,如果不合适就失败。而并发量过高,会导致自旋重试耗费大量的CPU资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值