Java反射|序列化|注解|多线程&并发|JVM面试题
Java反射|序列化|注解|多线程&并发|JVM面试题
Java反射面试题
1 、除了使用new创建对象之外,还可以用什么方法创建对象?
使用Java反射可以创建对象!
2 、Java反射创建对象效率高还是通过new创建对象的效率高?
通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低
3 、java反射的作用
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java
中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
4 、哪里会用到反射机制?
jdbc就是典型的反射
这就是反射。如hibernate,struts等框架使用反射实现的。
5 、反射的实现方式:
第一步:获取Class对象,有 4 中方法:
1 )Class.forName(“类的路径”);
2 )类名.class
3 )对象名.getClass()
4 )基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
6 、实现Java反射的类:
1 )Class:表示正在运行的Java应用程序中的类和接口
注意: 所有获取对象的信息都需要Class类来实现。
2 )Field:提供有关类和接口的属性信息,以及对它的动态访问权限。
3 )Constructor:提供关于类的单个构造方法的信息以及它的访问权限
4 )Method:提供类或接口中某个方法的信息
7 、反射机制的优缺点:
优点:
1 )能够运行时动态获取类的实例,提高灵活性;
2 )与动态编译结合
Class.forName('com.mysql.jdbc.Driver.class'); //加载MySQL的驱动类
缺点:
1 )使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
解决方案:
1 、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;
2 、多次创建一个类的实例时,有缓存会快很多
3 、ReflflectASM工具类,通过字节码生成的方式加快反射速度
2 )相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
8 、Java 反射 API
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
- Class 类:反射的核心类,可以获取类的属性,方法等信息。
- Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性
值。
- Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或
者执行方法。
- Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
9 、反射使用步骤(获取 Class 对象、调用对象方法)
- 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。
- 调用 Class 类中的方法,既就是反射的使用阶段。
- 使用反射 API 来操作这些信息。
10 、获取 Class 对象有几种方法
调用某个对象的 getClass()方法
调用某个类的 class 属性来获取该类对应的 Class 对象
使用 Class 类中的 forName() 静态方法(最安全/性能最好)
当我们获得了想要操作的类的 Class 对象后,可以通过 Class 类中的方法获取并查看该类中的方法
和属性。
11 、利用反射动态创建对象实例
Class 对象的 newInstance()
- 使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例,但是这种方法要求
该 Class 对象对应的类有默认的空构造器。
Person p=new Person();
Class clazz=p.getClass();
Class clazz=Person.class;
Class clazz=Class.forName("类的全路径"); (最常用)
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//获取 Person 类的所有方法信息
Method[] method=clazz.getDeclaredMethods();
for (Method m:method){
System.out.println(m.toString());
}
//获取 Person 类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for (Field f:field){
System.out.println(f.toString());
}
//获取 Person 类的所有构造方法信息
Constructor[] constructor=clazz.getDeclaredConstructors();
for (Constructor c:constructor){
System.out.println(c.toString());
}
调用 Constructor 对象的 newInstance()
- 先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()
方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。
Java序列化面试题
1 、什么是java序列化,如何实现java序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对
象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化的实现:将需要被序列化的类实现Serializable接
口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:
FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法
就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
2 、保存(持久化)对象及其状态到内存或者磁盘
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命
周期不会比 JVM 的生命周期更长。 但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保
存的对象。Java 对象序列化就能够帮助我们实现该功能。
3 、序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化, 在保存对象时,会把其状态保存为一组字节,在未来, 再将这些字节组装成对象。必须注意地是, 对象序列化保存
的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
4 、序列化用户远程对象传输
除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。 Java序列化API
为处理对象序列化提供了一个标准机制,该API简单易用。
5 、Serializable 实现序列化
在 Java 中, 只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。ObjectOutputStream 和 ObjectInputStream 对对象进
行序列化及反序列化通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。
6 、writeObject 和 readObject 自定义序列化策略
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//使用.newInstane 方法创建对象
Person p=(Person) clazz.newInstance();
//获取构造方法并创建对象
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
//创建对象并设置属性13/04/2018
Person p1 =(Person) c.newInstance("李四","男" ,20);
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。
7 、序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private
static final long serialVersionUID
8 、序列化并不保存静态变量
序列化子父类说明
要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
9 、Transient 关键字阻止该变量被序列化到文件中
- 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列
化后, transient 变量的值被设为初始值,如 int 型的是 0 ,对象型的是 null。 - 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串
等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在
客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的
数据安全。
10 、序列化(深 clone 一中实现)
在 Java 语言里深复制一个对象,常常可以先使对象实现 Serializable 接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从
流里读出来,便可以重建对象。
Java注解面试题
1 、 4 种标准元注解是哪四种?
元注解的作用是负责注解其他注解。 Java5.0 定义了 4 个标准的 meta-annotation 类型,它们被用来提供对其它 annotation 类型作说明。
@Target 修饰的对象范围
@Target说明了Annotation所修饰的对象范围: Annotation可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员
(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target
可更加明晰其修饰的目标
@Retention 定义 被保留的时间长短
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在
什么范围内有效),取值(RetentionPoicy)由:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在 class 文件中有效(即 class 保留)
- RUNTIME:在运行时有效(即运行时保留)
@Documented 描述-javadoc
@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因
此可以被例如 javadoc 此类的工具文档化。
@Inherited 阐述了某个被标注的类型是被继承的
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一
个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该
class 的子类。
2 、注解是什么?
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。 Annatation(注解)是一个接口,程
序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
多线程&并发面试题
JAVA 并发知识库
1 、Java中实现多线程有几种方法
继承Thread类;
实现Runnable接口;
实现Callable接口通过FutureTask包装器来创建Thread线程;
使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来
管理前面的三种方式)。
2 、继承 Thread 类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。 start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法。
3 、实现 Runnable 接口。
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口。
4 、ExecutorService、 Callable、 Future 有返回值线程
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行
Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程
了。
5 、基于线程池的方式
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
6 、 4 种线程池
public class MyThread extends Thread {
public void run () {
System.out .println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
public class MyThread extends OtherClass implements Runnable {
public void run () {
System.out .println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用
target.run ()
public void run() {
if (target != null) {
target.run ();
}
}
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List <Future> list = new ArrayList<Future>();
for ( int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " " );
// 执行任务并获取 Future 对象
Future f = pool .submit(c);
list .add (f);
}
// 关闭线程池
pool .shutdown();
// 获取所有并发任务的运行结果
for ( Future f : list ) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res: " + f.get(). toString());
}
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true ) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread(). getName() + " is running ..");
try {
Thread.sleep(3000 );
} catch ( InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。 调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。 因此,长时间保持空闲的线程池不会使用任何资
源。
newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程) ,这个线程
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
7 、如何停止一个正在运行的线程
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run () {
System.out .println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run () {
System.out .println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
1 、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2 、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的
方法。
3 、使用interrupt方法中断线程。
8 、notify()和notifyAll()有什么区别?
notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒
所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用
notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死
锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果
唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中.
9 、sleep()和wait() 有什么区别?
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于
Object 类中的。 - sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然
保持者,当指定的时间到了又会自动恢复运行状态 - 在调用 sleep()方法的过程中, 线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此
对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
10 、volatile 是什么?可以保证有序性吗?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语
义:
1 )保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他
线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
2 )禁止进行指令重排序。
volatile 不是原子性操作
什么叫保证部分有序性?
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果
已经对后面的操作可见;在其后面的操作肯定还没有进行;
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop ) {
System.out .println(getName() + " is running");
try {
sleep(1000 );
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true ; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main (String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out .println("Starting thread...");
m1.start();
Thread.sleep(3000 );
System.out .println("Interrupt thread...: " + m1.getName());
m1.stop = true ; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000 ); // 主线程休眠 3 秒以便观察线程m1的中断情况
System.out .println("Stopping application...");
}
}
x = 2; //语句 1
y = 0; //语句 2
flag = true ; //语句 3
x = 4; //语句 4
y = -1; //语句 5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1 、语句 2 前
面,也不会讲语句 3 放到语句 4 、语句 5 后面。但是要注意语句 1 和语句 2 的顺序、语句 4 和语句 5 的顺序是
不作任何保证的。
使用 Volatile 一般用于 状态标记量 和 单例模式的双检锁
11 、Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果
不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会
启动新线程 。
12 、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需
要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在
等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们
定义在Object类中因为锁属于对象 。
13 、为什么wait和notify方法要在同步块中调用?
- 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
- 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
- 还有一个原因是为了避免wait和notify之间产生竞态条件。
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得
该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对
象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用
notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。
这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
14 、Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机
制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。
而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其
它线程调用中断来改变 。
15 、Java中synchronized 和 ReentrantLock 有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一
个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行
线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需
要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配
合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指
令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经
拥有了那个对象锁,把锁的计算器加 1 ,相应的,在执行monitorexit指令时会将锁计算器就减 1 ,当计
算器为 0 时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释
放为止 。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以
下 3 项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性
能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象 。
16 、有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续
执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
实际上先启动三个线程中哪一个都行,
因为在每个线程的run方法中用join方法限定了三个线程的执行顺序
public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
17 、SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而
ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问
map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,
仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优
势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修
改,也不会抛出ConcurrentModificationException 。
18 、什么是线程安全
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。
但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会
产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运
行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
19 、Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且
只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入
到暂停状态后马上又被执行。
20 、Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的
Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和
ScheduledThreadPoolExecutor都有这些方法 。
21 、说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能
有一个线程执行。
public static void main (String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run () {
System.out .println("t1" );
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run () {
try {
// 引用t1线程,等待t1线程执行完
t1 .join ();
} catch (InterruptedException e) {
e.printStackTrace();
} S
ystem.out.println("t2" );
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run () {
try {
// 引用t2线程,等待t2线程执行完
t2 .join ();
} catch (InterruptedException e) {
e.printStackTrace();
} S
ystem.out.println("t3" );
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来
实现的,Java 的线程是映射到操作系统的原生线程之上的。
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的
转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对
锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销 。
22 、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗synchronized关键字
最主要的三种使用方式
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该
类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需
要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象, 因为访问静态 synchronized 方法占用的锁是当前
类的锁,而访问非静态synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上
锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因
为JVM中,字符串常量池具有缓存功能
23 、什么是线程安全?Vector是一个线程安全类吗?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样
的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下
也不会出现计算失误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似
的ArrayList不是线程安全的。
24 、volatile关键字的作用?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,
只有当前线程可以访问该变量,其他线程被阻塞住。 - volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
25 、简述一下你对线程池的理解
如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理
利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一的分配,调优和监控
26 、线程生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就
绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运
行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
27 、新建状态(NEW)
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
28 、就绪状态(RUNNABLE)
当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
29 、运行状态(RUNNING)
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
30 、阻塞状态(BLOCKED)
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才
有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列) :
运行(running)的线程执行 o.wait()方法, JVM 会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态
超时、 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
31 、线程死亡(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
- run()或 call()方法执行完成,线程正常结束。异常结束
- 线程抛出一个未捕获的 Exception 或 Error。调用 stop
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
32 、终止线程 4 种方式
正常运行结束
程序运行结束,线程自动结束。
使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况
下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或
false 来控制 while循环是否退出,代码示例 :
定义了一个退出标志 exit,当 exit 为 true 时, while 循环退出, exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,
这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
Interrupt 方法结束线程
使用 interrupt()方法来中断线程有两种情况:
1.线程处于阻塞状态: 如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的
interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,
从而让我们有机会结束这个线程的执行。 通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获
InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
2.线程未处于阻塞状态: 使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自
定义的标志来控制循环是一样的道理。
stop 方法终止线程(线程不安全)
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,
可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放
子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有
锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程
序错误。因
此,并不推荐使用 stop 方法来终止线程。
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run () {
while ( !exit ){
//do something
}
}
}
public class ThreadSafe extends Thread {
public void run () {
while ( !isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try {
Thread.sleep(5*1000 );//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
33 、start 与 run 区别
- start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行
结束, 此线程终止。然后 CPU 再调度其它线程。
34 、JAVA 后台线程
1. 定义:守护线程--也称“服务线程”, 他是后台线程, 它有一个特性,即为用户线程 提供 公共服务, 在没有用户线程可服务时会自动离
开。
2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
- 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的
setDaemon 方法。 - 在 Daemon 线程中产生的新线程也是 Daemon 的。
- 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的
生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程
依旧是活跃的。 - example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,
程序就不会再产生垃圾,垃圾回收器也就无事可做, 所以当垃圾回收线程是 JVM 上仅剩的线
程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统
中的可回收资源。 - 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周
期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依
赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候, JVM 就可以退
出了;如果还有一个或以上的非守护线程则 JVM 不会退出
35 、什么是乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新
的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样
则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。
36 、什么是悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上
锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,
获取不到,才会转换为悲观锁,如 RetreenLock。
37 、什么是自旋锁
自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换
进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要
设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时
争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于
线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是
占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的
消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值(1.6 引入了适应性自旋锁)
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,
会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM 对于自旋周期的选择, jdk1.5 这个限度是一定的写死的, 在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的
了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时
JVM 还针对当前 CPU 的负荷情况做了较多的优化, 如果平均负载小于 CPUs 则一直自旋, 如果有超过(CPUs/2)个线程正在自旋,则后来线
程直接阻塞, 如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞, 如果 CPU 处于节电模式则停止自
旋, 自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差) , 自旋时会适当放弃线程
优先级之间的差异。
自旋锁的开启
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
38 、Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。 他属于独占式的悲观锁,同时属于可重入锁。 Synchronized 作用范围**
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全
局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程; - synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。 它有多个队列,当多个线程一起访问某个对象监视器的
时候,对象监视器会将这些线程存储在不同的容器中。
Synchronized 核心组件
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List: 竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List: Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:任意时刻, 最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到所资源的线程被称为 Owner;
- !Owner:当前释放锁的线程。
Synchronized 实现
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争, JVM 会将
一部分线程移动到 EntryList 中作为候选竞争线程。 - Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定
EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。 - Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
JVM 中,也把这种选择行为称之为“竞争切换”。 - OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList
中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify
或者 notifyAll 唤醒,会重新进去 EntryList 中。 - 处于 ContentionList、 EntryList、 WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。 - Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时, 等待的线程会先
尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是
不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
资源。
参考: https://blog.csdn.net/zqz_zqz/article/details/70233767 - 每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的 - synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
程加锁消耗的时间比有用操作消耗的时间更多。 - Java1.6, synchronized 进行了很多的优化, 有适应自旋、锁消除、锁粗化、轻量级锁及偏向
锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做
了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。 - 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
- JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
39 、ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 他是一种可重入锁, 除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
Lock 接口的主要方法
void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经
被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和
lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,
当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一
直等待, 在未获得锁之前,当前线程并不继续向下执行.
void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程
并不持有锁, 却执行该方法, 可能导致异常的发生.
Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,
当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
getHoldCount() : 查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次
数。
getQueueLength() : 返回正等待获取此锁的线程估计数,比如启动 10 个线程, 1 个
线程获得锁,此时返回的是 9
getWaitQueueLength: (Condition condition)返回等待与此锁相关的给定条件的线
程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了
condition 对象的 await 方法,那么此时执行此方法返回 10
hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件
(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁
hasQueuedThreads(): 是否有线程等待此锁
isFair(): 该锁是否公平锁
isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分
别是 false 和 true
isLock(): 此锁是否有任意线程占用
lockInterruptibly() : 如果当前线程未被中断,获取锁
tryLock() : 尝试获得锁,仅在调用时锁未被线程占用,获得锁
tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持,
则获取该锁。
非公平锁
JVM 按随机、就近原则分配锁的机制则称为不公平锁, ReentrantLock 在构造函数中提供了
是否公平锁的初始化方式,默认为非公平锁。 非公平锁实际执行的效率要远远超出公平锁,除非
程序有特殊需要,否则最常用非公平锁的分配机制。
公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,
ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
40 、Condition 类和 Object 类锁方法区别区别
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
41 、tryLock 和 lock 和 lockInterruptibly 的区别
- tryLock 能获得锁就返回 true,不能就立即返回 false, tryLock(long timeout,TimeUnit
unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false - lock 能获得锁就返回 true,不能的话一直等待获得锁
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,
lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
42 、Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信
号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来
构建一些对象池,资源池之类的, 比如数据库连接池
实现互斥锁(计数器为 1 )
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,
表示两种互斥状态。
代码实现
// 创建一个计数阈值为 5 的信号量对象
// 只能 5 个线程同时访问
Semaphore semp = new Semaphore(5);
try { // 申请许可
semp .acquire();
try {
// 业务逻辑
} catch (Exception e) {
} finally {
// 释放许可
semp .release();
}
} catch ( InterruptedException e) {
}
43 、Semaphore 与 ReentrantLock 区别
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与
release()方法来获得和释放临界资源。经实测, Semaphone.acquire()方法默认为可响应中断锁,
与 ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被
Thread.interrupt()方法中断。
此外, Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock
不同,其使用方法与 ReentrantLock 几乎一致。 Semaphore 也提供了公平与非公平锁的机制,也
可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而
无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。
44 、可重入锁(递归锁)
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。 可重入锁,也叫
做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受
影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。
45 、公平锁与非公平锁
公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁(Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁。
46 、ReadWriteLock 读写锁
为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如
果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。 读写锁分为读锁和写
锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上
读锁,写的时候上写锁!
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现
ReentrantReadWriteLock。
47 、共享锁和独占锁
java 并发包提供的加锁模式分为独占锁和共享锁。
独占锁
独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如: ReadWriteLock。 共享锁则是一种
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等
待线程的锁获取模式。 - java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,
或者被一个 写操作访问,但两者不能同时进行。
48 、重量级锁(Mutex Lock)
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来
实现的。
而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么
Synchronized 效率低的原因。
因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。 JDK 中对 Synchronized 做的种种优化,其核心都是为了减少
这种重量级锁的使用。
JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
49 、轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量
级锁使用产生的性能消耗。
在解释轻量级锁的执行过程之前, 先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情
况,就会导致轻量级锁膨胀为重量级锁
50 、偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。 偏向锁的目的是在某个线程
获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令, 而
偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作
的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。
上面说过, 轻量级锁是为了在线程交替执行同步块时提高性能, 而偏向锁则是在只有一个线程执行同步块时进一步提高性能
51 、分段锁
分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践
52 、锁优化
减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。
锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互
斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]
JDK 并发包 1 。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如
LinkedBlockingQueue 从头部取出,从尾部放数据
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步
和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
锁消除
锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起。
参考: https://www.jianshu.com/p/39628e1180a9
53 、线程基本方法
线程相关的基本方法有 wait, notify, notifyAll, sleep, join, yield 等。
54 、线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的
是调用 wait()方法后, 会释放对象的锁。因此, wait 方法一般用在同步方法或同步代码块中。
55 、线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致
线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
56 、线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,
优先级高的线程有更大的可能性成功竞争得到 CPU 时间片, 但这又不是绝对的,有的操作系统对
线程优先级并不敏感。
57 、线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。 这
个线程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。 - 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
InterruptedException,从而使线程提前结束 TIMED-WATING 状态。 - 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异
常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。 - 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止
一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以
根据 thread.isInterrupted()的值来优雅的终止线程。
58 、Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
59 、为什么要用 join()方法?
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要
在子线程结束后再结束,这时候就要用到 join() 方法 。
60 、线程唤醒(notify)
Object 类中的 notify() 方法, 唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象
上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调
用其中一个 wait() 方法,在对象的监视器上等待, 直到当前的线程放弃此对象上的锁定,才能继
续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
61 、线程其他方法
- sleep():强迫一个线程睡眠N毫秒。
- isAlive(): 判断一个线程是否存活。
- join(): 等待线程终止。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线
程依赖于主线程结束而结束) - setName(): 为线程设置一个名称。
- wait(): 强迫一个线程等待。
11.notify(): 通知一个线程继续运行。 - setPriority(): 设置一个线程的优先级。
- getPriority()::获得一个线程的优先级。
62 、进程
(有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且
与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量
级的进程。
63 、上下文
是指某一时间点 CPU 寄存器和程序计数器的内容。
System.out .println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6();
thread1.setName("线程 B");
thread1.join ();
System.out .println("这时 thread1 执行完毕之后才能执行主线程");
64 、寄存器
是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内
存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速
度。
65 、程序计数器
是一个专用的寄存器, 用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令
的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
66 、PCB-“切换桢”
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下
文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。 PCB 还经常被称
作“切换桢”(switchframe)。 信息会一直保存到 CPU 的内存中,直到他们被再次使用。
67 、上下文切换的活动
1.挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
2.在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
3.跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序
中。
68 、引起线程上下文切换的原因
1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
4. 用户代码挂起当前任务,让出 CPU 时间;
5. 硬件中断;
69 、同步锁
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程
同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可
以使用 synchronized 关键字来取得一个对象的同步锁。
70 、死锁
何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
71 、线程池原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。 他的主要特点为: 线程复用; 控制最大并发数; 管理线程。
72 、线程复
每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run
方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写
Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实
现原理。 循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以
是阻塞的 。
73 、线程池的组成
一般的线程池主要分为以下 4 个组成部分:
1. 线程池管理器:用于创建并管理线程池
2. 工作线程:线程池中的线程
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor, Executors,
ExecutorService, ThreadPoolExecutor , Callable 和 Future、 FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:
- corePoolSize:指定了线程池中的线程数量。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。 - unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
74 、拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
- AbortPolicy : 直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。 - DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。 - DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
75 、Java 线程池工作过程
1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小。
76 、JAVA 阻塞队列原理
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,
TimeUnit unit , BlockingQueue<Runnable> workQueue) {
this (corePoolSize, maximumPoolSize, keepAliveTime, unit , workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:
1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放
入队列。
2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有
空的位置,线程被自动唤醒。
阻塞队列的主要方法 :
抛出异常:抛出一个异常;
特殊值:返回一个特殊值(null 或 false,视情况而定)
则塞:在成功操作之前,一直阻塞线程
超时:放弃前只在最大的时间内阻塞
插入操作
1 : public abstract boolean add(E paramE): 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果
当前没有可用的空间,则抛出 IllegalStateException。如果该元素是 NULL,则会抛出 NullPointerException 异常。
2 : public abstract boolean offer(E paramE): 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果
当前没有可用的空间,则返回 false。
3 : public abstract void put(E paramE) throws InterruptedException: 将指定元素插入此队列中,将等待可用的空间(如果有必要)
4 : offer(E o, long timeout, TimeUnit unit): 可以设定等待的时间, 如果在指定的时间
内, 还不能往队列中加入 BlockingQueue, 则返回失败。
获取数据操作:
1 : poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null;
2 : poll(long timeout, TimeUnit unit): 从 BlockingQueue 取出一个队首的对象, 如果在指定时间内, 队列一旦有数据可取, 则立即返
回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
3 : take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
4.drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不
需要多次分批加锁或释放锁。
77 、Java 中的阻塞队列
- ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
public void put(E paramE) throws InterruptedException {
checkNotNull(paramE);
ReentrantLock localReentrantLock = this .lock ;
localReentrantLock.lockInterruptibly();
try {
while (this .count == this .items.length)
this .notFull.await(); //如果队列满了,则线程阻塞等待
enqueue(paramE);
localReentrantLock.unlock();
} finally {
localReentrantLock.unlock();
}
}
78 、ArrayBlockingQueue(公平、非公平)
用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。 默认情况下不保证访问者公平的访问队列,所谓公平
访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往
队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建
一个公平的阻塞队列
79 、LinkedBlockingQueue(两个独立锁提高并发)
基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue
之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生
产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量
(Integer.MAX_VALUE)
80 、PriorityBlockingQueue(compareTo 排序实现优先)
是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。 可以自定义实现
compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造
参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
81 、DelayQueue(缓存失效、定时任务 )
是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实
现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才
能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:
- 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询
DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。 - 定 时 任 务 调 度 : 使 用 DelayQueue 保 存 当 天 将 会 执 行 的 任 务 和 执 行 时 间 , 一 旦 从
DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的
82 、SynchronousQueue(不存储数据、可用于传递数据)
是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线
程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给
另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和
ArrayBlockingQueue。
83 、LinkedTransferQueue
是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 ,
LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
- transfer 方法: 如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的
poll()方法时), transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如
果没有消费者在等待接收元素, transfer 方法会将元素存放在队列的 tail 节点,并等到该元素
被消费者消费了才返回。 - tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费
者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否
接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传
入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时
还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
84 、LinkedBlockingDeque
是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。
双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其
他的阻塞队列, LinkedBlockingDeque 多了 addFirst, addLast, offerFirst, offerLast,
peekFirst, peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队
列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另
外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同
于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。
在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在
“工作窃取”模式中。
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000 ,true );
85 、在 java 中守护线程和本地线程区别
java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool
on);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()
必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果
全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可
以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的
线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产
生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线
程时,Java 虚拟机会自动离开。
扩展:
Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护
进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break
的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。
86 、线程与进程的区别?
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
87 、什么是多线程中的上下文切换?
多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,
为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU
发生的切换数据等就是上下文切换。
88 、死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成
的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
1 、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2 、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3 、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4 、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁: 任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,
失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而
处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿: 一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执
行的状态。
Java 中导致饥饿的原因:
1 、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2 、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前
持续地对该同步块进行访问。
3 、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方
法),因为其他线程总是被持续地获得唤醒。
89 、Java 中用到的线程调度算法是什么?
采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优
先级上,如非特别需要,尽量不要用,防止线程饥饿。
90 、什么是线程组,为什么在 Java 中不推荐使用?
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,
也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使
用,推荐使用线程池。
91 、为什么使用 Executor 框架?
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、
耗资源的。
调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,
线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的
频繁交替也会消耗很多系统资源。
接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时
定期执行、线程中断等都不便实现。
92 、在 Java 中 Executor 和 Executors 的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的
完成,并可以使用 get()方法获取计算的结果。
93 、如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时间最长?
参考:
http://daiguahub.com/2016/07/31/使用 jstack 找出消耗 CPU 最多的线程代码/
94 、什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间
的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——
Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,
java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法
时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完
成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通
过引入一个 int 来累加来反映中间有没有变过)
95 、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优
势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类
的条件对象。
它的优势有:
可以使锁更公平
可以使线程在等待锁的时候响应中断
可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的
(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized
只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
96 、什么是 Executors 框架?
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。无限制的创建线程会引起应用程序内存溢出。所以创
建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一
个线程池。
97 、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者
模型?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非
空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是
从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7 提供了 7 个阻塞队列。分别是:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Java 5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是
用好,wait ,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后,可以使用阻
塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。
BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当
生产者线程试图向BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,
则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控
制线程之间的通信。
阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据
解析。
98 、什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能
更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。可以认为
是带有回调的 Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
用于产生结果,Future 用于获取结果。
99 、什么是 FutureTask?使用 ExecutorService 启动任务。
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运
算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行
包装,由于 FutureTask 也是调用了 Runnable接口所以它可以提交给 Executor 来执行。
100 、什么是并发容器的实现?
何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如
Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些
同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字
synchronized。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加
锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发
的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。
101 、多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消
息到达时才被唤醒。线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任
何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程
同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要
切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局
变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
102 、什么是竞争条件?你怎样发现和解决竞争?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race
condition)。
103 、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方
法?
当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执
行调用线程的代码,只会把 run 方法当作普通方法去执行。
104 、Java 中你怎样唤醒一个阻塞的线程?
在 Java 发展史上曾经使用 suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用 Object 类的 wait()和 notify()方法实现线程阻塞。
首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任
意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify
方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调
用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
105 、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。 Java 的 concurrent 包里面的 CountDownLatch 其实可以把它看作一
个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面
的值。你可以向 CountDownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await()方法都会阻塞,直到这个计数器的
计数值被其他的线程减为 0 为止。
所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只
出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountDownLatch 的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。
假如我们这个想要继续往下执行的任务调用一个 CountDownLatch 对象的 await()方法,其他的任务执行完自己的任务后调用同一个
CountDownLatch 对象上的 countDown()方法,这个调用 await()方法的任务将一直阻塞等待,直到这个 CountDownLatch 对象的计数值
减到 0 为止。
CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线
程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrie在释放等待线程后可以重用,所以称它为循环 的
barrier。
106 、什么是不可变对象,它对写并发应用有什么帮助
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable
Objects)。不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、
BigInteger 和 BigDecimal 等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些
常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是 final 类型;并且,
它被正确创建(创建期间没有发生 this 引用的逸出)。
107 、Java 中用到的线程调度算法是什么?
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是
指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的
一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权.
有两种调度模型: 分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的
CPU 的时间片这个也比较好理解。 java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的
线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
108 、什么是线程组,为什么在 Java 中不推荐使用?
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少
创建销毁线程的开销。
JVM面试题
1 、java中会存在内存泄漏吗,请简单描述。
会。自己实现堆载的数据结构时有可能会出现内存泄露,可参看effective java.
2 、 64 位 JVM 中,int 的长度是多数?
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说,在 32 位 和 64 位 的 Java 虚拟机中,int 类型的长度是
相同的。
3 、Serial 与 Parallel GC 之间的不同之处?
Serial 与 Parallel 在 GC 执行的时候都会引起 stop-the-world。它们之间主要不同 serial 收集器是默认的复制收集器,执行 GC 的时候只有
一个线程,而parallel 收集器使用多个 GC 线程来执行。
4 、 32 位和 64 位的 JVM,int 类型变量的长度是多数?
32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4 个字节。
5 、Java 中 WeakReference 与 SoftReference 的区别?
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率,但是 WeakReference ,一旦失去最后一个强引用,就会被 GC
回收,而软引用虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。
6 、JVM 选项 -XX:+UseCompressedOops 有什么作用?为什么要使用
当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。
这也会对 CPU缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM主要动机在于可以指定最大堆大小,通过压缩
OOP 可以节省一定的内存。通过-XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位的 OOP。
7 、怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
你可以检查某些系统属性如 sun.arch.data.model 或 os.arch 来获取该信息。
8 、 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5
GB,Solaris 大约3GB。 64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小
到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
9 、JRE、JDK、JVM 及 JIT 之间有什么不同?
JRE 代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。JDK 代表 Java 开发工具(Java development kit),是 Java 程序的开
发工具,如 Java编译器,它也包含 JRE。JVM 代表 Java 虚拟机(Java virtual machine),它的责任是运行 Java 应用。JIT 代表即时编译
(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主要的热点代码会被准换为
本地代码,这样有利大幅度提高 Java 应用的性能。
10 、解释 Java 堆空间及 GC?
当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从对空间中分配内
存。GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。
11 、JVM 内存区域
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程直接
映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可
以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了
在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
12 、程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有” 的内
存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
13 、虚拟机栈(线程私有)
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、
方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派(
Dispatch Exception)。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获
的异常)都算作方法结束。
14 、本地方法区(线程私有)
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-
linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一 。
15 、你能保证 GC 执行吗?
不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC的执行。
16 、怎么获取 Java 程序使用的内存?堆使用的百分比?
可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比
及堆内存的剩余空间。Runtime.freeMemory() 方法返回剩余空间的字节数,Runtime.totalMemory()方法总内存的字节数,
Runtime.maxMemory() 返回最大内存的字节数。
17 、Java 中堆和栈有什么区别?
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会
在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
18 、描述一下 JVM 加载 class 文件的原理机制
JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行
时查找和装入类文件中的类。
由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确
保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节
数组读入.class 文件,然后产生与所加载类对应
的 Class 对象。
加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内
存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对
类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执
行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定
义类加载器(java.lang.ClassLoader 的子类)。
从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的
Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类
加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类
加载器的说明:
- Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
- Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;
- System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性
java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
19 、GC 是什么?为什么要有 GC?
GC 是垃 圾收 集的 意思 ,内存 处理 是编 程人 员容 易出 现问 题的 地方 ,忘记 或者 错误的内 存回 收会 导致 程序 或系 统的 不稳 定甚 至
崩 溃, Java 提供 的 GC 功能 可以 自动监测 对象 是否 超过 作用 域从 而达 到自 动回 收内 存的 目的 ,Java 语言 没有 提供 释放已分 配内
存的 显示 操作 方法 。Java 程序 员不 用担 心内 存管 理, 因为 垃圾 收集 器会自动 进行 管理 。要 请求 垃圾 收集 ,可 以调 用下 面的 方
法 之一 :System.gc() 或Runtime.getRuntime().gc() ,但 JVM 可以 屏蔽 掉显 示的 垃圾 回收 调用 。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况
下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回
收。在 Java 诞生初期,垃圾回收是 Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今 Java 的
垃圾回收机制已经成为被诟病的东。移动智能终端用户通常觉得 iOS 的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就
在于 Android 系统中垃圾回收的不可预知性。
20 、堆(Heap-线程共享) -运行时数据区
是被线程共享的一块内存区域, 创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。 由于现代
VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。
21 、方法区/永久代(线程共享)
即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据.
HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理
这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小) 。
运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一
项信息是常量池 (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行
时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符
合规范上的要求,这样才会被虚拟机认可、装载和执行。
22 、JVM 运行时内存
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。
23 、新生代
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden
区、 ServivorFrom、 ServivorTo 三个区。
Eden 区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生
代区进行一次垃圾回收。
ServivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo
保留了一次 MinorGC 过程中的幸存者。
MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法。
1 : eden、 servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代
区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
2 : 清空 eden、 servicorFrom
然后,清空 Eden 和 ServicorFrom 中的对象;
3 : ServicorTo 和 ServicorFrom 互换
最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
24 、老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老
年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出
空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。 ajorGC 的耗时比较长,因为要扫
描再回收。 MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不
下的时候,就会抛出 OOM(Out of Memory)异常。
25 、永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域, 它和和存放实例的区域不同,GC
不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
26 、JAVA8 与元数据
在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大
的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入
nativememory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用
空间来控制。
27 、引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是
否可以回收。简单说,即一个对象如果没有任何与之关联的引用, 即他们的引用计数都不为 0 , 则说明对象不太可能再被用到,那么这个
对象就是可回收对象。
28 、可达性分析
为了解决引用计数法的循环引用问题, Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一
个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要经
过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
29 、标记清除算法( Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如
图
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
30 、复制算法(copying)
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块
内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话, Copying
算法的效率会大大降低。
31 、标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对象,而是将存活对象移向内存的一
端。然后清除端边界外的对象。如图:
32 、分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分
为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特
点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
33 、新生代与复制算法
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通
常并不是按照 1 : 1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次
使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
34 、老年代与标记复制算法
而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
- JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation), 它用来存储 class 类,常量,方法描述等。对永生代的回收主要
包括废弃常量和无用的类。 - 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分
配到老生代。 - 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后, EdenSpace 和 From Space 区的存活对象会被
挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理。 - 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
- 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。 默认情况下年龄到达 15 的对象会被移到老生代中。
35 、JAVA 强引用
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于
可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主
要原因之一。
36 、JAVA软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回
收。软引用通常用在对内存敏感的程序中。
37 、JAVA弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的
内存空间是否足够,总会回收该对象占用的内存。
38 、JAVA虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状
态。
39 、分代收集算法
当前主流 VM 垃圾收集都采用”分代收集” (Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中
的 新生代、老年代、永久代, 这样就可以根据各年代特点分别采用最适当的 GC 算法
40 、在新生代-复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集
41 、在老年代-标记整理算法
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理” 算法来进行回收, 不必进行内存复制, 且直接腾
出空闲内存。
42 、分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据
目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
43 、GC 垃圾收集器
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因
此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器, JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
44 、Serial 垃圾收集器(单线程、 复制算法)
Serial(英文连续) 是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。 Serial 是一个单线程的收集器,
它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结
束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的
开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
45 、ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器
完全一样, ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。 【Parallel:平行
的】
ParNew 虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认
垃圾收集器。
46 、Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器, 它重点关注的是程序达到一个
可控制的吞吐量(Thoughput, CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收
集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 自适
应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
57 、Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的
java 虚拟机默认的年老代垃圾收集器。在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
- 作为年老代中使用 CMS 收集器的后备垃圾收集方案。新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要
暂停所有的工作线程。新生代 ParallelScavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
58 、Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体
的吞吐量, Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器, 如果系统对吞吐量要求比较高,可以优先考虑新生代
Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图
59 、CMS 收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间, 和其他年老代使用标记-整
理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的
垃圾收集器来说更复杂。整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以
和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。CMS 收集器工作过程
60 、G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器, G1 收
集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。G1 收集器避免全区域垃圾收集,它把堆内存划分为大小
固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收
垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率
61 、JVM 类加载机制
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。
加载
加载是类加载过程中的一个阶段, 这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的入
口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生
成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概
念,比如一个类变量定义为:
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080 , 将 v 赋值为 8080 的 put static 指令是
程序被编译后, 存放于类构造器方法之中。
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v
赋值为 8080 。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中
的:
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080 , 将 v 赋值为 8080 的 put static 指令是程序被编译后, 存放于类构造器方法之
中。但是注意如果声明为:
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v
赋值为 8080 。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中
的:
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080 。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
符号引用
符号引用与虚拟机实现的布局无关, 引用的目标并不一定要已经加载到内存中。 各种虚拟机实现的内存布局可以各不相同,但是它们能接
受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存
在。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初
始阶段,才开始真正执行类中定义的 Java 程序代码。
类构造器
初始化阶段是执行类构造器方法的过程。 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会
保证子方法执行之前,父类的方法已经执行完毕, 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()
方法。注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
发定义常量所在的类。 - 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 - 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
62 、类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类, JVM 提供了 3 种类加载器:
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的, 或通过-Xbootclasspath 参数指定路径中的, 且被虚拟机认可(按文件名识别, 如 rt.jar) 的类。
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):
负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载, 当然我们也可以通过继承 java.lang.ClassLoader实现自
定义的类加载器。
public static int v = 8080 ;
public static final int v = 8080 ;
63 、双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此
所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载
的Class), 子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类
加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象
64 、OSGI( 动态模型系统)
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系统的一系列规范。
65 、动态改变构造
OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理, OSGi 技术提供一种
面向服务的架构,它能使这些组件动态地发现对方。
66 、模块化编程与热插拔
OSGi 旨在为实现 Java 程序的模块化编程提供基础条件,基于 OSGi 的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以
只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。
OSGi 描绘了一个很美好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。但并非所
有的应用都适合采用 OSGi 作为基础架构,它在提供强大功能同时,也引入了额外的复杂度,因为它不遵守了类加载的双亲委托模型。
67 、JVM内存模型
线程独占:栈,本地方法栈,程序计数器
线程共享:堆,方法区
68 、栈
又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法
出口等信息.调用方法时执行入栈,方法返回式执行出栈.
69 、本地方法栈
与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.
70 、程序计数器
保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行Native方法时,程序计数器为空.
71 、堆
JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆没有可用空间时,会抛出OOM异常.根
据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理
72 、方法区
又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永久代和1.8的元空间都是方法区的一种
实现。
73 、分代回收
分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间
年轻代->标记-复制
老年代->标记-清除
74 、堆和栈的区别
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存
储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连
续,会有碎片。
1 、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,
还是类变量,它们指向的对象都存储在堆内存中。
2 、共享性不同
栈内存是线程私有的。
堆内存是所有线程共有的。
3 、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
4 、空间大小
栈的空间大小远远小于堆的
75 、什么时候会触发FullGC
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
- 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错
误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过
大的对象及数组。 - Permanet Generation空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation
可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 - CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两
种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent
mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导
致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为
ms)来避免。 - 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做
了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于
6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧
生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。除了以上 4 种状况外,对于使用RMI来进行RPC或管理的Sun JDK应
用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执
行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc
76 、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序
可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的
指令长度和其他特性。
77 、对象分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量
的内存拷贝(新生代采用复制算法收集内存)。 - 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次Minor GC那么对象会进入Survivor区,之
后每经过一次Minor GC那么对象的年龄加 1 ,知道达到阀值对象进入老年区。 - 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以
直接进入老年代。 - 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则
进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
78 、描述一下JVM加载class文件的原理机制?
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时
查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保
这个类已经被加载、连接(验证、准备和解析)和初始化。
类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。
加载完成后,Class对象还不完整,所以此时的类还不可用。
当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引
用)三个步骤。最后JVM对类进行初始化,
包括:
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展
加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的
Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类
加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明
Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指
定的目录中记载类,是用户自定义加载器的默认父加载器。
79 、Java对象创建过程
1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在
后边讲)
2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”
3.将除对象头外的对象内存空间初始化为 0
4.对对象头进行必要设置
80 、简述Java的对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占
32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数
组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是 8 字节的整数倍( 8 字节对齐 )
81 、如何判断对象可以被回收
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对
象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链
相连时,则证明此对象是不可用的,不可达对象。
82 、JVM的永久代中会发生垃圾回收么
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信
息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据
区 (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
83 、垃圾收集算法
GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对
象,在标记完成后统一回收掉所有被标记的对象。
复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完
了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移
动,然后直接清理掉端边界以外的内存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当
的收集算法
84 、调优命令有哪些?
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、
JIT编译等运行数据。 - jmap,JVM Memory Map命令用于生成heap dump文件
- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生
成dump的分析结果后,可以在浏览器中查看 - jstack,用于生成java虚拟机当前时刻的线程快照。
- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数
85 、调优工具
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer
Tool)、GChisto。
-
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,
线程和类等的监控 -
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
-
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Javaheap分析工具,它可以帮助我们查找
内存泄漏和减少内存消耗 -
GChisto,一款专业分析gc日志的工具
86 、Minor GC与Full GC分别在什么时候发生?
新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
87 、你知道哪些JVM性能调优
设定堆内存大小
-Xmx:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC