JAVA八股文
1、Java中又哪几种方式来创建线程
1、集成Thread类,重写run方法,new对象调用start方法启动
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
}
public static void main(String[] args) {
ThreadDemo t1 = new ThreadDemo();
t1.start();
}
}
注意:重写run方法,启动时调用的是start方法
缺点:占用了继承名额
2、实现Runnable接口,实现run方法,new Thread对象,将实现类对象作为参数传入Thread对象中,然后通过Thread对象的start方法启动
public class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
}
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDemo2());
t1.start();
}
}
依然要用到Thread类,Thread类其实也实现了Runnable接口,本质上与第一种相似
可以使用匿名内部类 或者 Lambda的方式去实现,会更加方便
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
}
});
t2.start();
Thread t3 = new Thread(() -> System.out.println("线程:" + Thread.currentThread().getName() + "执行了"));
t3.start();
3、实现Callable,重写call方法,使用Thread + FutureTask才能启动
public class ThreadDemo3 implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new ThreadDemo3());
Thread t1 = new Thread(futureTask);
t1.start();
String result = futureTask.get();
System.out.println("线程的执行结果:" + result);
}
@Override
public String call() throws Exception {
System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
return "HelloWorld";
}
}
好处:能拿到线程的执行结果
还是要用到Thread对象来启动,所以本质上和1、2一样
4、线程池,调用execute方法执行线程任务,需要将Runnable的实现类对象作为线程任务传入进去
public class ThreadDemo4 implements Runnable {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.execute(new ThreadDemo4());
}
//程序不会自动结束,需要调用shutdown方法结束程序
executorService.shutdown();
}
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
}
}
总结,其实底层都是实现Runnable接口
2、为什么不建议使用Executors来创建线程池
Executors.newFixedThreadPool(10) 源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
发现创建的队列为 LinkedBlockingQueue ,是一个无界阻塞队列,如果使用该线程池执行任务,如果任务过多就会不断的添加到队列中,任务越多占用的内存就会愈多,最终可能耗尽内存,导致OOM。
Executors.newSingleThreadExecutor() 源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
同上,也是LinkedBlockingQueue,所以同样可能会耗尽内存。
总结:
除开又可能造成OOM之外,使用Executors创建线程池也不能自定义线程名称,不利于排查问题,所以建议直接使用ThreadPoolExecutor来定义线程池,这样可以灵活控制
3、线程池有哪几种状态?每种状态分别表示什么?
-
RUNNING
表示线程池正常运行,既能接受新任务,也会正常处理队列中的任务
-
SHUTDOWN
当调用线程池的 shutdown() 方法时,线程池旧会进入SHUTDOWN状态,表示线程池处于正在关闭状态,此状态下线程池不会接受新任务,但是会继续把队列中的任务处理完
-
STOP
当调用线程池的 shutdownnow() 方法时,线程池就进入STOP状态,表示线程池处于正在停止状态,此状态下线程池既不会接受新任务,也不会处理队列中的任务,并且正在运行的线程也会被中断。
-
TIDYING tidying
线程池中没有线程正在运行后,线程池的状态就会自动变为TIDYING,并且会调用 terminated() 方法,该方法是空方法,留给程序员进行扩展。
-
TERMINATED
terminated() 方法执行完之后,线程池状态就会变为 TERMINATED状态
4、Sychronized 和 ReentrantLock有哪些不同点?
Sychronized | ReentrantLock |
---|---|
Java中的一个关键字 | JDK提供的一个类 |
自动加锁与释放锁 | 需要手动加锁与释放锁 |
JVM层面的锁 | API层面的锁 |
非公平锁 | 手动指定公平锁或非公平锁 |
锁的是对象,锁信息保存在对象头中 | int类型的state属性来标识锁的状态 |
底层有锁升级过程(偏向锁->轻量级锁->重量级锁) | 没有锁升级过程trylock轻量级(自旋) 、lock重量级锁 |
5、ThreadLocal有哪些应用场景?底层是如果实现的?
-
ThreadLocal 是 Java 中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。
-
ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Nao的key为ThreadLocal对象,Map的value为需要缓存的值。
-
如果在线程池中使用 ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象回收,但线程池中的线程不会被回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏(给一份数据分配了内存,这份数据没有用,但是又被强引用,无法销毁),解决办法是,在使用了ThreadLocal对象后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
-
ThreadLocal经典的应用场景就是链接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
6、ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?
首先不管是公平锁还是非公平锁,底层实现都会使用AQS来进行排队,区别在于线程在使用lock()方法加锁时:
- 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队。
- 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁,竞争失败才会排队。
公平锁的底层执行流程:
非公平锁的底层执行流程:
另外,不管是公平锁还是非公平锁,一旦没有竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
7、Sychronized的锁升级过程是怎么样的?
- 偏向锁:在锁对象的对象头中记录一下当前获取到这把锁的线程ID,该线程下次如果又来获取这把锁就可以直接获取到了,也就是支持锁重入。
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就不会有唤醒线程的操作,阻塞和唤醒这两个操作都是需要操作系统去完成的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
todo:重点深挖
8、Tomcat中为什么要使用自定义类加载器?
一个Tomcat中会部署多个应用,而每个应用中都存在很多类,并且各个应用中的类都是独立的,全类名可能会相同,比如一个订单系统中可能存在 com.zyf.User类,一个库存系统中也可能存在com.zyf.User类,一个Tomcat不管内部部署了多少个应用,Tomcat启动之后就是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的 AppClassLoader
,那么就只能加载一个com.zyf.User类,这是由问题的,而在Tomcat中,会为部署的每个应用都生成一个类加载器实例,名字叫做WebAppClassLoader
,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义加载器实现了热加载功能。
9、JDK、JRE、JVM之间的区别
- JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等
- JRE( Java Runtime Environment) ,Java运行环境,用于运行Java的字节码文件。JRE中包括了JVM以及JVM工作所需要的类库,普通用户而只需要安装JRE来运行Java程序,而程序开发者必须安装JDK来编译、调试程序。
- JVM(Java Virtual Machine),Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件。
我们写Java代码,用txt就可以写,但是写出来的Java代码,想要运行,需要先编译成字节码,那就需要编译器,而JDK中就包含了编译器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是JVM(Java虚拟机),专门用来执行Java字节码的。
如果我们要开发Java程序,那就需要JDK,因为要编译Java源文件。
如果我们只想运行已经编译好的Java字节码文件,也就是*.class文件,那么就只需要JRE。
JDK中包含了JRE,JRE中包含了JVM。
另外,JVM在执行Java字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是有可能不一样的,所以就导致不同操作系统上的JVM是不一样的,所以我们在安装JDK时需要选择操作系统。
另外,JVM是用来执行Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运行,比如Apache Groovy, Scala and Kotlin 等等。
10、hashCode() 与 equals() 之间的关系
在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashCode来做一些提前的判断,比如:
- 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
- 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象
- 如果两个对象相等,那么他们的hashCode就一定相同
在Java的一些集合类的实现中,在比较两个对象是否相等时,会根据上面的原则,会先调用对象的hashCode()方法得到hashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同,如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals方法的实现会比较重,逻辑比较多,而hashCode()主要就是得到一个哈希值,实际上就一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode想比较一下。
所以我们就需要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法,一定要保证能遵守上述规则。
HashMap get方法源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
可以看到是先比较hash值相等再判断equals是否相等
11、String、StringBuffer、StringBuilder的区别
- String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
- StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高
12、泛型中extends和super的区别
- 表示包括T在内的任何T的子类
- 表示包括T在内的任何T的父类
注意:泛型只是编译期的限制