JAVA高级工程师面试看这篇就够了--基础篇

一. JAVA基础相关问题

1. HashMap底层是怎么实现的

HashMap底层是由数组和单向链表实现,在jdk1.8+又加入了红黑树。

2. HashMap的数组是怎么初始化怎么扩容的

HashMap在初始化对象时,可在括号中添加初始化数组大小,HashMap并不会直接使用该数值,而是使用当前传入数字的最大2的倍数。
当我们初始化时不传入长度时,则使用默认值16。
HashMap底层数组容量使用率高达75%时会自动扩容当前长度得2的倍数

3. HashMap什么时候会用到单向链表

当hash产生冲突时jdk1.7会使用头插法将新数据插在最前面,1.8以后使用尾插法

4. HashMap什么时候会用到红黑树

jdk1.8后引入了红黑树,当单向链表长度大于8的时候,将后面的数据存储在红黑树种(属于二叉树的一种,一个父两个叶子节点)反之小于8时又存入链表中

5. HashMap PUT的时候都做了那些操作

第一步,我们的put方法会去判断这个hashmap是否为null 或者长度是否为0
若为null或者长度为0 则新建一个(这也是平时在编程过程中需要经常注意的细节);

第二步,就用到了我们这个key值啦,put方法会根据这个key计算hash码来得到数组的位置,
(这里需要解释一下,我们的hashmap默认是由一个数组加链表组成的)
得到位置后当然是继续判断这个数组下标的值是否为null,
为null 自然是直接插入我们的value值,else;

第三步,判断key是否为null,当key!=null我们就可以覆盖value值,else if

第四步,判断数组后面跟着的这个链是否为树(TreeNode),是树呢,我们传入的值就会按照key,value的格式存入了,else

第五步,不是树就是链表,那么put方法就会遍历这个链表,

第六步,在遍历的时候呢我们会判断这个链表的长度是否大于8,大于呢就会将这个链表转换为树,再按照key,value的格式存入

第七步,小于则会判断链表中的key!=null,若kay!=null则覆盖,key==null我们的value就会插入

最后一步为判断扩容,当数组容量超过最大容量时就会扩容一倍(即二进制的进位)

6. HashMap 是否是线程安全的

HashMap不是线程安全的,我们点开HashMap的源码就可以发现,HashMap中没有添加synchronized关键字或者JAVA其他锁的机制,所以在高并发场景下不建议使用HashMap,会出现数据覆盖的情况;

7. 那么我们在高并发场景下需要存储KEY-VALUE结构的数据时使用什么

我们可以使用Hashtable或者ConcurrentHashMap;
Hashtable底层方法是使用synchronized关键字来实现线程安全,是将整个数组元素划分为一个大锁。
ConcurrentHashMap采用分段锁的方式,各个锁之间相互不干扰,锁的力度更细,所以效率也大大提高。
个人推荐使用ConcurrentHashMap

8. JAVA实现线程同步的方式有哪些

  1. 使用volatile关键字
    a.volatile关键字为域变量的访问提供了一种免锁机制
    b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
    c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
    d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
  2. 使用synchronized关键字,同步方法,同步代码块
    a.用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    b.用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
    注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
  3. 使用重入锁实现线程同步
    使用java并发包ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
    ReenreantLock类的常用方法有:
    ReentrantLock() : 创建一个ReentrantLock实例
    lock() : 获得锁
    unlock() : 释放锁
    注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
	public class Bank {
    private int count = 0;// 账户余额

    // 需要声明这个锁
    private Lock lock = new ReentrantLock();

    // 存钱
    public void addMoney(int money) {
        lock.lock();
        try {
            count += money;
            System.out.println(System.currentTimeMillis() + "存进:" + money);
        } finally {
            lock.unlock();
        }
    }

    // 取钱
    public void subMoney(int money) {
        lock.lock();
        try {

            if (count - money < 0) {
                System.out.println("余额不足");
                return;
            }
            count -= money;
            System.out.println(+System.currentTimeMillis() + "取出:" + money);
        } finally {
            lock.unlock();
        }
    }

    // 查询
    public void lookMoney() {
        System.out.println("账户余额:" + count);
    }
}
1、ReentrantLock()还可以通过public ReentrantLock(boolean fair)构造方法创建公平锁,即,优先运行等待时间最长的线程,这样大幅度降低程序运行效率。 
2、关于Lock对象和synchronized关键字的选择: 
(1)、最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 ??
(2)、如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。 
(3)、如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。
  1. ThreadLocal
    ThreadLocal 类的常用方法
    ThreadLocal() : 创建一个线程本地变量
    get() : 返回此线程局部变量的当前线程副本中的值
    initialValue() : 返回此线程局部变量的当前线程的"初始值"
    set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
