当前位置:   article > 正文

Java简单总结_class文件中可以看出指令重拍吗

class文件中可以看出指令重拍吗

谈谈你对Java的理解

  • 平台无关性  
  • GC
  • 语言特性:反射,泛型,兰姆达表达式
  • 面相对象
  • 类库
  • 异常处理

Java怎么实现平台无关性

  • 编译时:使用的是javac指令,将Java代码编译成.class字节码存放到指定的class文件中。

class文件保存的就是翻译成的二进制字节码,也就是Java类文件的中的属性,方法,以及类中的常量信息都会被分别存储在class文件中,当然还会添加一个公有的静态常量属性 .class,这个属性记录的类的相关信息,及类型信息即class的一个实例。有了这个class文件就可以进一步调用,使用java 命令。

Javap:jdk自带的反编译器,可以查看Java编译器生成的字节码。

谈谈你对Java的理解

  • 平台无关性  
  • GC
  • 语言特性:反射,泛型,兰姆达表达式
  • 面相对象
  • 类库
  • 异常处理

Java怎么实现平台无关性

  • 编译时:使用的是javac指令,将Java代码编译成.class字节码存放到指定的class文件中。

class文件保存的就是翻译成的二进制字节码,也就是Java类文件的中的属性,方法,以及类中的常量信息都会被分别存储在class文件中,当然还会添加一个公有的静态常量属性 .class,这个属性记录的类的相关信息,及类型信息即class的一个实例。有了这个class文件就可以进一步调用,使用java 命令。

Javap:jdk自带的反编译器,可以查看Java编译器生成的字节码。

JVM如何加载.class文件

package com.muke.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author CHENBT
 * @date 2019/08/04
 * getDeclaredMethod()
方法可以获取到类中所有的方法属性,但是继承的方法是拿不到的
 
* getMethod()方法只能拿到非private的方法,也可以拿到集成的方法
 
*
 */

