Java面试题 - 多线程(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云原生网关 MSE Higress,422元/月
简介: Java面试题 - 多线程(二)
30 Runnable接⼝和Callable接⼝的区别?
  1. Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;
  2. Callable接⼝中的call()⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
31 volatile关键字的作⽤?
  1. 多线程主要围绕可⻅性和原⼦性两个特性⽽展开,使⽤volatile关键字修饰的变量,保证了其在多线程之间的可⻅性,即每次读取到volatile变量,⼀定是最新的数据。
  2. 代码底层执⾏不像我们看到的⾼级语⾔—-Java程序这么简单,它的执⾏是Java代码–>字节码–>根据字节码执⾏对应的C/C++代码–>C/C++代码被编译成汇编语⾔–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进⾏重排序,多线程下可能会出现⼀些意想不到的问题。使⽤volatile则会对禁⽌语义重排序,当然这也⼀定程度上降低了代码执⾏效率。
32 Java中如何获取到线程dump⽂件?

死循环、死锁、阻塞、⻚⾯打开慢等问题,查看线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

  1. 获取到线程的pid,可以通过使⽤jps命令,在Linux环境下还可以使⽤ps -ef | grep java
  2. 打印线程堆栈,可以通过使⽤jstack pid命令,在Linux环境下还可以使⽤kill -3 pid
  3. 另外提⼀点,Thread类提供了⼀个getStackTrace()⽅法也可以⽤于获取线程堆栈。这是⼀个实例⽅法,因此此⽅法是和具体线程实例绑定的,每次获取到的是具体某个线程当前运⾏的堆栈。
33 线程和进程有什么区别?
  1. 进程是系统进⾏资源分配的基本单位,有独⽴的内存地址空间
  2. 线程是CPU独⽴运⾏和独⽴调度的基本单位,没有单独地址空间,有独⽴的栈,局部变量,寄存器, 程序计数器等。
  3. 创建进程的开销⼤,包括创建虚拟地址空间等需要⼤量系统资源
  4. 创建线程开销⼩,基本上只有⼀个内核对象和⼀个堆栈。
  5. ⼀个进程⽆法直接访问另⼀个进程的资源;同⼀进程内的多个线程共享进程的资源。
  6. 进程切换开销⼤,线程切换开销⼩;进程间通信开销⼤,线程间通信开销⼩。
  7. 线程属于进程,不能独⽴执⾏。每个进程⾄少要有⼀个线程,成为主线程
34 线程实现的⽅式有⼏种(四种)?
  1. 继承Thread类,重写run⽅法
  2. 实现Runnable接⼝,重写run⽅法,实现Runnable接⼝的实现类的实例对象作为Thread构造函数的target
  3. 实现Callable接⼝通过FutureTask包装器来创建Thread线程
  4. 通过线程池创建线程
35 ⾼并发、任务执⾏时间短的业务怎样使⽤线程池?并发不⾼、任务执⾏时间⻓的业务怎样使⽤线程池?并发⾼业务执⾏时间⻓的业务怎样使⽤线程池?
  1. ⾼并发、任务执⾏时间短的业务:线程池线程数可以设置为CPU核数+1,减少线程上下⽂的切换。
  2. 并发不⾼、任务执⾏时间⻓的业务要区分开看:
  • ------ 假如是业务时间⻓集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占⽤CPU,所以不要让所有的CPU闲下来,可以加⼤线程池中的线程数⽬,让CPU处理更多的业务
  • ------ 假如是业务时间⻓集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)⼀样吧,线程池中的线程数设置得少⼀些,减少线程上下⽂的切换
  1. 并发⾼、业务执⾏时间⻓,解决这种类型任务的关键不在于线程池⽽在于整体架构的设计,看看这些业务⾥⾯某些数据是否能做缓存是第⼀步,增加服务器是第⼆步,⾄于线程池的设置,设置参考(2)。最后,业务执⾏时间⻓的问题,
    也可能需要分析⼀下,看看能不能使⽤中间件对任务进⾏拆分和解耦。