public class Bank {
     private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){  
         @Override  
            protected Integer initialValue() {  
                // TODO Auto-generated method stub  
                return 0;  
            }  
     };


    // 存钱
    public void addMoney(int money) {
        count.set(count.get()+money);  
        System.out.println(System.currentTimeMillis() + "存进:" + money);  
    }

    // 取钱
    public void subMoney(int money) {
        if (count.get() - money < 0) {  
            System.out.println("余额不足");  
            return;  
        }  
        count.set(count.get()- money);  
        System.out.println(+System.currentTimeMillis() + "取出:" + money);  
    }

    // 查询
    public void lookMoney() {
        System.out.println("账户余额:" + count.get());
    }
}

ThreadLocal的原理:
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变 量副本,而不会对其他线程产生影响。
即每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,只是名字相同而已,两个线程间的count没有关系。所以就会发生上面的效果。

ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

ThreadLocal并不能替代同步机制,两者面向的问题领域不同。
1:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;
2:而threadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享变量,这样当然不需要对多个线程进行同步了。

9. JAVA实现多线程的方式有哪些

  1. 实现Runnable接口,并实现接口的run()方法
  2. 继承Thread类,重写run方法
  3. 实现Callable接口,重写call()方法
Runnable,Thread实现的接口都是无返回值并且无法抛出异常的;
Callable对象属于Executor框架中的功能类,Callable与Runnable接口类似,但是提供了比Runnable更强大的功能:
Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。
Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。
运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果。可以使用Future来监视目标线程调用call()方法的使用情况,当调用Future的get()方法以获取结果是,当前线程就会阻塞,直到call()方法结束返回结果为止;
一般推荐实现Runnable接口的方式,原因如下:Thread类定义了多种方法可以被派生类使用或重写,但是只有run方法是必须被重写的,在run方法中实现这个线程的主要功能。所以没有必要继承Thread,去修改其他方法;

10. JAVA多线程中sleep和wait方法区别和共同点?

1、两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
2、两者都可以暂停线程的执行。
3、Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
4、wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。

11. 谈谈你对JVM底层理解

在这里插入图片描述
从上图可以看出,JVM主要分为四个部分:

  1. 类加载器(ClassLoad) 在这里插入图片描述
在JVM启动时或者在类运行时将需要的class加载到JVM中。
类加载时间与过程:
类从被加载到虚拟机内存开始,在到卸载出内存为止,正式生命周期包括了:加载,验证,准备,解析,初始化,使用和卸载7个阶段。其中验证、准备、解析这个三个步骤被统称为连接(linking)。
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的   ,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定)

在以下几种情况下,会对未初始化的类进行初始化:
创建类的实例
对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,发现其父类没有被初始化,则需要先初始化父类
当虚拟机启动的时候,用户需要指定一个执行的主类,虚拟机会先初始化这个主类

Java默认提供的三个ClassLoader
BootStrap ClassLoader:被称为启动类加载机制,是Java类加载层次中最顶层的类加载器,负责加载JDK中核心类库。
Extension ClassLoader:被称为扩展类加载器,负责加载Java的扩展类库,Java虚拟机的实现会提供一个扩展目录,该类加载器在此目录里面查找并加载JavaAppClassLoader:被称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。一版来说,Java应用的类都  是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。

