多线程面试

多线程锁策略:

不仅仅局限于JAVA,各种编程语言都有可能涉猎到。

乐观锁和悲观锁

乐观锁:认为出现竞争的概率比较低(线程数目比较少,偶尔竞争一下)认为冲突概率比较低,做的工作少,成本低。

悲观锁:出现锁的竞争概率比较大,可能会频繁竞争。做的工作多,成本高。

操作系统提供的锁接口(Mutex)(互斥量)典型的悲观锁,(操作系统认为 竞争会比较大,出现锁竞争,存在竞争失败,什么时候唤醒,取决于调度器)

应用程序通过其他方式实现锁(CAS)相当于“用户态锁”,不太设计到内核和操作系统之间切换,更加高效。

JAVA中synchronized会根据前锁的冲突情况自动切换(也叫自适应的锁)

读写锁:

把读操作和写操作分开。普通的锁提供:读操作,写操作。读写锁提供:读加锁,写枷锁,解锁(进一步降低所冲突)写枷锁和写加锁之间互斥,读加锁和写加锁之间互斥,读和读之间不需要互斥,(适用于"少写多读")

synchronize不是读写锁

重量级锁,轻量级锁

重量级锁,工作量大,消耗资源多,锁更慢

轻量级锁,工作量少,消耗资源少,锁更快

类似乐观锁和悲观锁

mutex 重量级锁 遇到冲突就会产生内核态和用户态之间切换,以及线程的阻塞和调度,开销不可预期

CAS 轻量级锁 冲突之后不会涉及内核态和用户态的切换 会尝试重新获取锁,直到获取成功,线程没有放弃CPU,不涉及线程调度(自旋锁)

公平锁 非公平锁

符合先来后到规则(公平锁)

有插队情况(非公平锁)所有线程都是均等的机会。要实现公平额外处理(例如记录线程的竞争时间)

synchronize是非公平锁

自旋锁也是非公平锁

可重入锁,不可重入锁

一个线程连续对同一把锁加锁两次,是否出现问题

synchronize 可重入锁 针对场景进行优化 内部持有当前哪个线程获取到所,同时要维护一个计数器

如果同一个线程尝试再次获取锁,不会阻塞等待,计数器自增,释放锁,计数器自减

死锁:

产生死锁,线程就挂掉了,无法继续往下工作(严重bug)

典型场景:

一个线程一把锁, 可重入锁

两个线程两个锁‘ N个线程M把锁。

哲学家就餐问题:

五个哲学家,五支筷子 都拿起左手筷子 发现右手没有筷子 进入死锁状态

多个线程中涉及多把锁比较容易死锁

四大必要条件:

  • 互斥:进程对分配到的资源排它性使用。独占资源,是由资源本身的属性决定的。
  • 请求和保持:保持已有资源,同时请求新的资源,在请求过程中以及因为没有得到新资源而阻塞,已有资源仍然保持;
  • 不可剥夺:进程已有的资源在使用完之前不能被剥夺,只能自己释放;
  • 环路等待:必然存在一个进程资源环形请求链。

死锁预防:打破之前四个条件

  • 打破互斥在实际中应用不大;
  • 打破请求与保持,可以实行资源预先分配策略,即进程在运行前一次性申请所需要的全部资源,如果不能满足,则暂不运行。实际应用中,进程在执行时是动态的,不可预测的,并且资源利用率低,降低了进程并发性。
  • 打破不可剥夺,当请求新资源不能满足,需要释放已有资源,系统性能受到很大降低
  • 打破循环等待:实行资源有序分配策略。可以将资源事先分类编号,按号分配,使进程在申请、占用资源是不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。但是也有问题,合理编号困难,增大系统开销,另外也增加了进程对资源的占有时间。

死锁的危害:产生bug 线程无法继续工作

死锁的避免

典型案例(银行家算法(不用)实现复杂会引入新的bug、)

1.不要尝试在加锁代码中尝试获取其他锁(同一时刻只获取一把锁 可重入锁 不会死锁不会构成环路等待)(synchronize散落在代码各个地方不易发现)

2.约定一定的顺序进行加锁(按照顺序依次获取锁)

CAS机制

compare and swap 是一个原子操作 cpu级别 支持(SWAP/EXCHANGE)指令 一条指令完成交换

boolean CAS(address ,expectedValue ,newValue){
	if(*address==expectedValue){
	*adress=newValue
	return true;
	}
	return false;
}

基于CAS可以实现自旋锁/轻量级锁

class  clock{
      Thread owner=null;
      void lock(){
      	while(!CAS(&owner,null,Thread.currentThread())){
      	
      	}
      }
      }
}

先看owner中的记录是null(null该所没有被获取)没有线程持有然后就给当前线程设置

设置完成返回true循环结束

设置失败(owner中的值不为null)说明所已经被其他线程获取,继续循环再来尝试。

