这个线程面试题通常在第一轮面试或电话面试时被问到,这道多线程问题为了测试面试者是否熟悉 join
方法的概念。答案也非常简单——可以用 Thread 类的 join
方法实现这一效果
Lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞,如何保证完整性请看有关链接解释:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt207
wait
和 sleep
方法有什么区别?wait
方法多用于线程间通信,而 sleep
只是在执行时暂停。
对于sleep()方法方法是属于Thread类中的,而wait()方法属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态
这是一道相对困难的 Java 多线程面试题,考察点很多。它考察了面试者是否真正写过 Java 多线程代码,考察了面试者对并发场景的理解。并且可以根据面试者的代码问很多后续问题,如果用 wait()
和 notify()
方法成功实现了阻塞队列,那么让他用 Java 5 的并发类重新实现一次。
使用多线程可以解决问题,好比工人和母鸡下蛋,工人处于等待状态wait(),母鸡一旦下蛋notify()就会唤醒工人wait(),就会释放线程,加上锁让其流程同步;
死锁是由于多个线程同时互相访问资源造成的,都释放不了,都需要等待对方先释放,解决办法加锁顺序确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。加锁时限的控制另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求
死锁检测(死锁检测是一个更好的死锁预防机制)
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中
原子操作指的是不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程),常见的原子操作
1)除long和double之外的基本类型的赋值操作
2)所有引用reference的赋值操作
3)java.concurrent.Atomic.* 包中所有类的一切操作
count++不是原子操作,是3个原子操作组合
1.读取主存中的count值,赋值给一个局部成员变量tmp
2.tmp+1
3.将tmp赋值给count
可能会出现线程1运行到第2步的时候,tmp值为1;这时CPU调度切换到线程2执行完毕,count值为1;切换到线程1,继续执行第3步,count被赋值为1------------结果就是两个线程执行完毕,count的值只加了1;
还有一点要注意,如果使用AtomicInteger.set(AtomicInteger.get() + 1),会和上述情况一样有并发问题,要使用AtomicInteger.getAndIncrement()才可以避免并发问题
volatile
关键字是什么?你如何使用它?它和 Java 中的同步方法有什么区别?volatile是一个修饰变量的关键字
和synchronized不同是synchronized用于一段代码和方法,volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的,同一时刻只能由一个线程来修改被修饰变量的值,谨慎使用;
多个线程访问同一资源时,如果资源对访问顺序敏感,那就存在竞态条件。比如主线程main需要A文件,但必须先检测是否存在,如果不存在,主线程开始创建,创建时发现其它线程已经创建了A,再执行主线程main就会出现信息错误。解决办法是加锁;如何发现可以通过创建两个线程,同时执行打印1-50数字,如果有不同说明存在竞态条件;
线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用,一个线程转储可能包含一个单独的线程或者多个线程;如何分析线程转储首先从几个案例说起
比如
第一种情况,CPU负载高Java进程CPU占用率居高不下,导致系统吞吐率下降,CPU过高一般原因分为CPU密集型和死循环,死循环一般是逻辑错误导致的,我们在代码中应该慎用自旋。在一些带有状态机逻辑的代码中,加入次数限制,防止程序跑飞,CPU密集型,一般原因是算法效率低导致的。例如,正则表达式写的差,该用Map或者Set结构的却用了List来遍历;
第二种情况,响应慢(低负载),在CPU的占用率很低,只有几个线程在消耗CPU的时间片,然而应用的响应时间却很长,首先要想到的是IO操作出现问题,至于到底是网络IO还是本地IO,我们就可以通过线程转储来定位。定位到位置之后,可以使用缓存或者减少IO操作来提升系统响应速度。
第三种情况,应用/服务宕机,当一个应用活着却不能完成任何响应的时候,表明该服务已经宕机
现在分析:会发现大量的线程在同一个操作中罢工了,都不能够完成自己的操作,导致JVM没有可用线程,大部分原因都是死锁造成的,比如两个线程互相需要对方释放锁时,两边都处于等待获取锁状态,一边也不放手,就导致死锁;幸运的是JVM通常会检测死锁,通过工具分析转储可以快速定位到原因。如果想避免复杂的死锁,我们需要尽可能减小同步块的大小,并且在进行资源的访问时设置合理的超时时间,避免死等现象;
start()
方法会调用 run()
方法,为什么我们调用 start()
方法,而不直接调用 run()
方法?这是一个基本的 Java 多线程面试题。最初,我刚开始多线程编程时对此还有些困惑。如今我一般在 Java 中级面试的电话面试或一轮面试中遇到。
这道问题的答案是这样的。当你调用 start()
方法时,它会新建一个线程然后执行 run()
方法中的代码。如果直接调用 run()
方法,并不会创建新线程,方法中的代码会在当前调用者的线程中执行。
如果是wait阻塞,必须配合synchronized使用,调用之前必须持有锁,wait有了锁会处于等待状态直到notify获取同步锁才重新回到就绪状态
如果是await阻塞,Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法
用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态
CyclicBarriar
和 CountdownLatch
有什么区别?1.CyclicBarrier的某个线程运行到屏障点上之后,该线程立即停止运行,直到所有的线程都到达了这个屏障点,所有线程才依次按顺序被唤醒重新运行;CountDownLatch是某个线程运行到某个点上之后,只是给计数器数值减一,该线程扔继续运行;
2.CountDownLatch是利用AQS维护的同步队列来存放被阻塞的线程,所以是按FIFO的顺序被依次唤醒的
3.CyclicBarrier可重用的,因为内部计数器可重置;CountDownLatch不可重用,计数器值为0该CountDownLatch就不可再用。
不可变类就是创建该类的实例后,该实例的属性是不可改变的,
对编写并发应用时,方便测试使用、线程安全,没有同步问题、
不需要拷贝构造方法、不需要实现Clone方法、
可以缓存类的返回值,允许hashCode使用惰性初始化方式、
不需要防御式复制、适合用作Map的key和Set的元素(因为集合里这些对象的状态不能改变)、
类一旦构造完成就是不变式,不需要再次检查
内存干扰、竞态条件、死锁、活锁、线程饥饿是多线程和并发编程中比较有代表性的问题。这类问题无休无止,而且难于定位和调试。
进程是资源分配的最小单位,线程是程序执行的最小单位
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。老外的原话是这么说的 —-《Unix网络编程》
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。
操作系统的核心,它实际就是一个常驻内存的程序,不断地对线程队列进行扫描,利用特定的算法(时间片轮转法、优先级调度法、多级反馈队列调度法等)找出比当前占有CPU的线程更有CPU使用权的线程,并从之前的线程中收回处理器,再使待运行的线程占用处理器
按照特定的机制为多个线程分配CPU的使用权,线程才能执行
第一种方法:ThreadGroup有一个有用的方法:void uncaughtException(Thread t,Throwable e)该方法可以处理线程组内的任意线程所抛出的未处理异常;
第二种方法:Thread类也提供了两个方法设置异常处理器
static setDefaultUbcaughtExceptionHandler(Thread.UncaughtExceptionHanlder eh):为该线程类的***所有线程实例*设置默认的异常处理器
简单地说,线程组就是由线程组成的管理线程的类,这个类是java.lang.ThreadGroup类;因为线程组并没有提供太多有用的功能,而且他们提供的许多功能还是有缺陷的,比如它可以修改其它的线程。我们最好把线程组看做是一个不成功的试验;
Executor
框架比直接创建线程要好?使用框架创建线程池可以节约资源,可以存储很多线程,从而不用频繁的创建摧毁线程;
Executor
和 Executors、
ExecutorService 的区别?Executor
是ExecutorService的父 接口
Executor 接口定义了 execute()
方法用来接收一个Runnable
接口的对象,而 ExecutorService 接口中的 submit()
方法可以接受Runnable
和Callable
接口的对象,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown()
方法终止线程池。
Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor()
创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)
来创建固定线程数的线程池,newCachedThreadPool()
可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程
windows: 1. 使用Process Explorer,第三方工具定位,使用比较简单,容易上手。
2. 使用window自带的perfmon 性能监控工具进行监控,功能强大,但稍微有点复杂
linux : 1. top命令,找到cpu占用最高的进程
2. 查看该进程的线程, top -p
3. ctrl+H 切换到线程模式,找到占用cpu最高的线程。并把线程号转化为十六进制,printf "%x\n" <线程ID>
4. jstack <进程号>,把线程栈打印出来。找到对应的线程号就可以分析为什么线程会占用那么高的cpu了。