public class ReflactSimple {
   
public static void main(String[] args) {
       
try {
            Class clazz = Class.forName(
"com.muke.reflect.Rocket");
          
/* Method method[] = clazz.getDeclaredMethods();
            Constructor constructor = clazz.getConstructor();
            for(Method m : method){
                System.out.println(m.getName());
                m.invoke(r,String.class);
            }*/
           /* Rocket r = (Rocket)clazz.newInstance();
            Field f = clazz.getDeclaredField("name");
            f.set(r,"cbt");
           Method m = clazz.getDeclaredMethod("testHello");
           m.setAccessible(true);
           String s = (String)m.invoke(r);
            System.out.println(s);
            Method m1 = clazz.getMethod("testHello1");
            m1.invoke(r);*/
           
Constructor c = clazz.getConstructor(String.class);
           Rocket r =  (Rocket)c.newInstance(
"cbt");
            Method m = clazz.getMethod(
"testHello1",int.class);
            m.invoke(r,
4);
        }
catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
catch (IllegalAccessException e) {
            e.printStackTrace();
        }
catch (InstantiationException e) {
            e.printStackTrace();
        }
catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

ClassLoader

从编译到执行的过程

  • 编译器将Robot.java文件编译为Robot.class字节码文件。
  • ClassLoader将字节码抓换JVM中的Class<Robot>对象
  • JVM利用Class<Robot>对象实例化Robot对象

ClassLoader:ClassLoader主要工作在Class装载的加载阶段,主要作用是从系统外部获取Class二进制数据流。他是Java的核心组件,所有的Class都由ClassLoader进行加载,ClassLoader负责将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作。

ClassLoader的种类:

  • BootStrapClassLoader:C++编写,加载Java的核心库java.*
  • ExtClassLoader:Java编写,加载扩展库javax.*
  • AppClassLoader:Java编写,加载程序所在的目录(classpath下)
  • 自定义ClassLoader:Java编写,定制化加载

自定义ClassLoader的实现:

    关键函数:

寻找Class文件,读进二进制流,并且对读取到的流做处理,进而返回一个Class对象。

定义类,函数接受的参数是以一个字节数组的形式传入进来的,byte数组就是接受字节码的。之后就可以对类进行定义返回。

示例代码:

package com.muke.reflect;
import java.io.*;
/**
 * @author CHENBT
 * @date 2019/08/27
 */

public class MyClassLoader extends ClassLoader {
   
private String path;
   
private String classLoaderName ;
   
public MyClassLoader(String path, String classLoaderName){
       
this.classLoaderName = classLoaderName;
       
this.path = path;
    }
   
//用于查找类文件
   
@Override
   
public Class findClass(String name){
       
byte[] b = loadClassData(name);
       
return defineClass(name, b,0,b.length);
    }
   
// 用于加载类文件
   
private byte[] loadClassData(String name) {
        name =
path + name + ".class";
        InputStream in =
null;
        ByteArrayOutputStream out =
null;
       
try{
            in =
new FileInputStream(new File(name));
            out =
new ByteArrayOutputStream();
           
int i = 0;
           
while((i = in.read()) != -1){
                out.write(i);
            }
        }
catch (FileNotFoundException e) {
            e.printStackTrace();
        }
catch (IOException e) {
            e.printStackTrace();
        }
finally {
           
if(in != null){
               
try {
                    in.close();
                }
catch (IOException e) {
                    e.printStackTrace();
                }
            }
           
if(out != null){
               
try {
                    out.close();
                }
catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
       
return out.toByteArray();
    }

}

类加载器的双亲委派机制

首先自底向上检查时候有类加载器已经加载过这个类,然后在自顶向下尝试加载类。

为什么要使用双亲委派机制来加载类

  • 避免多分同样字节码的加载

类的加载方式

隐式加载    :new (可以得到类的对象,支持不同参数类型的构造形式)

显式加载:loadClass,forName等(必须通过newInstance方法才能得到类的对象)

loadClass和forName的区别

 

  • Class.forName得到的class对象是已经初始化过的
  • ClassLoader.loadClass得到的class是还没有链接过的,就只是完成了第一步类的加载,第二部和第三步都没有完成。

Java内存模型

元空间(MetaSpace)和永久代(PermGen)均是方法的实现,只是实现不同。方法区是指JVM的一种规范。在JDK7之后的永久代当中的字符串常量池已经被移动到了Java堆中,并且在JDK7之后使用元空间替代了永久代。其中区别是元空间使用本地内存,而永久代使用的是JVM内存。

JVM三大性能调优参数:

    -Xms:堆的初始值

    -Xmx:堆能达到的最大值

    -Xss:每个线程虚拟机栈(堆栈)的大小,影响并发线程数的大小

Java内存模型中堆和栈的区别 -内存分配策略

静态存储:编译时确定每个数据目标在运行时需要的存储空间需求

栈式存储:数据区需求在编译期未知,运行时模块入口前确定。规定在运行中进入一个程序模块的时候必须知道该程序模块所需要的数据区的大小才能分配器内存。

堆式存储:编译时和运行时模块入口都无法确定,动态分配

GC相关

Java垃圾回收机制

对象被判定为垃圾的标准

没有其他对象锁引用

两种方法:

  • 引用计数算法

通过判断对象的引用数量来决定对象是否可以被回收

每个对象实例都有一个引用计数器,被引用个+1,完成引用则-1

任何引用计数为0的对象实例可以被当做垃圾收集

优点:执行效率高,程序受执行影响较小

缺点:无法检测循环引用的情况,导致内存泄漏

  • 可达性分析法

通过判断对象的引用链是否可达来决定对象是否被回收

可以作为GC ROOT的对象

    虚拟机栈中引用的对象(栈帧中本地变量表中引用的对象);

    方法区中常量引用的对象

    方法区中类静态属性引用的对象

    本地方法栈中JNI(native方法)引用的对象

    活跃线程的引用对象

垃圾回收算法

标记-清除算法

    标记:从根集合开始扫描,对存活的兑现进行标记。

       清除:从堆内存从头到尾进行线性遍历,回收不可达的对象

复制算法:

       将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将存货的对象复制到另一面,然后再把已经使用过的内存一次性全部清理掉。

标记整理算法:

       将可回收的对象标记后,将存活的对象移动到一端,然后清理掉端边界以外的内存。

       避免内存的不连续性,适用于对象存活率较高的场景。

分代收集算法:

       年轻代:尽可能块速的收集到那些生命周期短的对象Eden区和两个Survivor区

       新创建的对象是要被放到Eden区的,如果对象较大,是直接被放到Survivor区的。From和To区也不是固定的,是随着垃圾回收的进行相互转换。

对象如何晋升到老年代

  • 长期存活的对象会进入到老年代,经历了一定的MinorGC次数(默认是15次)依然存活着的对象
  • Survivor区中放不下的对象
  • 大对象直接进老年区(-XX:+PretenuerSizeThreshold参数设置大对象的大小)

老年代:存放生命周期较长的对象

    使用标记清除算法和标记整理算法

   

垃圾收集器

Serial收集器(新):(-XX:+UseSerialGC,复制算法)

单线程收集,进行垃圾收集时必须暂停所有的工作线程。

简单高效,Client模式的年轻代默认收集器

ParNew收集器(新):(-XX:+UseParNewGC,复制算法)

多线程收集器,其余的行为,特点和Serial收集器一样

单核执行效率不如Serial,在多核执行才有优势

目前只有这个收集器能和CMS收集器配合工作

Parallel Scavenge收集器(新):(-XX:+UseParallalGC,复制算法)

吞吐量=运营用户代码时间/(运行用户代码时间+垃圾收集时间)

这个算法更关注吞吐量,在多核执行下才有优势,Server模式下默认的年轻代收集器

SerialOld收集器(老):(-XX:UseSerialOldGC,标记整理算法)

单线程收集,使用垃圾收集时,必须暂停所有的工作线程,简单高效,Client模式下默认的老年代收集器

Parallel Old收集器(老):(-XX:+UseParallelOldGC,标记整理算法)

    多线程,吞吐量优先

CMS收集器(老):(-XX:+UseConcMarkSweepGC,标记清除算法)

垃圾回收线程几乎能与工作线程同时工作

  • 初始标记:stop-the-world,只扫描到跟根对象直接关联的对象并做标记,虽然暂停了整个JVM,但是很快就完成了。
  • 并发标记:并发追溯标记,程序不会停顿。这个阶段应用程序的线程和并发标记的线程同时执行。
  • 并发预清理:虚拟机查找执行并发标记阶段从年轻代晋升到老年代的对象
  • 重新标记,暂停虚拟机,扫描CMS堆中的剩余对象
  • 并发清理:清理垃圾对象,程序不会停顿,应用线程和清理线程并发执行
  • 并发重置:重置CMS收集器的数据结构

G1收集器(-XX:+UseG1GC,复制+标记整理算法)

GarbageFirst收集器的特点:

  • 并发和并行,使用多个CPU缩短stop-the-world的时间,与用户线程并发执行
  • 分代收集
  • 空间整合
  • 可预测的停顿
  • 将整个Java堆内存划分为大小相等独立的Rregion
  • 新生代和老年代不再是物理隔离

 

引用

软引用可以用来实现高速缓存,因为软引用在内存不足时会被回收,内存足够时是不会被回收的,所以虽然在缓存里,但是不会出现内存溢出问题。

Java线程相关知识

 

进程和线程的区别

进程是自愿分配的最小单位,线程的CPU调度的最小单位。

  • 进程是自愿分配的最小单位,所有与该进程有关的资源都被记录在进程控制块(PCB)中,表示该进程拥有这些资源或者正在使用他们。
  • 进程是抢占处理机的调度单位,拥有一个完整的虚拟内存空间,进程调度时,不用的进程拥有不同的虚拟地址空间,而统一进程的不同线程,共享同一地址空间;相比于进程,线程于资源分配无关,它属于某一个进程,并与进程中的其他线程共享进程的资源。
  • 线程只由堆栈寄存器,程序计数器和TCB(线程控制块)组成。
  • 总结
    1. 线程不能看做独立应用而进程可以看做独立应用
    2. 进程有独立的地址空间,但相互不影响,线程只是进程的不同执行路径。
    3. 线程没独立的地址空间,多进程的程序比多线程的程序健壮
    4. 进程的切换比线程的切换开销大。
  • Java中进程的线程的关系
    1. Java对操作系统的提供的功能进行了封装,包括进程和线程
    2. 运行一个程序会产生一个进程,进程至少包含一个线程
    3. 每个进程对应一个JVM实例,多个线程共享JVM堆
    4. Java采用单线程编程模式,程序会自动创建主线程

Java中Thread中start和run方法的区别

  • 调用start方法会创建一个新的子线程并启动
  • run方法只是Thread中的一个普通的方法的调用

如实现处理线程的返回值

  • 让主线程等待,直到目标子线程返回值
  • 使用Thread的join()方法,这个方法是阻塞当前线程以等待子线程执行完才继续执行。
  • 通过Callable接口实现:通过Future Task OR 线程池获取

线程的状态

  • 新建(New):创建后尚未启动的线程的状态
  • 运行(Runnable):包含Running和Ready,处于此状态的线程有可能正在处于运行状态或者等待CPU正在为他分配CPU执行时间片。
  • 无限等待状态(Waiting):不会被分配CPU执行时间,需要显式唤醒

  • 限期等待(Timed Waiting):在一定时间后会被系统自动唤醒
  • 阻塞(Blocked):等待获取排它锁
  • 结束(Terminated):已终止线程的状态,线程已经执行结束

sleep方法和wait方法

  • sleep方法是Thread类的方法,wait方法是Object的方法
  • sleep方法可以在任何地方使用,但是wait方法只能在synchronized方法中或者synchronized块中使用
  • Thread.sleep()方法只会让出CPU,不会释放锁
  • Object的wait方法不仅会让出CPU,而且还会释放锁资源

notify和notifyAll的区别

  • 锁池EntryList
  • 等待池WaitSet

对于Java虚拟机中运行程序的每个对象来说都有两个池,锁池和等待池,而这两个池又与Object的wait,notify和notifyAll方法以及synchronized有关。

锁池:假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B,C想要调用这个对象的某个synchronized方法(或者块),由于B,C线程在进入这个对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程A所占用,此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。

等待池:假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线层不会去竞争该对象的锁。

  1. notifyAll()会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。没有获取到锁而已经待在锁池中的线程只能等待其他机会去获取锁。而不能再主动回到等待池当中
  2. notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。

yield()方法

当调用Thread.yield()方法时,会给线程调度器一个当前线程愿意让出CPU使用权的暗示,但是线程调度器可能会忽略这个暗示。

如何中断线程?

  • 通过调用stop方法中断线程,stop方法可以由一个线程停止另一个线程。这种方法太过暴力,且是不安全的。可能会出现资源没有释放或者其他的一些安全问题,因此这个方法被抛弃了。与之相类似的还有suspend方法和resume方法。
  • interrupt方法:这是目前使用的方法。这个方法也是通知线程该中断了,具体什么时候中断,还要看被线程自己去处理。
    1. 如果线程处于被阻塞状态(例如sleep,wait,join方法),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
    2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程也会正常运行,不受影响。
  • interrupt并不能真正的中断线程,而是需要被调用的线程自己进行配合。
    1. 在正常运行时,正常检查本线程的中断标志位。如果被设置了中断标志就自行停止线程。
    2. 如果线层处于正常的活动状态,那么会将该线程的中断标志设置为true,被设置中断标志的线程将正常运行,不受影响。

线程之间的状态及状态之间的转换

多线程并发原理

synchronized

  • 线程安全的主要原因
    1. 存在共享数据()也称为临界资源
    2. 存在多条线程共同操作这些共享数据
  • 解决问题的根本方法:同时刻只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。
  • 互斥锁的特性:
    1. 互斥性:即在同一时间值允许一个线程持有某个对象锁,通过这种特性来实现线程间的协调机制,这样在同一时间只有一个线程对需要同步的代码进行访问。互斥性也成为操作的原子性。
    2. 可见性:必须确保在被锁释放之前,对共享变量所做的修改,对之后获得该锁的另一个线程可见(即在获得锁市获取到的是共享变量更新的最新值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
  • 根据获取的锁的分类:获取对象锁和获取类锁
    1. 获取对象锁:

同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号中的实例对象。

同步非静态方法锁市当前对象的实例对象。

    1. 获取类锁

同步代码块(synchronized(类.class)),锁是小括号中的类对象(Class对象)。

同步静态方法

  • synchronized底层实现原理

对象在内存中的布局:对象头,实例数据和对其填充。

对象头

一般而言,synchronized使用的锁对象是存储在Java对象头里的,其主要结构是由Mark Word和Class Metadata Address组成,其中Class Metadata Address是对象指向他的类的元数据的,JVM通过这个指针确定该对象是那个类的数据。而Mark Word则用于存储对象自身的运行时数据,他是实现轻量级锁和偏向锁的关键。Mark Word默认存储的是对象的hashCode,分代年龄,锁类型,锁标志位信息等。由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word被设计成为一个非固定的数据结构,以便存储更多有效的数据。他会根据对象本身的状态,复用自己的存储空间。其中轻量级锁和偏向锁是Java6后对synchronized锁进行优化后新增加的。

Monitor

Java每个对象天生自带了一把看不见的锁,他叫做内部所或者Monitor锁,也叫做管程或者监视器锁,也可以描述为一种同步机制。

主要看一下重量级锁,也就是重量级锁。其中指针指向的是Monitor对象的起始地址,每个对象都存在着个Monitor与之关联,对象与其Monitor之间的关系有存在多种实现方式,如Monitor可以和对象一起创建销毁,或当前线程试图获取对象锁时自动生成。但当Monitor被一个线程持有后,它便处于锁定状态。在Java虚拟机即Hotspot中monitor是由ObjectMonitor实现的。位于Hotspot虚拟机原码即objectMonitor.hpp文件里,是通过c++实现。

ObjectMonitor() {

    _header       = NULL;

    _count        = 0;

    _waiters      = 0,

    _recursions   = 0;

    _object       = NULL;

    _owner        = NULL; 指向持有ObjectMonitor对象的线程

    _WaitSet      = NULL;

    _WaitSetLock  = 0 ;

    _Responsible  = NULL ;

    _succ         = NULL ;

    _cxq          = NULL ;

    FreeNext      = NULL ;

    _EntryList    = NULL ;

    _SpinFreq     = 0 ;

    _SpinClock    = 0 ;

    OwnerIsThread = 0 ;

    _previous_owner_tid = 0;

  }

  WaitSet和EntryList就是保存ObjectWaiter对象列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面,_owner 指向持有ObjectMonitor对象的线程,当多个线程同时访问同一块同步代码的时候,首先会进入到EntryList集合里面,当线程获取到对象的Monitor对象后,就进入到Object区域,并把Monitor中的_owner设置为当前线程,同时Monitor中的计数器_count就会+1 ,若线程调用wait方法就会释放当前线程持有的Monitor,_owner就会被恢复成null,_count -1,同时该线程即Object实例就会被放入WaitSet集合中等到唤醒。若当前线程执行完毕也会释放Monitor锁,并复位对应变量的值,以便其他线程继续获取Monitor锁。

图中可以看出,synchronized的开始是monitorenter指令,结束是monitorexit指令。

synchronized代码块:如果一个线程先于当前线程持有了ObjectMonitor的持有权,那么当前线程将会被阻塞在这里,直到持有该锁的线程执行完毕,即MonitorExit指令被执行,持有线程将释放Monitor锁,并设置计数器为0,其他线程将有机会持有Monitor。为了保证在方法异常完成时,monitorenter和monitorexit依然可以配对执行,便器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,他的目的就是用来执行monitorexit指令,在字节码中看出多了一个monitorexit指令。

synchronized方法:方法级的同步是隐式的,即无需通过字节码指令来控制,在synchronized方法中有个ACC_SYNCHRONIZED标志,用来区分一个方法是否是同步方法,当方法调用时调用指令会检查ACC_SYNCHRONIZED访问标志是否被访问,执行线程将会持有Monitor,然后执行方法,该方法不困是正常完成还是非正常完成都会释放Monitor,在方法执行期间,该线程持有了Monitor,其他任何线程都无法在获得同一个monitor,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理时,那么这个线程持有的monitor在异常抛出到方法外部时释放。

为什么会对synchronized嗤之以鼻?

  • 早期版本synchronized是重量级锁,依赖于底层操作系统Mutex Lock实现,而操作系统实现线程之间的切换时需要从用户态转换成核心态,这个状态的转换需要相对比较长的时间,时间成本相对较高。
  • JDK6以后,synchronized的性能得到了很大的提升,从jvm层面对其做了较大的优化。Hotspot花费了大量精力实现锁优化技术:自适应自旋锁(Adaptive Spining),锁消除(Lock Eliminate),锁粗化(Lock Coarsening),轻量级锁(Lightweight Locking),偏向锁(Biased Locking)等等。

自旋锁与自适应自旋锁

自旋锁

  • 许多情况下,共享数据的锁定状态只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得,在如今多处理器环境下,完全可以让另一个没有获取到锁的线程等待一会,但不放弃CPU的执行时间,这个等一会但不放弃CPU执行时间的行为即所谓的自旋。
  • 通过让线程执行忙循环等待锁的释放,不让出CPU。自旋锁在Java4就引入了,只是在Java4时实默认关闭的,在Java6后才默认是开启状态。自旋锁本质上与阻塞并不相同,先不考虑对多处理器的要求,如果锁占用的时间非常的短,那么自旋锁的性能将会非常的好。
  • 缺点:若锁被其他线程占用的时间非常的长,会带来许多性能上的开销。自选的线程是占用CPU的时间片,如果线层占用的CPU的时间较长,就会浪费很多CPU资源。因此自旋等待的时间应该有一定的限度,如果自旋超过了限定的尝试次数,任然没有获取到锁就应该采用传统的方式去挂起线程。可以通过以下方式进行设置自旋次数。

由于每个线程的执行时间是不固定的,所以很难合理的设置PreBlockSpin,这时候就出现了自适应自旋锁。

自适应自旋锁

  • 自旋的次数不在固定
  • 有前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上自旋等待刚刚成功获取过锁,兵器持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间,相反对于某个锁自旋很少成功获取到锁,那再以后要获取这个锁时,将可能省略掉自旋过程,避免浪费CPU资源;有了自适应自旋,JVM对程序的锁的转状态预测也将更加精准。

锁消除

更彻底的优化

  • JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

锁粗化

原则上在加锁的时候,尽可能将同步块的作用范围限制到尽量小的范围,即只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量进可能变小,在存在锁同步竞争中,也可以使得等待锁的线程尽可能早的拿到锁,大部分的情况是可以完成的。但是如果存在一连串操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的。那即使没有线程竞争,频繁的进行互斥锁操作也会有不必要性能操作。此时的操作就是通过加锁范围,避免反复加锁和解锁。

synchronized的四种状态

  • 无锁,偏向锁,轻量级锁,重量级锁

锁膨胀方向:无锁à偏向锁à轻量级锁à重量级锁

无锁就是并没有加入任何锁,此时的目标共享数据是没有被任何一条线程给占用的。

  • 偏向锁

减少统一线程获取锁的代价

    1. 大多数情况下,锁不存在多线程竞争,而且是由统一线程多次获得。

核心思想:如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变成了偏向锁结构,当该线程在此请求锁时,无需在做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

不适合锁竞争比较激烈的多线程场合。

  • 轻量级锁

轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,偏向锁就会升级成轻量级锁。

使用场景:线程交替执行同步块

若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

轻量级锁的加锁过程:

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为”01”状态),虚拟机首先将在当前当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这是线程堆栈与对象头的状态如图所示。

  1. 拷贝对象头中的Mark Word到锁记录中。
  2. 拷贝成功后虚拟机将使用CAS操作尝试将对象的Mark Word更新为Lock Record的指针,并将Lock Record中的owner指针指向object(的) Mark Word,如果更新成功,执行步骤(4),否则执行步骤(5)。
  3. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且Mark Word的锁标志位置为“00”,即表示此对象处于轻量级锁定状态,这个时候线程堆栈与对象头状态如图所示:
  4. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否会指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个锁,那就可以直接进入当前同步块的执行。否则说明有多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志状态值变为“10”,MarkWord中存储的就是执行重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而且当前线程尝试使用自旋来获取锁,  就是为了不让线程阻塞,而采用循环获取锁的过程。

解锁过程:

  1. 通过cas操作尝试把线程中复制的Displaced Mark Word替换为当前的Mark Word。
  2. 如果替换成功,则整个同步过程就完成了
  3. 如果替换失败,说明已经有其他线程尝试获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程

锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主存中;

当线程获取锁时,Java内存模型就会把该线程对应的本地内存置为无效,    从而使得被监视器保护的临界区代码必须从主存中读取共享变量。

synchronized和ReentrantLock的区别

ReentrantLock(重入锁)

  • 位于java.utils.concurrent.locks包下
  • 和CountDownLatch,FutureTask,Semaphore一样基于AQS实现
  • 能够实现不synchronized更细粒度的控制,如设置fairness(公平性)的设置
  • 调用lock后必须调用unlock释放锁

ReentrantLock公平性的设置

ReentrantLock fairLock = new ReentrantLock(true);这样的锁的创建方式就是公平锁,这里所谓的公平就是在竞争过程中,将锁赋予等待时间最长的线程,公平性是减少线程饥饿的办法。获取锁的顺序是按调用lock方法的循序。而非公平锁是每个线程抢占锁的顺序不一定,随机抢占,synchronized就是非公平锁。

JMM的内存可见性

什么是Java内存模型中的happens-before

Java内存模型

Java内存模型(Java Memory Model),本身是一种抽象的概念,并不真实存在,它描述的是一种规范或者格则,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成实例数组对象的元素)的访问方式。

jvm运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),用于存储线程私有的数据。而Java内存模型规定所有变量都存储在主内存中,主内存是共享数据区域,所有线程都可以访问,但线程对变量的操作即读取,赋值等必须在工作内存中进行。首先将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量写回到主内存中,不能直接操作主内存中的变量。那工作内存中存储者主内存中变量的副本拷贝,工作内存是每个线程的私有区域。因此不同的线程间无法访问彼此的工作内存,线程的通信也就是传值必须通过主内存来完成。

JMM主内存

  • 存储Java实例对象:所有线程创建的实例对象都存储在主内存中。
  • 包括类的变量,类信息,常量,静态变量等。
  • 属于数据共享的区域,多线程并发操作时会引发线程安全问题。

JMM工作内存

  • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
  • 还有字节码指示器,Native方法信息
  • 属于线程私有的数据区域,不存在线程安全问题

JMM与Java内存区域划分的是不同的 概念层次

  • JMM描述的是一组规则,围绕原子性,有序性和可见性展开
  • 相似点:都存在共享数据区域和私有数据区域。JMM中主内存中属于共享数据区,重某个程度上讲,应该包括了堆和方法区,工作内存属于私有数据区域,可以说包含程序计数器,虚拟机栈和本地方法栈。

主内存与工作内存的数据存储类型及操作方式的归纳

  • 对于一个实例对象中的成员方法,如果方法中包含本地变量是基本数据类型的,这写本地变量将直接存储在工作内存的栈帧结构中
  • 若本地变量是引用类型,引用存储在工作内存中,实例对象存储在主内存中。
  • 实例对象的成员变量,static变量,类信息均会存储在主内存中。
  • 在主内存中的实例对象可以被多线程共享。倘若,两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存中。

指令重排序

在执行程序的时候为了提高效率,处理器和编译器常常会对指令进行重排序,但是不能随意重排序。需要满足两个条件:

  • 在单线程环境下不能改变程序运行的结果
  • 若存钻数据依赖关系则不允许指令重排序

即如果无法通过happens-before原则推导的,才能进行指令的重排序。jvm内部的实现通常是通过依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证,也就是实现了各种happens-before规则。与此同时,更多的复杂度在于需要尽量确保各种编译器,各种体系结构的处理器能够提供一致的行为。

A操作的结果必须对B操作可见,则A与B存在happens-before关系,happens-before原则是他们是否存在竞争,线程是否安全的主要依据。依靠这个原则我们就能解决在并发环境下两个操作之间存在冲突的问题。

i = 1 ; //线程A执行

j = i ; //线程B执行

线程A happens-before线程B 操作, 线程B在执行后,j = 1 一定是成立的。如果他们之间不存在happens-before,那么j就不一定等于1。

happens-before八大原则

  • 程序的次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;一段代码在单线程中执行的结果是有序的,注意是执行结果有序,因为虚拟机处理器会对指令进行重排序,虽然重排序,但并不会影响程序的执行结果,即重排序后程序的执行结果和没有重拍时程序的执行结果是一样的,但是在多线程环境下就不能够保证了。
  • 锁定规则:一个unlock操作先行发生于对后面同一个锁的lcok操作。无论是在单线程还是在多线程,一个锁处于被锁定状态,必须先要执行unlock操作,然后才能继续后面的lock操作。
  • volatile规则:对一个变量的写操作先行发生于后面对这个变量的读操作。这个规则就可以保证可见性,一个变量被volatile修饰后,之后有一个线程读这个变量,那么这个写操作一定是happens-before这个读操作的。
  • 传递规则:如果操作A先发生于操作B,操作B先发生于操作C,那么操作A一定是先发生于操作C;
  • 线程启动规则:Thread对象的start方法先发生于次线程的每个动作;
  • 线程中断规则:对线程的interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程的终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行。
  • 对象终结原则:一个对象的初始化完成先发生于他的finalize()方法的开始;

如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,那么JVM可以对这两个操作进行重排序;如果操作A happens-before 操作B,那么操作A 在内存上所做的操作对操作B都是可见的。

volatile

jvm提供的轻量级同步机制

  • 保证被volatile修饰的共享变量对所有线程是可见的
  • 禁止指令的重排序优化

volatile修饰的共享变量在多线程环境下的写操作不能够保证线程安全

volatile是怎样实现立即可见的?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile值时,JMM会把该线程对应的工作内存置为无效,那么该线程只能从主内存中读取变量。

volatile如何禁止重拍优化

内存屏障(Memory Barrier),这是一个CPU指令,起作用有两个,一是保证特定操作的执行顺序,由于编译器和处理器都能执行指令的重拍优化,如果在指令这里插入一条memory barrier,那就告诉编译器和处理器,不管什么指令,都不能和这条指令重排序,就是不能打乱他们的顺序。通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。二是保证某些变量的可变性,即利用该特性实现volatile内存可见性,强制刷出CPU的各种缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

以上单利写法,通过使用synchronized代码块试图解决多线程请求单利时带来的重复创建单利对象的隐患。以上代码在多线程情况下依然还是有隐患的。原因在于某一个线程执行到第一次检测的时候,读到的instance不为null时,instance是实例对象可能还没完全初始化,因为instance = new Singleton();有一下三个步骤:首先为对象分配内存空间,其次初始化对象,然后就是设置instance指向刚分配的内存地址。此时instance不为空,这是时候就有可能发生重排序。将第三步提前,第二部后移,这是因为步骤2和步骤三不存在数据依赖的关系,而且无论排前还是排后执行程序的结果在单线程中并没有改变,因此这种重排序优化是允许的。但是指令重排只能保证串行语义的一致性,即单线程的一致性,并不会关心多线程的一致性。因此当一条线程访问instance不为null的时候,对象未必完成初始化,这就造成线程的安全问题。

解决方法:

使用volatile去禁止instance去进行指令重排序。

CAS(Compare And Swap)

synchronized这种独占锁属于悲观锁,悲观锁始终假定会发生并发冲突,因此会屏蔽一切可能会违反数据完整性的操作。除此之外还有乐观锁,他假设不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试。cas就是乐观锁。

一种高效实现线程安全的方法

  • 支持源自更新操作,适用于程序计数器,序列发生器等场景;所谓的序列发生器就是用来给变量自增的工具。
  • 属于乐观锁机制,号称lock-free
  • cas操作失败是由开发者决定是继续尝试还是执行另外的操作,因此执行失败的线程不会被阻塞挂起。

CAS思想

  • 保安三个操作数—内存位置(V),预期原值(A),和新值(B)。执行cas时,将内存位置的值和预期原值进行比较,如果一致,那么处理器将该位置的值更新为新值,否则处理器不做任何操作,这里的内存位置的值为主内存的地址。

缺点

  • 若循环时间长则开销很大
  • 只能保证一个共享变量的原子操作,多个共享变量还是得使用锁
  • ABA问题:如果一个内存地址初次读取的值是A,并且在准备赋值的时候检查他的值任然是A。如果在这段时间他的值曾经被改为了B。然后又被改为了A,那么cas就会误认为它从来没有被改变过。JUC中为了解决这个问题用了带有标记的院子引用类AtomicStempedReference,它可以控制变量值的版本来保证cas的正确性。

Java线程池

利用Executors创建不同的线程池满足不同的场景的要求

  1. newFixedThreadPool(int nThread):创建一个指定工作线程数量的线程池。每当一个任务去创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中,如果有工作线程退出,将会有新的工作线程被创建以补足nThread的数目。
  2. newCachedThreadPool():用来处理大量短时间工作任务的线程池,具有几个特点:
  • 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
  • 如果线程闲置的时间超过阈值(一般60秒),则会被终止并移出缓存
  • 系统长时间闲置的时候不会消耗资源
  1. newSingleThreadExecutor()创建一个单线程化的Executor,即创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线层来取代他,从而保证任务能够被顺利的执行。但工作线程最大的特点就是能够保证顺序的执行任务,并且在任意给定的时间不会有多个线程是活动的。
  2. newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize):可以进行定时或者周期性的工作调度的,两者的主要区别在于工作线程数,前者是单一的工作线程,后者是由多个线程组成。newSingleThreadScheduledExecutor也是如果线程异常结束,也是有新的线程进行替换的。
  3. newWorkStealingPool();他是JDK8才引入的。内部会构建ForkJoinPool,利用work-stealing算法,并行的处理任务,但是不保证处理顺序。

Fork/Join框架

