Java多线程篇--基本概念

在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java多线程中面试的基本概念。这个在面试中一般不会直接问,但是如果对一些知识点敞开描述的时候,可以深入聊下。如果对java多线程感兴趣的同学可以看下公众号里多线程系列的文章,也许会对你有些帮助。

 

说下并发和并行的区别?

并发是指在一段时间内有多个任务需要执行,你可以使用单个cpu串行:在cpu在执行多个任务的时候,每个时间点都只能执行一个任务,但是每次执行的时间片很短(每个任务切换很快),导致用户感觉有多个任务在执行。也可以使用多个cpu并行:在不同的cpu上同时执行多个任务。

并行是指多个任务在同一个时间点由不同的cpu一起执行。并行是一种cpu真正实现并发的方式。它们是两个不同的层面的概念。

 

说下线程和进程的区别?了解过守护线程么?

进程是资源分配的最小单位,线程是CPU调度的最小单位。而守护线程是一种幕后的线程,其优先级别较低,一般完成的是系统性的服务,当系统中最后一个非守护线程结束的时候,守护线程也会和jvm一起结束。常见的守护线程是垃圾回收线程。

 

多线程的三个基本特性是什么?Java 是通过什么方式解决的?

并发的三大特性是:有序性、可见性、原子性

有序性:指在java并发中,如果执行编码的两条指令按照代码的先后顺序执行。编译器,处理器为了提升执行效率会对代码编译过后的执行指令进行重新排序(指令重排序)。在as-if-serial中规定了执行重排序一定要满足执行的结果不受指令排序的影响,编译器和处理器都会遵守这个规则,但是as-if-serial是针对单线程的,它只定义了单线程下指令重排序不能影响执行结果,对于多线程的情况下,有可能会导致执行结果不可预料。为了解决有序性的问题,java提供了锁机制保障代码执行的先后顺序

 

可见性:指在多线程的环境下,如果一个线程修改了堆里面的变量,另一个线程是否立即感知到在java语言中,每个线程都保留了一份用到的变量在本地缓存,在主存中也有一份,其示意图如下,如何保证线程A修改之后,线程B也能感知,java提供了volatile,锁等机制来解决

                            

原子性:在执行一条指令的时候,对其他线程而言,要么执行成功,要么执行失败,没有中间状态。在保证原子性上,java提供了锁机制,atomic变量来保证。

 

线程的状态有哪些?它们是怎么转换的?Jvm使用什么指令查看线程状态?

一个线程从创建到结束要经过6个生命周期,在这六个生命周期中,分别有一些特别明显的标志来标识不同线程的状态,这6个生命周期分别是:

新建(NEW):我们在使用new的时候,创建了一个线程,这个创建出来的线程就是新建状态。这个状态类似于虚拟机给其他类创建一个对象一样,会给其分配一些内存空间。、

可运行(RUNNABLE):当我们对创建的线程,调用start函数的时候,这个时候该线程就处于就绪(READY),Java虚拟机会为该线程创建程序创建方法调用栈和程序计数器,等待调度运行等,当绪态的线程拿到了cpu之后,开始执行run函数里面的具体内容时,变成了运行态(RUN)。将这两个状态称为可运行状态。

等待(WAIT):等待状态的线程需要被显式唤醒,不然会一直等待。该状态的线程不能获取到CPU。

阻塞(BLOCKED):多个线程在进入临界区的时候排在外面等待进入临界区的线程,如果没有获取到锁会一直等待下去,所以在处理临界区代码的时候要尽量避免死锁。

超时等待(TIMED_WAITING):为了防止线程一直等待下去,设置了一个等待的时间,在等待时间未到的时候不能获取到CPU。

结束(TERMINATED):就是这个线程执行完了任务,或者遇到了没有捕获的异常,又或者主动关闭线程,那么这个线程的资源被收回,该线程也不会在占用cpu。

具体的状态转换图如下:

            

 