ClassLoader加载类的原理:
ClassLoader使用的是双亲委托机制来搜索加载类的,每一个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是组合关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它的ClassLoadre实例的父类加载器。
当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是有上之下一次检查的。
首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没有加载到,则把任务转交给Extension  ClassLoader试图加载,如果没有加载到,则转交给AppClassLoader进行加载,如果它也没有加载到话,则发挥给委托的发起者,有它到指定的文件系统或网络等URL中加载该类,如果都没有加载到此类,那么抛出ClassNotFoundException异常。.
否则将这个找到的类生成一个类的定义,并将它加载到内存中,最后返回这个类的内存中Class对象。
  1. 内存区(也叫运行时数据区)
是在jvm运行的时候操作所分配的内存区。运行时内存区主要分为5个区域:
1.方法区(Methd Area:用于存储类结构信息的地方,包括常量池,静态变量,构造函数。虽然JVM规范把方法区描述为堆的一个逻辑部分,但它却有个别名(non-heap 非堆)
2.Java堆(Heap:存储java实例或者对象的地方。这块是GC的主要区域。从存储内容上可以看到Java堆和方法区是被java线程共享的。
3.Java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,jvm就会为这个线程创建一个对应的java栈。在这个Java栈中又会包含多个帧栈,每运行一个方法就创建一个帧栈,由于存储局部变量,操作栈,方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个帧栈在Java栈中入栈和出栈的过程。所以java栈是私有。
4.程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVN程序是多线程执行的(线程轮转切换),所以为了保证线程切换回来,还能回复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
5.本地方法栈(Native Method Stack):和Java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
  1. 执行引擎
    负责执行class文件中包含的字节码指令
  2. 本地接口
    主要是调用C或C++实现的本地方法及返回结果

12. 谈谈你对JVM内存分配的理解

Java虚拟机是一次性分配一块较大的内存空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念。有了这块空间,如何进行分配和回收就跟GC机制有关系了。

Java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配。比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。综上所述:java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以几个地方的内存分配和回收是确定的,不需要管。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存分配和回收是动态的。一般说的垃圾回收也是针对这部分的。

13. 谈谈你对JVM垃圾回收(GC的理解)

垃圾收集器一般必须完成两件事:检测出垃圾,回收垃圾。检测垃圾一般分为以下几种:

  • 引用计数法:给对象增加一个引用计数器,每当有地方引用这个对象时,就在计数器上加1,引用失效就减1
  • 可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java堆中引用的对象,方法区常量池的引用对象。
  • 总之,jvm在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。一般回收有一下几种方法:
    1. 标记-清除(Mark-sweep):分为两个阶段,标记和清楚。标记所有需要回收的对象,然后统一回收这个是最基础的算法,后续的收集算法 都是基于这个算法扩展的。
      不足:效率低;标记清楚之后会产生大量的碎片。
    2. 复制(copying): 此算法把内存空间划分为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前区域,把正在使用中的对象复制到另外区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去还能进行相应的内存整理,不会出现“内存碎片”问题。当然,此算法的缺点也是很明显,就是需要双倍内存。
    3. 标记-整理(Mark-Compact):此算法结合了“标记-清除”和“复制”两个算法的有点。也是两个阶段,第一阶段从根节点开始标记所被引用的对象。第二阶段遍历整个堆,把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“复制”算法的空间问题
    4. 分代收集算法:这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策率,是基于这样一个事实:不同的对象的生命周期不一样的。因此,不同生命周期的对象采取不同的收集方式,以便提高回收效率。

14. 为什么要运用分代垃圾回收策率?

在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责和功能不同,所以也有着不同的生命周期。有的对象生命周期较长,有的对象生命周期较短。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对比较长,而对于存活时间较长的对象进行扫描的工作是徒劳的。因此就引入了分治思想,所谓分治思想就是因地制宜,将对象进行代划分,把不同的生命周期的对象放在不同的代上使用不同的垃圾回收方法。

将对象按其生命周期的不同划分成:新生代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收信息相关的是新生代,老年代。

新生代:是所有新对象产生的地方。新生代被分为3个部分:Ender(出生)区和两个Survivor(幸存者)区(From和To)。当Ender区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个Survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把他们转移到另一个Survivor区(假设为to区)。这样在一段时间内总会有一个空的Survivor区。经过多次GC后,仍然存活下来的对象会被转移到老年代内存空间。新生代使用复制算法。

年老带:在年轻代经过N次回收的仍然没有被清除的对象会放到老年代,可以说它们是久经沙场而不亡的一代,都是生命周期较长的对象。对于老年代和永久代,就不能再采用年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科,通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。老年代使用标记-整理算法。

持久代:用于存放静态文件,比如Java类,方法等。持久代对垃圾回收没有显著的影响。

15. 使用JAVA线程池给我们带来了那些好处?

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池,必须对其实现原理了如指掌;

16. JAVA线程池工作流程?

1)当提交一个新任务到线程池时,线程池判断corePoolSize线程池是否都在执行任务,如果有空闲线程,则创建一个新的工作线程来执行任务,直到当前线程数等于corePoolSize;

2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务

17. JAVA线程池都有那些参数?

  1. corePoolSize #核心线程数
  2. maximumPoolSize #最大线程数
  3. keepAliveTime #达到最大线程数数时候,线程池的工作线程空闲后,保持存活的时间
  4. unit #存活时间单位
  5. workQueue #阻塞队列
  6. handler #饱和策略
四种饱和策略:
AbortPolicy:不处理,直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务,即提交任务的线程。
DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉,不抛出异常。

17. JAVA线程池运行状态有哪些?

1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,且线程池中的任务数为0

2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

18. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

19. 什么是线程死锁?如何避免死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。就产生了线程死锁。
产生线程死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

19. 如何避免线程死锁?

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

  1. 破坏互斥条件,这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件,一次性申请所有的资源。
  3. 破坏不剥夺条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件,靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

20. java公平锁和非公平锁?

那如何能保证每个线程都能拿到锁呢,队列FIFO是一个完美的解决方案,也就是先进先出,java的ReenTrantLock也就是用队列实现的公平锁和非公平锁。

在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁。
公平锁示例:

package com.thread.fair;

import java.util.concurrent.locks.ReentrantLock;

public class MyFairLock {
    /**
     *     true 表示 ReentrantLock 的公平锁
     */
    private  ReentrantLock lock = new ReentrantLock(true);

    public   void testFail(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +"获得了锁");
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyFairLock fairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName()+"启动");
            fairLock.testFail();
        };
        Thread[] threadArray = new Thread[10];
        for (int i=0; i<10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i=0; i<10; i++) {
            threadArray[i].start();
        }
    }
}
Thread-0启动
Thread-0获得了锁
Thread-1启动
Thread-1获得了锁
Thread-2启动
Thread-2获得了锁
Thread-3启动
Thread-3获得了锁
Thread-4启动
Thread-4获得了锁
Thread-5启动
Thread-5获得了锁
Thread-6启动
Thread-6获得了锁
Thread-8启动
Thread-8获得了锁
Thread-7启动
Thread-7获得了锁
Thread-9启动
Thread-9获得了锁