  • Java7提供的用于并行执行任务的框架,是一个把大任务分割成许多的小任务去执行,然后在将分割成的小任务的结果汇总的结果作为大任务的结果的框架。 Work-stealing算法:某个线程从其他队列里窃取任务来执行。

为什么要使用线程池

  • 降低资源消耗,即通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的创建,分配和调优。

JUC的三个Executor接口

  • Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
  • ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
  • ScheduledExecutorService:支持Future和定期执行任务

线程池的状态

  • RUNNING:能接受新提交的任务,并且也能处理阻塞队列里的任务
  • SHUTDOWN:不能再接受新提交的任务,但是可以处理存量任务
  • STOP:不能接受新的任务,也不处理存量任务,会中断正在处理任务的线程,
  • TIDYING:所有的任务都已终止,正在进行最后的处理工作,次吃有效的线程数为0
  • TREMINATED:terminated()方法执行完进入该状态,什么都不做,只是一个标志性的方法

线程池的大小如何设定

  • CPU密集型:线程数=按照核数或者核数+1 设定
  • I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)

Java异常及常用的工具类体系

Java异常

Error和Exception的区别

从概念角度解释Java的异常处理机制

  • Error:程序无法处理的系统异常错误,编译器不做检查。Error一般和JVM相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法栈调用溢出等;如StackOverflowError,OutOfMemoryError等。对于这种错误,Java编译器不检查,对于这个错误导致的应用程序中断紧靠程序本身是无法恢复的,遇到这样的问题建议就让程序终止。
  • Exception:程序可以处理的异常,捕获后可能恢复的。遇到这类问题应该尽可能的处理,使程序恢复运行,而不是随意终止程序。

