几率大的多线程面试题(含答案)

其他面试题类型汇总:
Java校招极大几率出的面试题(含答案)----汇总
几率大的网络安全面试题(含答案)
几率大的多线程面试题(含答案)
几率大的源码底层原理,杂食面试题(含答案)
几率大的Redis面试题(含答案)
几率大的linux命令面试题(含答案)
几率大的杂乱+操作系统面试题(含答案)
几率大的SSM框架面试题(含答案)
几率大的数据库(MySQL)面试题(含答案)
几率大的JVM面试题(含答案)
几率大的现场手撕算法面试题(含答案)
临时抱佛脚必备系列(含答案)

注:知识还在积累中,不能保证每个回答都满足各种等级的高手们,若发现有问题的话,本人会尽快完善。
。◕‿◕。


本文面试题如下
线程和进程的区别?
Thread和Runnable的关系,区别
synchronized底层如何实现?锁优化,怎么优化?多线程中 synchronized 锁升级的原理是什么?
Synchronized和Lock的区别?
synchronized和ReentrantLock有什么区别呢? 使用场景
线程池的工作原理,Java 并发类库提供的线程池有哪几种? 分别有什么特点?
ReentrantLock 底层实现;
AtomicInteger底层实现原理是什么?
ThreadLocal的底层原理
voliate 的实现原理
happens-before原则有哪些
synchronized 和 volatile 的区别是什么?
线程池的几种方式(类型)与使用场景,线程池都有哪些状态?
CountDownLatch CyclicBarrier之间的区别,使用场景
写一个死锁
死锁是什么,产生死锁的条件。如何避免死锁?怎么定位死锁线程?
Java中synchronized 和 ReentrantLock 有什么不同?
volatile 变量和 atomic 变量有什么不同?
Java多线程中调用wait() 和 sleep()方法有什么不同?
什么是Java Timer类?如何创建一个有特定时间间隔的任务?
什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。
线程的状态有哪些
线程的 run() 和 start() 有什么区别?
Runnable和Callable的区别
什么是CAS
什么是AQS
Semaphore有什么作用
进程间的通信的几种方式
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?
CopyOnWrite是什么?
stop()和 suspend()方法的区别
一个线程运行时发生异常会怎样?
分布式锁的实现,目前比较常用的有以下几种方案:
Hashtable的size()方法为什么要做同步?
ConcurrentHashMap 的size()方法如何实现同步的?
线程池会有哪些漏洞/安全问题
notify和notifyAll方法的区别
如何判断线程是否安全?


线程和进程的区别?

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
关系:一个程序至少一个进程,一个进程至少一个线程。
Thread和Runnable的关系,区别
在这里插入图片描述
1) 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
2) Runnable 是接口。Thread 是类,且实现了Runnable接口。
3) 实现Runnable接口相比继承Thread类有如下好处:避免继承的局限,一个类可以实现多个接口。

synchronized底层如何实现?锁优化,怎么优化?

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

原理:

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

底层实现:

1)同步代码块是使用monitorenter和monitorexit指令实现的, ,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
2)同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。 synchronized方法是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示 Klass 做为锁对象。

Java对象头和monitor是实现synchronized的基础!
synchronized存放的位置:
synchronized用的锁是存在Java对象头里的。
其中, Java对象头包括:
Mark Word(标记字段): 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。它是实现轻量级锁和偏向锁的关键
Klass Pointer(类型指针): 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
monitor: 可以把它理解为一个同步工具, 它通常被描述为一个对象。 是线程私有的数据结构。

锁优化,怎么优化?

jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其他JavaThread访问的对象。( HotSpot JVM/JRockit JVM是支持锁降级的)
偏斜锁
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
自旋锁
自旋锁 for(;;)结合cas确保线程获取取锁
就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
轻量级锁
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的

锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

Synchronized和Lock的区别?

1)实现层面:synchronized(JVM层面)、Lock(JDK层面)
2)响应中断:Lock 可以让等待锁的线程响应中断,而使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
3)立即返回:可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,而synchronized却无法办到;
4)读写锁:Lock可以提高多个线程进行读操作的效率
5)可实现公平锁:Lock可以实现公平锁,而sychronized天生就是非公平锁
6)显式获取和释放:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

