1.线程生命周期时,如何进行回答?
线程的生命周期就是线程从创建到终止的过程。
里面共分为6中状态,分别是:NEW新建状态,RUNNABLE运行状态,BLOCKED阻塞状态,WAITING无限时等待状态,TIMED_WAITING有限时等待状态,TERMINATED终止状态。
当实例化线程对象后,线程处于NEW新建状态。
当线程调用start()方法后,线程切换为RUNNABLE运行状态。
处于RUNNABLE状态的线程,如果需要竞争锁,但没有持有锁,会切换为BLOCKED阻塞状态。竞争到锁后,切换回RUNNABLE运行状态。
如果处于RUNNABLE状态的线程,调用sleep()方法会切换为TIMED-WAITING有限时等待状态,到达时间后,会切换回RUNNABLE运行状态。
如果处于RUNNABLE状态的线程,调用wait()方法会切换为WAITING无限时等待状态,直到调用notify()或notifyAll()后才会切换回RUNNABLE运行状态。
所有处于运行或运行后状态的线程调用stop()会切换为TERMINATED终止状态。
也可以在线程处于BLOCKED阻塞状态,WAITING无限时等待状态,TIMED_WAITING有限时等待状态时调用interrupt()方法切换到TERMINATED终止状态
因为suspend()和resume()在1.2就被替换了,而线程的状态State在1.5之后才出现,所以suspend和resume控制线程的功能依然生效,但是暂停后线程依然是RUNNABLE运行状态。
2. Thread中常用方法
static sleep(long): 写在哪个线程,就让哪个线程休眠。参数表示休眠时间,单位毫秒
start(); 启动线程
getState();获取线程状态
interrupt(); 中断。只能中断阻塞/有限时等待/无限时等待状态的线程
suspend();暂停。过时
resume();恢复。过时
stop(); 停止线程。过时
3.死锁(Dead Lock)
当一个线程持有锁,但是因为各种原因,一直不释放,这时锁就叫做死锁。
4.线程通信
多个线程之间相互配合,相互切换完成一个任务,这种情况就叫做线程通信.
线程通信方法:
join()
wait()
5. join()方法
线程B.join(); 当前线程挂起,等待线程B执行结束后,当前线程恢复执行.
线程B.join(毫秒); 当前线程挂起,到达执行的毫秒数后当前线程恢复执行.
6.wait(),notify(),notifyAll()
都是Object中的实例方法.
都要求必须写在加锁的代码中.
wait(): 让当前线程等待.直到调用notify()或notifyAll()才能换线.线程处于WAITING(无限时等待状态)
wait(long): 让当前线程等待,到达指定时间后自动唤醒.线程处于TIMED_WAITING
对象.notify():唤醒一个线程.只能唤醒通过这个对象.wait()的线程.
对象.notifyAll():唤醒所有通过这个对象wait()的线程.
7. wait()和sleep()的区别(经典面试题)
wait()和sleep()都能让线程停止执行.
但是sleep()是Thread类的静态方法,方法参数为休眠时间.线程休眠后处于线程生命周期的TIMED_WAITING有限时等待状态.当到达休眠时间后,会自动恢复为RUNNABLE运行状态.
sleep()方法没有强制要求当前线程必须持有锁.
而wait()是Object中的实例方法,要求线程必须持有锁,在等待时会释放锁.
Object中提供了无参wait()和有参wait()方法,有参wait()参数表示等待时间,到达时间后会自动恢复,这个效果和sleep()是类似的.
如果使用的是无参的wait()必须通过调用notify()或notifyAll()才会唤醒,否则一直处于WAITING无限时等待状态.
这就是wait()和sleep()的区别.
8.并发
一段时间内存可以处理多个任务,当线程数超过cpu核心数的时候,以时间片轮换方式执行,线程竞争到cpu资源才能被执行.
并行时候也可以出现并发,当某个cpu达到处理线程上限,就会时间片轮转执行.
9.并行
多核cpu下同一时间多个线程同时执行,
10.创建线程的三种方式
1. 继承Thread类
new Thread(){
public void run(){
// 线程功能
}
}.start();
- 实现Runnable接口
new Thread(new Runnable(){
public void run(){
// 线程功能
}
}).start();
- 使用FutureTask+Callable接口(Java 1.5出现的)
FutureTask<Integer> ft = new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
return 123;
}
});
new Thread(ft).start();
Integer result = ft.get();// 不接收返回值,此行代码可以没有
11.三种方式区别
继承Thread类和实现Runnable接口方式类似,都使用继承Thread类相关方法.
Callable方式功能更加强大,可获取子线程执行结果.
ft.get()为同步代码.只有子线程完成执行完成才会返回结果.
12.线程同步
传统意义上::一个线程执行时,另一个线程等待执行完成再执行.
当多个线程访问共享资源(变量)时,保证共享资源结果正确的方式
13.锁是什么?
实现线程同步的技术方案.
Java中实现线程同步的三种锁方案:
1.上锁 synchronized
Lock
2原子类
3.单线程线程池
4.cas+volatile 根据实际场景
14. synchronized
生效:
如果是实例方法(没有static),保证同一个对象调用这个方法.
如果是静态方法,这个类的任何对象都会同步.
15. 对象锁和类锁(面试题)
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁
如果锁的生效范围是该类的任意对象,这个锁称为类锁.
当synchronized修饰实例方法或代码块参数为this时,为对象锁
如果锁的生效范围是同一个对象,这个锁称为对象锁.
16.锁的四种状态
无锁,偏向锁,轻量级锁,重量级锁
17.synchronized底层原理
在Java早期,synchronized叫做重量级锁,加锁过程需要操作系统在内核态访问核心资源,因此操作系统会在用户态与内核态之间切换,效率很低下。于是JDK1.6之后,JVM为了提高锁的获取与释放效率,对synchronized进行了优化,引入了偏向锁和轻量级锁,根据线程竞争情况对锁进行升级,在线程竞争不激烈的情况避免使用重量级锁。
实现主要是mark word钟,共64位,在低位字段记录了锁的类型 .
无锁001,偏向锁101,
偏向锁前54位记录线程id,在大多情况下,锁很少被多个线程同时竞争,而且总是由一个线程多次获得, 为了提高效率,把锁的线程id写入到锁对象的Mark word中,当线程访问资源结束时候,不会主动释放偏向锁,当线程再次访问资源时,jvm会查看mark word中的线程id是否是当前线程的,如果是可以继续访问资源,如果不是,就会升级自旋锁
自旋锁(轻量级锁):标志位00,竞争的线程在各自的线程栈帧中生成对应的Lock Record空间,用于存储锁对象目前的mark word的拷贝,用cas操作将mark word前62位设置为指向线程lock record,设置成功者获得锁,其他参与竞争未获得锁的线程则一直自旋等待,一直占用cpu子资源,当线程数量多,且锁释放时间长,不太适合轻量级锁,效率低,引入了重量级锁.所以轻量级锁效率不一定比重量级锁效率高
重量级锁:标志位11,前62位指向monitor中的mutex.当jvm在线程超过10次自旋,或者自旋次数超过cpu核数的一半,会升级到重量级锁,底层是用mutex互斥锁,由操作系统负责线程调度,减速自旋带来的cpu消耗,但是操作系统调度带来的线程阻塞会导致程序响应速度变慢,
mark word内容如下图:
monitor的结构如下图:
18.可重入锁底层原理
synchronized 和ReentrantLock都是可以重入锁.
可重入锁底层原理特别简单,就是计数器。
当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到这个锁时,如果是可重入锁就对持有锁数量再次加1(如果是不可重入锁,发现持有锁为1了,就不允许多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。
19.线程安全
多线程操作时,依然能够保证结果的正确性,这就叫做保证了线程安全.
Java保证线程安全通过线程同步实现的.
线程安全3要素(缺一不可):
原子性:内容是不可分的部分,从执行到结束不能被打断.
示例:i++ 本身不具备原子性.
保证原子性:synchronized.写到synchronized里面的代码,只能被一个线程访问.
可见性:
一个线程对内容进行修改,写入主内存中,另一个线程能够立即查看到修改后的效果.
保证可见性:
synchronized,volatile(缓存一致性协议)
20.JMM(Java Memory Model)java内存模型
流程:
1.需要把操作的变量复制到工作内存中
2.线程操作工作内存中的变量
3.当执行完成后才会把变量的值重新写入到主内存中
会导致数据修改后,其他线程无法立即获取到修改后的值,无法保证数据的可见性,所以为了保证数据的可见性,引入了缓存一致性协议,当一个线程修改了变量值,会立即通知其他线程工作内存中的变量获取最新的值.