36. 如果你提交任务时,线程池队列已满,这时会发⽣什么?

1、如果你使⽤的LinkedBlockingQueue,也就是⽆界队列的话,没关系,继续添加任务到阻塞队列中等待执⾏,因为LinkedBlockingQueue可以近乎认为是⼀个⽆穷⼤的队列,可以⽆限存放任务;

2、如果你使⽤的是有界队列⽐⽅说ArrayBlockingQueue的话,任务⾸先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使⽤拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

37 锁的等级:⽅法锁、对象锁、类锁?

⽅法锁(synchronized修饰⽅法时):

  1. 通过在⽅法声明中加⼊ synchronized关键字来声明 synchronized ⽅法。
  2. synchronized ⽅法控制对类成员变量的访问:
  3. 每个类实例对应⼀把锁,每个 synchronized ⽅法都必须获得调⽤该⽅法的类实例的锁⽅能执⾏,否则所属线程阻塞,⽅法⼀旦执⾏,就独占该锁,直到从该⽅法返回时才将锁释放,此后被阻塞的线程⽅能获得该锁,重新进⼊可执⾏状态。这种机制确保了同⼀时刻对于每⼀个类实例,其所有声明为 synchronized 的成员函数中⾄多只有⼀个处于可执⾏状态,从⽽有效避免了类成员变量的访问冲突。

对象锁(synchronized修饰⽅法或代码块):

  • 当⼀个对象中有synchronized method或synchronized block的时候调⽤此对象的同步⽅法或进⼊其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调⽤者占⽤,则需要等待此锁被释放。(⽅法锁也是对象锁)
  • java的所有对象都含有1个互斥锁,这个锁由JVM⾃动获取和释放。线程进⼊synchronized⽅法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized⽅法正常返回或者抛异常⽽终⽌,JVM会⾃动释放对象锁。这⾥也体现了⽤synchronized来加锁的1个好处,⽅法抛异常的时候,锁仍然可以由JVM来⾃动释放。

类锁(synchronized 修饰静态的⽅法或代码块):

  • 由于⼀个class不论被实例化多少次,其中的静态⽅法和静态变量在内存中都只有⼀份。所以,⼀旦⼀个静态的⽅法被申明为synchronized。此类所有的实例化对象在调⽤此⽅法,共⽤同⼀把锁,我们称之为类锁。
  • 对象锁是⽤来控制实例⽅法之间的同步,类锁是⽤来控制静态⽅法(或静态变量互斥体)之间的同步
38 如果同步块内的线程抛出异常会发⽣什么?

synchronized⽅法正常返回或者抛异常⽽终⽌,JVM会⾃动释放对象锁

39 并发编程(concurrency)并⾏编程(parallellism)有什么区别?
  • 解释⼀:并⾏是指两个或者多个事件在同⼀时刻发⽣;⽽并发是指两个或多个事件在同⼀时间间隔发⽣。
  • 解释⼆:并⾏是在不同实体上的多个事件,并发是在同⼀实体上的多个事件。
  • 解释三:在⼀台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群所以并发编程的⽬标是充分的利⽤处理器的每⼀个核,以达到最⾼的处理性能。
40 如何保证多线程下 i++ 结果正确?
  • volatile只能保证你数据的可⻅性,获取到的是最新的数据,不能保证原⼦性;
  • ⽤AtomicInteger保证原⼦性。
  • synchronized既能保证共享变量可⻅性,也可以保证锁内操作的原⼦性。
41⼀个线程如果出现了运⾏时异常会怎么样?
  • 如果这个异常没有被捕获的话,这个线程就停⽌执⾏了。
  • 另外重要的⼀点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被⽴即释放.
42 如何在两个线程之间共享数据?

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进⾏唤起和等待,⽐⽅说阻塞队列BlockingQueue就是为线程之间共享数据⽽设计的。

43 ⽣产者消费者模型的作⽤是什么?
  1. 通过平衡⽣产者的⽣产能⼒和消费者的消费能⼒来提升整个系统的运⾏效率,这是⽣产者消费者模型最重要的作⽤。
  2. 解耦,这是⽣产者消费者模型附带的作⽤,解耦意味着⽣产者和消费者之间的联系少,联系越少越可以独⾃发展⽽不需要受到相互的制约。
44. 怎么唤醒⼀个阻塞的线程?

如果线程是因为调⽤了wait()、sleep()或者join()⽅法⽽导致的阻塞;

  1. suspend与resume:Java废弃 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都⽆法访问被它占⽤的锁。直到对应的线程执⾏ resume() ⽅法后,被挂起的线程才能继续,从⽽其它被阻塞在这个锁的线程才可以继续执⾏。但是,如果 resume() 操作出现在 suspend() 之前执⾏,那么线程将⼀直处于挂起状态,同时⼀直占⽤锁,这就产⽣了死锁。⽽且,对于被挂起的线程,它的线程状态居然还是 Runnable。
  2. wait与notify:wait与notify必须配合synchronized使⽤,因为调⽤之前必须持有锁,wait会⽴即释放锁,notify则是同步块执⾏完了才释放
  3. await与singal:Condition类提供,⽽Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使⽤Lock锁后⽆法使⽤wait⽅法
  4. park与unpark:LockSupport是⼀个⾮常⽅便实⽤的线程阻塞⼯具,它可以在线程任意位置让线程阻塞。和Thread.suspenf()相⽐,它弥补了由于resume()在前发⽣,导致线程⽆法继续执⾏的情况。和Object.wait()相⽐,它不需要先获得某个对象的锁,也不会抛出IException异常。可以唤醒指定线程。

如果线程遇到了IO阻塞,⽆能为⼒,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

45 Java中⽤到的线程调度算法是什么

抢占式。⼀个线程⽤完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出⼀个总的优先级并分配下⼀个时间⽚给某个线程执⾏。

46 单例模式的线程安全性?

⽼⽣常谈的问题了,⾸先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建⼀次出来。单例模式有很多种的写法,我总结⼀下:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:⾮线程安全

(3)双检锁单例模式的写法:线程安全

47 线程类的构造⽅法、静态块是被哪个线程调⽤的?

线程类的构造⽅法、静态块是被new这个线程类所在的线程所调⽤的,⽽run⽅法⾥⾯的代码才是被线程⾃身所调⽤的。

48 同步⽅法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然也可以让它锁住整个对象)。同步⽅法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停⽌执⾏并需要等待获得这个对象上的锁。

synchronized(this)以及⾮static的synchronized⽅法(⾄于static synchronized⽅法请往下看),只能防⽌多个线程同时执⾏同⼀个对象的同步代码段。

如果要锁住多个对象⽅法,可以锁住⼀个固定的对象,或者锁住这个类的Class对象。

synchronized锁住的是括号⾥的对象,⽽不是代码。对于⾮static的synchronized⽅法,锁的就是对象本身也就是this。

49 如何检测死锁?怎么预防死锁?

概念:是指两个或两个以上的进程在执⾏过程中,因争夺资源⽽造成的⼀种互相等待的现象,若⽆外⼒作⽤,它们都将⽆法推进下去。此时称系统处于死锁;

死锁的四个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进⾏访问,若其他进程访问该资源,只能等待,直⾄占有该资源的进程使⽤完成后释放该资源
  • 请求和保持条件:进程获得⼀定的资源之后,⼜对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但⼜对⾃⼰获得的资源保持不放
  • 不可剥夺条件:是指进程已获得的资源,在未完成使⽤之前,不可被剥夺,只能在使⽤完后⾃⼰释放
  • 环路等待条件:是指进程发⽣死锁后,若⼲进程之间形成⼀种头尾相接的循环等待资源关系

死锁产⽣的原因:

  • 因竞争资源发⽣死锁 现象:系统中供多个进程共享的资源的数⽬不⾜以满⾜全部进程的需要时,就会引起对诸资源的竞争⽽发⽣死锁现象
  • 进程推进顺序不当发⽣死锁

