多线程常问的问题

本文深入探讨了多线程的并发与并行概念,详细解释了线程与进程的关系,以及守护线程的特性。文章还介绍了创建线程的多种方式,包括`Thread`、`Runnable`、线程池和`Callable`接口。讨论了`Runnable`与`Callable`接口的区别,并分析了线程状态、同步机制、锁的类型和优化。此外,还详细阐述了`synchronized`关键字、重入锁、读写锁、乐观锁(如`CAS`)以及`AQS`(AbstractQueuedSynchronizer)的工作原理。最后,文章提到了线程池的配置、并发容器和实战应用中的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发和并行:

并发:是同一时间多个线程同事在做。

并行:同一时刻多个事件同事进行。

 

线程和进程的关系:

进程是由线程组成,每个进程的资源是独立的,但是该进程中的资源线程是共享的。

线程是cpu调度的基本单位。

 

守护线程是作为一个提供服务的线程,只当它服务的所有的线程都关闭之后才关闭的线程。

 

创建线程的集中方式:

new Thread();

new Runnable();

线程池创建。

Callable接口和Future接口,future接口是线程执行结果返回的接口。

 

runnable和callable的接口:

runnable 和 callable 都是线程创建的接口。

runnable 接口中必须实现的方法是run方法,返回值void;

callable 接口中中必须实现的是call方法,返回值是一个泛型,接受主体是future和futurtask

 

线程有新建状态, 停止,等待, 超时等待,运行,阻塞。

 

sleep方法和wait方法都是加了同步锁的,在sleep方法中,会让出cpu,但是不会放弃同步锁,但是wait是放弃同步锁和cpu。

sleep是睡眠指定时间后继续执行,wait只阻塞,只有等notify方法才可以解开阻塞状态,当然wait可以家超时时间。

wait方法只能在同步块,同步方法中使用,或者是在加了锁之后使用。

sleep是Thread的静态方法,作用与当前线程;wait是object方法,每个对象都可以调用。

 

run方法是runnable接口中的方法,是线程执行的内容;线程调用start方法调用run方法,不可以直接调用run方法,如果直接调用就是属于方法的正常调用,而不是开启线程。

 

原子性,可见性,有序性。有序性包含在happens-before原则。

 

线程同步集中方式:

同步锁。

join,wait等阻塞方法

信号量

特殊变量和volatile关键字。

 

Thread.interrupt方法只让当前线程执行中断,但是中断只是一个状态的改变,并没有实际的操作,而实际的操作是在其他线程中判断了中断状态之后才进行的根据业务的实际操作。

 

synchronized:

3种形式:

修饰实例方法,对调用对象加锁。

修饰静态方法,对类对象加锁,意思是对该类对象的所有实例都可以使用。原理是对方法加标识,ACC_SUNCHRONIZED

方法块:双保险的单例案例,注意对象应该用volatile修饰,原理是在开始和结束时,会有标识,monitorEntry和moniterExit的来标识方法块的进入和退出。

 

偏向锁,轻量级锁,重量级锁:

偏向锁是在无竞争条件下的锁,获取锁对象的线程总是哪一个,那么给锁加个标识,在线程每次进入的时候都不会重新获取,而是验证一下即可。如果再次进入不能获取到偏向锁的时候,那么就会自动将偏向锁升级为轻量级锁。轻量级锁首先会经历无所状态,然后利用cas形式进行自旋,当自旋次数超过限定值,会自动升级为重量级锁。

 

自旋的概念:

当线程失去cpu使用权,或者失去了锁,那么该线程会进入一个阻塞队列中,在该阻塞队列的线程会一直自我询问,我的前一个节点是否是头节点,我能否获取获取到锁,拿到cpu控制权,如果可以出队列,获取锁,如果不可以出队列,加到队列末尾,继续自旋。

 

锁销除和锁粗话:

在编译阶段,编译器检测到数据不会存在竞争关系,那么会自动把加在竞争数据上的锁给去掉。

 

synchronized和reentrantLock:

