1、进程与线程的区别:(重点掌握)
答:进程与线程之间的主要区别可以总结如下。
知识点:
线程上下文切换比进程上下文切换快的原因,可以总结如下:
应用场景:
进程:需要安全稳定时用进程,需要速度时用进程,既要速度又要安全。
线程:I/O密集型,多核。
解析:
进程与线程的区别算是一个开场题目,旨在考察大家对进程与线程的理解,因为我们的多线程是指在一个进程中的多个线程。
前面我们说线程之间共享一个进程的资源和地址空间,那么线程可以拥有独属于自己的资源吗?
答:可以的,通过ThreadLocal可以存储线程的特有对象,也就是属于当前线程的资源。
进程之间常见的通信方式:
2、多线程与单线程的关系:
多线程与单线程之间的关系可以概括如下。
解析:
搞清楚多线程和单线程之间的区别,有助于我们理解为什么要使用多线程并发编程。多线程并发利用了CPU轮询时间片的特点,在一个线程进入阻塞状态时,可以快速切换到其余线程执行其余操作,这有利于提高资源的利用率,最大限度的利用系统提供的处理能力,有效减少了用户的等待响应时间。
但是,多线程并发编程也会带来数据的安全问题,线程之间的竞争也会导致线程死锁和锁死等活性故障。线程之间的上下文切换也会带来额外的开销等问题。
3、线程的状态有哪些?
待补充
4、多线程编程中的常用函数的比较和特性总结如下。
sleep 和 wait 的区别:
join 方法:当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行。
yield 方法:该方法使得线程放弃当前分得的 CPU 时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。
解析:
这个题目主要是考察 sleep和wait方法所处的类是哪个,并且考察其在休眠的时候对于互斥锁的处理。
5、线程活性故障有哪些?
答:由于资源的稀缺性或者程序自身的问题导致线程一直处于非Runnable状态,并且其处理的任务一直无法完成的现象被称为是线程活性故障。常见的线程活性故障包括死锁,锁死,活锁与线程饥饿。
解析:
每一个线程都有其特定的任务处理逻辑。由于资源的稀缺性或者资源本身的一些特性,导致多个线程需要共享一些排他性资源,比如说处理器,数据库连接等。当出现资源争用的时候,部分线程会进入等待状态。接下来,让我们依次介绍各种形式的线程活性故障吧。
6、线程死锁:(重点掌握)
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非Runnable)。死锁的产生必须满足如下四个必要条件:
那么,如何避免死锁的发生?
死锁总结:
关于线程活性故障中最常见的死锁,我们必须熟悉其产生的4个必要条件,根据必要条件还应该掌握其避免死锁的方法,锁排序法请大家务必熟练掌握。
线程锁死:
线程锁死是另一种常见的线程活性故障,与线程死锁不可以混为一谈。线程锁死的定义如下:
线程锁死是指等待线程由于唤醒其所需的条件永远无法成立,或者其他线程无法唤醒这个线程而一直处于非运行状态(线程并未终止)导致其任务 一直无法进展。
线程死锁和线程锁死的外部表现是一致的,即故障线程一直处于非运行状态使得其所执行的任务没有进展。但是锁死的产生条件和线程死锁不一样,即使产生死锁的4个必要条件都没有发生,线程锁死仍然可能已经发生。
线程锁死分为了如下两种:
信号丢失锁死是因为没有对应的通知线程来将等待线程唤醒,导致等待线程一直处于等待状态。
典型例子是等待线程在执行Object.wait( )/Condition.await( )前没有对保护条件进行判断,而此时保护条件实际上可能已经成立,此后可能并无其他线程更新相应保护条件涉及的共享变量使其成立并通知等待线程,这就使得等待线程一直处于等待状态,从而使其任务一直无法进展。
嵌套监视器锁死是由于嵌套锁导致等待线程永远无法被唤醒的一种故障。
比如一个线程,只释放了内层锁Y.wait(),但是没有释放外层锁X; 但是通知线程必须先获得外层锁X,才可以通过 Y.notifyAll()来唤醒等待线程,这就导致出现了嵌套等待现象。
活锁:
活锁是一种特殊的线程活性故障。当一个线程一直处于运行状态,但是其所执行的任务却没有任何进展称为活锁。比如,一个线程一直在申请其所需要的资源,但是却无法申请成功。
线程饥饿:
线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法运行的情况。线程调度模式有公平调度和非公平调度两种模式。在线程的非公平调度模式下,就可能出现线程饥饿的情况。
线程活性故障总结:
1、Java内存模型是怎么样保证原子性、可见性、有序性?
多线程环境下的线程安全主要体现在原子性,可见性与有序性方面。
原子性:
原子操作是指一系列的操作,要么全部发生,要么全部不发生,JMM 保证对除 long 和 double 外的基础数据类型的读写操作是原子性的。另外关键字 synchronized 也可以提供原子性保证。synchronized 的原子性是通过 Java 的两个高级的字节码指令 monitorenter 和 monitorexit 来保证的。
可见性
JMM 可见性的保证,一个是通过 synchronized,另外一个就是 volatile。volatile 强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程总是能够看到该变量的最新值。
有序性
有序性是指一个处理器上运行的线程所执行的内存访问操作在另外一个处理器上运行的线程来看是否有序的问题。对有序性的保证,主要通过 volatile 和一系列 happens-before 原则。volatile 的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。
happens-before 原则包括一系列规则,如:
知识点:
在单处理器中,为什么也会出现可见性的问题呢?
单处理器中,由于是多线程并发编程,所以会存在线程的上下文切换,线程会将对变量的更新当作上下文存储起来,导致其余线程无法看到该变量的更新。所以单处理器下的多线程并发编程也会出现可见性问题的。
可见性如何保证?
重排序:
为了提高程序执行的性能,Java编译器在其认为不影响程序正确性的前提下,可能会对源代码顺序进行一定的调整,导致程序运行顺序与源代码顺序不一致。
重排序是对内存读写操作的一种优化,在单线程环境下不会导致程序的正确性问题,但是多线程环境下可能会影响程序的正确性。
重排序举例:
Instance instance = new Instance()都发生了啥?
具体步骤如下所示三步:
第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。(在多线程下的单例模式中,我们必须通过volatile来禁止指令重排序)
2、谈谈你对synchronized关键字的理解。
补充:是一种非公平调度方式,如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。唤醒的资源消耗大。
知识点:
JVM对资源的调度分为公平调度和非公平调度方式。公平调度方式:按照申请的先后顺序授予资源的独占权。
3、谈谈你对volatile关键字的理解。
主要有两个方面:
第一个:操作变量,并不会拷贝副本。即对变量的赋值,会被强制刷新到主存中,对变量的读取会从主内存中重新加载。
第二个:可以阻止指令重排,避免读取到引用到未对象初始化的对象。
4、ReentrantLock和synchronized的区别
ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。
synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁。
1、Java中的线程池有了解吗?
java.util.concurrent.ThreadPoolExecutor类就是一个线程池。利用线程复用的思想,避免线程反复创建,而造成的资源消耗,同时也便于管理。客户端调用ThreadPoolExecutor.submit(Runnable task)提交任务。
线程池的参数:
JDK对各个字段的解释:
线程池的执行流程:核心 -> 阻塞 -> 最大 -> 拒绝
2、线程池有那些?(*)
3、ThreadLocal有了解吗?
使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal内部实现机制:
4、Atmoic有了解吗?
介绍Atomic之前先来看一个问题吧,i++操作是线程安全的吗?
i++操作并不是线程安全的,它是一个复合操作,包含三个步骤:
这是一个复合操作,不能保证原子性,所以这不是线程安全的操作。那么如何实现原子自增等操作呢?
这里就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子类了。AtomicInteger类提供了getAndIncrement和incrementAndGet等原子性的自增自减等操作。Atomic等原子类内部使用了CAS来保证原子性。
5、CountDownLatch和CyclicBarrier有了解吗?(*)
两个关键字经常放在一起比较和考察,下边我们分别介绍。
CountDownLatch是一个倒计时协调器,它可以实现一个或者多个线程等待其余线程完成一组特定的操作之后,继续运行。
CountDownLatch的内部实现如下:
CyclicBarrier是一个栅栏,可以实现多个线程相互等待执行到指定的地点,这时候这些线程会再接着执行,在实际工作中可以用来模拟高并发请求测试。
可以认为是这样的,当我们爬山的时候,到了一个平坦处,前面队伍会稍作休息,等待后边队伍跟上来,当最后一个爬山伙伴也达到该休息地点时,所有人同时开始从该地点出发,继续爬山。
CyclicBarrier的内部实现如下:
什么是happened-before原则?
JVM虚拟机对内部锁有哪些优化?
如何进行无锁化编程?
CAS以及如何解决ABA问题?
AQS(AbstractQueuedSynchronizer)的原理与实现。