java学习——java并发(2)

上一篇传送门:点我

继续填java并发的坑T_T,这个知识点既重要又有难度,需要好好去准备。

什么是守护线程?

守护线程是一种特殊的线程,它在后台运行,用于执行一些辅助性的任务。与普通线程不同的是,当程序中其他的非守护线程执行完毕之后,守护线程也会陆续地结束,而不会等待守护线程的执行结果。因此,守护线程通常被用于执行一些不重要的、不影响程序主逻辑的任务,如日志记录、性能监控等。

在实际应用中,守护线程的使用需要谨慎。因为它们可能会在程序结束时被突然中断,所以不适合执行一些需要保证完整性的任务
实际应用场景:聊天框和传输文件。传输文件是守护线程,关闭聊天框后,传输文件就会自动结束。

什么是死锁?说说死锁的必要条件

死锁是指两个或两个以上的进程(或线程,下文同)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
死锁的必要条件如下:
1.互斥条件: 某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有;
2.不可抢占条件: 进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放;
3.请求且保持条件: 进程至少已经占有一个资源,但此时又申请新的资源,由于申请的资源当前正在被其他进程所占有,所以此时该进程堵塞。但是,它在等待新资源的过程中,仍然会继续占用已占有的资源;
4.循环等待条件: 进程之间形成了循环等待的关系。每一个进程均占有若干种资源中的某一种,同时每一个进程还要求循环链中下一个进程所占有的资源。

如何解决死锁问题?

避免死锁的出现需要去破坏死锁出现的四个必要条件的至少其中一个。而由于互斥条件是锁本身的一个特征,是无法被破坏的,所以避免死锁问题是围绕破坏其他三个条件展开。
对于不可抢占条件,占有部分资源的线程在进一步去申请其他资源时,如果申请不到,就会主动释放它占有的资源;
对于请求且保持条件,可以在第一次线程执行的时候,一次性申请所有的共享资源;
对于循环等待条件,可以按照顺序来申请锁资源,相当于给资源一个编号,按照合理的编号顺序去申请,就可以避免循环等待问题的发生。

线程加锁有哪些方式?

1.synchronized关键字修饰代码块和方法
2.JUC包中的Lock接口和ReentrantLock实现类

谈谈synchronized关键字

synchronized关键字用于控制多个线程对共享资源的访问,以实现线程的同步与互斥。具体来说,synchronized可以修饰代码块方法,以确保同一时间只有一个线程可以执行被修饰的代码

当一个线程尝试访问一个被synchronized修饰的方法或代码块时,它会首先检查该方法或代码块是否已经被其他线程锁定。如果已经被锁定,则该线程会被阻塞,直到锁定被释放为止。如果该方法或代码块没有被锁定,则该线程会获得锁定,并执行相应的代码。在执行完毕后,该线程会释放锁定,以便其他线程可以访问该方法或代码块。

动态方法和静态方法的Synchronized有什么区别?

对于静态方法,synchronized 锁定的是类的Class对象。这意味着,无论创建了多少个类的实例,只有一个Class对象会被锁定。因此,当一个线程进入了一个synchronized静态方法,其他线程就不能进入该类中任何其他synchronized静态方法,直到第一个线程退出该方法。
对于动态方法,synchronized 锁定的是调用这个方法的对象实例。这意味着,当一个线程进入了一个对象的synchronized动态方法,其他线程就不能进入该对象的同一个或其他synchronized动态方法,但可以进入其他对象的synchronized动态方法。

Synchronized的锁升级过程是怎样的?

1.偏向锁: 当一个线程首次访问同步代码块时,如果锁对象的对象头中的threadId字段(线程id)为空,JVM会让当前线程持有偏向锁,并将threadId设置为该线程的ID。这样,在后续该线程再次访问同步代码块时,无需进行任何同步操作,提高了执行效率。偏向锁适用于只有一个线程访问共享资源的场景。

2.轻量级锁:多个线程尝试访问同步代码块时,如果偏向锁不再适用,JVM会将锁升级为轻量级锁。在轻量级锁状态下,线程会自旋(反复判断是否能获取到锁)一定次数来尝试获取锁,而不立即进入阻塞状态。这减少了线程上下文切换的开销,提高了性能。如果自旋超过一定次数或者有其他线程长时间占据锁,锁会进一步升级。

