2018 Java面试题(2019.08.01 更新) - Go语言中文社区

2018 Java面试题(2019.08.01 更新)


目录

Java基础:

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

重载和重写的区别

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

说说反射的用途及实现

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

MVC设计思想

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

Colletcion类库中常用类

 

进程和线程:

线程和进程的概念

并行和并发的概念

创建线程的方式及实现

进程间通信的方式

说说 CountDownLatch、CyclicBarrier 原理和区别

说说 Semaphore 原理

说说 Exchanger 原理

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

讲讲线程池的实现原理

线程池的几种实现方式 

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

Java中用到的线程调度算法

单例模式的线程安全性

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

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

多线程的上下文切换

 

锁机制:

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

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

产生死锁的四个条件

如何检查死锁

volatile 实现原理

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

synchronized 与 lock 的区别

AQS 同步队列

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

常见的原子操作类

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

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

Java 8 并发包下常见的并发类

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

 

数据库:

DDL、DML、DCL 分别指什么

explain 命令

脏读、幻读、不可重复读

事务的隔离级别

数据库的几大范式

说说分库与分表设计

分库与分表带来的分布式困境与对应之策

说说 SQL 优化之道

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

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

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

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

聚集索引与非聚集索引的区别

limit 20000 加载很慢怎么解决

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

 

JVM

JVM 运行时内存区域划分

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

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

JVM 性能调优

类加载器、双亲委派模型

类加载的过程

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

Java 内存模型 JMM


Java基础:

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

              

  • 封装:

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

  • 继承:

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

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

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

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

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

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

  1. 实现继承:是指使用基类的属性和方法而无需额外编码的能力;
  2. 接口继承:是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
  3. 可视继承:是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。
  • 多态:

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

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

  1. 覆盖:是指子类重新定义父类的虚函数的做法;
  2. 重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不用,或许参数类型不用、或许两者都不同)。

 

重载和重写的区别

  • override(重写)
  1. 方法名、参数、返回值相同;
  2. 子类方法不能缩小父类方法的访问权限;
  3. 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常);
  4. 存在于父类和子类之间;
  5. 方法被定义为final的不能被重写。
  • overload(重载)
  1. 参数类型、个数、顺序至少有一个不相同;
  2. 不能重载只有返回值不同的方法名;
  3. 存在于父类和子类、同类型中。
区别点 重载 重写(覆写)
英文 Overloading Overiding
定义 方法名称相同,参数的类型或个数不同 方法名称、参数类型、返回值类型全部相同
权限 对权限没要求 被重写的方法不能拥有更严格的权限
范围 发生在一个类中 发生在继承类中

 

 

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

          两者之间的区别:

  1. Integer是int的包装类;int是基本数据类型;
  2. Integer变量必须实例化后才能使用;int不需要;
  3. Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值;
  4. Integer的默认值是null;int的默认值是0。

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

 

说说反射的用途及实现

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

          反射机制的原理:

         

  1. 当 Student 类 new 一个对象的时候,会通知 JVM 去本地加载 Student.class 这个二进制文件;
  2. JVM 在本地磁盘寻找这个文件,找到之后就把它加载到 JVM 内存中,在加载的同时会生成一个对象来映射这个 class 文件,该对象中存储着 Student.class 的信息,包括字段、方法等,将该对象放在 JVM 的一块内存空间中;
  3. 在 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 的区别:

Runnable Callable
重写的方法是 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() 方法后面的程序。

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

两者的区别:

CountDownLatch CyclicBarrier
减计数方式 加计数方式
计算为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() 方法,清除数据。

 

讲讲线程池的实现原理

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

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建),则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程;
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储到这个工作队列里;如果工作队列满了,则进入下个流程;
  3. 判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

         

线程池的几种实现方式 

  • 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 的所有权,过程如下:

  1. 如果 monitor 的进入数为0,则该线程进入 monitor ,然后将进入数设置为1,该线程即为 monitor 所有者;
  2. 如果线程已经占有该 monitor ,只是重新进入,则进入 monitor 的进入数加1;
  3. 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为0,再重新尝试获取 monitor 的所有权。

 

synchronized 与 lock 的区别

类别 synchronized Lock
存在层次 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): 
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

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

 

Java 8 并发包下常见的并发类

Java 并发包常用类小结

 

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

  • 偏向锁:

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

  • 轻量级锁:

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

  • 重量级锁:

通过对象内部的监视器(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 列说明:

id select 识别符。这是 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
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/IbelieveSmile/article/details/81334205
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-25 01:45:15
  • 阅读 ( 1038 )
  • 分类:面试题

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