Exception

RuntimeException:运行时的异常,不可预知的,如数组下标越界访问空指针等,但是这种异常在编写程序时应当避免。

非RuntimeException

出RuntimeException以外的异常,从编译器角度讲是必须处理的异常,如果不处理,程序就不能编译通过。如IOException,SQLException等。

从责任角度看

  • Error是JVM需要承担的责任
  • RuntimeException是程序应该负担的责任
  • CheckedException可检查异常是Java编译器应该负担的责任

常见的Error以及Exception

RuntimeException

  • NullPointerException:空指针异常
  • ClassCastException:类型强制转换异常
  • IllegalArgumentException:传递非法参数异常
  • IndexOutOfBoundsException:下标越界异常
  • NumberFormatException:数字格式异常

非RuntimeException

  • ClassNotFoundException:找不到指定类异常
  • IOException:IO异常
  • SQLException:SQL异常

Error

  • NoClassDefFoundError:找不到class定义异常
  • Stack OverflowError:深递归导致栈被沾满的异常
  • OutOfMemoryError:内存溢出异常

Java异常处理机制

  • 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付给运行时系统。系统对象中包含了异常信息和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
  • 捕获异常:运行时系统将转为寻找合适的异常处理器即Exception Handler,潜在的异常处理是异常发生时一次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与抛出的异常类型向符时,即为合适的异常处理器,运行时系统从发生异常的方法开始,一次会查调用栈中的方法,直至找到含有异常处理器的方法并执行。当运行时系统遍历了调用栈都没有找到合适的异常处理器,则运行时系统终止,Java程序终止。