检查死锁:

  • 有两个容器,⼀个⽤于保存线程正在请求的锁,⼀个⽤于保存线程已经持有的锁。每次加锁之前都会做如下检测:
  • 检测当前正在请求的锁是否已经被其它线程持有,如果有,则把那些线程找出来
  • 遍历第⼀步中返回的线程,检查⾃⼰持有的锁是否正被其中任何⼀个线程请求,如果第⼆步返回真,表示出现了死锁

死锁的解除与预防:

  • 控制不要让四个必要条件成⽴。
50. HashMap在多线程环境下使⽤需要注意什么?

要注意死循环的问题,HashMap的put操作引发扩容,这个动作在多线程并发下会发⽣线程死循环的问题。

1、HashMap不是线程安全的;Hashtable线程安全,但效率低,因为是Hashtable是使⽤synchronized的,所有线程竞争同⼀把锁;⽽ConcurrentHashMap不仅线程安全⽽且效率⾼,因为它包含⼀个segment数组,将数据分段存储,给每⼀段数据配⼀把锁,也就是所谓的锁分段技术。

2、HashMap为何线程不安全:

  • put时key相同导致其中⼀个线程的value被覆盖;
  • 多个线程同时扩容,造成数据丢失;
  • 多线程扩容时导致Node链表形成环形结构造成.next()死循环,导致CPU利⽤率接近100%;

3、ConcurrentHashMap最⾼效;

51 什么是守护线程?有什么⽤?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作⽤——⽽其他的线程只有⼀种,那就是⽤户线程。所以java⾥线程分2种,

1、守护线程,⽐如垃圾回收线程,就是最典型的守护线程。

2、⽤户线程,就是应⽤程序⾥的⾃定义线程。

52 如何实现线程串⾏执⾏?

a. 为了控制线程执⾏的顺序,如ThreadA->ThreadB->ThreadC->ThreadA循环执⾏三个线程,我们需要确定唤醒、等待的顺序。这时我们可以同时使⽤ Obj.wait()、Obj.notify()与synchronized(Obj)来实现这个⽬标。

线程中持有上⼀个线程类的对象锁以及⾃⼰的锁,由于这种依赖关系,该线程执⾏需要等待上个对象释放锁,从⽽

保证类线程执⾏的顺序。

b. 通常情况下,wait是线程在获取对象锁后,主动释放对象锁,同时本线程休眠,直到有其它线程调⽤对象的notify()唤醒该线程,才能继续获取对象锁,并继续执⾏。⽽notify()则是对等待对象锁的线程的唤醒操作。但值得注意的是notify()调⽤后,并不是⻢上就释放对象锁,⽽是在相应的synchronized(){}语句块执⾏结束。释放对象锁后,JVM会在执⾏wait()等待对象锁的线程中随机选取⼀线程,赋予其对象锁,唤醒线程,继续执⾏。

53. 可以运⾏时kill掉⼀个线程吗?

a. 不可以,线程有5种状态,新建(new)、可运⾏(runnable)、运⾏中(running)、阻塞(block)、死亡(dead)。

b. 只有当线程run⽅法或者主线程main⽅法结束,⼜或者抛出异常时,线程才会结束⽣命周期。

54. 关于synchronized
  1. 在某个对象的所有synchronized⽅法中,在某个时刻只能有⼀个唯⼀的⼀个线程去访问这些synchronized⽅法
  2. 如果⼀个⽅法是synchronized⽅法,那么该synchronized关键字表示给当前对象上锁(即this)相当于synchronized(this){}
  3. 如果⼀个synchronized⽅法是static的,那么该synchronized表示给当前对象所对应的class对象上锁(每个类不管⽣成多少对象,其对应的class对象只有⼀个)
55. 分步式锁,程序数据库中死锁机制及解决⽅案

基本原理:⽤⼀个状态值表示锁,对锁的占⽤和释放通过状态值来标识。

三种分布式锁:

第一种:Zookeeper:

基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下。⼤致思想即为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的⽬录下,⽣成⼀个唯⼀的瞬时有序节点。判断是否获取锁的⽅式很简单,只需要判断有序节点中序号最⼩的⼀个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁⽆法释放,⽽产⽣的死锁问题