3.重量级锁: 当多个线程在轻量级锁状态下仍然无法获得锁时,锁将升级为重量级锁。在重量级锁状态下,未获得锁的线程会进入阻塞状态,等待锁的释放。这是最耗费系统资源的锁状态,因为它涉及线程的上下文切换内核态与用户态的切换

总的来说,synchronized锁的升级策略是为了在不同并发场景下提供最优的性能。偏向锁适用于单线程访问,轻量级锁适用于少量线程竞争的场景,而重量级锁则适用于高度竞争的场景。这种逐步升级的策略有助于减小性能开销,因为大多数情况下只有少数线程会争夺锁,而大部分时间可以避免阻塞和上下文切换。

此外,值得注意的是,锁升级后不能降级。即一旦锁从偏向锁升级为轻量级锁或重量级锁,它不会再降回到偏向锁状态。同样地,一旦锁从轻量级锁升级为重量级锁,它也不会再降回到轻量级锁状态。这种设计是为了保证锁的稳定性和一致性。

谈谈volatile关键字

volatile是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

谈谈Lock接口

Lock是JUC包下的一个接口,它提供了一种更灵活的线程同步机制,相比synchronized关键字,Lock提供了更广泛的锁定操作,并且允许更精细的控制。
Lock接口的实现类(如ReentrantLock)中提供了可重入的锁,这意味着同一个线程可以多次获取同一个锁而不会导致死锁。此外,Lock还支持尝试获取锁、能够被中断的获取锁以及定时获取锁等操作,这使得它在处理复杂的并发场景时更加灵活和高效。

谈谈synchronized和Lock的区别

1.二者都是解决线程安全的工具,synchronized是java内置的关键字,它可以用来修饰代码块和方法,实现线程同步。而Lock是JUC中的一个接口,它定义了加锁释放锁的方法,需要通过实现该接口来提供具体的锁实现。
2.synchronized通过将关键字修饰在方法上或者代码块上控制锁粒度,而Lock相比则会更加灵活,它通过lock()unlock()方法进行锁粒度的控制。
3.lock()提供了
非阻塞的竞争锁
的方法,通过trylock()方法去获取是否有线程正在使用锁。并且lock()提供了公平锁和非公平锁机制,所谓公平锁先申请的线程会先得到锁非公平锁就是随机或者按照其他优先级排序的获得锁。而synchronized只提供非公平锁

什么是可重入锁?

可重入锁的主要特点是允许同一个线程多次获得同一个锁。也就是说,如果一个线程已经持有了某个锁,那么它再次请求这个锁时,会立即成功并获得锁,而不会产生死锁或其他并发问题。这个特性在递归函数或有多个同步方法需要同一把锁时非常有用。
可重入锁的内部通常会有一个计数器来记录同一个线程获取锁的次数。每次线程获得锁时,计数器就会加一;每次线程释放锁时,计数器就会减一。当计数器为零时,表示锁已经完全释放,其他线程可以获取该锁。

ReentrantLock中的公平锁和非公平锁的底层实现

不管是公平锁还是非公平锁,它们的底层实现都会使用AQS来进行排队,区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队;如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。ReentrantLock默认是非公平锁,性能更高一点。
可重入锁:同一个线程可以多次获取同一个锁,而不会造成死锁。

说说你对AQS的理解?

AQS(Abstract Queued Synchronizer)是多线程同步器,它是JUC包中多个组件的底层实现,比如Lock、CountDownLatch、Semaphore都用到了AQS。
从本质上来说,AQS提供了两种锁的机制,分别是排它锁共享锁
所谓排它锁就是存在多个线程去竞争同一共享资源的时候,同一个时刻,只允许一个线程去访问这样一个共享资源,也就是说,多个线程中,只能有一个线程去获得这样的一个锁的资源,比如Lock中的ReentrantLock可重入锁,它的实现就是用到了AQS中的排它锁功能;共享锁也称为读锁,就是在同一个时刻允许多个线程同时获得一个锁的资源,在共享锁定的数据上不能加排它锁进行修改,直到所有共享锁都被释放,比如CountDownLatch以及Semaphore都用到了AQS中的共享锁的功能。
AQS作为排它锁来说,它的整个设计体系中,需要解决三个核心的问题:
1.互斥变量的设计以及如何保证多线程同时更新互斥变量的时候线程的安全性;
2.未竞争到锁资源的线程的等待以及竞争到锁的资源释放锁之后的唤醒;
3.对于锁竞争的公平性和非公平性,AQS采用了一个int类型的互斥变量state,用来记录锁竞争的状态,0表示当前没有任何线程竞争锁资源,而大于等于1则表示已经有线程正在持有锁资源,一个线程来获取锁资源的时候,首先会判断state是否等于0,即无锁状态,如果是,则把这个state变量更新为1表示占用了锁,而这个过程中,如果多线程同时去做这样的操作,就会导致线程安全性问题。针对以上问题,AQS采用了CAS机制去保证state互斥变量更新的原子性。未获得到锁的线程会通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出(FIFO)的原则去加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从这样一个双向链表的头部去唤醒下一个等待的线程,再去竞争锁。
关于锁竞争的公平性和非公平性问题,AQS的处理方式是在竞争锁资源的时候,公平锁需要去判断双向链表中是否有阻塞的进程,如果有则需要排队等待。而非公平锁的处理方式是,不管双向链表中是否存在等待竞争锁的线程,它都会先直接去尝试更改互斥变量state去竞争锁。假设在一个临界点,获得锁的线程释放锁,此时state为0,而当前的这个线程去抢占锁的时候,正好可以把state修改为1,那么这个时候就表示他可以拿到锁,而这个过程就是非公平的。