Java异常处理消耗性能的地方

  • try-catch 块影响JVM的重排序优化的发挥
  • 遇到异常便会实例化一个Exception,并且都会对当时的栈进行快照,开销大

集合框架

 

HashMap

put()方法的逻辑:

  1. 如果HashMap未被初始化过,就初始化
  2. 对key求hash值,然后计算下标
  3. 如果没有碰撞,直接放入对应的桶中
  4. 如果碰撞了,以链表的形式链接到后面
  5. 如果链表的长度超过阈值,就把链表转为红黑树
  6. 链表长度低于6,就将红黑树转为链表
  7. 如果节点已经存在,就替换旧值
  8. 如果桶满了(容量16*加载因子),就需要扩容

ConcurrentHashMap

  1. 判断Node数组是否初始化,没有则进行初始化操作
  2. 通过Hash定位数组的索引坐标,是否有node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环
  3. 检查到内部正在扩容,就帮助他一块扩容,helpTransfer()方法
  4. if(f !=null),则使用synchronized锁住f元素(链表/红黑二叉树的头元素);如果是Node则执行链表的添加操作,如果是TreeNode则执行红黑树的添加操作
  5. 判断链表长度已经到达临临界值8,就把链表调整成树

JUC

JUC包的主要分类

  • 线程执行器executor
  • 锁locks
  • 原子变量类atomic
  • 并发工具类tools
  • 并发集合类collections