【优点】锁安全性⾼,zk可持久化,且能实时监听获取锁的客户端状态。⼀旦客户端宕机,则瞬时节点随之消失,zk因⽽能第⼀时间释放锁。这也省去了⽤分布式缓存实现锁的过程中需要加⼊超时时间判断的这⼀逻辑。

【缺点】性能开销⽐较⾼。因为其需要动态产⽣、销毁瞬时节点来实现锁功能。所以不太适合直接提供给⾼并发的场景使⽤。

【实现】可以直接采⽤zookeeper第三⽅库curator即可⽅便地实现分布式锁。

【适⽤场景】对可靠性要求⾮常⾼,且并发程度不⾼的场景下使⽤。如核⼼数据的定时全量/增量同步等。)

第二种memcached:

memcached带有add函数,利⽤add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。⽽add的话则相反,add会添加第⼀个到达的值,并返回true,后续的添加则都会返回false。利⽤该点即可很轻松地实现分布式锁。

【优点】并发⾼效

【缺点】 memcached采⽤列⼊LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。memcached⽆法持久化,⼀旦重启,将导致信息丢失。

【使⽤场景】⾼并发场景。需要 1)加上超时时间避免死锁; 2)提供⾜够⽀撑锁服务的内存空间; 3)稳定的集群化管理。

第三种Redis:

redis分布式锁即可以结合zk分布式锁锁⾼度安全和memcached并发场景下效率很好的优点,其实现⽅式和memcached类似,采⽤setnx即可实现。需要注意的是,这⾥的redis也需要设置超时时间,以避免死锁。可以利⽤jedis客户端实现。

数据库死锁机制和解决⽅案:

  1. 死锁:死锁是指两个或者两个以上的事务在执⾏过程中,因争夺锁资源⽽造成的⼀种互相等待的现象。
  2. 处理机制:解决死锁最有⽤最简单的⽅法是不要有等待,将任何等待都转化为回滚,并且事务重新开始。但是有可能影响并发性能。
  • ------超时回滚,innodb_lock_wait_time设置超时时间;
  • ------wait-for-graph⽅法:跟超时回滚⽐起来,这是⼀种更加主动的死锁检测⽅式。InnoDB引擎也采⽤这种⽅式。
56. spring单例为什么没有安全问题(ThreadLocal)

1、ThreadLocal:spring使⽤ThreadLocal解决线程安全问题;ThreadLocal会为每⼀个线程提供⼀个独⽴的变量副本,从⽽隔离了多个线程对数据的访问冲突。因为每⼀个线程都拥有⾃⼰的变量副本,从⽽也就没有必要对该变量进⾏同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。概括起来说,对于多线程资源共享的问题,同步机制采⽤了“以时间换空间”的⽅式,⽽ThreadLocal采⽤了“以空间换时间”的⽅式。前者仅提供⼀份变量,让不同的线程排队访问,⽽后者为每⼀个线程都提供了⼀份变量,因此可以同时访问⽽互不影响。在很多情况下,ThreadLocal⽐直接使⽤synchronized同步机制解决线程安全问题更简单,更⽅便,且结果程序拥有更⾼的并发性。

2、单例:⽆状态的Bean(⽆状态就是⼀次操作,不能保存数据。⽆状态对象(Stateless Bean),就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。)适合⽤不变模式,技术就是单例模式,这样可以共享实例,提⾼性能。

57. 线程池原理

使⽤场景:假设⼀个服务器完成⼀项任务所需时间为:T1-创建线程时间,T2-在线程中执⾏任务的时间,T3-销毁线程时间。如果T1+T3远⼤于T2,则可以使⽤线程池,以提⾼服务器性能;

组成:

  1. 线程池管理器(ThreadPool):⽤于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
  2. ⼯作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执⾏任务;
  3. 任务接⼝(Task):每个任务必须实现的接⼝,以供⼯作线程调度任务的执⾏,它主要规定了任务的⼊⼝,任务执⾏完后的收尾⼯作,任务的执⾏状态等;
  4. 任务队列(taskQueue):⽤于存放没有处理的任务。提供⼀种缓冲机制。

