vlambda博客
学习文章列表

2022 java中高级常见面试题及答案

Java基础:

面向对象的特征:继承、封装和多态


封装:

      封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏;

继承:

     它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展;

     通过继承创建的新类成为"子类"或者"派生类";

     被继承的类成为"基类"、"父类"或"超类";

     继承的过程,就是从一般到特殊的过程;

     一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。


继承概念的实现方式有三类:

实现继承:是指使用基类的属性和方法而无需额外编码的能力;

接口继承:是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

可视继承:是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。


多态:

      是允许你将父对象设置成为一个或者更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。


多态概念的实现方式有两种:

覆盖:是指子类重新定义父类的虚函数的做法;

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不用,或许参数类型不用、或许两者都不同)。

 

重载和重写的区别

override(重写)

方法名、参数、返回值相同;

子类方法不能缩小父类方法的访问权限;

子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常);

存在于父类和子类之间;

方法被定义为final的不能被重写。

overload(重载)

参数类型、个数、顺序至少有一个不相同;

不能重载只有返回值不同的方法名;

存在于父类和子类、同类型中。

区别点重载重写(覆写)

英文 Overloading Overiding

定义方法名称相同,参数的类型或个数不同方法名称、参数类型、返回值类型全部相同

权限对权限没要求被重写的方法不能拥有更严格的权限

范围发生在一个类中发生在继承类中


int 和 Integer 有什么区别;Integer的值缓存范围

两者之间的区别:

Integer是int的包装类;int是基本数据类型;

Integer变量必须实例化后才能使用;int不需要;

Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值;

Integer的默认值是null;int的默认值是0。

Integer的值缓存范围:-128 ~ 127


说说反射的用途及实现

          Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能成为Java语言的反射机制。


反射机制的原理:

当 Student 类 new 一个对象的时候,会通知 JVM 去本地加载 Student.class 这个二进制文件;

JVM 在本地磁盘寻找这个文件,找到之后就把它加载到 JVM 内存中,在加载的同时会生成一个对象来映射这个 class 文件,该对象中存储着 Student.class 的信息,包括字段、方法等,将该对象放在 JVM 的一块内存空间中;

在 JVM 内存中为 Student 开辟一块空间,来存储 student1。

     反射的本质就是当获取到表示 Student.class 的对象后,反向获取 Student 类的信息。


Java反射框架提供以下功能:

在运行时判断任意一个对象所属的类;

在运行时构造任意一个类的对象;

在运行时判断任意一个类所具有的成员变量和方法(通过反射设置可以调用private);

在运行时调用任意一个对象的方法。


Http 请求的 GET 和 POST 方式的区别

GET产生一个TCP数据包:

       对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

POST产生两个TCP数据包:

        对于POST方式的请求,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。


MVC设计思想

 首先,MVC不是一种设计模式,而是一种设计思想,接下来看下两个概念的区别:

设计模式:是一种固定的方法,不灵活,有特定的使用场景;

设计思想:是一种思想,比较灵活,由多种设计模式组合实现。


接下来说说MVC的设计思想:

M(Model):主要功能提供数据(主要用来提供数据,并不关心数据让谁显示(Controller 负责给M要数据,然后控制数据让哪一个View来显示));

V(View):主要功能是展示数据(主要有数据即可,不关心数据来源);

C(Controller):主要功能协调V层与M层,作为V层与M层沟通的桥梁。

 

什么是Java序列化和反序列化;如何实现Java序列化;或者请描述Serializable接口的作用

序列化:把对象转换为字节序列的过程称为对象的序列化;

反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

如何实现Java序列化:实现serializable接口


ObjectOutputStream(对象输出流):

它的 writeObject(Object obj) 方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;


ObjectInputStream(对象输入流):

它的 readObject() 方法从一个源输入流中读取字节序列,再把他们反序列化为一个对象,并将其返回。


Colletcion类库中常用类

Collection是List、set父接口,不是Map父接口。


JavaList

List集合:ArrayList   LinkList   Vector 等;

Vector:是List接口下线程安全的结婚;

List是有序的;

ArrayList  和 LinkList 的区别:

ArrayList:适合于查询较多的场合,实现了基于动态数组的数据结构;

LinkList:适合于插入较多的场合,实现了基于链表的数据结构(双向链表)。

JavaMap

Map集合:HashMap HashTable ConcurrentHashMap  LinkedHashMap 等;

HashMap 不是线程安全的;

HashTable ControllerHashMap  SynchronizedMap 是线程安全的;

HashMap 的键值都可以为NULL,HashTable不行;

按添加顺序使用LinkedHashMap,按自然顺序使用TreeMap,自定义排序用TreeMap;

HashSet 和 HashTree 的区别:

HashSet:哈希表实现,数据是无序的,可以放入一个NULL值;

TreeSet:二叉树实现,数据是自动排好序的,不允许放入NULL值。

 

进程和线程:

线程和进程的概念

线程:单个进程中执行的每个任务就是一个线程。线程是进程中执行运算的最小单位;

进程:是执行中的一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程;进程表示资源分配的基本概念,又是调度原型的基本单位,是系统中的并发执行的单位。

并行和并发的概念

并行:指应用能够同时执行不同的任务;

并发:指应用能够交替执行不同的任务。

创建线程的方式及实现

继承 Thread 类创建线程;

              A)定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程要完成的任务,因此把 run() 方法称为执行体;

              B)创建 Thread 子类的实例,即创建了线程对象;

              C)调用线程对象的 start() 方法来启动该线程。


实现 Runnable 接口创建线程;

              A)定义 runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体;

              B)创建 Runnable 接口实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;

              C)调用线程对象的 start() 方法来启动该线程。

使用 Callable 和 Future 创建线程。

              A)创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值;

              B)创建 Callable 实现类的实例,使用 Future Task 类来包装 Callable 对象,该 Future Task 对象封装了该 Callable 对象的 call() 方法的返回值;

              C)使用 Future Task 对象作为 Thread 对象的 target 创建并启动新线程;

              D)调用 Future Task 对象的 get() 方法来获得子线程执行结束后的返回值。


三种方式的比较:

1> 通过 Runnable 和 Callable 创建多线程:

     优势:

           A)线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类;

          B)在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势:

         C)编程稍微复杂,如果要访问当前线程,必须使用 Thread.currentThread() 方法。

         D)  Runnable 和 Callable 的区别:

RunnableCallable

重写的方法是 run()重写的方法是call()

Runnable的任务是不能返回值的Callable的任务执行后可返回值