根本来说synchronized是关键字,而reentrantlock是一个类,基于此,后者就有类的特性,所以,方法的多样,灵活,全面。比如可以支持超时获取,可以支持中断等,另外他们的实现原理不同,后者是使用系统哭中的unsafe类的方法实现的(cas也是利用此中方法实现)。他们同样都支持重入锁。

 

总结起来,后者比前者多了,都支持可重入,默认是非公平锁,获取锁中断,获取锁超时,可实现公平锁,支持condition类的监控(其实就是在实现接口的是绑定多个条件),性能好。 所以中断等待是接口reentrantLock实现的;该接口可以实现公平锁和非公平锁,syn只能实现非公平;选择通知,在syn中一些阻塞唤醒靠object的基础方法,而lock中可以得到监控器condition,每一个锁可以创建多个condition,而condition也具有灵活性,支持很多方法;在幸能方面,俩者在1.6后持平。

 

syn和volatile的区别:

最主要syn支持原子性,volatile不支持; 前者修饰方法,方法快,后者之修饰变量;前者造成方法阻塞,后者操作内存更新数据;前者可以被重排序优化,后者不会发生重排序优化。

 

重排序: 为了让减少阻塞,所以在编译阶段和cpu执行阶段都会执行重排序。

 

volatile保证了可见性,不保证原子性;而可见性是java内存模型保障的。用volatile修饰之后,该变量会在内存中保留一份,而每个线程都会保存一份他的副本,都某一个副本的值被改变之后,该值会刷到主内存中,并声明其他副本的值是无效,所以其他副本想要使用值得时候,只能重新从主内存中获取。

 

重入锁和读写锁得区别(ReentrantLock 和 ReentrantReadWriteLock):

重入锁是独占锁,同时只允许一个线程操作;

读写锁允许多个线程同时操作,但不允许读写,写写同时操作。

他们都是基于AQS实现;

读写锁得同步器时基于AQS实现的,需要在同步状态上维护多个读线程和一个写线程,

读写锁实现原理时利用高低位,在32位得锁标识中,高16位代表读锁,低16位代表写;

读锁获取过程:获取锁,锁状态是否位0,不是0,查看低16是否为0,xx

 

读写锁特点:

写锁可以降级为读锁,读不可以降级为写锁;

都支持获取锁中断;

写锁可以拿到监听器condition;读锁不可以;

默认时非公平,因为非公平效率更高;

在读锁中不能加写锁,会死锁;

写锁中可以加读锁,时所降级;

 

悲观锁:

数据库中,表锁,行锁,读锁,写锁;Java中synchronized和reentrantLock,适用于写多读少得场景;

乐观锁:采用cas算法得设计,比如数据库得乐观锁,Java中原子类,使用读多写少场景;

cas: 需要知道更新内存中那个值,需要知道原来得值, 需要知道改变后的值;

cas 得aba问题: 一个线程将数值从 a改成b,另一个线程由b改成a,从结果上看是没有问题得,但是并不能知道是否做了改变;

cas得循环时间开销大得问题: 自旋式cas代表如果不能成功更新就会一直循环,对cpu得损耗很大。

cas得只能保证一个共享变量得原子操作,当操作一个共享变量得时候有效,当操作多个共享变狼得时候是无效得。所以可以通过另一个原子操作类,atomicReference来操作对象得cas更新;

 

 

AQS(abstractQueuedSynchronizer):

aqs是构建锁和同步器的框架。

aqs原理:

核心思想:

如果请求的资源空闲,则将当前请求的资源线程设置为有效的资源线程,并将资源设置为锁定状态,如果资源处于锁定状态,则需要一套阻塞等待和唤醒所分配的机制,这个机制使用CLH队列实现的,并将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列(虚拟是说不纯在的意思,只有前后节点关联关系的信息而已)

aqs使用一个int成员变量state来标识同步状态,通过内置FIFO 获取线程排队工作,AQS使用CAS对该状态的修改进行跟踪;通过getState, setState 和compareSetState进行操作。

共享方式:

独占 (exclusive) 和共享 (share)

独占锁分为公平和非公平, 公平是按照CLH队列顺序那锁,不公平是无视队列顺序抢锁。