synchronized和ReentrantLock有什么区别呢?

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制。编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

1)ReentrantLock 使用起来比较灵活,可以对获取锁的等待时间进行设置,可以获取各种锁的信息,但是必须有释放锁的配合动作;
2)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
3)ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
4)synchronized是关键字,ReentrantLock是类
5)Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的
6)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
7)ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
8)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

场景:
在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发

线程池的工作原理 ,Java 并发类库提供的线程池有哪几种? 分别有什么特点?

1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作
线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这
个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程
来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
在这里插入图片描述

1.先讲下作用

减少资源的开销 可以减少每次创建销毁线程的开销提高响应速度 由于线程已经创建成功提高线程的可管理性

2.讲实现线程池

主要有两部分组成,多个工作线程和一个阻塞队列。其中 工作线程是一组已经处在运行中的线程,它们不断地向阻塞队列中领取任务执行。而 阻塞队列用于存储工作线程来不及处理的任务。

3.细分讲下线程的组成创建一个线程池需要要的一些核心参数。

corePoolSize:基本线程数量 它表示你希望线程池达到的一个值。线程池会尽量把实际线程数量保持在这个值上下。
maximumPoolSize:最大线程数量 这是线程数量的上界。 如果实际线程数量达到这个值: 阻塞队列未满:任务存入阻塞队列等待执行 阻塞队列已满:调用饱和策略 。keepAliveTime:空闲线程的存活时间 当实际线程数量超过corePoolSize时,若线程空闲的时间超过该值,就会被停止。 PS:当任务很多,且任务执行时间很短的情况下,可以将该值调大,提高线程利用率。
timeUnit:keepAliveTime的单位
runnableTaskQueue:任务队列 这是一个存放任务的阻塞队列,可以有如下几种选择:1)ArrayBlockingQueue 它是一个由数组实现的阻塞队列,FIFO。
2) LinkedBlockingQueue 它是一个由链表实现的阻塞队列,FIFO。 吞吐量通常要高于
3)ArrayBlockingQueue。fixedThreadPool使用的阻塞队列就是它。 它是一个无界队列。 4)SynchronousQueue 它是一个没有存储空间的阻塞队列,任务提交给它之后必须要交给一条工作线程处理;如果当前没有空闲的工作线程,则立即创建一条新的工作线程。 cachedThreadPool用的阻塞队列就是它。 它是一个无界队列。
5)PriorityBlockingQueue 它是一个优先权阻塞队列。handler:饱和策略 当实际线程数达到maximumPoolSize,并且阻塞队列已满时,就会调用饱和策略。
AbortPolicy 默认。直接抛异常。 CallerRunsPolicy 只用调用者所在的线程执行任务。 DiscardOldestPolicy 丢弃任务队列中最久的任务。 DiscardPolicy 丢弃当前任务。

4.运行机制

当有请求到来时:
1.若当前实际线程数量 少于 corePoolSize,即使有空闲线程,也会创建一个新的工作线程;2 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
3.若当前实际线程数量 小于 maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务;
4.若当前实际线程数量已经达到maximumPoolSize,并且阻塞队列已满,则使用饱和策略。

Java 并发类库提供的线程池有哪几种? 分别有什么特点?

Executors 目前提供了 5 种不同的
线程池创建配置:
1)newCachedThreadPool():用来处理大量短时间工作任务的线程池。当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;其内部使用 SynchronousQueue 作为工作队列。
2)newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。
3)newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态
4)newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
5)newWorkStealingPool(int parallelism),Java 8 才加入这个创建方法,并行地处理任务,不保证处理顺序。
6)ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

ReentrantLock 底层实现

https://blog.csdn.net/u011202334/article/details/73188404

AQS原理:

AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。

概念+实现:

ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有 锁之 类的概念)的一种。加锁和解锁都需要显式写出,注意一定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。

和synhronized相比:

synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
1)可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
2)可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是
不可中断锁,而ReentrantLock则z,dz提供了中断功能。
3)公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

lock()和unlock()是怎么实现的呢?

由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。而Sync是ReentrantLock的内部类, 其扩展了AbstractQueuedSynchronizer。