可以看到,获取锁的线程顺序正是线程启动的顺序。

非公平锁示例:

public class MyNonfairLock {
    /**
     *     false 表示 ReentrantLock 的非公平锁
     */
    private  ReentrantLock lock = new ReentrantLock(false);

    public  void testFail(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +"获得了锁");
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyNonfairLock nonfairLock = new MyNonfairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName()+"启动");
            nonfairLock.testFail();
        };
        Thread[] threadArray = new Thread[10];
        for (int i=0; i<10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i=0; i<10; i++) {
            threadArray[i].start();
        }
    }
}
Thread-1启动
Thread-0启动
Thread-0获得了锁
Thread-1获得了锁
Thread-8启动
Thread-8获得了锁
Thread-3启动
Thread-3获得了锁
Thread-4启动
Thread-4获得了锁
Thread-5启动
Thread-2启动
Thread-9启动
Thread-5获得了锁
Thread-2获得了锁
Thread-9获得了锁
Thread-6启动
Thread-7启动
Thread-6获得了锁
Thread-7获得了锁

可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程。
最后
那非公平锁和公平锁适合什么场合使用呢,他们的优缺点又是什么呢?

优缺点:
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

使用场景
使用场景的话呢,其实还是和他们的属性一一相关,举个栗子:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁会给业务增强很多的可控制性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值