Java面试核心知识2

发布于:2025-02-10 ⋅ 阅读:(110) ⋅ 点赞:(0)

一、JAVA集合

1.1. 接口继承关系和实现

集合类存放于Java.util包中,主要有3种:set(集)、list(列表包含Queue)和map(映射)。

1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。

2. Iterator:迭代器,可以通过迭代器遍历集合中的数据

3. Map:是映射表的基础接口

 

1.2. List Java 的List 是非常常用的数据类型。List是有序的Collection。Java List一共三个实现类: 分别是ArrayList、Vector 和LinkedList。 

1.2.1. ArrayList(数组)

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数 组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数 组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进 行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

1.2.2. Vector(数组实现、线程同步)

Vector 与ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此, 访问它比访问ArrayList慢。

1.2.3. LinkList(链表)

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较 慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆 栈、队列和双向队列使用。 

1.3. Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重 复。对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断 的,如果想要让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方 法。

1.3.1.1. HashSet(Hash表)

哈希表边存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不 同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的 hashcode 方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls结果为true ,HashSet就视为同一个元素。

如果equals 为false就不是 同一个元素。 哈希值相同equals为false的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相 同的元素放在一个哈希桶中)。也就是哈希一样的存一列。如图1表示hashCode值不相同的情 况;图2表示hashCode值相同,但equals不相同的情况。

 HashSet 通过hashCode值来确定元素在内存中的位置。一个hashCode位置上可以存放多个元 素。

1.3.1.2. TreeSet(二叉树)

1. TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增 加一个对象都会进行排序,将对象插入的二叉树指定的位置。

2. Integer 和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自 己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使 用。

3. 在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序

4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整 数、零或正整数。

1.3.1.3. LinkHashSet(HashSet+LinkedHashMap)

对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。 LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法 操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并 通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操 作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

1.4. Map

 

1.4.1. HashMap(数组+链表+红黑树)

HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快 的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记 录的值为null。HashMap 非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导 致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍 HashMap 的结构。 

大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色 的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。 1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。

2. loadFactor:负载因子,默认为 0.75。 

3. threshold:扩容的阈值,等于 capacity * loadFactor

1.4.1.2. JAVA8实现

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑 树 组成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的 具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决 于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

1.4.2. ConcurrentHashMap

1.4.2.1. Segment段

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一 些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的 意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。 1.4.2.2. 线程安全(Segment 继承 ReentrantLock 加锁)

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每 个 Segment 是线程安全的,也就实现了全局的线程安全。 

1.4.2.3. 并行度(默认16)

concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16, 也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支 持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时 候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实 每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

1.4.2.4. Java8 实现 (引入了红黑树)

Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。 

 1.4.3. HashTable(线程安全)

Hashtable 是遗留类,很多映射的常用功能与HashMap 类似,不同的是它承自Dictionary 类, 并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap, 因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全 的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

1.4.4. TreeMap(可排序)

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序, 也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。 如果使用排序的映射,建议使用TreeMap。 在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的 Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

参考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html

1.4.5. LinkHashMap(记录插入顺序)

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

参考1:http://www.importnew.com/28263.html

二、 JAVA多线程并发

2.1.1. JAVA并发知识库

2.1.2. JAVA线程实现/创建方式 

2.1.2.1. 继承Thread类

Thread 类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方 法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线 程,并执行run()方法。

public class MyThread extends Thread {

      public void run() {

            System.out.println("MyThread.run()");

           }

 }

MyThread myThread1 = new MyThread();

myThread1.start();

4.1.2.2. 实现Runnable接口。

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个 Runnable 接口。

public class MyThread extends OtherClass implements Runnable {

    public void run() {

    System.out.println("MyThread.run()");

    }

}

//启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myThread = new MyThread();

Thread thread = new Thread(myThread); thread.start();

//事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用 target.run() public void run() {

    if (target != null) {

    target.run();

}

}

4.1.2.3. ExecutorService、Callable、Future有返回值线程

  有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行 Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务 返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程 了。

 //创建一个线程池  
  ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
  // 创建多个有返回值的任务  
  List<Future> list = new ArrayList<Future>();  
  for (int i = 0; i < taskSize; i++) {  
  Callable c = new MyCallable(i + " ");  
  // 执行任务并获取Future对象  
  Future f = pool.submit(c);   
  list.add(f);  
  }  
  // 关闭线程池  
  pool.shutdown();  
   // 获取所有并发任务的运行结果  
  for (Future f : list) {  
  // 从Future对象上获取任务的返回值,并输出到控制台  
  System.out.println("res:" + f.get().toString());  
  } 

1.1.2.4. 基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销 毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

// 创建线程池 
ExecutorService threadPool = Executors.newFixedThreadPool(10); 
while(true) { 
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行 
@Override 
public void run() { 
System.out.println(Thread.currentThread().getName() + " is running .."); 
try { 
Thread.sleep(3000); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
} 
} 
}); 
} 
}

1.1.3. 4 种线程池 Java 里面线程池的顶级接口是Executor,但是严格意义上讲 Executor 并不是一个线程池,而 只是一个执行线程的工具。真正的线程池接口是ExecutorService。

1.1.3.1. newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行 很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造 的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并 从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资 源。

1.1.3.2. newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大 多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务, 则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何 线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之 前,池中的线程将一直存在。

