zoukankan      html  css  js  c++  java
  • Java面试

    目录

    Java基础:

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

    重载和重写的区别

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

    说说反射的用途及实现

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

    MVC设计思想

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

    Colletcion类库中常用类

     

    进程和线程:

    线程和进程的概念

    并行和并发的概念

    创建线程的方式及实现

    进程间通信的方式

    说说 CountDownLatch、CyclicBarrier 原理和区别

    说说 Semaphore 原理

    说说 Exchanger 原理

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

    讲讲线程池的实现原理

    线程池的几种实现方式 

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

     

    锁机制:

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

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

    产生死锁的四个条件

    如何检查死锁

    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 对象,表示异步计算的结果。

    		<p>它提供了检索计算是否完成的方法,以等待计算的完成,</p>
    
    		<p>并检索计算的结果。通过 Future 对象可以了解任务执行情况,</p>
    
    		<p>可取消任务的执行,还可以获取执行结果。&nbsp;</p>
    		</td>
    	</tr></tbody></table></div><p>2&gt; 使用继承 Thread 类的方式创建多线程:</p>
    

         优势:

              如果要访问当前线程,则无需使用 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

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

    1. ExecutorService executorService = Executors.newCachedThreadPool();
    2. for (int i = 0; i < 10; i++) {
    3. final int index = i;
    4. executorService.execute(new Runnable() {
    5. @Override
    6. public void run() {
    7. log.info("task:{}",index);
    8. }
    9. });
    10. }
    11. executorService.shutdown();
    • Executors.newFixedThreadPool

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

    1. ExecutorService executorService = Executors.newFixedThreadPool(3);
    2. for (int i = 0; i < 10; i++) {
    3. final int index = i;
    4. executorService.execute(new Runnable() {
    5. @Override
    6. public void run() {
    7. log.info("task:{}",index);
    8. }
    9. });
    10. }
    11. executorService.shutdown();
    • Executors.newScheduledThreadPool

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

    1. ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
    2. // 延迟一秒之后,每隔三秒执行一次
    3. executorService.scheduleAtFixedRate(new Runnable() {
    4. @Override
    5. public void run() {
    6. log.info("scheduled run");
    7. }
    8. },1,3,TimeUnit.SECONDS);
    9. // 也可以使用timer的schedule方法来实现定时功能
    10. Timer timer = new Timer();
    11. timer.schedule(new TimerTask() {
    12. @Override
    13. public void run() {
    14. log.info("timer run");
    15. }
    16. },new Date(),5*1000);
    • Executors.newSingleThreadExecutor

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

    1. ExecutorService executorService = Executors.newSingleThreadExecutor();
    2. for (int i = 0; i < 10; i++) {
    3. final int index = i;
    4. executorService.execute(new Runnable() {
    5. @Override
    6. public void run() {
    7. log.info("task:{}",index);
    8. }
    9. });
    10. }
    11. 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() 方法让一个线程终止运行。

    锁机制:

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

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

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

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

    • 存在共享数据(也称临界资源);
    • 存在多条线程共同操作共享数据。

    保证线程安全的方法

    • 竞争与原子操作
    • 同步与锁
    • 可重入
    • 过度优化

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

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

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

    产生死锁的四个条件

    • 互斥条件

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

    • 请求和保持条件

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

    • 不可剥夺条件

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

    • 环路等待条件

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

    如何检查死锁

    通过 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  

    		<p>&nbsp;</p>
    		</td>
    		<td>
    		<p>select 类型,可以为一下任何一种:</p>
    
    		<ul><li>SIMPLE:简单 slelect(不使用UNION或子查询)</li>
    			<li>PRIMARY:最外面的 select</li>
    			<li>UNION:UNION 中的第二个或后面的 select 语句</li>
    			<li>DEPENDENT UNION:UNION 中的第二个或后面的 select 语句,取决于外面的查询</li>
    			<li>UNION RESULT:UNION 的结果</li>
    			<li>SUBQUERY:子查询中的第一个 select</li>
    			<li>DEPENSENT SUBQUERY:子查询中的第一个 select,取决于外面的查询</li>
    			<li>DERIVED:导出表的 select (from 子句的子查询)</li>
    		</ul></td>
    	</tr><tr><td>table</td>
    		<td>输出的行所引用的表</td>
    	</tr><tr><td>type</td>
    		<td>
    		<p>联接类型。下面给出各个联接类型,按照从最佳类型到最坏类型进行排序:</p>
    
    		<ul><li>system:表仅有一行(=系统表)。这是const联接类型的一个特例</li>
    			<li>const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const 表很快,因为它们只读取一次</li>
    			<li>eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了 const 类型</li>
    			<li>ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取</li>
    			<li>ref_or_null:该联接类型如同 ref ,但是添加了 MySQL 可以专门搜索包含 NULL 值的行</li>
    			<li>index_merge:该联接类型表示使用了索引合并优化方法</li>
    			<li>unique_subquery:该类型替换了下面形式的 IN 子查询的ref:value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery 是一个索引查找函数,可以完全替换子查询</li>
    			<li>index_subquery:该联接类型类似于 unqiue_subquery 。可以替换 ID 子查询,但只适合下列形式的子查询的非唯一索引:value IN(SELECT key_column FROM single_table WHERE some_expr)</li>
    			<li>range:只检索给定范围的行,使用一个索引来选择行</li>
    			<li>index:该联接类型与 ALL 相同,除了只有索引树被扫描。这通常比 ALL 快,因为索引文件通常比数据文件小</li>
    			<li>ALL:对于每个来自于先前的表的行组合,进行完整的表扫描</li>
    		</ul></td>
    	</tr><tr><td>
    		<p>possible_keys</p>
    
    		<p>&nbsp;</p>
    		</td>
    		<td>指出 MySQL 能使用哪个索引在该表中找到行</td>
    	</tr><tr><td>
    		<p>key</p>
    
    		<p>&nbsp;</p>
    		</td>
    		<td>显示&nbsp;MySQL 实际觉得使用的键(索引)。如果没有选择索引,键是 NULL</td>
    	</tr><tr><td>
    		<p>key_len</p>
    
    		<p>&nbsp;</p>
    		</td>
    		<td>显示&nbsp;MySQL 决定使用的键长度。如果键是 NULL,则长度为 NULL</td>
    	</tr><tr><td>ref</td>
    		<td>显示使用哪个列或常数与 key 一起从表中选择行</td>
    	</tr><tr><td>
    		<p>rows</p>
    
    		<p>&nbsp;</p>
    		</td>
    		<td>&nbsp;显示&nbsp;MySQL 认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数</td>
    	</tr><tr><td>filtered</td>
    		<td>显示了通过条件过滤出的行数的百分比估计值</td>
    	</tr><tr><td>Extra</td>
    		<td>
    		<p>该列包含&nbsp;MySQL 解决查询的详细信息</p>
    
    		<ul><li>Disinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行</li>
    			<li>Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行</li>
    			<li>range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用</li>
    			<li>Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行</li>
    			<li>Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息</li>
    			<li>Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果</li>
    			<li>Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户</li>
    			<li>Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描</li>
    			<li>Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表</li>
    		</ul></td>
    	</tr></tbody></table></div><p>&nbsp;</p>
    

    数据库事务 ACID

    • Atomicity(原子性):原子性要求每个事务中的所有操作要么全部完成,要么就像全部没有发生一样;如果事务中的部分操作失败了,则整个事务失败,结果就是数据库中的状态保持没变;
    • Consistency(一致性):一致性确保了任何事务都会使数据库从一种合法的状态变为另一种合法的状态;
    • Isolation(隔离性):隔离性保证了并发执行多个事务对系统状态的影响和串行化执行多个事务对系统状态的影响是一样的;
    • Durability(持久性):持久性保证了一个事务一旦被提交以后,其状态就保持不变,甚至是发生了主机断电、崩溃、错误等。

    脏读、幻读、不可重复读

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

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

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

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

    事务的隔离级别

    • Serializable(串行化):可避免脏读、不可重复读、幻读的发生;
    • Repeatable read(可重复读):可避免脏读、不可重复读的发生;
    • Read commited(读已提交):可避免脏读的发生;
    • Read uncommited(读未提交):最低级别,任何情况都无法保证。

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

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

    数据库的几大范式

    1. 第一范式:确保每列的原子性;
    2. 第二范式:确保表中的每列都和主键相关,要求每个表值描述一件事情;
    3. 第三范式:确保每列都和主键列直接相关,而不是间接相关。

    说说分库与分表设计

    • 分表:对于访问极为频繁且数据量巨大的单表来说,我们首先要做的就是减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的吞吐;
    • 分库:分表能够解决单表数据量过大带来的查询效率下降的问题,但是却无法给数据库的并发处理能力带来质的提升。所以需要对数据库进行拆分,从而提高数据库的写入能力。

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

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

    说说 SQL 优化之道

    • 减少查询字段数;
    • 表关联尽量用主键;
    • 查询条件尽量避免模糊查询;
    • 避免使用排序字段,排序字段尽量使用主键;
    • 尽量使用限制查询条件;
    • 查询条件使用有效索引。

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

    两者的区别:

    1. InnoDb 支持事务,MyISAM 不支持。这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而 MyISAM 就不可以了;
    2. MyISAM 适合查询以及插入为主的应用,InnoDB 适合频繁修改以及设计到安全性较高的应用;
    3. InnoDB 支持外键, MyISAM 不支持;
    4. 从 MySql 5.5 以后,InnoDB  是默认引擎;
    5. InnoDb 不支持 FULLTEXT 类型的索引;
    6. InnoDb 中不保存表的行数,如 select count(*) from table 时,InnoDB 需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。主要的是,当 count(*) 语句包含 where 条件时 MyISAM 特需要扫描整个表;
    7. 对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM 表中可以和其它字段一起建立联合索引;
    8. 清空整个表时,InnoDb 是一行一行的删除,效率非常慢。MyISAM 则会重新建表;
    9. 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 调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。

    JVM 性能调优

    类加载器、双亲委派模型

    • 类加载器:
      • 启动类加载器(BootStrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过 -Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt、jar )的类;
      • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOMElibext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库;
      • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath) 上的类库。
    • 双亲委派模型:
      • 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
      • 好处:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类;

    类加载的过程

    • 加载:
      • 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.class 对象,作为方法区这个类的各种数据的入口。
    • 链接:
      • 验证:这一阶段的主要目的是为了确保 class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;
      • 准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
      • 解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
        • 符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一直的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中;
        • 直接引用:是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
    • 初始化:
      • 初始化阶段是类加载的最后一个阶段。前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其他操作都是由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

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

    • 强引用:只要引用存在,垃圾回收器永远不会回收;
    • 软引用:非必须引用,内存溢出之前进行回收;
    • 弱引用:第二次垃圾回收时回收;
    • 虚引用:垃圾回收时回收,无法通过引用取到对象值。

    Java 内存模型 JMM

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

    特性:

    • 原子性:指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响;
    • 可见性:指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值;
    • 有序性:指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。
  • 相关阅读:
    圣杯布局,不太明白为什么后面的元素会飘上来
    CSS实现宽高成比例缩放
    javascript原生ajax;
    http-关于application/x-www-form-urlencoded等字符编码的解释说明
    jQuery判断滚动条滚到页面底部脚本
    下拉顶部刷新简单实现
    swig模板下拉框应用
    swig模板中文资料
    张宵 20200924-2 功能测试
    张宵 20200924-5 四则运算试题生成
  • 原文地址:https://www.cnblogs.com/wldan/p/10707507.html
Copyright © 2011-2022 走看看