run()方法不可以抛出异常call() 方法可以抛出异常

 

运行 Callable 任务可以看到一个 Future 对象,表示异步计算的结果。

它提供了检索计算是否完成的方法,以等待计算的完成,

并检索计算的结果。通过 Future 对象可以了解任务执行情况,

可取消任务的执行,还可以获取执行结果。 


2> 使用继承 Thread 类的方式创建多线程:

     优势:

          如果要访问当前线程,则无需使用 Thread.currentThread() 方法,使用 this即可;

     劣势:

         已经继承了 Thread 类,不能再继承其他父类。


进程间通信的方式

          进程间通信(IPC,InterProcess  Communication)是指在不同进程之间传播或交换信息。


无名管道通信

          管道是一种半双工(即数据只能在一个方向上流动)的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程关系。


高级管道通信

          将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们称为高级管道方式。


有名管道通信

          有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。


消息队列通信

          是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。


信号量通信

          信号量(Semaphore)是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及统一进程内不用线程之间的同步手段。


信号

          信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。


共享内存通信

          共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是对快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。


套接字通信

          套接字(socket):套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。


说说 CountDownLatch、CyclicBarrier 原理和区别

CountDownLatch:

          是同步辅助类,它可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0后,被 await() 方法阻塞的线程将会唤醒,实现线程间的同步。

          一个或N个线程等待其它线程的关系。


CyclicBarrier:

          是同步辅助类,它允许一组线程互相等待,直到所有线程都达到某个特公共屏障点(也可以叫同步点),即相互等待的线程都完成调用 await() 方法,所有被屏障拦截的线程才会继续运行 await() 方法后面的程序。


各个线程内部相互等待的关系。


两者的区别:

CountDownLatchCyclicBarrier

减计数方式加计数方式

计算为0时释放所有等待的线程计数达到指定值时释放所有等待线程

计数为0时,无法重置计数达到指定值时,计数置为0重新开始

调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞

不可重复利用可重复利用

两者的使用场景,下面的文章说的很清楚:

CountDownLatch 和 CyclicBarrier 的使用场景


说说 Semaphore 原理

Semaphore(信号量) 是用来控制同时访问特定资源的线程数量,它通过协调各个线程,保证合理的使用公共资源。


线程可以通过 acquire() 方法来获取信号量的许可。当信号量中没有可用的许可的时候,线程阻塞,直到有可用的许可为止。线程可以通过 release() 方法释放它持有的信号量的许可。


Semaphore 内部基础AQS的共享模式,所以实现都委托给了Sync类。


Semaphore 有两种模式:

公平模式:

调用 acquire() 的顺序就是获取许可证的顺序,遵循FIFO;

非公平模式:

为抢占式的,可能一个新的获取线程恰好在一个许可证释放时得到这个许可证,而前面还有等待的线程。


说说 Exchanger 原理

主要用于两个工作线程之间交换数据。


Java Exchanger 原理


ThreadLocal 原理分析;ThreadLocal为什么会出现OOM,出现的深层次原理

          ThreadLocal 是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问。通常是类中的 private  static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如用户ID或者事务ID)相关联。


          ThreadLocal 为什么会出现OOM:

          ThreadLocal 的实现是这样的:每个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object;


          ThreadLocal 里面使用了一个存在弱引用的 map ,map 的类型是 ThreadLocal.ThreadLocalMap。Map 中的 key 为一个 ThreadLocal 实例本身,而这个 key 使用弱引用指向 ThreadLocal 。当 ThreadLocal 实例置为 null 后,没有任何强引用指向 ThreadLocal 实例,所以ThreadLocal 将会被 GC 回收。但是我们的 value 却不能回收,而这块 value 永远不会被访问到。所以存在着内存泄漏。因为存在一条从 Current Thread 链接过来的强引用,只有当 Thread 结束以后, Current Thread 就不会存在栈中,强引用断开,Current Thread、Map Value 将全部GC回收。

          如何避免内存泄漏:

          每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。


讲讲线程池的实现原理

   提交一个任务到线程池中,线程池的处理流程如下:


判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建),则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程;

线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储到这个工作队列里;如果工作队列满了,则进入下个流程;

判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

          

线程池的几种实现方式 

Executors.newCachedThreadPool

          创建一个可缓存的线程池,如果线程池的长度超过处理的需要,可以灵活回收空闲线程,若无可回收,则新建线程。

        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {

            final int index = i;

            executorService.execute(new Runnable() {

                @Override

                public void run() {

                    log.info("task:{}",index);

                }

            });

        }

        executorService.shutdown();

Executors.newFixedThreadPool

          创建一个定长线程池,可以控制线程最大并发数,超出的线程会在队列中等待。


        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {

            final int index = i;

            executorService.execute(new Runnable() {

                @Override

                public void run() {

                    log.info("task:{}",index);

                }

            });

        }

        executorService.shutdown();

Executors.newScheduledThreadPool

          创建一个定长线程池,支持定时、周期性的任务执行。


        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

 

        // 延迟一秒之后,每隔三秒执行一次

        executorService.scheduleAtFixedRate(new Runnable() {

            @Override

            public void run() {

                log.info("scheduled run");

            }

        },1,3,TimeUnit.SECONDS);

 

        // 也可以使用timer的schedule方法来实现定时功能

        Timer timer = new Timer();

        timer.schedule(new TimerTask() {

            @Override

            public void run() {

                log.info("timer run");

            }

        },new Date(),5*1000);

Executors.newSingleThreadExecutor

          创建一个单线程化的线程池,只会用唯一一个工作线程执行任务。


        ExecutorService executorService = Executors.newSingleThreadExecutor();

 

        for (int i = 0; i < 10; i++) {

            final int index = i;

            executorService.execute(new Runnable() {

                @Override

                public void run() {

                    log.info("task:{}",index);

                }

            });

        }


        executorService.shutdown();

          线程池的相关信息可以看下这个链接:线程池相关概念


线程的生命周期;状态是如何转移的

线程的生命周期:


新建(New Thread)

          当创建一个 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动);

          例如:Thread t1 = new Thread();


就绪(Runnable)

          线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队等候得到 CPU 资源;

          例如:ti.start();