lock():
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。( “非公平”即体现在这里)。
设置state失败,走到了else里面。我们往下看acquire。

  1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

  2. 第二步,入队。( 自旋+CAS组合来实现非阻塞的原子操作)

  3. 第三步,挂起。 让已经入队的线程尝试获取锁,若失败则会被挂起

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,
如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

在这里插入图片描述
在这里插入图片描述

AtomicInteger底层实现原理是什么?

AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作,以 volatile 的 value 字段,记录数值,以保证可见性,Unsafe 会利用 value 字段的内存地址偏移,直接完成操作。

voliate 的实现原理

volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的, 加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。。
happen-before原则保证了程序的“有序性,对volatile变量的写操作 happen-before 后续的读操作.
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。
对该变量的写操作之后,编译器会插入一个写屏障。对该变量的读操作之前,编译器会插入一个读屏障。
线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

happens-before原则有哪些:

程序顺序规则:单线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;

锁定规则:一个unlock操作先行发生于对同一个锁的lock操作;

volatile变量规则:对一个Volatile变量的写操作先行发生于对这个变量的读操作;

线程启动规则:Thread对象的start()方法先行发生于此线程的其他动作;

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

synchronized 和 volatile 的区别是什么?

* volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
* volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
* volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

ThreadLocal的底层原理

ThreadLocal,该类提供了线程局部 (thread-local) 变量,ThreadLocal会为每个线程创建变量的副本,线程之间互不影响,这样就不存在线程安全问题。在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals用来存储实际的变量副本容器, 键值为当前ThreadLocal变量,value为变量副本。
1)初始时,在Thread的threadLocals为空,调用ThreadLocal变量调用get()方法或者set()方法,就会对threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals中。
2)然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

概括:

ThreadLocal,很多地方叫做线程本地变量,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。ThreadLocal相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。在进行get之前,必须先set,否则会报空指针异常;否则必须重写initialValue()方法。
场景:数据库连接和session管理
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
使用:
set(obj):向当前线程中存储数据 get():获取当前线程中的数据 remove():删除当前线程中的数据

实现原理:

ThreadLocal并不维护ThreadLocalMap(ThreadLocalMap是Thread的)并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。 每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值( ThreadLocalMap 是个弱引用类,内部 一个Entry由ThreadLocal对象和Object构成,
为什么要用弱引用呢?
如果是直接new一个对象的话,使用完之后设置为null后才能被垃圾收集器清理,如果为弱引用,使用完后垃圾收集器自动清理key,程序员不用再关注指针。)
操作细节
进行set,get等操作都是首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key ,再做相应的处理。
内存泄露问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。
每次操作set、get、remove操作时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。
当然,当 如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法,此时该仍然可能会导致内存泄漏。 这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

使用场景

Web系统Session的存储
当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。

ThreadLocal是如何为每个线程创建变量的副本的

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
总结:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3)在进行get之前,必须先set,否则会报空指针异常;
   如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

线程池的几种方式与使用场景

1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。适用:执行长期的任务,性能好很多
2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
适用:执行很多短期异步的小程序或者负载较轻的服务器

3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。
适用:一个任务一个任务执行的场景

4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)
适用:周期性执行任务的场景

线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。

SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。

STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。

TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。

TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

CountDownLatch CyclicBarrier之间的区别,使用场景

概括性的:

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
细分:
CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
2)CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
3)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

使用场景:

需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,可以使用CountDownLatch
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限,作用是限制某段代码块的并发数。

写一个死锁

觉得这个问题真的很不错,经常说的死锁四个条件,背都能背上,那写一个看看,思想为:定义两个ArrayList,将他们都加上锁A,B,线程1,2,1拿住了锁A ,请求锁B,2拿住了锁B请求锁A,在等待对方释放锁的过程中谁也不让出已获得的锁。
在这里插入图片描述
在这里插入图片描述

死锁是什么,产生死锁的条件。如何避免死锁?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 避免: 1)尽量避免使用多个锁,并且只有需要时才持有锁定位死锁,嵌套的 synchronized 或者 lock 非常容易出问题。
2)如果必须使用多个锁,尽量设计好锁的获取顺序
3)使用带超时的方法,为程序带来更多可控性。Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait。
4)尽量不要几个功能用同一把锁。