executo

executor,即线程执行器,就是一个任务的执行和调度的框架,次外,在tools下还有和executor相关的Executors,用于创建ExecutorService,ScheduledExecutorsService,ThreadFactory和Callable对象。

locks

在Java5以前协调共享对象的访问时可以使用的调机制只有synchronized和volatile,在之后便出现了JUC的locks了,locks里引入了显式锁,方便对线程间的共享资源进行更细粒度的锁控制。Condition是Lock对象创建的,一个Lock对象可以创建多个Condition对象,主要用于将线程的等待唤醒即wait,notify,notifyAll对象化。不管是Lock还是Condition,都是就要AQS实现的。而AQS的底层是调用LockSupport.unpark和LockSupport.park()方法去实现线程的阻塞和唤醒的。

ReentrantReadWriteLock,可重入的读写锁,写写排斥,写读排斥,读读共享。

atomic

这个当中指一个操作是不可中断的。在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程锁干扰。所以所谓原子类就是具有原子操作特征的类。这些类的底层是借用CAS操作实现的。

tools

  • CountDownLatch:让主线程等待一组事件发生后继续执行

事件指的就是CountDownLatch里面的countDown()方法CyclicBarrier

其他线程调用完countDown()方法还是会继续执行的,并不是调用了countDown()方法后就执行完成了,是可以执行你之后的操作了。通过调用countDown()方法也就说我这个线程对主没有影响了,具体主线程是否可以执行就看其他线程对你有没有影响了。在图中,主线程引用了CountDownLatch之后,就进入了等待状态,此时CountDownLatch里面有个类似count的变量,一开始初始化为一个整数(就是事件的个数),每当一个子线程调用countDown()之后这里的count就-1,直到所有的子线程都调用了countDown()之后,即count编程0之后,线程才可以重新恢复到执行的状态。

  • CyclicBarrier:阻塞当前线程等待其他线程