运行(Running)

          线程获得 CPU 资源正在执行任务(run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。


阻塞(Blocked)

          由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入阻塞状态。

          正在睡眠:调用 sleep(long t) 方法 可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

          正在等待:调用 wait() 方法(调用 motify() 方法回到就绪状态)。

          被另一个线程所阻塞:调用 suspend() 方法(调用 resume() 方法恢复)。


死亡(Dead)

          当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这是线程不可能再进入就绪状态等待执行。

          自然终止:正常运行 run() 方法后终止;

          异常终止:调用 stop() 方法让一个线程终止运行。


Java中用到的线程调度算法

        抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。


       操作系统中可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。


线程调度器(Thread Schedulder)

负责为 Runnable 状态的线程分配CPU时间,一旦创建一个线程并启动它,它的执行变依赖于线程调度器的实现;

时间分片(Time Slicing)

将可用的CPU时间分配给可用的 Runnable 线程的过程。

 

单例模式的线程安全性

某个类的实例在多线程环境下置灰被创建一次出来。


写法:

饿汉式单例模式:线程安全

懒汉式单例模式:非线程安全

双检锁单例模式:线程安全

 

线程类的构造方法、静态块是被哪个线程调用的?

线程类的构造方法、静态块是被 new 这个线程类所在的线程调用的,而 run() 方法里面的代码才是被线程自身所调用的。


sleep() 和 wait() 有什么区别?

相同点:

    两者都可以用来放弃CPU一定的时间;

不同点:

   如果线程持有某个对象的监视器:

sleep() 不会放弃这个对象的监视器;

wait() 会放弃这个对象的监视器。

 

多线程的上下文切换

指CPU控制权由一个已经正在运行的线程切换到另一个就绪并等待获取CPU执行权的线程的过程。


锁机制:

什么是线程安全?如何保证线程安全?

线程安全:是指要控制多个线程对某个资源的有序访问或修改,而这些线程之间没有产生冲突。


线程安全问题都是由 全部变量 和 静态变量 引起的。


造成线程安全问题的主要诱因有两点:

存在共享数据(也称临界资源);

存在多条线程共同操作共享数据。


保证线程安全的方法

竞争与原子操作

同步与锁

可重入

过度优化


重入锁的概念;重入锁为什么可以防止死锁?

重入锁:指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取对象上的锁。而其他的线程是不可以的。


死锁:如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进。这个情况就是死锁。处于死锁状态的进程成为死锁进程。


产生死锁的四个条件

互斥条件

进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;

请求和保持条件

进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但是又对自己获得的的资源保持不放;

不可剥夺条件

是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完成后自己释放;

环路等待条件

是指进程发生死锁后,必然存在一个进程 -- 资源之间的环形链。


如何检查死锁

通过 jConsole(JDK 自带的图形化界面工具) 检查死锁


volatile 实现原理

volatile 定义:

Java 编程语言允许线程访问共享变量,为了确保共享变量能被准备和一致的更新,线程应该确保通过排它锁单独获得这个变量


禁止指令重排

刷新内存

synchronized 实现原理(对象监视器)

依赖 JVM 实现。


每个对象都有一个监视器锁(monitor)。当 montior 被占用时,就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:


如果 monitor 的进入数为0,则该线程进入 monitor ,然后将进入数设置为1,该线程即为 monitor 所有者;

如果线程已经占有该 monitor ,只是重新进入,则进入 monitor 的进入数加1;

如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为0,再重新尝试获取 monitor 的所有权。

 

synchronized 与 lock 的区别

类别synchronizedLock

存在层次Java的关键字,在jvm层面上是一个类

锁的释放1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁

锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待

锁状态无法判断可以判断

锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)

性能少量同步大量同步

 

AQS 同步队列

用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置FIFO队列来完成资源获取线程的排队工作。


CAS 无锁的概念;乐观锁和悲观锁

乐观锁:

总是认为不会产生并发问题,每次取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或者 CAS  操作实现;


悲观锁:

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。synchronized 的思想就属于悲观锁。


常见的原子操作类

Java 中的原子操作类


什么是 ABA 问题;出现 ABA 问题 JDK 是如何解决的

ABA 问题:

如果另一个线程修改 V 值,假设值原来是 A,先修改成 B,再修改回成 A,当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。

如何解决 ABA 问题:

用 AtomicStampedReference 解决 ABA 问题。


乐观锁的业务场景及实现方式

乐观锁(Optimistic Lock): 

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。


乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。


偏向锁、轻量级锁、重量级锁、自旋锁的概念

偏向锁:

为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径;

轻量级锁:

为了在无多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗;

重量级锁:

通过对象内部的监视器(montior)实现,其中 montior 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高;

自旋锁:

 就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否很快释放锁。


锁优化相关知识点

数据库:

DDL、DML、DCL 分别指什么

DML(data manipulation language):数据库操作语言。就是我们经常用到的 SELECT 、UPDATE 、INSERT 、DELETE 等;

DDL (data definition language):数据库定义语言。就是我们经常用到的 CREATE 、ALTER 、DROP 等。DDL 主要是用在定义或改变表的结构、数据类型、表之间的链接和约束等初始化工作上;

DCL (data control language):数据库控制语言。是用来设置或改变数据库用户或角色权限的语句,包括( GRANT 、DENY 、REVOKE 等)语句。

 

explain 命令

explain 命令显示了 Mysql 如何使用索引来处理 SELECT 语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。


使用方法:在 SELECT 语句前加上 explain 就可以了。


explain 列说明:

idselect 识别符。这是 select 的查询序列号

  select_type  


select 类型,可以为一下任何一种:

SIMPLE:简单 slelect(不使用UNION或子查询)

PRIMARY:最外面的 select

UNION:UNION 中的第二个或后面的 select 语句

DEPENDENT UNION:UNION 中的第二个或后面的 select 语句,取决于外面的查询

UNION RESULT:UNION 的结果

SUBQUERY:子查询中的第一个 select

DEPENSENT SUBQUERY:子查询中的第一个 select,取决于外面的查询

DERIVED:导出表的 select (from 子句的子查询)

table输出的行所引用的表

type

联接类型。下面给出各个联接类型,按照从最佳类型到最坏类型进行排序:


system:表仅有一行(=系统表)。这是const联接类型的一个特例

const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const 表很快,因为它们只读取一次

eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了 const 类型

ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取

ref_or_null:该联接类型如同 ref ,但是添加了 MySQL 可以专门搜索包含 NULL 值的行

index_merge:该联接类型表示使用了索引合并优化方法

unique_subquery:该类型替换了下面形式的 IN 子查询的ref:value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery 是一个索引查找函数,可以完全替换子查询

index_subquery:该联接类型类似于 unqiue_subquery 。可以替换 ID 子查询,但只适合下列形式的子查询的非唯一索引:value IN(SELECT key_column FROM single_table WHERE some_expr)

