(划重点)java面试之多线程。

Java面试线程问题

在任何的java面试中线程和并发都是不可或缺的一部分,下面整理了一些面试中的经典线程题目,让我们由浅入深,由面到点,一起来聊一聊多线程。

一、从面入手

      在这里主要对线程等进行宏观的概括

1.简单聊聊多线程。

   让我们看看维基百科中的定义:线程(英语:thread)是操作系统能夠進行運算调度的最小單位。它被包含在进程之中,是行程中的實際運作單位。一条线程指的是进程中一个单一顺序的控制流,一個进程中可以並行多個线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。此外,从资源分配的角度看,进程是资源所有资源分配的基本单位,线程则是CPU调度的基本单位,即使在单线程进程中也是如此。由此可见

   线程一个进程内部的执行路径,那么多线程就是进行执行的多条路径。多线程是为了同步完成多项 任务,不是为了提高运行效率,而是为了提高 资源使用效率来提高系统的效率。线程是在同一时间需要完成多项 任务的时候实现的。同时线程与cpu之间也是密不可分的(线程是CPU调度的基本单位)。如今我们的计算机大部分都是多核cpu,下面我们说一说多线程在多核cpu中的运行机制。

   理论上线程可以在不同的cpu上运行,但是现实中线程数量远远超过CPU的数量,所以多个线程轮流使用CPU,由于CPU切换线程的速度很快(以纳秒或者毫秒为单位),所以在人可感知的时间范围内(比如1秒钟内)多个线程看起来是同时在运行。在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时, 多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。,这只是一些简单的理解如要深入理解,可查看https://blog.csdn.net/jomie/article/details/24878643。

2.并发与并行 

解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生

解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。 

解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

3.线程安全

对应查看线程是否安全我们可以从很多方式入手。

明确哪些代码是多线程运行代码。
明确共享数据。

明确多线程运行代码中哪些语句是操作共享数据的。

在java中我们也应明确jvm模型的三个特性,原子性,有序性及可见性。

原子性:原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始就不会被其他线程干 扰

有序性:Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线 程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

可见性:可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存(不使用缓存),以及每使用前立即从内存刷新。

这里只做简单概括,深入理解请参考点击打开链接

4.java中的多线程。

  随着jdk的不断更新,java内置的线程类也在不断发展变化。

   抢占式和协作式是两种常见的进程/线程调度方式,操作系统非常适合使用抢占式方式来调度它的进程,它给不同的进程分配时 间片,对于长期无响应的进程,它有能力剥夺它的资源,甚至将其强行停止(如果采用协作式的方式,需要进程自觉、主动地释放资源,也许就不知道需要等到什么时候了)。Java语言一开始就采用协作式的方式,并且在后面发展的过程中,逐步废弃掉了粗暴的stop/resume/suspend这样的方法,它们是违背协作式的不良设计,转而采用wait/notify/sleep这样的两边线程配合行动的方式。

    在比较稳定的JDK 1.0.2版本中,已经可以找到Thread和ThreadUsage这样的类,这也是线程模型中最核心的两个类。整个版本只包含了这样几个包:java.io、 java.util、java.net、java.awt和java.applet,所以说Java从一开始这个非常原始的版本就确立了一个持久的线程模型

值得一提的是,在这个版本中,原子对象AtomicityXXX已经设计好了,这里给出一个例子,说明i++这种操作时非原子的,而使用原子对象可以保证++操作的原子性:

   这时保持线程同步的方法是通过关键字 synchronized锁住代码。非静态方法使用synchronized修饰,相当于synchronized(this)。静态方法使用synchronized修饰,相当于synchronized(Lock.class)。对于synchronicized的详细使用请查看https://blog.csdn.net/luoweifu/article/details/46613015

   在jdk1.2版本中,正式引入了ThreadLocal这个类,他可以说成事线程的一种设计模式。每一个线程都挂载了一个ThreadLocalMap。ThreadLocal这个类的使用很有意思,get方法没有key传入,原因就在于这个key就是当前你使用的这个ThreadLocal它自己。ThreadLocal的对象生命周期可以伴随着整个线程的生命周期。因此,倘若在线程变量里存放持续增长的对象(最常见是一个不受良好管理的map),很容易导致内存泄露。

  jdk1.5是一个转折,java在1.5之后有着巨大的变化,就线程而言,引入了lock锁,并发包,线程池等等等等,对应初学者而言,我对于这方面不是特别熟悉,通过参考其他大牛的博文,简要的说明一下ReentrantLock,第一次接触ReentrantLock是在currenthashmap的源码里,currenthashmap的内置锁sgment继承与ReentrantLock。

   ReentrantLock感觉上是synchronized的增强版,synchronized的特点是使用简单,一切交给JVM去处理,但是功能上是比较薄弱的。在JDK1.5之前,ReentrantLock的性能要好于synchronized,由于对JVM进行了优化,现在的JDK版本中,两者性能是不相上下的。如果是简单的实现,不要刻意去使用ReentrantLock。相比于synchronized,ReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。


二、以点入手

   在这里主要说明一些java面试中线程方面的常见问题。

   1.java中实现多线程的方式。

     这基本上是一道送分题,在不考虑线程池中的callable接口来说,java实现线程的方式有两种,继承Thread类或实现Runable接口。

   2.waitsleep方法的不同

  最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

 3.用Java实现阻塞队列

  这个初步可以用wait()和notify()来实现,如果你掌握1.5之后的并发包,那么可以写出更好的方式。

 4.用Java编程一个会导致死锁的程序,你将怎么解决?

  首先要明确死锁的概念,不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。所以死锁问题我们最后是在代码编写之前就去解决它。

  避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁

 5.Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?

   volatile是java内置的关键字,他可以保证线程的可见性但不能保证原子性。synchronized都可以保证

     6.JavaCycliBarriarCountdownLatch有什么区别?

   CyclicBarrier可以重复使用已经通过的障碍,而CountdownLatch不能重复使用。建议去看一下并发包中的这两个类。

  7.Java中什么是线程调度

   这个问题以我理解就是线程中的优先线程、休眠及线程让步问题。

  8.ExecutorExecutors的区别

       Executor使用线程池来管理线程,可以重复利用已经创建出来的线程而不是每次都必须新创建线程,节省了一部分的开销。线程池也可以很方便的管理线程的大小和当前在执行的线程数量。你可以认为Executor是帮助你管理Thread的一个前人帮你封装好的工具。

  9.线程池的主要参数

   corePoolSize

在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,(除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程)。

默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。

maxPoolSize

当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

keepAliveTime

当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

allowCoreThreadTimeout

是否允许核心线程空闲退出,默认值为false。

queueCapacity

任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。


 10.ThreadLocal

         与正常的同步相比ThreadLocal从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

 

   11.Java中Runnable和Callable有什么不同?

   Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

  

12.Java内存模型是什么

 Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:

  • 线程内的代码能够按先后顺序执行,这被称为程序次序规则。
  • 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
  • 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
  • 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
  • 一个线程的所有操作都会在线程终止之前,线程终止规则。
  • 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
  • 可传递性

13. FutureTask

 在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

14.Semaphore

 Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

15.ReadWriteLock

一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。

16.什么是阻塞式方法

   阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。


  


  




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值