等待其他线程,会阻塞自己的当前线程,所有的线程都必须同时到达了栅栏才能继续执行。也可以是所有线程到达栅栏处就触发另外一个预先设置执行的线程。

和CountDownLatch一样,CyclicBarrier里面也有一个计数器,如图中,每一个线程调用一次await()计数器就会-1,且在这些线程调用await()方法是计数器不为0,这些线程也会被阻塞。当TA线程在所有线程到达栅栏时才会跟着其他线程一起执行。与CountDownLatch不同之处在于CountDownLatch在子线程执行完countDown方法后子线程是可以继续执行的,但是CyclicBarrier是阻塞了子线程,等到所有线程都执行完之后才一起执行。

  • Semaphore:控制某个资源可被同时访问的线程个数。

通过aquire去获取一个许可,如果没有,就等待,一旦利用这个资源执行完业务逻辑之后,线程就会调用release方法释放一个许可出来。

  • Exchanger:交换器,两个线程到达同步点后,相互交换数据

它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。Exchanger会产生一个同步点,一个线程先执行到达同步点就会被阻塞,直到另外一个线程也进入到同步点,那当两个都到了同步点后就开始换数据。线程方法中调用Exchanger.exchange方法就是所谓的同步点了。Exchanger只能用于两个线程交换数据。将交换的数据放在了String girl = exchanger.exchange("我其实暗恋你很久了。。。");里面,执行后就交换了。