range:只检索给定范围的行,使用一个索引来选择行

index:该联接类型与 ALL 相同,除了只有索引树被扫描。这通常比 ALL 快,因为索引文件通常比数据文件小

ALL:对于每个来自于先前的表的行组合,进行完整的表扫描

possible_keys


指出 MySQL 能使用哪个索引在该表中找到行

key


显示 MySQL 实际觉得使用的键(索引)。如果没有选择索引,键是 NULL

key_len


显示 MySQL 决定使用的键长度。如果键是 NULL,则长度为 NULL

ref显示使用哪个列或常数与 key 一起从表中选择行

rows


 显示 MySQL 认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数

filtered显示了通过条件过滤出的行数的百分比估计值

Extra

该列包含 MySQL 解决查询的详细信息


Disinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行

Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行

range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用

Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行

Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息

Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果

Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户

Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描

Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表

 

数据库事务 ACID

Atomicity(原子性):原子性要求每个事务中的所有操作要么全部完成,要么就像全部没有发生一样;如果事务中的部分操作失败了,则整个事务失败,结果就是数据库中的状态保持没变;

Consistency(一致性):一致性确保了任何事务都会使数据库从一种合法的状态变为另一种合法的状态;

Isolation(隔离性):隔离性保证了并发执行多个事务对系统状态的影响和串行化执行多个事务对系统状态的影响是一样的;

Durability(持久性):持久性保证了一个事务一旦被提交以后,其状态就保持不变,甚至是发生了主机断电、崩溃、错误等。

 

脏读、幻读、不可重复读

脏读:是指一个事务处理过程中读取了另一个未提交的事务中的数据;

幻读:是事务非独立执行时发生的一种现象;

不可重复读:是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。


事务的隔离级别

Serializable(串行化):可避免脏读、不可重复读、幻读的发生;

Repeatable read(可重复读):可避免脏读、不可重复读的发生;

Read commited(读已提交):可避免脏读的发生;

Read uncommited(读未提交):最低级别,任何情况都无法保证。

以上四种级别中,隔离级别最高的是 Serializable ,级别最低的是 Read uncommited 。级别越高,执行效率就越低。


在 Mysql 数据库默认的隔离级别为 Repeatable read 级别。

数据库的几大范式

第一范式:确保每列的原子性;

第二范式:确保表中的每列都和主键相关,要求每个表值描述一件事情;

第三范式:确保每列都和主键列直接相关,而不是间接相关。

 

说说分库与分表设计

分表:对于访问极为频繁且数据量巨大的单表来说,我们首先要做的就是减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的吞吐;

分库:分表能够解决单表数据量过大带来的查询效率下降的问题,但是却无法给数据库的并发处理能力带来质的提升。所以需要对数据库进行拆分,从而提高数据库的写入能力。

 

说说 SQL 优化之道

减少查询字段数;

表关联尽量用主键;

查询条件尽量避免模糊查询;

避免使用排序字段,排序字段尽量使用主键;

尽量使用限制查询条件;

查询条件使用有效索引。


存储引擎的 InnoDB 与 MyISAM 区别、优缺点、使用场景

两者的区别:

InnoDb 支持事务,MyISAM 不支持。这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而 MyISAM 就不可以了;

MyISAM 适合查询以及插入为主的应用,InnoDB 适合频繁修改以及设计到安全性较高的应用;

InnoDB 支持外键, MyISAM 不支持;

从 MySql 5.5 以后,InnoDB  是默认引擎;

InnoDb 不支持 FULLTEXT 类型的索引;

InnoDb 中不保存表的行数,如 select count(*) from table 时,InnoDB 需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。主要的是,当 count(*) 语句包含 where 条件时 MyISAM 特需要扫描整个表;

对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM 表中可以和其它字段一起建立联合索引;

清空整个表时,InnoDb 是一行一行的删除,效率非常慢。MyISAM 则会重新建表;

InnoDb 支持行锁(某些情况下还是锁整表,如 update table set a = 1 where user like '%lee%')。

InnoDB 与 MyISAM 的优缺点:

InnoDb :这种类型是事务安全的。

优点:支持事务,支持外键,并发量较大,适合大量 update;

缺点:查询数据相对较慢,不适合大量的 select。

MyISAM:

优点:查询数据相对较快,适合大量的 select ,可以全文索引;

缺点:不支持事务,不支持外键,并发量较小,不适合大量 update。

使用场景:


如果你的应用程序一定要使用事务,毫无疑问你要选择 InnoDb 引擎;

如果你的应用程序对查询性能要求较高,就要使用 MyISAM 引擎。

 

索引类别(B+树索引、全文索引、哈希索引);索引的区别

FULLTEXT:即为全文索引。目前只有MyISAM 引擎支持;

HASH:即为哈希索引。HASH 索引可以一次定位,不需要像属性索引那样逐层查找,因此具有极高的效率;

BTREE:即为B+树索引。BTREE 索引就是一种将索引值按一定的算法,存入一个属性的数据结构中(二叉树),每次查询都是从树的入口 root 开始,依次遍历 node,获取 leaf 。这是 MySQL 里默认和最常用的索引类型。

 

什么是自适应哈希索引(AHI)

InnoDB 存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。AHI 是通过缓冲池的 B+ 树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB 存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。


为什么要用 B+tree 作为 MySql 索引的数据结构

鉴于 BTREE 具有良好的定位特性,其常被用于对检索时间要求苛刻的场合,例如:

BTREE 索引是数据库中存取和查找文件(称为记录或键值)的一种方法;

硬盘中的节点也是 BTREE 结构的。与内存相比,硬盘必须花城北的时间来存取一个数据元素,这是因为硬盘的机械部件读取数据的速度远远赶不上纯电子媒体的内存。与一个节点两个分支的二元树相比, BTREE 利用多个分支(称为子树)的节点,减少获取记录时所经历的节点数,从而达到节省存取时间的目的。

 

limit 20000 加载很慢怎么解决

limit n 等价于 limit 0,n

让 limit 走索引去查询,例如:order by 索引字段,或者 limit 前面跟 where 条件走索引字段等。


常见的几种分布式 ID 的设计方案

UUID(Universally Unique Identifier):16字节128位,通常以36长度的字符串表示。 

优点:本地生成 ID,不需要进行远程调用,时延低,性能高;

缺点:

UUID 过长,很多场景不适用,比如用 UUID 做数据库索引字段;

没有排序,无法保证趋势递增。