怎么定位死锁线程?

最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。
如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。
首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。
其次,调用 jstack 获取线程栈:${JAVA_HOME}\bin\jstack your_pid
然后,分析得到的输出,具体片段如下:
在这里插入图片描述

Java中synchronized 和 ReentrantLock 有什么不同?

通过Lock接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等

volatile 变量和 atomic 变量有什么不同?

这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

Java多线程中调用wait() 和 sleep()方法有什么不同?

Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。需要注意的是,sleep()并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable,并且根据线程调度,它将得到执行。
java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。
java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。
在这里插入图片描述
在这里插入图片描述

什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
在 java.util.concurrent.atomic 包中添加原子变量类之后,这种情况才发生了改变。所有原子变量类都公开比较并设置原语(与比较并交换类似),这些原语都是使用平台上可用的最快本机结构(比较并交换、加载链接/条件存储,最坏的情况下是旋转锁)来实现的。 java.util.concurrent.atomic 包中提供了原子变量的 9 种风格( AtomicInteger; AtomicLong; AtomicReference; AtomicBoolean;原子整型;长型;引用;及原子标记引用和戳记引用类的数组形式,其原子地更新一对值)。

一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误

线程的状态有哪些 :

状态:在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State:
新建(NEW)new,表示线程被创建出来还没真正启动的状态
就绪,运行(RUNNABLE)runnable,表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。
在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。
阻塞(BLOCKED)blocked,这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。
等待(WAITING)waiting,表示正在等待其他线程采取某些操作。
计时等待(TIMED_WAIT)timed_wait:其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,
终止(TERMINATED)terminated,不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

线程的 run() 和 start() 有什么区别?

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
底层start()方法是使用C语言写的,调用JVM_startThread,开启子线程,然后调用里面的run方法
在这里插入图片描述

Runnable和Callable的区别

Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
Call方法可以抛出异常,run方法不可以。
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

什么是CAS

CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种乐观锁。CAS
操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。 当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
缺点:
1)ABA问题:一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了。
解决办法:可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
2)CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
3)CAS造成CPU利用率增加。

什么是AQS

AQS是AbustactQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore
技术是 CAS自旋Volatile变量:它使用了一个Volatile成员变量表示同步状态,通过CAS修改该变量的值,修改成功的线程表示获取到该锁;若没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。
AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)

(final)不可变对象对多线程有什么帮助?为什么喜欢用final变量

前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
Final 变量在并发当中,原理是通过禁止cpu的指令集重排序,保证了对象的内存可见性, final 域能确保初始化过程的安全性, 防止对象引用在对象被完全构造完成前被其他线程拿到并使用( fianl 可以保证正在创建中的对象不能被其他线程访问到)

Semaphore有什么作用

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。

进程间的通信的几种方式

管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(signal):用于通知接收进程某个事件已经发生;
消息队列:
共享内存:可以说这是最有用的进程间通信方式。多个进程可以访问同一块内存空间
信号量:进程之间及同一种进程的不同线程之间得同步和互斥手段
套接字:用于网络中不同机器之间的进程间通信

线程同步的方式

互斥量 Synchronized/Lock:信号量 Semphare:事件(信号),Wait/Notify

为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

在Java中,任意对象都可以当作锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在Object类里。
为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?其目的在于确保等待线程从Wait()返回时能够感知通知线程对共享变量所作出的修改。如果不在同步范围内使用,就会抛出java.lang.IllegalMonitorStateException的异常。

CopyOnWrite是什么?

即写时复制的容器,适用于读操作远多于修改操作的并发场景中,先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。是一种读写分离的思想,读和写不同的容器

stop()和 suspend()方法的区别

反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。
suspend()方法容易发生死锁。调用 suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。 此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。

一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。

分布式锁的实现,目前比较常用的有以下几种方案:

分布式锁应该是怎么样的?
1)可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2)这锁要是一把可重入锁(避免死锁)
3)这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
4)有高可用的获取锁和释放锁功能
5)获取锁和释放锁的性能要好

1)基于数据库实现分布式锁

1)最简单的方式可能就是直接创建一张锁表
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

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