什么是阻塞队列?

阻塞队列是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程将会阻塞,直到队列变为非空时;当队列满时,存储元素的线程将会阻塞,直到队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

为什么要用阻塞队列?

1.多线程合作: 阻塞队列天生就是多线程合作的,生产者线程在队列尾部插入元素,消费者线程在队列头部移除元素。
2.线程阻塞与唤醒: 阻塞队列可以在尝试对一个空的队列进行获取操作,或者对一个满的队列进行插入操作时,将线程挂起,然后在队列发生变化时唤醒线程,从而避免了轮询。
3.数据的缓冲和流量的削峰填谷: 阻塞队列可以看作是一个缓冲区平衡了生产者和消费者的处理能力。如果生产者处理得很快,那么生产者处理的数据会暂时存放在队列中,等待消费者去处理。相反,如果消费者的处理能力大于生产者,那么消费者也只是等待数据准备好了,再去处理。

ArrayBlockingQueue和LinkedBlockingQueue的区别

ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性,它们的区别如下:
1.ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构;
2.ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLock和takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。

谈谈悲观锁和乐观锁

乐观和悲观说的是对于锁的处理方式。

悲观锁认为每次访问数据时,其他线程都会修改该数据,因此在访问数据前需要先加锁,确保在此期间其他线程无法修改数据,如synchronized和ReentrantLock,性能损耗相对较大。

乐观锁则与悲观锁相反,它认为并发冲突较少,多个线程同时修改数据的概率较低,因此在访问数据时不会立即加锁,而是在更新数据时检查数据是否被其他线程修改过。如果数据被修改过,则更新操作会失败。
乐观锁会给数据表加一个版本号version的字段,每修改一次,版本号加一,线程A读取数据时会同时读取版本号,提交更新数据时,会去比较现在的版本号与之前读取的版本号是否一致,若不一致,则说明数据已经被更改,那样就会重试更新,CAS是乐观锁的一种常见实现方式。

在选择使用悲观锁还是乐观锁时,需要根据具体的应用场景和需求进行权衡。如果系统对数据的一致性和安全性要求非常高,且并发冲突较少,可以考虑使用悲观锁,如银行系统中的转账操作;如果系统对数据的并发性能要求较高,且可以容忍一定程度的数据不一致性,可以考虑使用乐观锁,如电商网站中的商品下单操作

谈谈CAS机制

CAS的全称是Compare And Swap(Java实现类Unsafe把CAS解释为Compare And Set),翻译过来就是比较并交换的意思。它的主要功能是保证在多线程的环境下,对于共享变量修改的原子性。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。否则,处理器不做任何操作。
当变量的预期值A和内存地址V中的实际值不同时,该线程将会重新获取内存地址V的当前值,并重新计算要修改的新值B,并重新尝试更新变量。这个重新尝试的过程被称为自旋

在CAS机制中,存在ABA问题:线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却是发生变化了。
可以使用版本号解决ABA问题的发生,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A - 2B-3A。
在Java中是用AtomicStampReference解决ABA问题的,本质是有一个int值作为版本号时间戳),每次更改前先取到这个版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(也可以自己定义加别的值)。

线程池的基本原理