数据库自增长序列或字段:最常见的方式,利用数据库,全数据库唯一。

优点:

简单,代码方便,性能可以接受;

数字 ID 天然排序,对分页或者需要排序的结果很有帮助。

缺点:

不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理;

在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险;

在性能达不到要求的情况下,比较难于扩展;

如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦;

分表分库的时候会有麻烦。

Filcker 方法:主要思路采用了 MySql 自增长 ID 的机制(auto_increment + replace into)。

优点:充分借助数据库的自增 ID 机制,可靠性高,生成有序的ID;

缺点:

ID 生成性能一来单台数据库读写性能;

依赖数据库,当数据库异常时整个系统不可用。

Redis 生成 ID:主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID 。可以用 Redis 的原子操作 INCR 和 INCRBY 来实现。

优点:

不依赖与数据库,灵活方便,且性能优于数据库;

数字 ID 天然排序,对分页或者需要排序的结果很有帮助。

缺点:

如果系统中没有 Redis ,还需要引入新的组件,增加系统复杂度;

需要编码和配置的工作量比较大。

snowflake 算法:snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID(5个 bit 是数据中心,5个 bit 是机器 ID),12bit 作为毫秒内的流水号 (意味着每个节点在每毫秒可以产生4096个 ID),最后还有个符号位,永远是0。

优点:

不依赖与数据库,灵活方便,且性能优于数据库;

ID 按照时间在单机上是递增的。

缺点:在单机上是递增的,但是由于涉及到分布式环境,每台机器上的始终不可能完全同步,也许有时候也会出现不适全局递增的情况。

JVM

JVM 运行时内存区域划分

 首先了解下什么是 JVM 内存:


Java 源代码文件(.java后缀)会被 Java 编译器编译为字节码文件(.class后缀),然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行引擎执行。在整个程序执行过程中,JVM 会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一段被称作为 Runtime Data Area(运行时数据区),也就是我们常说的 JVM 内存。因此,在 Java 中我们常常说到的内存管理就是针对这块空间进行管理(如何分配和回收空间)。

根据《Java 虚拟机规范》的规定,运行时数据区通常包括这几个部分:

程序计数器(Program Counter Register):

虚拟机栈(VM Stack):

描述的是 Java 方法执行的内存模型,方法执行的同时会创建一个栈祯,用于存储方法中的局部变量表、操作数栈、动态链接、方法的出口等信息,每个方法从调用直到执行完成的过程,就对应着一个栈祯在虚拟机栈中入栈到出栈的过程;

本地方法栈(Native Method Stack):

跟虚拟机栈类似。区别在于本地方法栈是为 Native 方法服务而虚拟机栈是为 Java 方法服务;

方法区(Method Area):

与堆(Heap)一样所有线程所共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

堆(Heap):

Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程所共享的一块内存区域,虚拟机启动时创建,几乎所有对象的实例都存储在堆中,所有的对象和数组都要在堆上分配内存。

 

常见的 GC 回收算法及其含义

什么是 GC:

GC 是将 Java 的无用的堆对象进行清理,释放内存,以免发生内存泄漏。

常见的回收算法:

标记-清除 算法:

标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

清除阶段:清除所有未被标记的对象;

复制算法(新生代的 GC):

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清楚正在使用的内存块中的所有对象;

标记-整理 算法(老年代的 GC):

标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

整理阶段:将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间;

分带收集算法:

存活率低:少量对象存活,适合用复制算法:在新生代中,每次 GC 时都发现有大批对象死去,只有少量存活(新生代中98%的对象都是 "朝生夕死"),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成 GC;

存活率高:大量对象存活,适合用标记-清除/标记-整理:在老年代中,因为对象存活率高,没有额外空间对他进行分配担保,就必须使用 标记-清除/标记-整理 算法进行 GC。


常见的 JVM 性能监控和故障处理工具类

jmap:观察运行中的 JVM 物理内存的占用情况;

jps:列出所有的 JVM 实例;

jvmtop:一个轻量级的控制台程序用来监控机器上运行的所有 java 虚拟机;

jstack:观察 jvm 中当前所有线程的运行情况和线程当前状态;

jstat:JVM 统计监测工具;

jconsole:内置 Java 性能分析器。

 

JVM 性能调优

调优的目的:为了令应用程序使用最小的硬件消耗来承载更大的吞吐。

JVM 调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。


类加载器、双亲委派模型

类加载器:

启动类加载器(BootStrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过 -Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt、jar )的类;

扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库;

应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath) 上的类库。

双亲委派模型:

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。

好处:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类;

 

类加载的过程

加载:

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.class 对象,作为方法区这个类的各种数据的入口。

链接:

验证:这一阶段的主要目的是为了确保 class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;

准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。

符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一直的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中;

直接引用:是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化:

初始化阶段是类加载的最后一个阶段。前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其他操作都是由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

强引用、软引用、弱引用、虚引用

强引用:只要引用存在,垃圾回收器永远不会回收;

软引用:非必须引用,内存溢出之前进行回收;

弱引用:第二次垃圾回收时回收;

虚引用:垃圾回收时回收,无法通过引用取到对象值。

Java 内存模型 JMM

JMM 解决了 可见性和有序性的问题,而锁解决了原子性的问题。


特性

原子性:指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响;

可见性:指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值;

有序性:指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。


Spring 概述

1. 什么是spring?

Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。

2. 使用Spring框架的好处是什么?

  • 轻量:Spring 是轻量的,基本的版本大约2MB。

  • 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。

  • 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。

  • 容器:Spring 包含并管理应用中对象的生命周期和配置。

  • MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。

  • 事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。

  • 异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。

3.  Spring由哪些模块组成?

以下是Spring 框架的基本模块:

  • Core module

  • Bean module

  • Context module

  • Expression Language module

  • JDBC module

  • ORM module

  • OXM module

  • Java Messaging Service(JMS) module

  • Transaction module

  • Web module

  • Web-Servlet module

  • Web-Struts module

  • Web-Portlet module

4. 核心容器(应用上下文) 模块。

这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。

5. BeanFactory – BeanFactory 实现举例。

Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。

最常用的BeanFactory 实现是XmlBeanFactory 类。

6. XMLBeanFactory 

最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。

7. 解释AOP模块

AOP模块用于发给我们的Spring应用做面向切面的开发, 很多支持由AOP联盟提供,这样就确保了Spring和其他AOP框架的共通性。这个模块将元数据编程引入Spring。

8. 解释JDBC抽象和DAO模块。