基于CAS实现原子类

对int long 进行操作可能不是原子类尤其是++ – 原子类保证++ --也是原子类

实际开发:多线程计数 服务器收到多少请求

伪代码 //自增1

int  getADD(t){
	do{
	oldValue=this.readOldValue  //从内存中对旧的值(不是寄存器 缓存)
	}while(!CAS(add,oldValue,Oldvalue++))
	return oldValue
}
CAS的ABA问题

类似于买了手机不知道是新手机还是翻新机

oldValue=load()  之后可能会存在其他操作将旧的值改了,之后又改回去了  cas无法发现

cas(address,oldValue,newValue)   
//转账情况   三个线程两个进行转账,一个入账   (小概率出现)

解决方法:

在进行CAS修改的时候 记录数据上次修改的时间,CAS比较的时候不光要比较旧值,还应对照修改的时间,都一致在修改,否则不修改。(通过修改时间便可以知道这个值变了没)也可以记录版本号。

synchronize工作原理

jvm中的源码来看(c++来完成)。

工作过程:“锁膨胀”

1.场景:多个线程进行尝试i++ 但不是同时运行

​ 第一个线程首次进行加锁 不是真加锁,而只是在对象头里面通过特殊标志位,标记一下(某线程取得这个锁)(偏向锁)

​ 当第一个线程一偏向锁的状态进行i++过程中。第二个线程也来进行竞争锁

​ 发生了锁的竞争,第一个线程立即取得索的状态,第二个线程也会加锁,真正的进行锁的竞争(轻量级锁)

	接下来其他线程也来进行i++操作,竞争激烈自旋锁也不容易取得锁,吃的cpu变多了

轻量级锁进一步膨胀,重量级锁mutex 获取到锁的线程继续工作,没有获取到锁的进入内核态阻塞等待

目的是在不同场景下适用不同的锁 提高效率

优化手段

1.消除锁(stringbuilder stringbuffer)

程序员无脑加锁 白白浪费很所资源

JVM或者编译器发现不涉及线程安全 自动把程序员加的锁去掉

2.锁的粗化(锁的“粒度”)

StringBuffer多线程进行append,不能消除锁。把连续的加锁解锁操作合并到一次加解锁(锁的粒度变大)

Callable

和runnable类似runnable(创建线程的时候使用Runnable来描述具体的任务是啥,不带返回值Runnable.run 返回void)

Callable也是interface 带返回值 需要重写的是call方法,返回一个具体的值。明确结果需要搭配futureTask类来使用(获取一个未来的时间的结果)

JUC的其他组件

java.util.concurrent(并发)

Reentrant Lock (可重入锁)

Reentrantlock lock 有两个分开方法 lock()用于加锁 unlock()用于解锁 (必须由程序员来调用Unlock() 忘记就会产生严重bug)

缺点是:容易遗漏unlock

优点:更加灵活:把加锁放在一个方法里面,解锁放到一个方法里面

提供了trylock

synchronize锁已经占用会尝试加锁 就会阻塞等待

trylock 锁已经占用 trylock直接返回 或者等待一定时间返回

标准库中还有其他的锁

读写锁

AbstractQueueSynchronize(AQS)标准库提供抽象类 自己实现一个锁重建锁

Semaphore 信号量, 计算机中处理并发编程中的一个概念

信号量是一个计数器,表示资源的个数(开发过程中资源限定的个数)

P操作:申请可用资源(可用资源数量减一)

V操作:释放一个可用资源(可用资源加一)

+1 -1都是原子的

信号量技术为0 接下来进行p操作就会阻塞,直到其他线程是放资源(执行V操作)才会执行

二元信号量 资源个数只有1 此时就是一个锁

CountDownLatch

类似于计数器效果(跑步比赛,8各选手速度有快有慢,得八个选手都跑完了才比赛结束)

下载一个文件(多线程下载 每个线程下载一份 都下载完才下载完成)

使用场景

分布式系统: 访问淘宝 入口服务器 会访问 用户服务器 商品 服务器 购物车服务器 优惠券服务器

原子类

AtomicInteger:

+±- 这类操作都是原子的

juc线程池 线程安全的集合类

线程池

线程池用来解决频繁创建销毁线程比较低效

需要把线程提前准备好 放到池子中 需要时从池子中取不是像系统申请(纯用户态代码)

线程池是一个过渡方案

更好的优化解决方案 协程

java 里面的线程池ThreadPool=>ExcutoSrevice

创建方式 直接使用Threadpool 也可以使用过Executors“工厂类”来完成比较简单

Threadpool 比较简单 种类也比较优先

ThreadPool 比较复杂 种类更多 实际开发推荐使用ThreadPool

在这里插入图片描述

corePoolsize 核心线程数 maximumpoolsize 最大线程数

相当于 正式员工 剩余的是临时工

线程池比较空闲 工作量不饱和 正式员工不辞退