collections

BlockingQueue:提供可阻塞的入队和出队操作。如果队列满了,出队操作将阻塞,如果队列空了,出队操作将阻塞。

主要用户生产者消费者模式,在多线程场景时,生产者线程在队列尾部添加元素,消费者线程在队列头部添加元素,而通过这样的方式能够达到生产和消费进行个例的目的。

BlockingQueue

  • ArrayBlockingQueue:一个有边界的阻塞队列,内部实现是一个数组,有边界的意思是他的容量是有限的,必须在其初始化的时候就指定容量大小,容量大小一旦指定就不可改变了。以先进先出的顺序存储数据,最先插入的对象是在尾部,最先移出的对象是在头部。
  • LinkedBlockingQueue:阻塞队列大小配置是可选的,在初始化是指定大小,就是有界的,如果不指定,就是无边界的,说是无边界的,其实是采用了默认大小,为Integer.MAX_VALUES。内部实现是一个链表,和ArrayBlockQueue一样,LinkedBlockingQueue也是以先进先出的方式存储数据。最先插入的对象是尾部,最先移出的对象是头部。
  • PriorityBlockingQueue:是一个带有优先级的队列,而不是先进先出的队列,元素是按照优先级顺序被移出的,该队列没有上限,但是如果队列为空,那么取元素的队列take就会被阻塞,所以他的检索操作take是受阻的。另外该队列的元素是要具备可比性的。这样才能按照优先级进行操作。
  • DealyQueue:支持延时获取元素的无界阻塞队列。队列中的元素必须实现Dealy接口,在创建元素的时候可以指定多久才能从队列中获取当前元素。只有在延时期满的时候才能从队列中获取元素。
  • SynchronousQueue:队列中进允许容纳一个元素,当一个线程插入一个元素后会被阻塞,直到另一个线程把这个元素消费掉。
  • LinkedTransferQueue:可以认为是SynchronousQueue和LinkedBlockingQueue的结合,性能要比LinkedBlockingQueue好,因为他是无锁操作,比SynchronousQueue能存储更多的元素。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列,即双端队列。这个是和窃取模式及work-stealing相关连的,一个生产者消费者设计中所有的消费者值共享一个工作队列,在窃取工作的设计中,每个消费者都有一个自己的双端队列,如果一个消费者完成了自己双端队列的全部工作,他可以窃取其他消费者的双端队列里的末尾的任务,因为工作者线程并不会竞争一个共享的任务队列。所以窃取模式比正常的生产者消费者模式有更好的伸缩性。大多数时候他能访问自己的双端队列,减少竞争,但一个工作者必须要访问另一个队列的时候,他会从尾部截取,而不是从头部,进而进一步降低对双端队列的争夺。

 

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/742781
推荐阅读
相关标签
  

闽ICP备14008679号