Java面试集锦(一)之Java多线程
sleep来自Thread类,wait来自Object类:sleep不会释放对象锁,wait会释放锁;sleep到时自动唤醒,wait需配合notify或notifyAll。wait和notify必须在同步方法或同步块中调用,否则抛IllegalMonitorStateException。ThreadLocal为每个线程提供变量副本实现隔离,其ThreadLocalMap的key是弱引用,线程迟迟不结束又不清理Entry就可能内存泄漏。
- sleep属于Thread类且不释放锁,wait属于Object类且会释放锁。
- wait需配合notify或notifyAll使用,sleep指定时间自动唤醒。
- 在非同步范围调用wait或notify会抛IllegalMonitorStateException。
- ThreadLocal给每个线程变量副本,隔离数据共享实现线程安全。
- ThreadLocalMap中key为null的Entry由getEntry、set、remove清理。
Java多线程
1. 多线程

新建状态: 一个新产生的线程从新状态开始了它的生命周期。它保持这个状态直到程序 start 这个线程。
运行状态:当一个新状态的线程被 start 以后,线程就变成可运行状态,一个线程在此状态下被认为是开始执行其任务
就绪状态:当一个线程等待另外一个线程执行一个任务的时候,该线程就进入就绪状态。当另一个线程给就绪状态的线程发送信号时,该线程才重新切换到运行状态。
休眠状态: 由于一个线程的时间片用完了,该线程从运行状态进入休眠状态。当时间间隔到期或者等待的时间发生了,该状态的线程切换到运行状态。
终止状态: 一个运行状态的线程完成任务或者其他终止条件发生,该线程就切换到终止状态。
2. 僵死进程
计算机的计算模型大部分是基于空间和时间来考虑的。僵死进程唯一占用的空间是pid空间,这个空间如果不能合理的应用就会造成浪费,之所以保留这个空间,是为了让父进程感知子进程已经终止这个行为。时间方面,这个感知过程是一个异步的过程。
3. 创建线程的方式
继承 Thread 类
实现 Runnable 接口
使用 Executor 框架
法一:继承Thread类
1.1定义一个类继承Thread
1.2重写run方法
1.3创建对象
1.4调用start方法开启线程
线程对象调用run()方法和start()方法区别?
调用run方法不开启线程,仅是对象调用方法。
调用start方法开启线程,并让jvm调用run方法在开启的线程中执行。
run()方法用来执行线程体中具体的内容
start()方法用来启动线程对象,使其进入就绪状态
法二:实现Runnable接口
2.1定义一个类实现Runnable接口。
2.2覆盖run()方法。
2.3创建子类对象。
2.4创建Thread类对象,将实现Runnable接口的子类对象作为参数传递给Thread类对象的构造函数。
2.5调用start方法开启线程。
Runnable优点:避免了继承Thread类的单继承局限性,更加符合面向对象
3. 线程安全类
何为线程安全的类
在线程安全性的定义中,最核心的概念就是 正确性。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
线程安全类
在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非线程安全的类。 下面是这些线程安全的同步的类:
vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck:堆栈类,先进后出
hashtable:就比hashmap多了个线程安全
enumeration:枚举,相当于迭代器
除了这些之外,其他的都是非线程安全的类和接口。
线程安全的类其方法是同步的,每次只能一个访问。是重量级对象,效率较低。
4. 如何确保线程安全
在Java中可以有很多方法来保证线程安全,诸如:
通过加锁(Lock/Synchronized)保证对临界资源的同步互斥访问;
使用volatile关键字,轻量级同步机制,但不保证原子性;
使用不变类 和 线程安全类(原子类,并发容器,同步容器等)。
5. 什么是死锁
两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中
6. wait()与 sleep()的区别
sleep()来自 Thread 类,wait()来自 Object 类;
调用 sleep()方法,线程不会释放对象锁。而调用 wait 方法线程会释放对象锁;
sleep()睡眠后不出让系统资源,wait 让其他线程可以占用 CPU;
sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。而 wait()需要配合 notify()
或者 notifyAll()使用。
7. 为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用
wait/notify机制是依赖于Java中Synchronized同步机制的,其目的在于确保等待线程从Wait()返回时能够感知通知线程对共享变量所作出的修改。如果不在同步范围内使用,就会抛出java.lang.IllegalMonitorStateException的异常。
8. 什么是 ThreadLocal?ThreadLocal 和 Synchonized 的区别?
线程局部变量。是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。
synchronized 是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而 ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
9. ThreadLocal中的内存泄露问题(OOM):
如果ThreadLocal被设置为null后,并且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将被回收。这样的话,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value就会形成内存泄露。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数为例。
要注意的是ThreadLocalMap的key是一个弱引用。在这里我们分析一下强引用key和弱引用key的差别
强引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。
弱引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set()、getEntry()和remove()函数时会清除所有key为null的Entry。
ThreadLocalMap仅仅含有这些被动措施来补救内存泄露问题,如果在之后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。在使用线程池的情况下,如果不及时进行清理,内存泄漏问题事小,甚至还会产生程序逻辑上的问题。所以,为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。
10. 多线程常见问题
上下文切换
多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 CPU 通过给每个线程分配一定的时间片,由于时间非常短通常是几十毫秒,所以 CPU 可以不停的切换线程执行任务从而达到了多线程的效果。
但是由于在线程切换的时候需要保存本次执行的信息,在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。
上下文切换是非常耗效率的。
通常有以下解决方案:
采用无锁编程,比如将数据按照 Hash(id) 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。
采用 CAS(compare and swap) 算法,如 Atomic 包就是采用 CAS 算法。
合理的创建线程,避免创建了一些线程但其中大部分都是处于 waiting 状态,因为每当从 waiting 状态切换到 running 状态都是一次上下文切换。
死锁
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。
常用的解决方案如下:
尽量一个线程只获取一个锁。
一个线程只占用一个资源。
尝试使用定时锁,至少能保证锁最终会被释放。
资源限制
当在带宽有限的情况下一个线程下载某个资源需要 1M/S,当开 10 个线程时速度并不会乘 10 倍,反而还会增加时间,毕竟上下文切换比较耗时。如果是受限于资源的话可以采用集群来处理任务,不同的机器来处理不同的数据,就类似于开始提到的无锁编程
11. synchronized 关键字原理
1)synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式:
同步普通方法,锁的是当前对象。
同步静态方法,锁的是当前 Class 对象。
同步块,锁的是 () 中的对象。
实现原理: JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的
2)锁优化
synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。
轻量锁
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如
原创不易,完成人机校验,阅读全文
常见问题
sleep和wait的区别是什么?
sleep是Thread类的静态方法,睡眠后不释放对象锁、不出让系统资源,时间到自动唤醒;wait是Object类的方法,调用后线程释放对象锁让出CPU,必须配合notify或notifyAll唤醒。
ThreadLocal为什么会造成内存泄漏?
ThreadLocal被回收后ThreadLocalMap会存在key为null的Entry,而ThreadLocalMap依附于线程,线程迟迟不结束这些value就无法访问形成内存泄漏,应及时调用remove清理。