如果正式工比临时工多 认为线程创建销毁时间短了但占用资源多

keepAlivetime 临时工允许摸鱼的时间

unit 摸鱼时间

workqueue 线程池的任务队列 队列长度 优先级等

handler 如果队列满了 线程池如何处理新的事务 拒绝策略 ()

ThreadPoolExecutor.AbortPolicy,直接异常 ThreadPoolExecutor.CallerRunsPolicy, 让调用者来确认方式ThreadPoolExecutor.DiscardOldestPolicy, 丢弃最早的任务ThreadPoolExecutor.DiscardPolicy丢弃最新的任务

threadFactory 线程工厂 创建辅助类

线程安全集合类 Arraylist linkedlist queue hashmap treemap hashset treeset线程不安全 多线程修改一个ArrayList不安全

vector stack’ hashtable 线程安全

1.多线程环境下使用顺序表

自己加锁

collections.synchronizedlist 相当于顺序表上加了一层壳子

copyonwritearraylist 让不同的线程使用不同的变量(没加锁)(写时拷贝,多线程读取这个东西 一份数据 进行了修改立刻拷贝)

2**.多线程环境下使用队列 都是接口**

Blockuingqueue(一头进一头出)

blockdeque(两头进两头出)

Arrayblockingqueue

linkedblockingqueue

priorityblockingqueue

transferqueue 只包含一个元素的阻塞队列

3.多线程下使用哈希表

hashtable 不推荐 单纯使用synchronized 进行加锁

concurrenthashmap 内部针对多线程做了优化

1.并不是针对整个对象加锁 而是分成很多锁 每个链表/红黑树奉陪一把锁 当两个线程恰好修改一个链表设计锁的冲突 1.8之后 这样分 1.7 分段锁 多个链表共用一把锁

2.针对读操作比较激进 直接不加锁 (涉及边界时间 或者用户 没要求允许延迟)要想更加准确使用读写锁

3.内部广泛使用cas操作提高效率 获取元素个数没加锁 直接cas

修改元素 获取链表的下标的时候 用cas

4.针对扩容进行优化

hashtable 扩容 需要拷贝

hashtable如果某个线程触发扩容 这个t就负责整个扩容过程 若果是ConcurrntHashMap 扩容任务分担开 一次扩容一点。

hashmap允许key为null

hashtable和concurrenthashmap不允许key为null

1.8之后 这样分 1.7 分段锁 多个链表共用一把锁

2.针对读操作比较激进 直接不加锁 (涉及边界时间 或者用户 没要求允许延迟)要想更加准确使用读写锁

3.内部广泛使用cas操作提高效率 获取元素个数没加锁 直接cas

修改元素 获取链表的下标的时候 用cas

4.针对扩容进行优化

hashtable 扩容 需要拷贝

hashtable如果某个线程触发扩容 这个t就负责整个扩容过程 若果是ConcurrntHashMap 扩容任务分担开 一次扩容一点。

hashmap允许key为null

hashtable和concurrenthashmap不允许key为null

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
线程是 Python 中重要的概念之一,让程序能够同时执行多个任务,提高了程序的效率。下面是一些关于 Python 多线面试可能会问到的常见问题以及它们的答案: 1. 什么是线程?与进程有什么区别? 线程是程序中执行的最小单位,一个进程可以包含多个线程。线程共享进程的资源,但每个线程都有自己的堆栈和局部变量。与进程相比,线程更轻量级,创建和销毁线程的开销更小,但线程之间的同步和通信更加复杂。 2. 如何在 Python 中创建线程? 在 Python 中,可以使用 `threading` 模块来创建和管理线程。可以通过继承 `threading.Thread` 类或者直接调用 `threading.Thread(target=func)` 来创建线程。 3. 线程的状态有哪些? Python 中的线程有几种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。 4. 如何实现多线程同步? Python 提供了多种同步机制来保证线程安全,例如锁、条件变量、信号量等。其中最常用的是 `Lock` 和 `Rlock` 对象,可以使用 `acquire()` 方法获取锁并使用 `release()` 方法释放锁。 5. Python 中的 GIL 是什么?对多线程有什么影响? GIL(全局解释器锁)是为了保证 Python 中的内存管理机制有效运行而引入的。它限制了同一进程内同一时间只能有一个线程执行 Python 字节码,因此在多线程场景下,由于 GIL 的存在,多线程无法充分利用多核 CPU 的优势。 6. 有没有其他方式可以实现并发执行?比如使用进程池? 除了多线程,Python 还支持多进程编程。可以使用 `multiprocessing` 模块来创建和管理进程,通过 `Pool` 类可以方便地创建进程池,实现并发执行。 以上是一些常见的关于 Python 多线程的面试问题及其答案,希望对你有帮助!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

力争做大牛的小王

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

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

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

打赏作者

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

抵扣说明:

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

余额充值