在jvm中,我们一般使用jstack -f pid >pid

 

简述一下JMM,as-if-serial语义,happens-before模型?

Java程序员希望java内存模型简单易懂,有很强的规则,而编译器,处理器则希望java内存模型没有那么多的规则,它们可以尽可能的优化代码的执行顺序。而JMM的设计就是为了平衡两者之差,在JMM中规定了只要不改变程序的执行结果(单线程和正确同步的多线程),编译器和处理器怎么优化都行。对程序员来说,JMM提供了happens-before模型来帮助理解java的内存可见性。Happens-before模型描述了两个操作的执行顺序,happens-before关系定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)

根据这两条定义,其规定了几个基本的执行规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

相比于happens-before模型,as-if-serial语义则是描述的单线程的内存可见性问题。该语义规定了不管怎么重排序(编译器和处理器为了提高并行度),在单线程的情况下,程序的执行结果不能被改变。为了实现as-if-serial语义,编译器和处理器必须要满足数据依赖性原则。数据依赖性描述的是如果一个语句的执行结果依赖于上面某条语句的执行结果,那么这两个语句就不能被重排序

 

了解过死锁么?解决死锁的方法有哪些?

发生死锁有以下4个前提条件:

互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放

请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。

不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用

循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

当以上四个条件满足的时候,就会发生死锁,死锁的解决方案一般是预防,检测,避免。常见的有超时机制(破坏第四个条件),innodb中会使用死锁检测来避免死锁问题。

 

CAS了解过么?

CAS本身compareAndSwap的缩写,其实现原理是通过比较当前值是否是预期值,如果是预期值的话,原子性的更新值。在这个过程中,有几个关键的参数,当前值(Current),预期值(Expect),更新后的值(New)。那么其具体的实现原理如下:

                                

Cas的一个重大优势是不加锁,增大并发。但是其也存在弊端:

  1. ABA问题:当一个线程读出的值是C,在更新E=C的过程中,如果另一个线程将C 的值改变,那么这样更新肯定会失败,但是如果另一个线程将C的值改变后又将C的值变回来,那么这个时候当前线程是无法感知到原来的C值已经发生了变化,换句话说就是这个线程是无法感知到另一个线程在那段时间的操作,所以最后校验值的时候发现是一样的,所以也会更新成功。这就是经典的ABA问题,就是一个线程将C值A改成B之后,又将该值该回了A,更新的那个线程如果只是判断A值是否存在的话,那样有可能就会有问题。那么ABA在实际中会带来哪些问题呢?举个例子,我们在CAS时,使用的是一个数组的长度进行判断,如果我们认为数组的长度没有发生变化的话,我们就可以继续添加值,但是在这里的话,有可能数组的内容发生了改变,这样就导致了可能发生错误了。当然了,ABA问题也不是不可以避免的,在实际中,往往通过加一个version号来表示当前的版本的版本,每次在更新的时候,版本就依次往上加1,那样ABA就会变成1A-》2B-》3A,这样的一个过程就很容器判断是否是原来的A了。
  2. 线程空转:这个从上面的流程图中很容易就理解了,通过自旋不断判断的话,当然会导致cpu上升,并且这种空转在大多数的时候是无效的。

3)只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子 性,这个时候就可以用锁

 

缓存一致性协议了解么?

由于cpu的执行速度远远大于IO的速度,为了减少和IO的交互,增高效率,cpu内部会有高速缓存(cache)。当程序运行的时候,高速缓存中会从主存中保存一份副本数据。每次读写都在高速缓存中操作,这个在单线程下是没有问题的,但是多线程下会导致数据不一致的情况(其他线程中的数据没有同步该线程修改的数据)。为了解决这个问题(其实就是上图中可见性描述),通常有两种方案进行解决:锁和缓存一致性协议。由于在总线上加锁的机制导致效率低下,所以缓存一致性协议就变得关键了。具体原理是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

                        

本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

 

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                               

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值