【Java面试】2024年3月最新面试题系列 之 多线程系列

A、锁

1、可重入锁的原理

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

2、锁的分类

1、互斥锁:用来保证数据的完整性,可以保证任一时刻,只有一个线程访问该对象。如果一个线程获取到互斥锁,其他线程想要获取这个锁,就会获取失败,进入睡眠,等待被唤醒。
2、自旋锁:为了实现保护共享资源。跟互斥锁一样,任何时候都会只能有一个线程获取到锁。互斥锁是如果资源被占用,资源申请者只能进入睡眠,等待被唤醒。自旋锁不会引起调用者的睡眠,如果自旋锁被占用,其他线程就会一直循环看自旋锁是否被释放,等待获取。自旋锁的底层是由“do-while”循环实现的。因为一直自旋,占用着CPU资源,所以如果不能短时间的获取锁,就会造成CPU的浪费,所以比较适合能短时间拿到锁的情况。
3、读写锁:是一种特殊的自旋锁。它将对共享资源的访问对象划分为读锁和写锁,读锁只能对资源进行访问,写锁只能对资源进行写操作。这种锁可以提高并发性,在多线程系统中,允许多个线程来访问共享数据,最大的读者线程数为实际的逻辑CPU数。写锁是排他性的,一个读写锁同时只能有一个写者或者多个读者,但不能同时既有读者又有写者。(如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁)
4、悲观锁:每次去拿数据时都会认为别人会修改,所以每次拿数据时都会上锁。数据库中的行锁、表锁、读锁、写锁都是悲观锁。
5、乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候,会判断在自己拿锁的期间别人有没有修改这个数据,如果数据一致,则更新成功,否则重试更新操作,直到更新成功。一般使用版本号机制或者CAS机制。
  
版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

3、synchronized为什么可重入

因为synchronized锁定线程时是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的。每一个synchronized锁关联线程时都会关联这个线程持有者和计数器。当一个线程获取到synchronized时,JVM会记录这个线程,并将计数器设置为1。其他线程想要获取锁,必须等待。当该线程如果想要再次获取锁,将计数器累加,当线程退出锁时,计数器会递减。如果计数器为0,则会释放锁。

4、synchronized与Lock的区别(ReentrantLock)

1LockReentrantLock)使用起来比较灵活,但是必须有释放锁的配合动作。
2LockReentrantLock)必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁。
3LockReentrantLock)只适用于代码块锁,而 synchronized 可用于修饰类、方法、代码块等。

5、乐观锁与悲观锁的区别

悲观锁:每次去拿数据时都会认为别人会修改,所以每次拿数据时都会上锁;
乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候,会判断在自己拿锁的期间别人有没有修改这个数据,如果数据一致,则更新成功,否则重试更新操作,直到更新成功。
乐观锁适用于写不较少的情况,这样可以省去锁的开销,加大了系统的吞吐量。但如果写比较多,就比较适用于悲观锁。

6、死锁有哪些情况?有没有遇到过死锁

死锁一般出现在操作数据库时,当事务并发时,每个事务都持有锁或者已经在等待锁,每个事务都需要再继续持有锁,然后事物之间产生加锁的循环等待,形成死锁。
  避免预防死锁:A、类似的业务逻辑以固定的顺序访问表和行。
              B、大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
              C、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概 率。
              D、降低隔离级别,如果业务允许,将隔离级别调低也是较好的选择 。
              E、为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁(或者说是表锁)
              F、给锁加上等待超时时间  innodb_lock_wait_timeout

7、synchronized用在方法和类上各有什么作用

synchronized用在方法上,表示对当前类的实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块。
synchronized用在类上就相当于时 static synchronized,相当于控制了这个类的所有实例的并发访问,此时类对象的所有同步方法都将被Lock
B、分布式锁

1、说一下分布式锁

分布式锁满足的条件:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
                 2、高可用的获取锁与释放锁; 
                 3、高性能的获取锁与释放锁; 
                 4、具备可重入特性; 
                 5、具备锁失效机制,防止死锁; 
                 6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

分布式锁实现的方式:1、基于数据库实现分布式锁; 
                 2、基于缓存(Redis等)实现分布式锁; 
                 3、基于Zookeeper实现分布式锁;

2、基于数据库实现分布式锁

实现思想:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
  
缺点:1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
     2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
     3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
     4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
        2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
        3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
        4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

3、基于缓存(Redis等)实现分布式锁

使用setNX,当Key不存在时,设置一个字符串类型的k,返回1,当k存在,则直接返回。用expire设置k的超时时间,避免出现死锁。使用delete删除k。
实现思想:1、获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
        2、获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
        3、释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
  
缺点:
在这种场景(主从结构)中存在明显的竞态:
    客户端A从master获取到锁,
    在master将锁同步到slave之前,master宕掉了。
    slave节点被晋级为master节点,
    客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