线程池的基本原理是通过维护一定数量的线程来执行任务,以达到降低系统资源消耗、提高响应速度和增强线程可管理性的目的。线程池内部包含一个线程集合和一个任务队列
当有新任务提交时,线程池会优先检查有没有空闲的核心线程,如果有,则使用空闲的核心线程来执行任务,如果没有空闲的核心线程,此时就会把任务放入到任务队列中,如果此时任务队列也满了,就会判断线程数是否达到最大的线程数,如果没有用达到最大线程数,此时创建一个线程去执行任务。如果既不能创建线程,并且任务队列也满了,那么此时会采用线程池中的拒绝策略进行任务拒绝。
在这里插入图片描述

线程池的作用是什么?(为什么要用线程池)

1.使用线程池能够减少线程频繁创建与销毁所带来的性能开销,因为线程的创建会涉及到CPU的上下文切换(这是因为操作系统需要为新线程分配CPU时间片,并在不同线程之间进行切换),内存分配等工作。
2.线程池本身有参数控制线程创建的数量,这能够避免无休止的线程创建,从而起到保护资源的作用。

线程池的核心参数有哪些?

corePoolSize(核心线程数):线程池中最少的线程数,即在线程池中一直保持的线程数量,不受空闲时间的影响。

maximumPoolSize(最大线程数):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

keepAliveTime(线程存活时间):当线程池中的线程数超过核心线程数时,多余的线程会被回收。这个参数即为非核心线程的存活时间,超过此时间将被回收。

unit(时间单位):这是keepAliveTime参数的时间单位,如秒、毫秒等。

workQueue(任务队列):这是一个用于存储等待执行的任务的队列。当线程池中的线程数达到核心线程数时,新任务将被加入任务队列等待执行。任务队列的类型和容量会影响线程池的性能和行为。

threadFactory(线程工厂):这是一个用于创建新线程的对象。通过线程工厂,可以定制线程的名字、线程组、优先级等属性。这对于调试和跟踪线程非常有用。

handler(拒绝策略):当所有线程都在繁忙,且任务队列已满时,新提交的任务将被拒绝。这时就需要一个拒绝策略来处理这种情况。常见的拒绝策略有抛出异常、直接丢弃任务、丢弃队列中最老的任务等。

线程池的核心线程数和最大线程数大小该如何设置?

对于CPU密集型任务,由于计算较多,可以设置线程数为CPU核心数+1
对于I/O密集型任务,由于读取任务较多,可以设置线程数为2*CPU核心数

有哪些线程池?

(1)newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。
(2)newSingleThreadExecutor只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime为0, 工作队列使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。
(3)newCachedThreadPool按需创建新线程的线程池。核心线程数为0,最大线程数为Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。
(4)newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为DelayQueue。适用于需要多个后台线程执行周期任务。
(5)newWorkStealingPool:用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。

线程池有哪些队列?

(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。
(2)LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool使用了该队列。
(3)SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。
Executors.newCachedThreadPool使用了该队列。
(4)PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。
(5)DelayQueue:一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。

线程池有哪些拒绝策略?

(1)AbortPolicy中止策略。默认的拒绝策略,直接抛出RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
(2)DiscardPolicy抛弃策略。什么都不做,直接抛弃被拒绝的任务。
(3)DiscardOldestPolicy抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用
(4)CallerRunsPolicy调用者运行策略。在调用者线程中执行该任务,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者,由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

核心线程数量和最大线程数量一致会发生什么情况?

当核心线程数量和最大线程数量设置为一致时,线程池不会创建新的线程,除非现有的线程因超时或异常退出而被移除。这种情况下,线程池将一直维持固定数量的线程,不会根据任务负载的变化动态调整线程数。
(1)资源浪费: 如果任务提交速度很快,线程池中的线程可能会一直保持在最大线程数,导致资源浪费,降低系统性能。
(2)无法应对突发流量: 如果在短时间内出现大量任务提交,而线程池的大小一直保持不变,可能无法及时处理所有任务,导致响应时间增加。

使用队列需要注意什么?

使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

为什么线程池是先添加队列而不是先创建最大线程?

创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

线程只能在任务到达时才启动吗?

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是可以使用prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

如何终止线程池?

(1)shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕
(2)shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过Thread.interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

核心线程怎么实现一直存活?

核心线程在获取任务时,通过阻塞队列的take()方法实现一直阻塞(存活)。
在这里插入图片描述

下一篇传送门:点我

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值