通过使用JDBC抽象和DAO模块,保证数据库代码的简洁,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层。它还利用Spring的AOP 模块给Spring应用中的对象提供事务管理服务。

9. 解释对象/关系映射集成模块。

Spring 通过提供ORM模块,支持我们在直接JDBC之上使用一个对象/关系映射映射(ORM)工具,Spring 支持集成主流的ORM框 架,如Hiberate,JDO和 iBATIS SQL Maps。Spring的事务管理同样支持以上所有ORM框架及JDBC。

10.  解释WEB 模块。

Spring的WEB模块是构建在application context 模块基础之上,提供一个适合web应用的上下文。这个模块也包括支持多 种面向web的任务,如透明地处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。它也有对Jakarta Struts的支持。

12.  Spring配置文件

Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。

13.  什么是Spring IOC 容器?

Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。

14.  IOC的优点是什么?

IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。

15. ApplicationContext通常的实现是什么?

  • FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。

  • ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。

  • WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。

16. Bean 工厂和 Application contexts  有什么区别?

Application contexts提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的 bean发布事件。另外,在容器或容器内的对象上执行的那些不得不由bean工厂以程序化方式处理的操作,可以在 Application contexts中以声明的方式处理。Application contexts实现了MessageSource接口,该接口 的实现以可插拔的方式提供获取本地化消息的方法。

17. 一个Spring的应用看起来象什么?

  • 一个定义了一些功能的接口。

  • 这实现包括属性,它的Setter , getter 方法和函数等。

  • Spring AOP。

  • Spring 的XML 配置文件。

  • 使用以上功能的客户端程序。

依赖注入

18. 什么是Spring的依赖注入?

依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。

19.  有哪些不同类型的IOC(依赖注入)方式?

  • 构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。

  • Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。

20. 哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?

你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。

Spring Beans

21.什么是Spring beans?

Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中<bean/> 的形式定义。

Spring 框架定义的beans都是单件beans。在bean tag中有个属性”singleton”,如果它被赋为 TRUE,bean 就是单件,否则就是一个 prototype bean。默认是TRUE,所以所有在Spring框架中的beans 缺省都是单 件。

22. 一个 Spring Bean 定义 包含什么?

一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。

23. 如何给Spring 容器提供配置元数据?

这里有三种重要的方法给Spring 容器提供配置元数据。

XML配置文件。

基于注解的配置。

基于java的配置。

24. 你怎样定义类的作用域? 

当定义一个<bean> 在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定 义。如,当Spring要在需要的时候每次生产一个新的bean实例,bean的scope属性被指定为prototype。另一方面,一个bean每次 使用的时候必须返回同一个实例,这个bean的scope 属性 必须设为 singleton。

25. 解释Spring支持的几种bean的作用域。

Spring框架支持以下五种bean的作用域:

  • singleton : bean在每个Spring ioc 容器中只有一个实例。

  • prototype:一个bean的定义可以有多个实例。

  • request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。

  • session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

  • global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

缺省的Spring bean 的作用域是Singleton.

26. Spring框架中的单例bean是线程安全的吗?

不,Spring框架中的单例bean不是线程安全的。

27. 解释Spring框架中bean的生命周期。

  • Spring容器 从XML 文件中读取bean的定义,并实例化bean。

  • Spring根据bean的定义填充所有的属性。

  • 如果bean实现了BeanNameAware 接口,Spring 传递bean 的ID 到 setBeanName方法。

  • 如果Bean 实现了 BeanFactoryAware 接口, Spring传递beanfactory 给setBeanFactory 方法。

  • 如果有任何与bean相关联的BeanPostProcessors,Spring会在postProcesserBeforeInitialization()方法内调用它们。

  • 如果bean实现IntializingBean了,调用它的afterPropertySet方法,如果bean声明了初始化方法,调用此初始化方法。

  • 如果有BeanPostProcessors 和bean 关联,这些bean的postProcessAfterInitialization() 方法将被调用。

  • 如果bean实现了 DisposableBean,它将调用destroy()方法。

28.  哪些是重要的bean生命周期方法? 你能重载它们吗?

有两个重要的bean 生命周期方法,第一个是setup , 它是在容器加载bean的时候被调用。第二个方法是 teardown  它是在容器卸载类的时候被调用。

The bean 标签有两个重要的属性(init-method和destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct和@PreDestroy)。

29. 什么是Spring的内部bean?

当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean,为了定义inner bean,在Spring 的 基于XML 的 配置元数据中,可以在 <property/>或 <constructor-arg/> 元素内使用<bean /> 元素,内部bean通常是匿名的,它们的Scope一般是prototype。

30. 在 Spring中如何注入一个java集合?

Spring提供以下几种集合的配置元素:

  • <list>类型用于注入一列值,允许有相同的值。

  • <set> 类型用于注入一组值,不允许有相同的值。

  • <map> 类型用于注入一组键值对,键和值都可以为任意类型。

  • <props>类型用于注入一组键值对,键和值都只能为String类型。

31. 什么是bean装配? 

装配,或bean 装配是指在Spring 容器中把bean组装到一起,前提是容器需要知道bean的依赖关系,如何通过依赖注入来把它们装配到一起。

32. 什么是bean的自动装配?

Spring 容器能够自动装配相互合作的bean,这意味着容器不需要<constructor-arg>和<property>配置,能通过Bean工厂自动处理bean之间的协作。

33. 解释不同方式的自动装配 。

有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。

  • no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。

  • byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。

  • byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。

  • constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。

  • autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。

34.自动装配有哪些局限性 ?

自动装配的局限性是:

  • 重写: 你仍需用 <constructor-arg>和 <property> 配置来定义依赖,意味着总要重写自动装配。

  • 基本数据类型:你不能自动装配简单的属性,如基本数据类型,String字符串,和类。

  • 模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。

35. 你可以在Spring中注入一个null 和一个空字符串吗?

可以。

Spring注解

36. 什么是基于Java的Spring注解配置? 给一些注解的例子.

基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。

以@Configuration 注解为例,它用来标记类可以当做一个bean的定义,被Spring IOC容器使用。另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。

37. 什么是基于注解的容器配置?

相对于XML文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明。

开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不是使用xml表述bean的装配关系。

38. 怎样开启注解装配?

注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 <context:annotation-config/>元素。

39. @Required  注解

这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显式的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializationException。

40. @Autowired 注解

@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它的用法和@Required一样,修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。

41. @Qualifier 注解