4、基于Zookeeper实现分布式锁

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,规定同一个目录下只能有一个唯一的文件名。
实现步骤:1、创建一个目录myLock;
        2、线程A想获取锁,就在myLock目录下创建临时的顺序结点;
        3、获取myLock目录下的所有子结点,然后获取比自己小的兄弟结点,如果不存在,说明当前线程的顺序最小,获取锁;
        4、线程B获取所有的结点,判断自己是不是最小的,并监听比自己次小的结点;
        5、线程A处理完,删除自己的结点,线程B监听到变更事件,判断自己是不是最小的结点,如果是,则获取锁。

5、三种实现分布式锁的选择

1、从理解的难易程度角度(从低到高)
	数据库 > 缓存 > Zookeeper
2、从实现的复杂性角度(从低到高)
	Zookeeper >= 缓存 > 数据库
3、从性能角度(从高到低)
	缓存 > Zookeeper >= 数据库
4、从可靠性角度(从高到低)
	Zookeeper > 缓存 > 数据库

5、分布式锁使用场景

分布式锁为了解决分布式场景下全局加锁的问题。在单体项目中可以使用synchronize完成对于不同线程之间的资源争抢问题。但是在分布式场景下,synchronize只能对其中一个项目进行资源控制,进程之间的资源增强仍然无法解距。换言之,可以将分布式锁理解为对于整个分不是系统的synchronize。通常使用独立与线程之外的工具控制资源,如redis及框架redisson。
C、线程池

1、线程池有哪些

1Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
2Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
3Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
4Executors.newScheduledThreadPool(n):创建一个可以在给定延迟后运行命令或者定期地执行的线程池

2、线程池的参数

1int corePoolSize:线程池中的常驻核心线程数
2int maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值一定大于等于1
3long keepAliveTime:多余空闲线程的存活时间
4TimeUnit unit:long keepAliveTime 时间单位
5BlockingQueue workQueue:任务队列,被提交但尚未被执行的任务
6ThreadFactory threadFactory:生成线程的线程工厂,,一般默认即可
7、handler:拒绝策略,当队列满了之后,并且工作线程大于corePoolSize最大线程数,时来拒绝请求执行的runnale策略

3、线程池的拒绝策略

1AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理
2CallerRunsPolicy:CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
3DiscardPolicy:该策略默默地丢弃无法处理的任务,不会抛异常也不会执行,如果允许任务丢失,则这是最好的策略。
4DiscardOldestPolicy:DiscardOldestPolicy策略的作用是,当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。

4、线程创建的方法,线程池创建的方法

1、继承Tread类
	创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
	重写Thread类的run()方法
	创建Thread子类的对象
	通过此对象调用start()方法
		
2、实现Runnable接口
	创建一个实现了Runable接口的类
	实现类去实现Runnable中的抽象方法:run()
	创建实现类的对象
	将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
	通过Thread类的对象调用start()
		
	//比较
	优先使用runnable的方式
	因为它没有继承类,不会存在单继承的局限性,同时tread也是实现自runnable接口,都是需要调用run方法,将线程要执行的逻辑生命在run方法中
			
3、实现callable接口  jdk5.0新增
	创建一个实现callable的实现类
	实现call方法,将此线程需要执行的操作声明在call()中
	创建callable实现类的对象
	将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
	将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
		
	与使用runnable方式相比,callable功能更强大些:
		runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
		方法可以抛出异常
		支持泛型的返回值
		需要借助FutureTask类,比如获取返回结果
		
4、使用线程池创建方法

5、线程的几种状态?顺序是什么?

新建、就绪、运行、阻塞、死亡

6、线程池的好处

提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理

7、线程池的底层工作原理

1、主线程通过execute创建一个线程,发现corePool为空,则直接放入corePool
2、如果corePool满了,其余线程则进入workQueue等待,如果workQueue也满了
3、如果workQueue也满了,则执行扩容机制,扩容至最大线程数maximumPoolSize
4、如果maximumPoolSize也满了,则执行拒绝策略
5、当线程数逐渐少了下来之后,keepAliveTime时间到之后,则会清除多余空闲线程
D、分布式事务

1、分布式事务怎么做、为什么要有分布式事务?

如果一张表数据量比较大,有1000万条数据,就需要分库分表。此时如果有一个操作需要访问不同表中的数据,而且还需要保证数据的一致性,就需要用到分布式事务。
  分布式事务通过三种方式可以实现:1、基于数据库实现分布式锁; 
                             2、基于缓存(Redis等)实现分布式锁; 
                             3、基于Zookeeper实现分布式锁;

2、seata原理

Seata 是一款开源的分布式事务解决方案,是一个业务层的XA(两阶段提交)解决方案。致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了一系列的(ATTCCSAGAXA)事务模式,为用户打造一站式的分布式解决方案。
  
  XA的两阶段提交分为Prepare阶段和Commit阶段:
  1、阶段一为准备(prepare)阶段。即所有的RM锁住需要的资源,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向Transaction Manager报告已准备就绪。
  2、阶段二为提交阶段(commit)。当Transaction Manager确认所有参与者都准备好(ready)后,向所有参与者发送commit命令。
  