原理:线程池技术正是关注如何缩短或调整T1,T3时间的技术,从⽽提⾼服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者⼀些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

⼯作流程:

1、线程池刚创建时,⾥⾯没有⼀个线程(也可以设置参数prestartAllCoreThreads启动预期数量主线程)。任务队列是作为参数传进来的。不过,就算队列⾥⾯有任务,线程池也不会⻢上执⾏它们。

2、当调⽤ execute() ⽅法添加⼀个任务时,线程池会做如下判断:

  • 如果正在运⾏的线程数量⼩于 corePoolSize,那么⻢上创建线程运⾏这个任务;
  • 如果正在运⾏的线程数量⼤于或等于 corePoolSize,那么将这个任务放⼊队列;
  • 如果这时候队列满了,⽽且正在运⾏的线程数量⼩于 maximumPoolSize,那么还是要创建⾮核⼼线程⽴刻运⾏这个任务;
  1. 如果队列满了,⽽且正在运⾏的线程数量⼤于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

3、当⼀个线程完成任务时,它会从队列中取下⼀个任务来执⾏。

4、当⼀个线程⽆事可做,超过⼀定的时间(keepAliveTime)时,线程池会判断,如果当前运⾏的线程数⼤于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的⼤⼩。

58. java锁多个对象

例如: 在银⾏系统转账时,需要锁定两个账户,这个时候,顺序使⽤两个synchronized可能存在死锁的情况

59. java线程如何启动

1、继承Thread类;

2、实现Runnable接⼝;

3、直接在函数体内:

⽐较:

1、实现Runnable接⼝优势:

  • 1)适合多个相同的程序代码的线程去处理同⼀个资源
  • 2)可以避免java中的单继承的限制
  • 3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独⽴。

2、继承Thread类优势:

  • 1)可以将线程类抽象出来,当需要使⽤抽象⼯⼚模式设计时。
  • 2)多线程同步

3、在函数体使⽤优势

  • 1)⽆需继承thread或者实现Runnable,缩⼩作⽤域。
60. java中加锁的⽅式有哪些,如何实现怎么个写法?

1、java中有两种锁:⼀种是⽅法锁或者对象锁(在⾮静态⽅法或者代码块上加锁),第⼆种是类锁(在静态⽅法或者class上加锁);

2、注意:其他线程可以访问未加锁的⽅法和代码;synchronized同时修饰静态⽅法和实例⽅法,但是运⾏结果是交替进⾏的,这证明了类锁和对象锁是两个不⼀样的锁,控制着不同的区域,它们是互不⼲扰的。

61、如何保证数据不丢失

1、使⽤消息队列,消息持久化;

2、添加标志位:未处理 0,处理中 1,已处理 2。定时处理。

62、ThreadLocal为什么会发⽣内存泄漏?

1、threadlocal原理图:

2、OOM实现:

  • ThreadLocal的实现是这样的:每个Thread 维护⼀个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。 2、也就是说 ThreadLocal 本身并不存储值,它只是作为⼀个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使⽤ ThreadLocal 的弱引⽤作为 Key 的,弱引⽤的对象在 GC 时会被回收。
  • ThreadLocalMap使⽤ThreadLocal的弱引⽤作为key,如果⼀个ThreadLocal没有外部强引⽤来引⽤它,那么系统 GC的时候,这个ThreadLocal势必会被回收,这样⼀来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会⼀直存在⼀条强引⽤链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远⽆法回收,造成内存泄漏。

3、预防办法:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap⾥所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

(1)使⽤static的ThreadLocal,延⻓了ThreadLocal的⽣命周期,可能导致内存泄漏。

(2)分配使⽤了ThreadLocal⼜不再调⽤get(),set(),remove()⽅法,那么就会导致内存泄漏,因为这块内存⼀直存在。