当有多个相同类型的bean却只有一个需要自动装配时,将@Qualifier 注解和@Autowire 注解结合使用以消除这种混淆,指定需要装配的确切的bean。

Spring数据访问

42.在Spring框架中如何更有效地使用JDBC? 

使用SpringJDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate (例子见这里here)

43. JdbcTemplate

JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理。

44. Spring对DAO的支持

Spring对数据访问对象(DAO)的支持旨在简化它和数据访问技术如JDBC,Hibernate or JDO 结合使用。这使我们可以方便切换持久层。编码时也不用担心会捕获每种技术特有的异常。

45. 使用Spring通过什么方式访问Hibernate? 

在Spring中有两种方式访问Hibernate:

  • 控制反转  Hibernate Template和 Callback。

  • 继承 HibernateDAOSupport提供一个AOP 拦截器。

46. Spring支持的ORM

Spring支持以下ORM:

  • Hibernate

  • iBatis

  • JPA (Java Persistence API)

  • TopLink

  • JDO (Java Data Objects)

  • OJB

47.如何通过HibernateDaoSupport将Spring和Hibernate结合起来?

用Spring的 SessionFactory 调用 LocalSessionFactory。集成过程分三步:

  • 配置the Hibernate SessionFactory。

  • 继承HibernateDaoSupport实现一个DAO。

  • 在AOP支持的事务中装配。

48. Spring支持的事务管理类型

Spring支持两种类型的事务管理:

  • 编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。

  • 声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。

49. Spring框架的事务管理有哪些优点?

  • 它为不同的事务API  如 JTA,JDBC,Hibernate,JPA 和JDO,提供一个不变的编程模式。

  • 它为编程式事务管理提供了一套简单的API而不是一些复杂的事务API如

  • 它支持声明式事务管理。

  • 它和Spring各种数据访问抽象层很好得集成。

50. 你更倾向用那种事务管理类型?

大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。

Spring面向切面编程(AOP)

51.  解释AOP

面向切面的编程,或AOP, 是一种编程技术,允许程序模块化横向切割关注点,或横切典型的责任划分,如日志和事务管理。

52. Aspect 切面

AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

52. 在Spring AOP 中,关注点和横切关注的区别是什么?

关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。

54. 连接点

连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个应用程序执行Spring AOP的位置。

55. 通知

通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段。

Spring切面可以应用五种类型的通知:

  • before:前置通知,在一个方法执行前被调用。

  • after: 在方法执行之后调用的通知,无论方法执行是否成功。

  • after-returning: 仅当方法成功完成后执行的通知。

  • after-throwing: 在方法抛出异常退出时执行的通知。

  • around: 在方法执行之前和之后调用的通知。

56. 切点

切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。

57. 什么是引入? 

引入允许我们在已存在的类中增加新的方法和属性。

58. 什么是目标对象? 

被一个或者多个切面所通知的对象。它通常是一个代理对象。也指被通知(advised)对象。

59. 什么是代理?

代理是通知目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。

60. 有几种不同类型的自动代理?

BeanNameAutoProxyCreator

DefaultAdvisorAutoProxyCreator

Metadata autoproxying

61. 什么是织入。什么是织入应用的不同点?

织入是将切面和到其他应用类型或对象连接或创建一个被通知对象的过程。

织入可以在编译时,加载时,或运行时完成。

62. 解释基于XML Schema方式的切面实现。

在这种情况下,切面由常规类以及基于XML的配置实现。

63. 解释基于注解的切面实现

在这种情况下(基于@AspectJ的实现),涉及到的切面声明的风格与带有java5标注的普通java类一致。

Spring 的MVC

64. 什么是Spring的MVC框架?

Spring 配备构建Web 应用的全功能MVC框架。Spring可以很便捷地和其他MVC框架集成,如Struts,Spring 的MVC框架用控制反转把业务对象和控制逻辑清晰地隔离。它也允许以声明的方式把请求参数和业务对象绑定。

65. DispatcherServlet

Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。

66. WebApplicationContext

WebApplicationContext 继承了ApplicationContext  并增加了一些WEB应用必备的特有功能,它不同于一般的ApplicationContext ,因为它能处理主题,并找到被关联的servlet。

67. 什么是Spring MVC框架的控制器?

控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层,允许用户创建多种用途的控制器。

68. @Controller 注解

该注解表明该类扮演控制器的角色,Spring不需要你继承任何其他控制器基类或引用Servlet API。

69. @RequestMapping 注解

该注解是用来映射一个URL到一个类或一个特定的方处理法上。


Spring的优点:

① Spring能有效地组织你的中间层对象,不管你是否选择使用了EJB;

② Spring能消除在许多工程中常见的对Singleton的过多使用。(因为它降低了系统的可测试性和面向对象的程度);

③ 通过一种在不同应用程序和项目间一致的方法来处理配置文件,Spring能消除各种各样自定义格式的属性文件的需要。Inversion of Control的使用帮助完成了这种简化;

④通过把对接口编程而不是对类编程的代价几乎减少到没有,Spring能够促进养成好的编程习惯;

⑤Spring被设计为让使用它创建的应用尽可能少的依赖于他的APIs。在Spring应用中的大多数业务对象没有依赖于Spring;

⑥使用Spring构建的应用程序易于单元测试;

⑦Spring能使EJB的使用成为一个实现选择,而不是应用架构的必然选择。你能选择用POJOs或local EJBs来实现业务接口,却不会影响调用代码;

⑧Spring帮助你解决许多问题而无需使用EJB。Spring能提供一种EJB的替换物,他们适用于许多web应用。例如:Spring能使用AOP提供声明性事务管理而不通过EJB容器;

⑨Spring为数据存取提供了一个一致的框架不论使用的是JDBC还是O/R mapping产品;

Java并发编程

1) 什么是线程?

  线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。

2) 线程和进程有什么区别?

  线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

3) 如何在Java中实现线程?

  在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程,也可以创建Executor执行器来执型Callable接口对象。

4) 用Runnable还是Thread?

  这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。

6) Thread 类中的start() 和 run() 方法有什么区别?

  这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

7) Java中Runnable和Callable有什么不同?

  Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

8) Java中CyclicBarrier 和 CountDownLatch有什么不同?

  CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用,并且CyclicBarrier 可以在在构造时传入一个Runnable对象,在所有线程都同步过后,执行该Runnable对象

9) Java内存模型是什么?

  Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:

  • 线程内的代码能够按先后顺序执行,这被称为程序次序规则。

  • 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。

  • 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。

  • 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。

  • 一个线程的所有操作都会在线程终止之前,线程终止规则。

  • 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。