不同的自定义同步器征用共享资源的方式也是不一样的,自定同步器在实现时是需要实现资源state的获取和释放即可,至于具体线程队列的维护,aqs在上层已经帮我们实现了。

底层是模板方法模式,所以重写aqs的方法时,需要将其中的几个方法重写了。

应用:countdownlatch; Semaphore; cycliBarrier; 重入锁和读写锁;

Semaphore 时共享的,同时最多同时又n个线程同时执行,可以分为公平和非公平状态。

countdownlatch 让执行完逻辑的n个线程进行等待,当n个线程执行完成的时候再继续执行。

countdownlatch三种场景:

当一个线程运行时,需要等待n个线程运行完毕;

实现多个线程开始执行任务的最大并行量,发令枪式的场景。

死锁检测:

cyclicBarrier: 循环屏障; 当一组线程都到达屏障的时候,程序才可以继续执行。每个线程到达屏障的时候执行await表示到达屏障,然后线程被阻塞。

场景是在银行计算流水合计。

 

countdownlatch和cycliBarrier的区别:

cylicBarrier可以使用reset功能,多次使用,而countdownlatch只能使用一次。

 

线程池:

优点:更好管理,降低资源消耗,提高相应。

Runnable和Callable接口:

Executors.callable(Runnable task);

Executors.callable(Runnable task, Object result);

 

execute()和submit()区别:

execute()不需要返回结果,submit可以返回结果,返回的是futur接口,并有一个泛型标识返回结果类型,然后利用get方法阻塞获取结果,也可以设置超时时间。

 

线程池设置参数:

核心线程个数:调用prestartAllCoreThreads()方法,提前启动核心线程;

最大线程数;

保活单位:

保活时间:

阻塞队列: ArrayBlockingQueue; LinkedBlockingQueue;SynchronousQueue; PriorityBlcokingQueue;

arr,必须设置队列大小,且需要设置公平,或者是非公平;

饱和策略:

拒绝; 直接抛出异常; 随机丢弃队列中的线程; 用调用者线程来执行任务;

 

线程数选择:

1. io密集型,因为压力在io上,而不在cpu上,所以建议选择数量大的线程;

2.cpu密集,cpu压力大,应该根据实际情况确定,一般是计算机逻辑内核数;

3.内存使用:

4.下游的抗并发能力:比如直接查询数据库,调用下游接口时都需要考量;

 

并发容器:

ConcurrentHashMap;线程安全的HashMap

CopyOnWriteArrayList:线程安全的list, 在读多写少的场景下,远好于Vector

ConcurrentLinkedQueue: 高效并发队列,使用链表实现,可以看作时线程安全的LinkedList,时一个非阻塞队列,利用cas实现的。

BlockingQueue: 这个一个接口,JDK内部通过数组和链表实现了它,标识阻塞队列,非常适合用于数据共性的通道; 当队列是空的时候,取数据的方法阻塞;当队列慢的时候,放数据的方法阻塞;

ConcurrentSkipListMap:跳表的实现,可快速查找。

ConcurrentSkipListMap: 对平衡树的增删会导致对整个平衡树的调整,但是对调表的节点插入修改只需要对全局做修改即可。

实战:

在AQS中,大量使用了LockSupport类,尤其是其中的park和unpark方法,这个方法的作用和wait和notify方法类似,但是locksupport可以不先获取锁,所以当执行unpark方法的时候,是不会报出中断异常的,但会讲锁的状态设设置改变,也就是后来,正式对他做park处理的时候,是不能成功的。

如果利用wait和notify方法,最后注意是否有方法一直在阻塞,是否需要使用ontify唤醒最后阻塞的线程。

lock.newCondition创建出的一个东西其实是一个队列,那么我们对对这个lock去创建n个condition,就是n个队列,那么这样的好处就是我们可以对每个队列中的线程进行控制,比如说await,或者是signal。

 

TransferQueue:容量是0,是因为是生产者阻塞模式,就是说生产者放入道队列中消息之后,就开始阻塞,知道消费者取出消息来,才可以继续执行。 这个和现实中的场景类似的是,递东西,只有有人接才性,如果讲递的过程放到一个容器中,那么transferqueue就是这个容器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值