目录
13、为什么要用线程池?为什么多线程是必要的?线程池解决什么问题
18、执行execute()方法和submit()方法的区别是什么呢?
25、能不能给我简单介绍一下 AtomicInteger 类的原理
26、AQS(AbstractQueuedSynchronizer)原理
30、CyclicBarrier和CountDownLatch的区别
31、CountDownLatch和CyclicBarrier的区别?
42、ConcurrentHashMap是如何保证线程安全的?
44、ArrayBlockingQueue和LinkedBlockingQueue的区别?
2、新生代和老年代。对象如何进入老年代,新生代怎么变成老年代?
4、对象在什么时候可以被回收,调用finalize方法后一定会被回收吗?
11、GC一定会导致停顿吗,为什么一定要停顿?任意时候都可以GC吗还是在特定的时候?
3、MySql数据库在什么情况下出现死锁?产生死锁的四个必要条件?如何解决死锁?
4、现在发现sql查询很慢,如何分析哪里出了问题,应该如何优化?
10、建立了索引,索引就一定会被命中吗?或者说索引什么时候失效
16、COUNT()和 COUNT(1)的区别? COUNT(列名)和 COUNT()的区别?
7、Spring的配置方式,如何装配bean?bean的注入方法有哪些?
4、Java反射是什么?为什么要用反射,有什么好处,哪些地方用到了反射?
10、给你一个Person对象p,如何将该对象变成JSON表示?
10、Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别
一、线程知识
1、进程间通信的方式?
- 管道。分为几种管道。普通管道PIPE:单工,单向传输,只能在父子或者兄弟进程间使用;流管道,半双工,可双向传输,只能在父子或兄弟进程间使用;命名管道:可以在许多并不相关的进程之间进行通讯。
- 消息队列。消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号。用于通知接收进程某个事件已经发生
- 信号量。信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 共享内存。共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC(进程间通信) 方式,它往往与其他通信机制如信号量配合使用,来实现进程间的同步和通信。
- 套接字。可用于不同机器间的进程通信。
2、线程间通信的方式?
- 锁机制。包括互斥锁、条件变量、读写锁。互斥锁以排他方式防止数据被并发修改;读写锁允许多个线程同时读取,对写操作互斥;条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
- 信号量(Semaphore) 机制。包括无名线程信号量和命名线程信号量
- 信号(Signal)机制。类似进程间的信号处理
3、可以有几种方式实现生产者-消费者模型
- wait()/notify()
- await()/signal()
- BlockingQueue:其
take()
和和put()
方法就是阻塞的
4、讲一下Java内存模型
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
5、volatile和synchronized讲一下?
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
6、synchronized和重入锁的区别
- synchronized是JVM的内置锁,而重入锁是Java代码实现的。重入锁是synchronized的扩展,可以完全代替后者。重入锁的独有功能:
- 可以响应中断:主要由
lockInterruptibly()
实现,这是一个可以对中断进行响应的锁申请动作,锁中断可以避免死锁。 - 锁的申请可以有等待时限,用
tryLock(int second)
可以实现限时等待,如果超时还未获得锁会返回false,也防止了线程迟迟得不到锁时一直等待,可避免死锁。 - 公平锁,即锁的获得按照线程先来后到的顺序依次获得,不会产生饥饿现象。synchronized的锁默认是不公平的,重入锁可通过传入构造方法的参数实现公平锁。
- 重入锁可以绑定多个Condition条件,这些condition通过调用await/singal实现线程间通信。
7、synchronized作了哪些优化?
- 偏向锁:偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远也不需要再进行同步。偏向锁是在无竞争的情况下把整个同步都消除掉,CAS操作也没有了。适合于同一个线程请求同一个锁,不适用于不同线程请求同一个锁,此时会造成偏向锁失效。
- 轻量级锁:如果偏向锁失效,虚拟机不会立即挂起线程,会使用一种称为轻量级锁的优化手段,轻量级锁的加锁和解锁都是通过CAS操作完成的。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,表示其他线程抢先得到了锁,轻量级锁将膨胀为重量级锁。
- 自旋锁:锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力--自旋锁。如果共享数据的锁定状态只有很短的一段时间,为了这段时间去挂起和恢复线程(都需要转入内核态)并不值得,所以此时让后面请求锁的那个线程稍微等待一下,但不放弃处理器的执行时间。这里的等待其实就是执行了一个忙循环,这就是所谓的自旋。虚拟机会让当前线程做几个循环,若干次循环后如果得到了锁,就顺利进入临界区;如果还是没得到,这才将线程在操作系统层面挂起。
8、Java线程生命周期的状态?
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIME_WAITING
- TERMINATED
NEW表示刚刚创建的线程,此时线程还没有开始执行。调用 start()
后线程进入RUNNABLE状态,线程在执行过程中遇到synchronized同步块,就进入BLOCKED阻塞状态,此时线程暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待,区别是前者是无时间限制的等待,后者是有时限的等待。等待可以是执行 wait()
方法后等待 notify()
方法将其唤醒,也可以是通过 join()
方法等待的线程等待目标线程的执行结束。一旦等待了期望事件,线程再次执行,从等待状态变成RUNNABLE状态。线程执行结束后,进入TERMINATED状态。
9、被notify()唤醒的线程可以立即得到执行吗?
被notify唤醒的线程不是立刻可以得到执行的,因为 notify()
不会立刻释放锁, wait()
状态的线程也不能立刻获得锁;等到执行 notify()
的线程退出同步块后,才释放锁,此时其他处于 wait()
状态的线程才能获得该锁。
10、sleep、wait、yield的区别和联系?
- sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。调用sleep后不会释放锁。
- yield() 使得线程放弃CPU执行时间,但是不使线程阻塞(不释放锁),线程从运行状态进入就绪状态,随时可能再次分得 CPU 时间。有可能当某个线程调用了yield()方法暂停之后进入就绪状态,它又马上抢占了CPU的执行权,继续执行。
- wait()是Object的方法,会使线程进入阻塞状态,和sleep不同,wait会同时释放锁。wait/notify在调用之前必须先获得对象的锁。
11、Thread类中的start和run方法区别?
run方法只是一个普通方法调用,还是在调用它的线程里执行。
start才是开启线程的方法,run方法里面的逻辑会在新开的线程中执行。
12、Java中线程的创建方式有哪些?
- 继承Thread,重写
run()
方法 - 实现Runnable接口,重写
run()
方法,然后作为参数传给Thread - 实现Callable接口,重写
call()
方法,call方法有返回值。使用FutureTask包装Callable实现类,其中FutureTask实现了Runnable和Future接口,最后将FutureTask作为参数传入Thread中 - 由线程池创建并管理线程。
13、为什么要用线程池?为什么多线程是必要的?线程池解决什么问题
-
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
14、多线程是解决什么问题的
多线程可以同时运行多个过程,简单说下多线程开发的益处:
1.多线程开发可以将耗时操作放入子线程,将UI刷新加入主线程,防止页面卡顿。
2.在并发操作时使用多线程,如C/S架构的服务器端并发线程响应用户的请求。
3.在多核CPU系统中,使用线程可以提高程序响应速度,提高CPU和内存的利用率。
4.改善程序结构。将一个复杂的的进程分为多个线程,减少类之间的耦合。
5.将程序分块管理,方便程序的开发和维护。
6.可以随时停止任务。 可以分别设置各个任务的优先级以优化性能。
15、线程池的原理、线程池使用时的注意事项
虽然线程池是构建多线程应用程序的强大机制, 但使用它并不是没有风险的。 (1) 线程池的大小。 多线程应用并非线程越多越好, 需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。 一般来说, 如果代码结构合理的话, 线程数目与 CPU数量相适合即可。 如果线程运行时可能出现阻塞现象, 可相应增加池的大小; 如有必要可采用自适应算法来动态调整线程池的大小, 以提高 CPU 的有效利用率和系统的整体性能。使用ThreadPoolExecutor的构造方法创建 (2) 并发错误。 多线程应用要特别注意并发错误, 要从逻辑上保证程序的正确性, 注意避免死锁现象的发生。 (3) 线程泄漏。 这是线程池应用中一个严重的问题, 当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
16、Java中线程池怎么实现的,核心参数讲一讲?
- corePoolSize:指定了线程池中线程的数量;
- maximumPoolSize:线程池中的最大线程数量;
- keepAliveTime:当线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间;
- unit:上一个参数keepAliveTime的单位
- 任务队列,被提交但还未被执行额任务
- threadFactory:线程工厂,用于创建线程,一般用默认工厂即可。
- handler:拒绝策略。当任务太多来不及处理的时候,采用什么方法拒绝任务。
最重要的是任务队列和拒绝策略。
- 任务队列主要有ArrayBlockingQueue有界队列、LinkedBlockingQueue无界队列、SynchronousQueue直接提交队列。
- 使用ArrayBlockingQueue,当线程池中实际线程数小于核心线程数时,直接创建线程执行任务;当大于核心线程数而小于最大线程数时,提交到任务队列中;因为这个队列是有界的,当队列满时,在不大于最大线程的前提下,创建线程执行任务;若大于最大线程数,执行拒绝策略。
- 使用LinkedBlockingQueue时,当线程池中实际线程数小于核心线程数时,直接创建线程执行任务;当大于核心线程数而小于最大线程数时,提交到任务队列中;因为这个队列是有无界的,所以之后提交的任务都会进入任务队列中。newFixedThreadPool就采用了无界队列,同时指定核心线程和最大线程数一样。
- 使用SynchronousQueue时,该队列没有容量,对提交任务的不做保存,直接增加新线程来执行任务。newCachedThreadPool使用的是直接提交队列,核心线程数是0,最大线程数是整型的最大值,keepAliveTime是60s,因此当新任务提交时,若没有空闲线程都是新增线程来执行任务,不过由于核心线程数是0,当60s就会回收空闲线程。
当线程池中的线程达到最大线程数时,就要开始执行拒绝策略了。有如下几种
- 直接抛出异常
- 在调用者的线程中,运行当前任务
- 丢弃最老的一个请求,也就是将队列头的任务poll出去
- 默默丢弃无法处理的任务,不做任何处理
17、Executors 返回线程池对象的弊端
-
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
-
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
18、执行execute()方法和submit()方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用
get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
19、读写锁用过没?
ReadWriteLock即读写锁,它有两个方法如下,分别返回一个读锁和写锁,即读写锁分离。
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
20、自旋锁是什么,为什么要用自旋锁?自选锁的缺点?
就是在一个线程获取锁的时候,锁已经被其他线程获取,但是锁只会持续很短的一段时间,为此将线程挂起、恢复并不值得,因为挂起和恢复锁都会消耗很大的性能,假定该线程很快就能获取锁,于是让后面的线程进入一个忙循环,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环 。这就是自旋锁。
自旋锁的好处:不会使线程状态发生改变,即一直处于用户态,不会转入内核态(用户态和内核态的切换系统开销很大)。不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
自旋锁的缺点:一直占用CPU时间,如果锁被占用时间很短,自旋等待效果就很好,如果锁占用时间太长,自旋的线程只会白白消耗CPU资源。
21、进程和线程的区别?
进程是资源分配的最小单位,线程是进程的最小执行单位。进程是线程的容器,即进程可以包含多个线程,多个线程可以共享数据
22、线程的死锁指什么?如何检测死锁?如何解决死锁?
死锁是指两个或两个以上的线程在执行的过程中,互相占用对象想获得的资源不释放,造成了互相等待,结果所有的线程都无法向前。
死锁的检测:可以采用等待图(wait-for gragh)。采用深度优先搜索的算法实现,如果图中有环路就说明存在死锁。
解决死锁:
破坏锁的四个必要条件之一可以预防死锁
加锁顺序保持一致。不同的加锁顺序很可能导致死锁,比如哲学家问题:A先申请筷子1再申请筷子2,而B先申请筷子2再申请筷子1,最后谁也得不到一双筷子(同时拥有筷子1和筷子2)
撤消或挂起进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。
23、避免死锁
-
避免一个线程同时获得多个锁
-
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
-
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
-
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
24、可重入锁与非可重入锁的区别(未完善)
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
25、能不能给我简单介绍一下 AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
26、AQS(AbstractQueuedSynchronizer)原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS定义两种资源共享方式
-
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
-
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。
27、Semaphore(信号量)-允许多个线程同时访问
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。示例代码如下:
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
执行 acquire
方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release
方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 Semaphore经常用于限制获取某种资源的线程数量。
28、CountDownLatch (倒计时器)
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch 的两种典型用法
①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown()
,当计数器的值变为0时,在CountDownLatch上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch
对象,将其计数器初始化为 1 :new CountDownLatch(1)
,多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
29、CyclicBarrier(循环栅栏)
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await
方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier 的应用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
30、CyclicBarrier和CountDownLatch的区别
对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
31、CountDownLatch和CyclicBarrier的区别?
CountDownLatch强调一个线程等待其他所有线程,通过cdl.await()让当前线程等待在倒计数器上,每有一个线程执行完,cdl.countDown(),将计数减1,减到0时通知当前线程执行。简单的说就是一个线程等待,直到他所等待的其他线程都执行完成,当前线程才可以继续执行。
cyclicBarrier强调线程之间互相等待,只要有一个线程还没到来,所有线程会一起等待。可以传入一个Runnable作为计数完成要执行的任务。每有一个线程调用cyc.await()计数减1,减到0时会执行一次该Runnable。简单地说就是线程之间互相等待,等所有线程都准备好,即调用await()方法之后,执行一次Runnable,此时所有线程开始同时执行!
CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。
32、synchronized和lock区别
一个是基于JVM 一个是基于API Lock更加灵活 提供了获取锁可以被中断,超时获取锁、公平锁
33、悲观锁和乐观锁有什么区别
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized
和 ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
34、ReentranLock源码,设计原理,整体过程
35、如何批量执行异步任务
36、多线程同步和互斥有哪几种实现方法
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另外一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,每个线程访问时的排他性。当有若干个线程都要使用某一个共享资源时,任何时刻最多只允许一个线程去使用,其他线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可以分为两类:用户模式和内核模式
1、用户模式:原子操作,临界区
2、内核模式:事件、信号量、互斥量
内核模式就是利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换内核态,只在用户态完成操作
互斥就是加锁
同步就是通知
37、多线程就一定好吗?快吗??
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
38、上下文切换
当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
39、如何减少上下文切换
-
减少锁的使用。因为多线程竞争锁时会引起上下文切换。
-
使用CAS算法。这种算法也是为了减少锁的使用。CAS算法是一种无锁算法。
-
减少线程的使用。人物很少的时候创建大量线程会导致大量线程都处于等待状态。
-
使用协程。
40、CPU线程调度?
- 协同式线程调度:线程的执行时间以及线程的切换都是由线程本身来控制,线程把自己的任务执行完后,主动通知系统切换到另一个线程。
- 抢占式调度模式:线程的执行时间和切换都是由系统来分配和控制的。不过可以通过设置线程优先级,让优先级高的线程优先占用CPU。
从线程的角度来说,一个主动,一个被动(被系统把握)
Java虚拟机默认采用抢占式调度模型。
41、HashMap在多线程下有可能出现什么问题?
1.8以前会造成死循环:结构被破坏,指向混乱,产生循环链表。1.8已经解决
并发put,造成键值对丢失,如果两个线程同时读取到当前node,在链表尾部插入,先插入的线程是无效的,会被后面的线程覆盖掉。
42、ConcurrentHashMap是如何保证线程安全的?
JDK 7中使用的是分段锁,内部分成了16个Segment即分段,每个分段可以看作是一个小型的HashMap,每次put只会锁定一个分段,降低了锁的粒度:
-
首先根据key计算出一个hash值,找到对应的Segment
-
调用Segment的lock方法(Segment继承了重入锁),锁住该段内的数据,所以并没有锁住ConcurrentHashMap的全部数据
-
根据key计算出hash值,找到Segment中数组中对应下标的链表,并将该数据放置到该链表中
-
判断当前Segment包含元素的数量大于阈值,则Segment进行扩容(Segment的个数是不能扩容的,但是单个Segment里面的数组是可以扩容的)
JDK 8中使用了CAS+synchronized保证线程安全,也采取了数组+链表/红黑树的结构。
put时使用synchronized锁住了桶中链表的头结点。
有这么一个问题,ConcurrentHashMap,有三个线程,A先put触发了扩容,扩容时间很长,此时B也put会怎么样?此时C调用get方法会怎么样?C读取到的元素是旧桶中的元素还是新桶中的
A先触发扩容,ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,并没有去锁定新桶。
- 在某个桶的迁移过程中,别的线程想要对该桶进行put操作怎么办?一旦某个桶在迁移过程中了,必然要获取该桶的锁,所以其他线程的put操作要被阻塞。因此B被阻塞。
- 某个桶已经迁移完成(其他桶还未完成),别的线程想要对该桶进行put操作怎么办?该线程会首先检查是否还有未分配的迁移任务,如果有则先去执行迁移任务,如果没有即全部任务已经分发出去了,那么此时该线程可以直接对新的桶进行插入操作(映射到的新桶必然已经完成了迁移,所以可以放心执行操作)
- ConcurrentHashMap的get操作没有加锁,所以可以读取到值,不过是旧桶中的值。
43、ThreadLocal的作用和实现原理?
对于共享变量,一般采用同步的方式保证线程的安全,而ThreadLocal是为每一个线程都提供了一个线程内的局部变量,每个线程只能访问到属于它的副本。
从源码中可以看出:每一个线程拥有一个ThreadLocalMap,这个map存储了该线程拥有的所有局部变量。
set时先通过Thread.currentThread()获取当前线程,进而获取到当前线程的ThreadLocalMap,然后以ThreadLocal自己为key,要存储的对象为值,存到当前线程的ThreadLocalMap中。
get时也是先获得当前线程的ThreadLocalMap,以ThreadLocal自己为key,取出和该线程的局部变量。
44、ArrayBlockingQueue和LinkedBlockingQueue的区别?
ArrayBlockingQueue是基于数组的,是有界的阻塞队列,初始化时需要指定大小,并且不允许扩容
LinkBlockingQueue是基于链表的,是无界队列,初始化时不需要指定大小,可以无限扩容
ArrayBlockingQueue读写共用一把锁,因此put和take是互相阻塞的;而LinkedBlockingQueue使用了两把锁,一把putLock和一把takeLock,实现了锁分离,使得put和take写数据和读数据可以并发的进行。
45、synchronized内部实现原理?
同步方法使用ACC_SYNCHRONIZED标记符来实现同步。
同步代码块。JVM采用 monitorenter
、 monitorexit
两个指令来实现同步。
同步方法通过 ACC_SYNCHRONIZED
关键字隐式的对方法进行加锁。当线程要执行的方法被标注上 ACC_SYNCHRONIZED
时,需要先获得锁才能执行该方法。
同步代码块通过 monitorenter
和 monitorexit
执行来进行加锁。当线程执行到 monitorenter
的时候要先获得所锁,才能执行后面的方法。当线程执行到 monitorexit
的时候则要释放锁。
每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁,即可重入锁。换句话说,一个线程获取到锁之后可以无限次地进入该临界区。
- Synchronized原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
在Java中,为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
。而这两个字节码指令,在Java中对应的关键字就是 synchronized
。通过 monitorenter
和 monitorexit
指令,可以保证被 synchronized
修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用 synchronized
来保证方法和代码块内的操作是原子性的。
Synchronized可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 synchronized
修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
Synchronized有序性
有序性即程序执行的顺序按照代码的先后顺序执行。由于 synchronized
修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。
46、什么叫做锁的可重入?
同一个线程可以多次获取同一个锁,即一个线程获取到锁之后可以无限次地进入该临界区 (对于ReentrantLock来说,通过调用 lock.lock()
);当然锁的释放也需要相同次数的unlock()操作。注意:除了ReentrantLock,synchronized的锁也是可重入的。
二、JVM知识
1、什么是java虚拟机
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 java程序从源代码到运行过程:.Java源代码经过 JDK 的Javac 到 .class文件,.class文件经过JVM变成机器可执行的二进制机器码
2、新生代和老年代。对象如何进入老年代,新生代怎么变成老年代?
Java堆分为新生代和老年代。在新生代又被划分为Eden区,From Sruvivor和To Survivor区,比例是8:1:1,所以新生代可用空间其实只有其容量的90%。对象优先被分配在Eden区。
- 不过大对象比如长字符串、数组由于需要大量连续的内存空间,所以直接进入老年代。这是对象进入老年代的一种方式,
- 还有就是长期存活的对象会进入老年代。在Eden区出生的对象经过一次Minor GC会若存活,且Survivor区容纳得下,就会进入Survivor区且对象年龄加1,当对象年龄达到一定的值,就会进入老年代。
- 在上述情况中,若Survivor区不能容纳存活的对象,则会通过分配担保机制转移到老年代。
- 同年龄的对象达到suivivor空间的一半,大于等于该年龄的对象会直接进入老年代。
3、新生代的GC和老年代的GC?
发生在新生代的GC称为Minor GC,当Eden区被占满了而又需要分配内存时,会发生一次Minor GC,一般使用复制算法,将Eden和From Survivor区中还存活的对象一起复制到To Survivor区中,然后一次性清理掉Eden和From Survivor中的内存,使用复制算法不会产生碎片。
老年代的GC称为Full GC或者Major GC:
-
当老年代的内存占满而又需要分配内存时,会发起Full GC
-
调用System.gc()时,可能会发生Full GC,并不保证一定会执行。
-
在Minor GC后survivor区放不下,通过担保机制进入老年代的对象比老年代的内存空间还大,会发生Full GC;
-
在发生Minor GC之前,会先比较历次晋升到老年代的对象平均年龄,如果大于老年代的内存,也会触发Full GC。如果不允许担保失败,直接Full GC。
4、对象在什么时候可以被回收,调用finalize方法后一定会被回收吗?
在经过可达性分析后,到GC Roots不可达的对象可以被回收(但并不是一定会被回收,至少要经过两次标记),此时对象被第一次标记,并进行一次判断:
- 如果该对象没有调用过或者没有重写finalize()方法,那么在第二次标记后可以被回收了;
- 否则,该对象会进入一个FQueue中,稍后由JVM建立的一个Finalizer线程中去执行回收,此时若对象中finalize中“自救”,即和引用链上的任意一个对象建立引用关系,到GC Roots又可达了,在第二次标记时它会被移除“即将回收”的集合;如果finalize中没有逃脱,那就面临被回收。
因此finalize方法被调用后,对象不一定会被回收。
5、哪些对象可以作为GC Roots?
-
虚拟机栈中引用的对象
-
本地方法栈中引用的对象
-
方法区中类静态属性引用的对象(static)
-
方法区中常量属性引用的对象(final)
6、讲一讲垃圾回收算法?
- 标记-清除 一般用于老年代的垃圾回收
- 复制 一般用于新生代的垃圾回收
- 标记-整理(压缩) 一般用于老年代的垃圾回收
- 分代 新生代复制算法 老年代 标记清除或者标记整理 标记清除会产生空间碎片
7、介绍下类加载器和类加载过程?
类加载器:启动类加载器 扩展类加载器 应用程序加载器
类加载过程:加载、验证、准备、解析、初始化、使用、卸载
加载
-
通过一个类的全限定名获取定义该类的二进制字节流
-
将字节流表示的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成这个类的Class对象,作为方法区这个类的各种数据的访问入口
验证
-
文件格式验证:比如检查是否以魔数0xCAFEBABE开头
-
元数据验证:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。比如检查该类是否继承了被final修饰的类。
-
字节码验证,通过数据流和控制流的分析,验证程序语义是合法的、符合逻辑的。
准备。 为类变量(static)分配内存并设置默认值。比如static int a = 123在准备阶段的默认值是0,但是如果有final修饰,在准备阶段就会被赋值为123了。
解析。
将常量池中的符号引用替换成直接引用的过程。包括类或接口、字段、类方法、接口方法的解析。
初始化。
按照程序员的计划初始化类变量。如static int a = 123,在准备阶段a的值被设置为默认的0,而到了初始化阶段其值被设置为123。
8、什么是双亲委派模型,有什么好处?如何打破双亲委派模型?
类加载器之间满足双亲委派模型,即:除了顶层的启动类加载器外,其他所有类加载器都必须要自己的父类加载器。当一个类加载器收到类加载请求时,自己首先不会去加载这个类,而是不断把这个请求委派给父类加载器完成,因此所有的加载请求最终都传递给了顶层的启动类加载器。只有当父类无法完成这个加载请求时,子类加载器才会尝试自己去加载。
双亲委派模型的好处?使得Java的类随着它的类加载器一起具备了一种带有优先级的层次关系。Java的Object类是所有类的父类,因此无论哪个类加载器都会加载这个类,因为双亲委派模型,所有的加载请求都委派给了顶层的启动类加载器进行加载。所以Object类在任何类加载器环境中都是同一个类。安全。
如何打破双亲委派模型?例如线程:Thread.setContextClassLoader(),没有指定加载器的情况下会交给父类去加载。使用OSGi可以打破。OSGI(Open Services Gateway Initiative),或者通俗点说JAVA动态模块系统。可以实现代码热替换、模块热部署。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
9、说一说CMS和G1垃圾收集器?各有什么特点。
CMS:针对老年代的垃圾收集器,目的是尽可能地减少用户线程的停顿时间。
- 初始标记:标记从GC Roots能直接关联到的对象,会暂停用户线程
- 并发标记:即在堆中堆对象进行可达性分析,从GC Roots开始找出存活的对象,可以和用户线程一起进行
- 重复标记:修正并发标记期间因用户程序继续运作导致标记产生变动的对象的标记记录
- 并发清除:并发清除标记阶段中确定为不可达的对象
缺点:
- 由于是基于标记-清除算法,所以会产生空间碎片
- 无法处理浮动垃圾,即在清理期间由于用户线程还在运行,还会持续产生垃圾,而这部分垃圾还没有被标记,在本次无法进行回收。
- 对CPU资源敏感:会引发Full GC
G1:在使用G1收集器时,Java堆的内存划分为多个大小相等的独立区域,新生代和老年代不再是物理隔离。G1跟踪各个区域的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域。
- 初始标记:标记与GC Roots直接关联的对象,会暂停用户线程(Stop the World)
- 并发标记:并发从GC Roots开始找出存活的对象,可以和用户线程一起进行
- 最终标记:修正并发标记期间因用户程序继续运作导致标记产生变动的对象的标记记录
- 筛选回收:清除标记阶段中确定为不可达的对象,具体来说对各个区域的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
10、CMS和G1的区别?
G1堆的内存布局和其他垃圾收集器不同,它将整个Java堆划分成多个大小相等的独立区域(Region)。G1依然保留了分代收集,但是新生代和老年代不再是物理隔离的,它们都属于一部分Region的集合,因此仅使用G1就可以管理整个堆。
CMS基于标记-清除,会产生空间碎片;G1从整体看是标记-整理,从局部(两个Region之间)看是复制算法,不会产生空间碎片。
G1能实现可预测的停顿。
11、GC一定会导致停顿吗,为什么一定要停顿?任意时候都可以GC吗还是在特定的时候?
GC进行时必须暂停所有Java执行线程,这被称为Stop The World。为什么要停顿呢?因为可达性分析过程中不允许对象的引用关系还在变化,否则可达性分析的准确性就无法得到保证。所以需要STW以保证可达性分析的正确性。
程序执行时并非在所有地方都能停顿下来开始GC,只有在“安全点”才能暂停。安全点指的是:HotSpot没有为每一条指令都生成OopMap(Ordinary Object Pointer),而是在一些特定的位置记录了这些信息。这些位置就叫安全点。
12、与垃圾回收相关的JVM参数:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
三、数据库
1、数据库设计的三大范式?
第一范式1NF: 数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性。如订单信息列为orderInfo = "DD1024 2018.5.18",必须拆分为orderId和orderTime。
第二范式2NF: 在满足第一范式的基础上,表中的所有列都必需依赖于主键(和主键有关系),其他和主键没有关系的列可以拆分出去。通俗点说就是:一个表只描述一件事情。比如order表中有orderId、orderTime、userId和userName,只有前两列依赖于订单表,后两列需要拆分到user表中。
第三范式3NF: 在满足第二范式的基础上,要求数据不能有传递关系。表中的每一列都要与主键直接相关,而不是间接相关(表中的每一列只能依赖于主键)。比如order表中有orderId、orderTime、userId和userName,根据orderId可以查出userId,根据userId又可以查出userName,这就是数据的传递性,完全可以只留下userId这一列。
2、MySql的事务隔离级别?推荐使用哪种?
- 读未提交:允许脏数据、不可重复读、幻读
- 读已提交:允许不可重复读、幻读
- 可重复读:允许幻读
- 串行化:最严格 都不允许
事务四大特性ACID :原子性、持久性、隔离性、一致性
- 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency):事务开始前和结束后,数据的完整性约束没有被破环。比如A向B转了钱,转账前后钱的总数不变。
- 隔离性(Isolation):多个用户并发访问数据数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间的数据相互隔离。比如事务A和事务B都修改同一条记录,这条记录就会被重复修改或者后者会覆盖前者的修改记录。
- 持久性(Durability):事务完成后,事务对数据库的更新被保存到数据库,其结果是永久的
- 脏读:事务A读取到了事务B修改但未提交的数据。如果此时B回滚到修改之前的状态,A就读到了脏数据。
- 不可重复读:事务A多次读取同一个数据,此时事务B在A读取过程中对数据修改并提交了,导致事务A在同一个事务中多次读取同一数据而结果不同。
- 幻读:事务A对表进行修改,这个修改涉及到表中所有的行,但此时事务B新插入了一条数据,事务A就会发现居然还有数据没有被修改,就好像发生幻觉一样。
脏读是读取到事务未提交的数据,
不可重复度读读取到的是提交后的数据,只不过在一次事务中读取结果不一样。
不可重复读侧重于修改,幻读侧重于新增或删除。
解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
所以一般使用MySQL默认的可重复读即可。MVCC(多版本并发控制)使用undo_log使得事务可以读取到数据的快照(某个历史版本),从而实现了可重复读。MySQL采用Next-Key Lock算法,对于索引的扫描不仅是锁住扫描到的索引,还锁住了这些索引覆盖的范围,避免了不可重复读和幻读的产生。
3、MySql数据库在什么情况下出现死锁?产生死锁的四个必要条件?如何解决死锁?
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象,若无外力作用两个事务都无法推进,这样就产生了死锁。下去 死锁的四个必要条件:
互斥条件
请求和保持条件
不可剥夺条件
环路等待条件
解决死锁的方法:
-
加锁顺序保持一致。不同的加锁顺序很可能导致死锁,比如哲学家问题:A先申请筷子1在申请筷子2,而B先申请筷子2在申请筷子1,最后谁也得不到一双筷子(同时拥有筷子1和筷子2)
-
超时,为其中一个事务设置等待时间,若超过这个阈值事务就回滚,另一个等待的事务就能得以继续执行。
-
及时检测出死锁,回滚undo量最小的事务。一般是采用等待图(wait-for gragh)。采用深度优先搜索的算法实现,如果图中有环路就说明存在死锁。
4、现在发现sql查询很慢,如何分析哪里出了问题,应该如何优化?
开启慢查询,查找哪些sql语句执行得慢。
使用explain查看语句的执行计划,比如有没有使用到索引,是否启用了全表扫描等。
查询慢,很大可能是因为没有使用索引或者索引没有被命中。
还有其他的原因,比如发生了死锁,硬件、网速等原因。
优化手段:为相关列添加索引,并且确保索引可以被命中。优化sql语句的编写。
5、索引的好处?
索引是对数据库表中一个或多个列的值进行排序的结构。MySql中索引是B+树,在查找时可以利用二分查找等高效率的查找方式,以O(lg n)的时间找到。因此索引可以加快查询速度。
6、哪些情况需要建立索引?
- 查询经常用到的列
- where条件中经常出现的列
- 主键和外键
- 经常需要排序、分组、联合操作的列
7、哪些情况不适合建立索引?
- 查询中很少使用的字段
- 数值太少的字段
- 唯一性不太差的字段
- 更新频繁的字段
- 不会出现在where后的字段
- 索引适合建立在小字段上,text和blob等大字段不适合建立索引
8、索引的最左匹配原则了解吗?
建了一个(a,b,c)的联合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,但是有时在条件查询时只会匹配到a或者(a, b)而不会匹配到(a, b, c)。
9、如何建立复合索引,可以使sql语句能尽可能匹配到索引?
-
等于条件的索引放在前面(最左),范围查询放在后面。
a=1AND b=2AND c>3AND d=4
,建立(a, b, d, c)就是不错的选择; -
先过滤后排序(ORDER BY)如
SELECT*FROM t WHERE c=100 and d='xyz'ORDER BY b
建立(c, d, b)联合索引就是不错的选择 -
对于索引列的查询,一般不建议使用LIKE操作,像
LIKE'%abc'
这样的不能命中索引;不过LIKE'abc%'
可以命中索引。
10、建立了索引,索引就一定会被命中吗?或者说索引什么时候失效
-
使用了
not in,<>,!=
则不会命中索引。注:<>
是不等号 -
innoDB引擎下,若使用OR,只有前后两个列都有索引才能命中(执行查询计划,type是index_merge),否则不会使用索引。
-
模糊查询中,通配符在最前面时,即
LIKE'%abc'
这样不能命中索引 -
对列进行函数运算的情况(如 where md5(password) = "xxxx")
-
联合索引中,遇到范围查询时,其后的索引不会被命中
-
存了数字的char或varchar类型,常见的如用字符串表示的手机号,在查询时不加引号,则不会命中(如where phone=‘13340456789’能命中,where phone=13340456789不能命中)
-
当数据量小时,MySQL发现全表扫描反而比使用索引查询更快时不会使用索引。
11、为什么要使用联合索引?
-
减少开销。建了一个(a,b,c)的联合索引,相当于建了(a),(a,b),(a,b,c)三个索引
-
覆盖索引。减少了随机IO操作。同样的有复合索引(a,b,c),如果有如下的sql:
select a,b,c from table where a=1 and b = 1
。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作 -
效率高。索引列越多,通过索引筛选出的数据越少。比如有1000W条数据的表,有如下sql:select * from table where a = 1 and b =2 and c = 3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W*10%=100w 条数据,然后再回表从100w条数据中找到符合b=2 and c= 3的数据,然后再排序,再分页;如果是复合索引,通过索引筛选出1000w *10% *10% *10%=1w,然后再排序、分页。
12、既然索引可以加快查询速度,索引越多越好是吗?
大多数情况下索引能大幅度提高查询效率,但数据的变更(增删改)都需要维护索引,因此更多的索引意味着更多的维护成本和更多的空间 (一本100页的书,却有50页目录?)而且过小的表,建立索引可能会更慢
13、主键和唯一索引的区别?
- 主键是一种约束,唯一索引是索引,一种数据结构。
- 主键一定是唯一索引,唯一索引不一定是主键。
- 一个表中可以有多个唯一索引,但只能有一个主键。
- 主键不允许空值,唯一索引允许。
- 主键可以做为外键,唯一索引不行;
14、聚集索引与非聚集索引的区别?
- 对于聚集索引,表记录的排列顺序和与索引的排列顺序是一致的;非聚集索引不是
- 聚集索引就是按每张表的主键构造一棵B+树,每张表只能拥有一个聚集索引;一张表可以有多个非聚集索引
- 聚集索引的叶子结点存放的是整张表的行记录数据;非聚集索引的叶子结点并不包含行记录的全部数据,除了包含键值还包含一个书签——即相应行数据的聚集索引键。因此通过非聚集索引查找时,先根据叶子结点的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。
15、InnoDB和MyISAM引擎的区别?
- InnoDB支持事务,MyISAM不支持
- InnoDB是行锁设计,MyISAM是表锁设计
- InnoDB支持外键,MyISAM不支持
- InnoDB采用聚集的方式,每张表按照主键的顺序进行存放。如果没有主键,InnoDB会为每一行生成一个6字节的ROWID并以此为主键;MyISAM可以不指定主键和索引
- InnoDB没有保存表的总行数,因此查询行数时会遍历整表;而MyISAM有一个变量存储可表的总行数,查询时可以直接取出该值
- InnoDB适合联机事务处理(OLTP),MyISAM适合联机分析处理(OLAP)
16、COUNT()和 COUNT(1)的区别? COUNT(列名)和 COUNT()的区别?
COUNT(*)
和 COUNT(1)
没区别。 COUNT(列名)
和 COUNT(*)
区别在于前者不会统计列为NULL的数据,后者会统计。
17、数据库中悲观锁和乐观锁讲一讲?
悲观锁:总是假设在并发下会出现问题,即假设多个事务对同一个数据的访问会产生冲突。当其他事务想要访问数据时,会在临界区提前加锁,需要将其阻塞挂起。比如MySQL中的排他锁(X锁)、和共享锁(S锁)
乐观锁: 总是假设任务在并发下是安全的,即假设多个事务对同一个数据的访问不会发生冲突,因此不会加锁,就对数据进行修改。当遇到冲突时,采用CAS或者版本号、时间戳的方式来解决冲突。数据库中使用的乐观锁是版本号或时间戳。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,不用加锁就尝试对数据进行修改,在修改之前先检查一下版本号,真正提交事务时,再检查版本号有,如果不相同说明已经被其他事务修改了,可以选择回滚当前事务或者重试;如果版本号相同,则可以修改。
提一下乐观锁和MVCC的区别,其实MVCC也利用了版本号,和乐观锁还是能扯上些关系。
MVCC主要解决了读-写的阻塞,因为读只能读到数据的历史版本(快照);OCC主要解决了写-写的阻塞,多个事务对数据进行修改而不加锁,更新失败的事务可以选择回滚或者重试。
18、覆盖索引是什么?
如果一个索引包含(或覆盖)所有需要查询的字段的值,即只需扫描索引而无须回表,这称为“覆盖索引”。
19、MySQL中JOIN和UNION什么区别?
UNION 操作符用于合并两个或多个 SELECT 语句的结果集。UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相同的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。
JOIN用于连接两个有关联的表,筛选两个表中满足条件(ON后的条件)的行记录得到一个结果集。从结果集中SELECT的字段可以是表A或者表B中的任意列。
JOIN常用的有LEFT JOIN、RIGHT JOIN、INNER JOIN。
- LEFT JOIN会以左表为基础,包含左表的所有记录,以及右表中匹配ON条件的记录,对于未匹配的列,会以NULL表示。
- LEFT JOIN会以右表为基础,包含右表的所有记录,以及左表匹配ON条件的记录,对于未匹配的列,会以NULL表示。
- INNER JOIN,产生两个表的交集(只包含满足ON条件的记录)
- FULL OUTER JOIN 产生两个表的并集
20、WHERE和HAVING的区别?
- WHERE过滤的是行,HAVING过滤分组。
- WHERE能完成的,都可以用HAVING(只是有时候没必要)
- WHERE在分组前对数据进行过滤,HAVING在分组后对数据进行过滤
- WHERE后不能接聚合函数,HAVING后面通常都有聚合函数
21、SQL注入是什么,如何防止?
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。
比如在登录界面,如果用户名填入 'xxx'OR1=1--
就能构造下面的SQL语句,因为OR 1=1,password被注释掉,因此无论name和password填入什么都能登录成功。
SELECT * FROM USER WHERE NAME='xxx' OR 1=1 -- and password='xxx';
使用PrepareStatement,可以防止sql注入攻击,sql的执行需要编译,注入问题之所以出现,是因为用户填写 sql语句参与了编译。使用PrepareStatement对象在执行sql语句时,会分为两步,第一步将sql语句 "运送" 到mysql上预编译,再回到java端拿到参数运送到mysql端。预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该sql语句的语法结构了。用户填写的 sql语句,就不会参与编译,只会当做参数来看。从而避免了sql注入问题。
四、Spring知识
1、Spring有什么好处(特性),怎么管理对象的?
- IOC:Spring的IOC容器,将对象之间的创建和依赖关系交给Spring,降低组件之间的耦合性。即Spring来控制对象的整个生命周期。其实就是平常说的DI或者IOC。
- AOP:面向切面编程。可以将应用各处的功能分离出来形成可重用的组件。核心业务逻辑与安全、事务、日志等这些非核心业务逻辑分离,使得业务逻辑更简洁清晰。
- 使用模板消除了样板式的代码。比如使用JDBC访问数据库。
- 提供了对像关系映射(ORM)、事务管理、远程调用和Web应用的支持。
Spring使用IOC容器创建和管理对象,比如在XML中配置了类的全限定名,然后Spring使用反射+工厂来创建Bean。BeanFactory是最简单容器,只提供了基本的DI支持,ApplicationContext基于BeanFactory创建,提供了完整的框架级的服务,因此一般使用应用上下文。
2、什么是IOC?
IOC(Inverse of Control)即控制反转。可以理解为控制权的转移。传统的实现中,对象的创建和依赖关系都是在程序进行控制的。而现在由Spring容器来统一管理、对象的创建和依赖关系,控制权转移到了Spring容器,这就是控制反转。
3、什么是DI?DI的好处是什么?
DI(Dependency Injection)依赖注入。对象的依赖关系由负责协调各个对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系。通俗点说就是Spring容器为对象注入外部资源,设置属性值。DI的好处是使得各个组件之间松耦合,一个对象如果只用接口来表明依赖关系,这种依赖可以在对象毫不知情的情况下,用不同的具体类进行替换。
IOC和DI其实是对同一种的不同表述。
4、什么是AOP,AOP的好处?
AOP(Aspect-Orientid Programming)面向切面编程,可以将遍布在应用程序各个地方的功能分离出来,形成可重用的功能组件。系统的各个功能会重复出现在多个组件中,各个组件存在于核心业务中会使得代码变得混乱。使用AOP可以将这些多处出现的功能分离出来,不仅可以在任何需要的地方实现重用,还可以使得核心业务变得简单,实现了将核心业务与日志、安全、事务等功能的分离。
具体来说,散布于应用中多处的功能被称为横切关注点,这些横切关注点从概念上与应用的业务逻辑是相分离的,但是又常常会直接嵌入到应用的业务逻辑中,AOP把这些横切关注点从业务逻辑中分离出来。安全、事务、日志这些功能都可以被认为是应用中的横切关注点。
通常要重用功能,可以使用继承或者委托的方式。但是继承往往导致一个脆弱的对像体系;委托带来了复杂的调用。面向切面编程仍然可以在一个地方定义通用的功能,但是可以用声明的方法定义这个功能要在何处出现,而无需修改受到影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(Aspect)。好处在于:
-
每个关注点都集中在一个地方,而非分散在多处代码中;
-
使得业务逻辑更简洁清晰,因为这样可以只关注核心业务,次要的业务被分离成关注点转移到切面中了。
AOP术语介绍
通知:切面所做的工作称为通知。通知定义了切面是什么,以及在何时使用。Spring切面可以应用5种类型的通知
-
前置通知(Before):在目标方法被调用之前调用通知功能;
-
后置通知(After):在目标方法被调用或者抛出异常之后都会调用通知功能;
-
返回通知(After-returning):在目标方法成功执行之后调用通知;
-
异常通知(After-throwing):在目标方法抛出异常之后调用通知;
-
环绕通知(Around):通知包裹了被通知的方法,在目标方法被调用之前和调用之后执行自定义的行为。
连接点:可以被通知的方法
切点:实际被通知的方法
切面:即通知和切点的结合,它是什么,在何时何处完成其功能。
引入:允许向现有的类添加新方法或属性,从而可以在无需修改这些现有的类情况下,让它们具有新的行为和状态。
织入:把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
-
编译期,切面在目标类编译时织入。
-
类加载期,切面在目标类加载到JVM时被织入。
-
运行期,切面在应用运行的某个时刻被织入,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring AOP构建在动态代理基础之上,所以Spring对AOP的支持仅限于方法拦截。
Spring的切面是由包裹了目标对象的代理类实现的。代理类封装了目标类,并拦截被通知方法的调用,当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。其实切面只是实现了它们所包装bean相同接口的代理。
5、AOP的实现原理:Spring AOP使用的动态代理。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
Spring使用动态代理,代理类封装了目标类,当代理拦截到方法调用时,在调用目标bean的方法之前,会执行切面逻辑。
6、Spring的生命周期?
Spring创建、管理对象。Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期。
- 实例化:Spring对bean进行实例化
- 填充属性:Spring将值和bean的引用注入到bean对应的属性中
- 调用BeanNameAware的setBeanName()方法:若bean实现了BeanNameAware接口,Spring将bean的id传递给setBeanName方法
- 调用BeanFactoryAware的setBeanFactory()方法:若bean实现了BeanFactoryAware接口,Spring调用setBeanFactory方法将BeanFactory容器实例传入
- 调用ApplicationContextAware的setApplicationContext方法:如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext方法将bean所在的应用上下文传入
- 调用BeanPostProcessor的预初始化方法:如果bean实现了BeanPostProcessor,Spring将调用它们的叛postProcessBeforeInitialization方法
- 调用InitalizingBean的afterPropertiesSet方法:如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet方法
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialzation方法
- 此时bean已经准备就绪,可以被应用程序使用,它们将一直驻留在应用杀死那个下文中,直到该应用的上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destroy方法。
7、Spring的配置方式,如何装配bean?bean的注入方法有哪些?
-
XML配置,如
<beanid="">
-
Java配置即JavaConfig,使用
@Bean
注解 -
自动装配,组件扫描(component scanning)和自动装配(autowiring),
@ComponentScan
和@AutoWired
注解
bean的注入方式有:
-
构造器注入
-
属性的setter方法注入
-
接口注入
推荐对于强依赖使用构造器注入,对于弱依赖使用属性注入。
8、bean的作用域?
-
单例(Singleton):在整个应用中,只创建bean一个实例。
-
原型(Prototype):每次注入或通过Spring应用上下文获取时,都会创建一个新的bean实例。
-
会话(Session):在Web应用中,为每个会话创建一个bean实例。
-
请求(Request):在Web应用中,为每个请求创建一个bean实例。
默认情况下Spring中的bean都是单例的。
9、Spring中涉及到哪些设计模式?
-
工厂方法模式。在各种BeanFactory以及ApplicationContext创建中都用到了;
-
单例模式。在创建bean时用到,Spring默认创建的bean是单例的;
-
代理模式。在AOP中使用Java的动态代理;
-
策略模式。比如有关资源访问的Resource类
-
模板方法。比如使用JDBC访问数据库,JdbcTemplate。
-
观察者模式。Spring中的各种Listener,如ApplicationListener
-
装饰者模式。在Spring中的各种Wrapper和Decorator
-
适配器模式。Spring中的各种Adapter,如在AOP中的通知适配器AdvisorAdapter
10、MyBatis和Hibernate的区别和应用场景?
Hibernate :是一个标准的ORM(对象关系映射) 框架; SQL语句是自己生成的,程序员不用自己写SQL语句。因此要对SQL语句进行优化和修改比较困难。适用于中小型项目。
MyBatis: 程序员自己编写SQL, SQL修改和优化比较自由。 MyBatis更容易掌握,上手更容易。主要应用于需求变化较多的项目,如互联网项目等。
五、Java基础
1、静态编译和动态编译
-
静态编译:在编译时确定类型,绑定对象
-
动态编译:运行时确定类型,绑定对象
2、Java中的错误和异常?
Java中的所有异常都是Throwable的子类对象,Error类和Exception类是Throwable类的两个直接子类。
Error:包括一些严重的、程序不能处理的系统错误类。这些错误一般不是程序造成的,比如StackOverflowError和OutOfMemoryError。
Exception:异常分为运行时异常和检查型异常。
-
检查型异常要求必须对异常进行处理,要么往上抛,要么try-catch捕获,不然不能通过编译。这类异常比较常见的是IOException。
-
运行时异常,可处理可不处理,在编译时可以通过,异常在运行时才暴露。比如数组下标越界,除0异常等。
3、Java的集合类框架介绍一下?
首先接口Collection和Map是平级的,Map没有实现Collection。
Map的实现类常见有HashMap、TreeMap、LinkedHashMap和HashTable等。其中HashMap使用散列法实现,低层是数组,采用链地址法解决哈希冲突,每个数组的下标都是一条链表,当长度超过8时,转换成红黑树。TreeMap使用红黑树实现,可以按照键进行排序。LinkedHashMap的实现综合了HashMap和双向链表,可保证以插入时的顺序(或访问顺序,LRU的实现)进行迭代。HashTable和HashMap比,前者是线程安全的,后者不是线程安全的。HashTable的键或者值不允许null,HashMap允许。
Collection的实现类常见的有List、Set和Queue。List的实现类有ArrayList和LinkedList以及Vector等,ArrayList就是一个可扩容的对象数组,LinkedList是一个双向链表。Vector是线程安全的(ArrayList不是线程安全的)。Set的里的元素不可重复,实现类常见的有HashSet、TreeSet、LinkedHashSet等,HashSet的实现基于HashMap,实际上就是HashMap中的Key,同样TreeSet低层由TreeMap实现,LinkedHashSet低层由LinkedHashMap实现。Queue的实现类有LinkedList,可以用作栈、队列和双向队列,另外还有PriorityQueue是基于堆的优先队列。
4、Java反射是什么?为什么要用反射,有什么好处,哪些地方用到了反射?
反射:允许任意一个类在运行时获取自身的类信息,并且可以操作这个类的方法和属性。这种动态获取类信息和动态调用对象方法的功能称为Java的反射机制。
反射的核心是JVM在运行时才动态加载类或调用方法/访问属性。它不需要事先(写代码的时候或编译期)知道运行对象是谁,如 Class.ForName()
根本就没有指定某个特定的类,完全由你传入的类全限定名决定,而通过new的方式你是知道运行时对象是哪个类的。 反射避免了将程序“写死”。
反射可以降低程序耦合性,提高程序的灵活性。new是造成紧耦合的一大原因。比如下面的工厂方法中,根据水果类型决定返回哪一个类。
public class FruitFactory {
public Fruit getFruit(String type) {
Fruit fruit = null;
if ("Apple".equals(type)) {
fruit = new Apple();
} else if ("Banana".equals(type)) {
fruit = new Banana();
} else if ("Orange".equals(type)) {
fruit = new Orange();
}
return fruit; }}
class Fruit {}
class Banana extends Fruit {}
class Orange extends Fruit {}
class Apple extends Fruit {}
但是我们事先并不知道之后会有哪些类,比如新增了Mango,就需要在if-else中新增;如果以后不需要Banana了就需要从if-else中删除。这就是说只要子类变动了,我们必须在工厂类进行修改,然后再编译。如果用反射呢?
public class FruitFactory {
public Fruit getFruit(String type) {
Fruit fruit = null;
try {
fruit = (Fruit) Class.forName(type).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return fruit;
}
}
class Fruit {}
class Banana extends Fruit {}
class Orange extends Fruit {}
class Apple extends Fruit {}
如果再将子类的全限定名存放在配置文件中。
class-type=com.fruit.Apple
那么不管新增多少子类,根据不同的场景只需修改文件就好了,上面的代码无需修改代码、重新编译,就能正确运行。
哪些地方用到了反射?举几个例子
-
加载数据库驱动时
-
Spring的IOC容器,根据XML配置文件中的类全限定名动态加载类
-
工厂方法模式中(如上)
5、反射机制优缺点
-
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
-
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
6、说说你对面向对象、封装、继承、多态的理解?
-
封装:隐藏实现细节,明确标识出允许外部使用的所有成员函数和数据项。 防止代码或数据被破坏。
-
继承:子类继承父类,拥有父类的所有功能,并且可以在父类基础上进行扩展。实现了代码重用。子类和父类是兼容的,外部调用者无需关注两者的区别。
-
多态:一个接口有多个子类或实现类,在运行期间(而非编译期间)才决定所引用的对象的实际类型,再根据其实际的类型调用其对应的方法,也就是“动态绑定”。
Java实现多态有三个必要条件:继承、重写、向上转型。
-
继承:子类继承或者实行父类
-
重写:在子类里面重写从父类继承下来的方法
-
向上转型:父类引用指向子类对象
public class OOP {
public static void main(String[] args) {
/* * 1. Cat继承了Animal
* 2. Cat重写了Animal的eat方法
* 3. 父类Animal的引用指向了子类Cat。
* 在编译期间其静态类型为Animal;在运行期间其实际类型为Cat,因此animal.eat()将选择Cat的eat方法而不是其他子类的eat方法 */
Animal animal = new Cat();
printEating(animal);
}
public static void printEating(Animal animal) {
animal.eat();
}
}
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
@Override
void eat() {
System.out.println("Cat eating...");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog eating...");
}
}
实现不可变对象的策略?比如JDK中的String类。
-
不提供setter方法(包括修改字段、字段引用到的的对象等方法)
-
将所有字段设置为final、private
-
将类修饰为final,不允许子类继承、重写方法。可以将构造函数设为private,通过工厂方法创建。
-
如果类的字段是对可变对象的引用,不允许修改被引用对象。 1)不提供修改可变对象的方法;2)不共享对可变对象的引用。对于外部传入的可变对象,不保存该引用。如要保存可以保存其复制后的副本;对于内部可变对象,不要返回对象本身,而是返回其复制后的副本。
7、Java序列话中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。功能是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
8、==和equals的区别?
== 对于基本类型,比较值是否相等,对于对象,比较的是两个对象的地址是否相同,即是否是指相同一个对象。
equals的默认实现实际上使用了==来比较两个对象是否相等,但是像Integer、String这些类对equals方法进行了重写,比较的是两个对象的内容是否相等。
对于Integer,如果依然坚持使用==来比较,有一些要注意的地方。对于[-128,127]区间里的数,有一个缓存。因此
Integer a = 127;Integer b = 127;System.out.println(a == b); // true
Integer a = 128;Integer b = 128;System.out.println(a == b); // false
// 不过采用new的方式,a在堆中,这里打印false
Integer a = new Integer(127);
Integer b = 127;
System.out.println(a == b);
对于String,因为它有一个常量池。所以
String a = "gg" + "rr";String b = "ggrr";System.out.println(a == b); // true
// 当然牵涉到new的话,该对象就在堆上创建了,所以这里打印falseString a = "gg" + "rr";String b = new String("ggrr");System.out.println(a == b);
9、接口和抽象类的区别?
-
接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
-
接口中的实例变量默认是final类型的,而抽象类中则不一定
-
一个类可以实现多个接口,但最多只能实现一个抽象类
-
一个类实现接口的话要实现接口的所有方法,而抽象类不一定
-
接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
10、给你一个Person对象p,如何将该对象变成JSON表示?
本质是考察Java反射,因为要实现一个通用的程序。实现可能根本不知道该类有哪些字段,所以不能通过get和set等方法来获取键-值。使用反射的getDeclaredFields()可以获得其声明的字段。如果字段是private的,需要调用该字段的 f.setAccessible(true);
,才能读取和修改该字段。
import java.lang.reflect.Field;import java.util.HashMap;
public class Object2Json {
public static class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
public static void main(String[] args) throws IllegalAccessException {
Person p = new Person(18, "Bob");
Class<?> classPerson = p.getClass();
Field[] fields = classPerson.getDeclaredFields();
HashMap<String, String> map = new HashMap<>();
for (Field f: fields) {
// 对于private字段要先设置accessible为true
f.setAccessible(true);
map.put(String.valueOf(f.getName()), String.valueOf(f.get(p)));
}
System.out.println(map);
}
}
得到了map,再弄成JSON标准格式就好了。
11、JDBC中sql查询的完整过程?操作事务呢?
ResultSet维持一个指向当前行记录的cursor(游标)指针**
-
注册驱动
-
建立连接
-
准备sql语句
-
执行sql语句得到结果集
-
对结果集进行遍历
-
关闭结果集(ResultSet)
-
关闭statement
-
关闭连接(connection)
由于JDBC默认自动提交事务,每执行一个update ,delete或者insert的时候都会自动提交到数据库,无法回滚事务。所以若需要实现事务的回滚,要指定 setAutoCommit(false)
。
-
true
:sql命令的提交(commit)由驱动程序负责 -
false
:sql命令的提交由应用程序负责,程序必须调用commit或者rollback方法
JDBC操作事务的格式如下,在捕获异常中进行事务的回滚。
try {
con.setAutoCommit(false);//开启事务… …. … con.commit();//try的最后提交事务
} catch() {
con.rollback();//回滚事务
}
12、实现单例,有哪些要注意的地方?
就普通的实现方法来看。
-
不允许在其他类中直接new出对象,故构造方法私有化
-
在本类中创建唯一一个static实例对象
-
定义一个public static方法,返回该实例
public class SingletonImp { // 饿汉模式
private static SingletonImp singletonImp = new SingletonImp(); // 私有化 (private)该类的构造函数
private SingletonImp() { }
public static SingletonImp getInstance() {
return singletonImp;
}
}
饿汉模式:线程安全,不能延迟加载。
public class SingletonImp4 {
private static volatile SingletonImp4 singletonImp4;
private SingletonImp4() {}
public static SingletonImp4 getInstance() {
if (singletonImp4 == null) {
synchronized (SingletonImp4.class) {
if (singletonImp4 == null) {
singletonImp4 = new SingletonImp4();
}
}
}
return singletonImp4;
}
}
双重检测锁+volatile禁止语义重排。因为 singletonImp4=newSingletonImp4();
不是原子操作。
public class SingletonImp6 {
private SingletonImp6() {}
// 专门用于创建Singleton的静态类
private static class Nested {
private static SingletonImp6 singletonImp6 = new SingletonImp6();
}
public static SingletonImp6 getInstance() {
return Nested.singletonImp6;
}
}
静态内部类,可以实现延迟加载。
最推荐的是单一元素枚举实现单例。
-
写法简单
-
枚举实例的创建默认就是线程安全的
-
提供了自由的序列化机制。面对复杂的序列或反射攻击,也能保证是单例
public enum Singleton {
INSTANCE;
public void anyOtherMethod() {}
}
13、Spring事务传播机制
14、Spring隔离级别
六.SpringCloud面试题口述
1、SpringCloud和Dubbo
SpringCloud和Dubbo都是现在主流的微服务架构
SpringCloud是Apache旗下的Spring体系下的微服务解决方案
Dubbo是阿里系的分布式服务治理框架
从技术维度上,其实SpringCloud远远的超过Dubbo,Dubbo本身只是实现了服务治理,而SpringCloud现在以及有21个子项目以后还会更多
所以其实很多人都会说Dubbo和SpringCloud是不公平的
但是由于RPC以及注册中心元数据等原因,在技术选型的时候我们只能二者选其一,所以我们常常为用他俩来对比
服务的调用方式Dubbo使用的是RPC远程调用,而SpringCloud使用的是 Rest API,其实更符合微服务官方的定义
服务的注册中心来看,Dubbo使用了第三方的ZooKeeper作为其底层的注册中心,实现服务的注册和发现,SpringCloud使用Spring Cloud Netflix Eureka实现注册中心,当然SpringCloud也可以使用ZooKeeper实现,但一般我们不会这样做
服务网关,Dubbo并没有本身的实现,只能通过其他第三方技术的整合,而SpringCloud有Zuul路由网关,作为路由服务器,进行消费者的请求分发,SpringCloud还支持断路器,与git完美集成分布式配置文件支持版本控制,事务总线实现配置文件的更新与服务自动装配等等一系列的微服务架构要素
2、技术选型
目前国内的分布式系统选型主要还是Dubbo毕竟国产,而且国内工程师的技术熟练程度高,并且Dubbo在其他维度上的缺陷可以由其他第三方框架进行集成进行弥补
而SpringCloud目前是国外比较流行,当然我觉得国内的市场也会慢慢的偏向SpringCloud,就连刘军作为Dubbo重启的负责人也发表过观点,Dubbo的发展方向是积极适应SpringCloud生态,并不是起冲突
3、Rest和RPC对比
其实如果仔细阅读过微服务提出者马丁福勒的论文的话可以发现其定义的服务间通信机制就是Http Rest
RPC最主要的缺陷就是服务提供方和调用方式之间依赖太强,我们需要为每一个微服务进行接口的定义,并通过持续继承发布,需要严格的版本控制才不会出现服务提供和调用之间因为版本不同而产生的冲突
而REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只是通过一个约定进行规范,但也有可能出现文档和接口不一致而导致的服务集成问题,但可以通过swagger工具整合,是代码和文档一体化解决,所以REST在分布式环境下比RPC更加灵活
这也是为什么当当网的DubboX在对Dubbo的增强中增加了对REST的支持的原因
4、文档质量和社区活跃度
SpringCloud社区活跃度远高于Dubbo,毕竟由于梁飞团队的原因导致Dubbo停止更新迭代五年,而中小型公司无法承担技术开发的成本导致Dubbo社区严重低落,而SpringCloud异军突起,迅速占领了微服务的市场,背靠Spring混的风生水起
Dubbo经过多年的积累文档相当成熟,对于微服务的架构体系各个公司也有稳定的现状
5、SpringBoot和SpringCloud
SpringBoot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务
而SpringCloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等
技术维度并相同,并且SpringCloud是依赖于SpringBoot的,而SpringBoot并不是依赖与SpringCloud,甚至还可以和Dubbo进行优秀的整合开发
总结:
SpringBoot专注于快速方便的开发单个个体的微服务
SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务
SpringBoot不依赖于SpringCloud,SpringCloud依赖于SpringBoot,属于依赖关系
SpringBoot专注于快速,方便的开发单个的微服务个体,SpringCloud关注全局的服务治理框架
6、微服务之间是如何独立通讯的
远程过程调用(Remote Procedure Invocation)
也就是我们常说的服务的注册与发现
直接通过远程过程调用来访问别的service。
优点:简单,常见,因为没有中间件代理,系统更简单
缺点:只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应
降低了可用性,因为客户端和服务端在请求过程中必须都是可用的
二、消息
使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。
优点:
把客户端和服务端解耦,更松耦合
提高可用性,因为消息中间件缓存了消息,直到消费者可以消费
支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应
缺点:
消息中间件有额外的复杂性
7、什么是服务熔断?什么是服务降级
在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩
为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。
所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝"当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。
服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值
8、微服务的优缺点分别是什么?说下你在项目开发中碰到的坑
优点
每一个服务足够内聚,代码容易理解
开发效率提高,一个服务只做一件事
微服务能够被小团队单独开发
微服务是松耦合的,是有功能意义的服务
可以用不同的语言开发,面向接口编程
易于与第三方集成
微服务只是业务逻辑的代码,不会和HTML,CSS或者其他界面组合
开发中,两种开发模式
前后端分离
全栈工程师
可以灵活搭配,连接公共库/连接独立库
缺点
分布式系统的负责性
多服务运维难度,随着服务的增加,运维的压力也在增大
系统部署依赖
服务间通信成本
数据一致性
系统集成测试
性能监控
9、你所知道的微服务技术栈有哪些?请列举一二
多种技术的集合体
我们在讨论一个分布式的微服务架构的话,需要哪些维度
维度(SpringCloud)
服务开发:SpringBoot、Spring、SpringMVC
服务配置与管理:Netfilx公司的Archaiusm,阿里的Diamond
服务注册与发现:Eureka,ZooKeeper
服务调用:Rest,RPC,gRPC
服务熔断器:Hystrix
服务负载均衡:Ribbon,Nginx
服务接口调用:Feign
消息队列:Kafka,RabbitMq,ActiveMq
服务配置中心管理:SpringCloudConfing
服务路由(API网关):Zuul
事件消息总线:SpringCloud Bus
10、Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别
1.ZooKeeper保证的是强一致性(二阶段提交),Eureka保证的是最终一致性(事务补偿机制)
ZooKeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的
Eureka各个节点是平等关系,只要有一台Eureka就可以保证服务可用,而查询到的数据并不是最新的
自我保护机制会导致
Eureka不再从注册列表移除因长时间没收到心跳而应该过期的服务
Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点(高可用)
当网络稳定时,当前实例新的注册信息会被同步到其他节点中(最终一致性)
Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper一样使得整个注册系统瘫痪
2.ZooKeeper有Leader和Follower角色,Eureka各个节点平等
3.ZooKeeper采用过半数存活原则,Eureka采用自我保护机制解决分区问题
4.Eureka本质上是一个工程,而ZooKeeper只是一个进程
11、总结:
最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:
Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务
12、什么是Nacos
Nacos致力于帮助您发现、配置和管理微服务。Nacos提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
七、分布式
1、什么是CAP
计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致性(Consistence) :所有节点访问同一份最新的数据副本 可用性(Availability):每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据 分区容错性(Partition tolerance) : 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
2、什么是Base
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 基本可用: 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 软状态: 软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时; 最终一致性: 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。