10) Java中的volatile 变量是什么?

  volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。考虑Java的线程模型,volatile修饰的变量会及时的进行刷新,不缓存

11) 什么是线程安全?Vector是一个线程安全类吗? 

  如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。

12) Java中什么是竞态条件?举个例子说明。

  竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。

13) Java中如何停止一个线程?

  Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。

14) 一个线程运行时发生异常会怎样?

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

15) 如何在两个线程间共享数据?

  线程是共享进程内的资源的,你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。

16) Java中notify 和 notifyAll有什么区别?

  这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法只能唤醒一个线程进入就绪态。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

17) 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

  这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

18) 什么是ThreadLocal变量?

  ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

19) 什么是FutureTask?

  在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

20) Java中interrupted 和 isInterruptedd方法的区别?

  interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

21) 为什么wait和notify方法要在同步块中调用?

  主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。

22) 为什么你应该在循环中检查等待条件?

  处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因,你可以在Eclipse中创建模板调用wait和notify试一试。如果你想了解更多关于这个问题的内容,我推荐你阅读《Effective Java》这本书中的线程和同步章节。

23) Java中的同步集合与并发集合有什么区别?

  同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

24) Java中堆和栈有什么不同?

  为什么把这个问题归类在多线程和并发面试题里?因为栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。 

25) 什么是线程池?为什么要使用它?

  创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。

26) 如何写代码来解决生产者消费者问题?

  在现实中你解决的许多线程问题都属于生产者消费者模型,就是一个线程生产任务供其它线程进行消费,你必须知道怎么进行线程间通信来解决这个问题。比较低级的办法是用wait和notify来解决这个问题,比较赞的办法是用Semaphore 或者 BlockingQueue来实现生产者消费者模型。

27) 如何避免死锁?


  Java多线程中的死锁 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

28) Java中活锁和死锁有什么区别?

  这是上题的扩展,活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

29) 怎么检测一个线程是否拥有锁?

30) 你如何在Java中获取线程堆栈?

  对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows你可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。

31) JVM中哪个参数是用来控制线程的栈堆栈小的

  这个问题很简单, -Xss参数用来控制线程的堆栈大小。你可以查看JVM配置列表来了解这个参数的更多信息。

32) Java中synchronized 和 ReentrantLock 有什么不同?

  Java在过去很长一段时间只能通过synchronized关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过Lock接口提供了更复杂的控制来解决这些问题。ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。

33) 有三个线程T1,T2,T3,怎么确保它们按顺序执行?

  在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

34) Thread类中的yield方法有什么作用?

  Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

35) Java中ConcurrentHashMap的并发度是什么?

  ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。

36) Java中Semaphore是什么?

  Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。

37)如果你提交任务时,线程池队列已满。会时发会生什么?

  这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常。

38) Java线程池中submit() 和 execute()方法有什么区别?

  两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

39) 什么是阻塞式方法?

  阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

40) Swing是线程安全的吗?为什么?

  你可以很肯定的给出回答,Swing不是线程安全的,但是你应该解释这么回答的原因即便面试官没有问你为什么。当我们说swing不是线程安全的常常提到它的组件,这些组件不能在多线程中进行修改,所有对GUI组件的更新都要在AWT线程中完成,而Swing提供了同步和异步两种回调方法来进行更新。

41) Java中invokeAndWait 和 invokeLater有什么区别?

  这两个方法是Swing API 提供给Java开发者用来从当前线程而不是事件派发线程更新GUI组件用的。InvokeAndWait()同步更新GUI组件,比如一个进度条,一旦进度更新了,进度条也要做出相应改变。如果进度被多个线程跟踪,那么就调用invokeAndWait()方法请求事件派发线程对组件进行相应更新。而invokeLater()方法是异步调用更新组件的。

42) Swing API中那些方法是线程安全的?

  这个问题又提到了swing和线程安全,虽然组件不是线程安全的但是有一些方法是可以被多线程安全调用的,比如repaint(), revalidate()。JTextComponent的setText()方法和JTextArea的insert() 和 append() 方法也是线程安全的。

43) 如何在Java中创建Immutable对象?

  这个问题看起来和多线程没什么关系, 但不变性有助于简化已经很复杂的并发程序。Immutable对象可以在没有同步的情况下共享,降低了对该对象进行并发访问时的同步化开销。可是Java没有@Immutable这个注解符,要创建不可变类,要实现下面几个步骤:通过构造方法初始化所有成员、对变量不要提供setter方法、将所有的成员声明为私有的,这样就不允许直接访问这些成员、在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。

44) Java中的ReadWriteLock是什么?

  一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。

45) 多线程中的忙循环是什么?

  忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

46)volatile 变量和 atomic 变量有什么不同?

  这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

47) 如果同步块内的线程抛出异常会发生什么?

  这个问题坑了很多Java程序员,若你能想到锁是否释放这条线索来回答还有点希望答对。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。

48) 单例模式的双检锁是什么?

  这个问题在Java面试中经常被问到,但是面试官对回答此问题的满意度仅为50%。一半的人写不出双检锁还有一半的人说不出它的隐患和Java1.5是如何对它修正的。它其实是一个用来创建线程安全的单例的老方法,当单例实例第一次被创建时它试图用单个锁进行性能优化,但是由于太过于复杂在JDK1.4中它是失败的,我个人也不喜欢它。无论如何,即便你也不喜欢它但是还是要了解一下,因为它经常被问到。

49) 如何在Java中创建线程安全的Singleton?

  这是上面那个问题的后续,如果你不喜欢双检锁而面试官问了创建Singleton类的替代方法,你可以利用JVM的类加载和静态变量初始化特征来创建Singleton实例,或者是利用枚举类型来创建Singleton,我很喜欢用这种方法。

50) 写出3条你遵循的多线程最佳实践

  这种问题我最喜欢了,我相信你在写并发代码来提升性能的时候也会遵循某些最佳实践。以下三条最佳实践我觉得大多数Java程序员都应该遵循:

  • 给你的线程起个有意义的名字。这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。

  • 避免锁定和缩小同步的范围 锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

  • 多用同步类少用wait 和 notify 首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。

  • 多用并发集合少用同步集合 这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。

51) 如何强制启动一个线程?

  这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。

52) Java中的fork join框架是什么?

  fork join框架是JDK7中出现的一款高效的工具,Java开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。

53) Java多线程中调用wait() 和 sleep()方法有什么不同?

  Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。