1.1.3.3. newScheduledThreadPool 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);       
scheduledThreadPool.schedule(newRunnable(){          
@Override  
public void run() { 
System.out.println("延迟三秒"); 
} 
}, 3, TimeUnit.SECONDS); 
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){         
@Override             
public void run() { 
System.out.println("延迟 1 秒后每三秒执行一次"); 
} 
},1,3,TimeUnit.SECONDS);

1.1.3.4. newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去! 

1.1.4. 线程生命周期(状态) 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。 在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞 (Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自 运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

1.1.4.1. 新建状态(NEW)

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配 内存,并初始化其成员变量的值

1.1.4.2. 就绪状态(RUNNABLE):

当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。

1.1.4.3. 运行状态(RUNNING):

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状 态。

1.1.4.4. 阻塞状态(BLOCKED):

阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状 态。

阻塞的情况分三种:

等待阻塞(o.wait->等待对列): 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue) 中。

同步阻塞(lock->锁池) 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线 程放入锁池(lock pool)中。

其他阻塞(sleep/join) 运行(running)的线程执行Thread.sleep(long ms)或 t.join()方法,或者发出了I/O请求时, JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O 处理完毕时,线程重新转入可运行(runnable)状态。

1.1.4.5. 线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。

正常结束 1. run()或call()方法执行完成,线程正常结束。

异常结束 2. 线程抛出一个未捕获的Exception或Error。

调用stop 3. 直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

1.1.5. 终止线程4种方式

1.1.5.1. 正常运行结束 程序运行结束,线程自动结束。

1.1.5.2. 使用退出标志退出线程

一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的 运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如: 最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while 循环是否退出,代码示例: 

public class ThreadSafe extends Thread { 
public volatile boolean exit = false;  
public void run() {  
while (!exit){ 
//do something 
} 
}  
}

定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit 时,使用了一个Java 关键字volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只 能由一个线程来修改exit的值。

1.1.5.3. Interrupt 方法结束线程

使用interrupt()方法来中断线程有两种情况:

1. 线程处于阻塞状态:如使用了sleep,同步锁的wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt 方法线程就会结束,实 际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正 常结束run方法。

2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

public class ThreadSafe extends Thread { 
public void run() {  
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 
try{ 
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出 
}catch(InterruptedException e){ 
e.printStackTrace(); 
break;//捕获到异常之后,执行break跳出循环 
} 
} 
}  
}

1.1.5.4. stop 方法终止线程(线程不安全)

   程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关 闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是: thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子 线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈 现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因 此,并不推荐使用stop方法来终止线程。

1.1.6. sleep 与 wait 区别

1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于 Object 类中的。

1.1.7. start 与 run 区别

1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。

2. 通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运 行。 3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运 行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

1.1.8. JAVA后台线程

1. 定义:守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公 共服务,在没有用户线程可服务时会自动离开。

2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

3. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程 的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。

4. 在Daemon线程中产生的新线程也是Daemon的。

5. 线程则是JVM级别的,以Tomcat 为例,如果你在Web 应用中启动一个线程,这个线程的 生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程 依旧是活跃的。

6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线 程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统 中的可回收资源。

7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依 赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退 出了;如果还有一个或以上的非守护线程则JVM不会退出。

1.1.9. JAVA锁

1.1.9.1. 乐观锁

   乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为 别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数 据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。 java 中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入 值是否一样,一样则更新,否则失败。

1.1.9.2. 悲观锁

  悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人 会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。 java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到, 才会转换为悲观锁,如RetreenLock。

1.1.9.3. 自旋锁

     自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁 的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋), 等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程 也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁 的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

      自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来 说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会 导致线程发生两次上下文切换!

     但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合 使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量 线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗, 其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;

自旋锁时间阈值(1.6引入了适应性自旋锁)

自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择 自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而 会影响整体系统的性能。因此自旋的周期选的额外重要!

JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应 性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥 有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当 前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2) 个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner 发生了变化则延迟自旋 时间(自旋计数)或进入阻塞,如果CPU处于节电模式则停止自旋,自旋时间的最坏情况是CPU 的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差),自旋时会适当放 弃线程优先级之间的差异。

自旋锁的开启

JDK1.6 中-XX:+UseSpinning 开启;

-XX:PreBlockSpin=10 为自旋次数;

JDK1.7 后,去掉此参数,由jvm控制;

4.1.9.4. Synchronized 同步锁

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重 入锁。

Synchronized 作用范围

1. 作用于方法时,锁住的是对象的实例(this);

2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;

3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized 核心组件

1) Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;

2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到Entry List中;

4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;

5) Owner:当前已经获取到所资源的线程被称为Owner;

6) !Owner:当前释放锁的线程。

Synchronized 实现

1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下, ContentionList 会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将 一部分线程移动到EntryList中作为候选竞争线程。

2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList 中的某个线程为OnDeck线程(一般是最先进去的那个线程)。

3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck, OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM中,也把这种选择行为称之为“竞争切换”。

4. OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify 或者notifyAll 唤醒,会重新进去EntryList中。

5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统 来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

6. Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。

参考:https://blog.csdn.net/zqz_zqz/article/details/70233767

7. 每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加 上monitorenter 和monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的

8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。

9. Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向 锁等,效率有了本质上的提高。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做 了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking来禁用偏向锁。 


网站公告

今日签到

点亮在社区的每一天
去签到