63 jdk8中对ConcurrentHashmap的改进
  1. Java 7为实现并⾏访问,引⼊了Segment这⼀结构,实现了分段锁,理论上最⼤并发度与Segment个数相等。
  2. Java 8为进⼀步提⾼并发性,摒弃了分段锁的⽅案,⽽是直接使⽤⼀个⼤的数组。同时为了提⾼哈希碰撞下的寻址性能,Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红⿊树(寻址时间复杂度为O(long(N)))。

其数据结构如下图所示:

64 concurrent包下有哪些类?

ConcurrentHashMap、Future、FutureTask、AtomicInteger…

65 线程a,b,c,d运⾏任务,怎么保证当a,b,c线程执⾏完再执⾏d线程?

1、CountDownLatch类

⼀个同步辅助类,常⽤于某个条件发⽣后才能执⾏后续进程。给定计数初始化CountDownLatch,调⽤countDown()⽅法,在计数到达零之前,await⽅法⼀直受阻塞。

重要⽅法为countdown()与await();

2、join⽅法

将线程B加⼊到线程A的尾部,当A执⾏完后B才执⾏。

3、notify、wait⽅法,Java中的唤醒与等待⽅法,关键为synchronized代码块,参数线程间应相同,也常⽤Object作为参数。

66 ⾼并发系统如何做性能优化?如何防⽌库存超卖?

1、⾼并发系统性能优化:优化程序,优化服务配置,优化系统配置

  • 尽量使⽤缓存,包括⽤户缓存,信息缓存等,多花点内存来做缓存,可以⼤量减少与数据库的交互,提⾼性能。
  • ⽤jprofiler等⼯具找出性能瓶颈,减少额外的开销。
  • 优化数据库查询语句,减少直接使⽤hibernate等⼯具的直接⽣成语句(仅耗时较⻓的查询做优化)。
  • 优化数据库结构,多做索引,提⾼查询效率。
  • 统计的功能尽量做缓存,或按每天⼀统计或定时统计相关报表,避免需要时进⾏统计的功能。
  • 能使⽤静态⻚⾯的地⽅尽量使⽤,减少容器的解析(尽量将动态内容⽣成静态html来显示)。
  • 解决以上问题后,使⽤服务器集群来解决单台的瓶颈问题。

2.防⽌库存超卖:

  • 悲观锁:在更新库存期间加锁,不允许其它线程修改;
  • 数据库锁:select xxx for update;
  • 分布式锁;
  • 乐观锁:使⽤带版本号的更新。每个线程都可以并发修改,但在并发时,只有⼀个线程会修改成功,其它会返回失败。
  • redis watch:监视键值对,作⽤时如果事务提交exec时发现监视的监视对发⽣变化,事务将被取消。
  • 消息队列:通过 FIFO 队列,使修改库存的操作串⾏化。

总结:总的来说,不能把压⼒放在数据库上,所以使⽤ “select xxx for update” 的⽅式在⾼并发的场景下是不可⾏的。FIFO 同步队列的⽅式,可以结合库存限制队列⻓,但是在库存较多的场景下,⼜不太适⽤。所以相对来说,我会倾向于选择:乐观锁 / 缓存锁 / 分布式锁的⽅式。

67 线程池的参数配置,为什么java官⽅提供⼯⼚⽅法给线程池?

⼯⼚⽅法作⽤:ThreadPoolExecutor类就是Executor的实现类,但ThreadPoolExecutor在使⽤上并不是那么⽅便,在实例化时需要传⼊很多歌参数,还要考虑线程的并发数等与线程池运⾏效率有关的参数,所以官⽅建议使⽤Executors⼯程类来创建线程池对象。

说说java同步机制,java有哪些锁,每个锁的特性?
说说volatile如何保证可⻅性,从cpu层⾯分析?
目录
相关文章
|
24天前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
63 0
|
1月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
103 16
|
2月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
2月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
3月前
|
缓存 Java API
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
341 0
|
3月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
310 83
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
146 0
|
3月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
282 83

热门文章

最新文章

下一篇
日志分析软件