2) 借助数据中自带的锁来实现分布式的锁(select *** for update)

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。 通过connection.commit()操作来释放锁

基于缓存(redis,memcached,tair)实现分布式锁

基于 REDIS 的 SETNX()、EXPIRE() 方法( 设置过期时间)做分布式锁

基于Zookeeper实现分布式锁

每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

Hashtable的size()方法为什么要做同步?

对于类的非同步方法,可以多条线程同时访问。如果A线程执行了put方法,而B线程正在执行size方法,导致数据不一致。

ConcurrentHashMap 的size()方法如何实现同步的?

1) JDK 8 推荐使用mappingCount 方法(另外的叫size方法),因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值
2)在没有并发的情况下,使用一个名为 baseCount 的volatile 变量就足够了,当并发的时候,CAS 修改 baseCount 失败后,就会使用 CounterCell 类了,会创建一个这个对象,通常对象的 volatile value 属性是 1。在计算 size 的时候,会将 baseCount 和 CounterCell 数组中的元素的 value 累加,得到总的大小,但这个数字仍旧可能是不准确的。
3) 还有一个需要注意的地方就是,这个 CounterCell 类使用了 @sun.misc.Contended 注解标识,这个注解是防止伪共享的。是 1.8 新增的。使用时,需要加上 -XX:-RestrictContended 参数。size()/mappingCount()–>sumCount(){使用了baseCount变量和CounterCell数组},在put的时候调用了 addCount()方法

JDK1.7 和 JDK1.8 对 size 的计算是不一样的。 1.7 中是先不加锁计算三次,如果三次结果不一样在加锁JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。

线程池会有哪些漏洞/安全问题安全性问题

notify和notifyAll方法的区别

notify只会唤醒等待该锁的其中一个线程。notifyAll:唤醒等待该锁的所有线程。
1)永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
2)永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。

如何判断线程是否安全?

考虑原子性,可见性,有序性。
1.明确哪些代码是多线程运行的代码,
2.明确共享数据 对共享变量的操作是不是原子操作 , 当某一个线程对共享变量进行修改的时候,对其他线程是可见的
保证原子性的是加锁或者同步, 提供了volatile关键字来保证可见性, synchronized和锁和 volatile都能保证有序性
JVM还通过被称为happens-before原则隐式地保证顺序性。
3.明确多线程运行代码中哪些语句是操作共享数据.

1.该对象是否会被多个线程访问修改 ,是的话是否有加锁操作。
2.注意静态变量. ,由于静态变量是属于该类和该类下所有对象共享,可直接通过类名访问/修改,因此在多线程的环境下.可以断言所有对静态变量的修改都会发生线程安全问题

  • 54
    点赞
  • 337
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
当涉及到 Android 多线程面试题时,以下是一些常见的问题和答案: 1. 什么是线程和进程? - 进程是计算机中运行的程序的实例,它有自己的内存空间和资源。 - 线程是进程中的执行单位,一个进程可以有多个线程,共享进程的资源。 2. 为什么在 Android 中使用多线程? - 在 Android 应用中使用多线程可以提高性能和响应速度。 - 长时间运行的任务可以在后台线程中执行,避免阻塞主线程。 3. Android 中实现多线程的方式有哪些? - 使用 Thread 类创建新线程。 - 使用 AsyncTask 类在后台执行异步任务。 - 使用 HandlerThread 类在后台处理消息。 - 使用线程池来管理和复用线程。 4. 什么是 ANR(Application Not Responding)? - ANR 是指应用程序无法在一定时间内响应用户输入的情况。 - 当主线程被长时间阻塞时,系统会弹出 ANR 对话框,提示用户应用程序停止响应。 5. 如何避免在主线程中执行耗时操作? - 将耗时操作放在后台线程中执行,例如使用异步任务或线程池。 - 使用 Handler 或 HandlerThread 处理异步操作的结果。 6. 什么是线程同步和线程安全? - 线程同步是指在多个线程访问共享资源时,保证数据的一致性和正确性。 - 线程安全是指在多线程环境下,对共享资源的访问不会导致数据错误或不一致。 这些问题只是多线程面试中的一部分,还有其他更深入的问题可以探讨。希望这些答案能帮助到您,祝您面试顺利!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值