包含三种角色:
  1Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  2Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  3Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
  
一个分布式事务在Seata中的执行流程:
  1TMTC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID2XID 在微服务调用链路的上下文中传播。
  3RMTC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC4TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
  5TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
E、异步编排 CompletableFuture

1、异步编排怎么实现

CompletableFuture实现了Future接口,拥有Future的所有功能,可以获取异步执行的结果,和取消正在执行的任务。
  
  CompletableFuture 有四个静态的构造函数:
  1CompletableFuture.runAsync(Runnable runnable);
	2CompletableFuture.runAsync(Runnable runnable, Executor executor);
	3CompletableFuture.supplyAsync(Supplier<U> supplier);
	4CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
		其中runAync接受的是Runable的实例,他没有返回值;supplyAsync又返回值。都可以带有 Executor ,表示让任务在制定的线程池中执行。如果没有指定,则默认在 ForkJoinPool.commonPool() 线程池中执行。

2、异步编排中阻塞主线程完成的方法是什么?

jion()get() 方法
不同处在于:join() 会抛出 unchecked Exception异常
F、线程安全

1、Java中线程安全的类

1Java.util.concurrent.atomic包下的原子类
  AtomicIntegerAtomicBooleanAtomicLong
2、常见的集合类
  ListVector
  MapConcurrentHashMap HashTable
3、可变字符串 StringBuffer
G、Volatile

Volatile关键字

1、读操作:每次都是从主内存中读取数据,然后写入虚拟机栈中
2、写操作:Java内存模型JMM会触发 Store -- write 操作,将修改后的变量值修改到虚拟机栈中,从而保证了线程间的可见性。
3volatilesynchronized的区别:由volatile修饰的类型,不会执行加锁操作,也不会执行线程阻塞,比synchronized更轻量。
4volatile可以保证并发编程的可见性、有序性、部分原子性(基本数据类型 及 内存地址)
  可见性:通过写操作
  有序性:通过禁止指令重排的方式做到有序性的。
  		【
  				Java代码在编译阶段没有做指令重排
          Java代码在运行阶段做了指令重排。是因为CPU的乱序执行导致的(乱序执行会造成线程拿到没有初始化的对象)
          加volatile关键字禁止了指令重排序。
       】
  
5volatile关键字的作用:1、使异步写刷回内存的时间更短;
                      2、为了读写的有序性和可见性。
  
1CAS
```java
比较并交换,预防ABA问题。

即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

2、volatile可以保证线程安全吗?

volatile不能保证线程的安全性,如果想要线程安全,就必须得满足原子性。
H、ThreadLock

1、ThreadLock原理

ThreadLock是为每一个使用共享变量的线程都提供一个独立的副本,使访问这个共享变量的多个线程之间不会彼此影响。与线程的同步机制相比较而言,它采用的是“以空间换时间”的逻辑,所以相对而言更高效。

2、ThreadLocal为什么会内存泄漏

由于TreadLock为每一个线程都提供了一个变量副本,当所有的副本总量超出内存大小时,就会出现内存泄漏问题。
I、线程

1、Runnable与Callable的区别

与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值,返回值还是范型的;
callaalbe接口,方法可以抛出异常,runnable一场只能内部消化,无法往外抛;
callaalbe接口可以借助FutureTask类,获取返回结果。

2、高并发的项目需求,你要怎么进行架构设计?

从以下几个方面考虑:
  1、系统拆分,将一个系统拆分成多个系统,然后每一个系统连接一个数据库;
  2、利用缓存,大部分的高并发场景都是读多写少,将数据同步到缓存,然后用户读取是从缓存读取;
  3、使用消息中间件MQMQ有削峰填谷的作用,让消息在队列中慢慢消费,提高并发性。
  4、读写分离,可以设置主从数据库,主库写入,从库读取,实现读写分离;
  5、利用ElasticSearch,因为es是分布式的,可以随便扩容,分布式天然可以支撑高并发。

3、对于高并发环境,除了搭建集群还可以怎么解决?

1HTML静态化:如果页面不需要对大量内容频繁更新,可以采用HTML静态化的方式;
2、图片服务器分离:对web服务器来讲,图片是最消耗资源的。这是大型网站采用的策略,可以降低提供页面访问请求的服务器系统的压力。
4、缓存:对于不需要实时更新的数据进行缓存;
5、镜像:可以在EduNet教育网站内搭建镜像站点,数据进行定时更新或者实时更新。
6、负载均衡:终极解决方案。

4、sleep和wait的区别

1、相同点:
		一旦执行方法以后,都会使得当前的进程进入阻塞状态
2、不同点:
		1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
		2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
		3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放

5、线程的可见性

1、加volatile关键字
2、加synchronized关键字

6、如何实现子线程的有序性

1、使用volatile关键字
2、使用synchronized关键字
3、使用显示锁Lock来保证有序性

持续更新中。。。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

栈、小生

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

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

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

打赏作者

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

抵扣说明:

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

余额充值