代码复习遇到知识点回顾(一)

《1》静态代码块和构造代码块的区别

首先在执行顺序上:

静态代码块 > main 方法 > 构造代码块 > 构造方法

注意:在 JVM 加载类时,就会执行其静态代码块,所以静态代码块一定优于静态方法;

执行次数上:

静态代码块只执行一次,构造代码块每创建一个对象,就会执行一次;

在继承关系上,执行顺序:引文:静态代码块 构造代码块 构造方法的执行顺序及注意问题

class Fu {
    // 静态代码块
    static {
        System.out.println("Fu static code");
    }
    // 构造代码块
    {
        System.out.println("Fu code");
    }
    // 构造方法
    public Fu(){
        System.out.println("Fu GouZao");
    }
}

class Zi extends Fu{
    static {
        System.out.println("Zi static code");
    }
    {
        System.out.println("Zi code");
    }
    public Zi(){
        // 隐写了 super();
        System.out.println("Zi GouZao");
    }
}

public class Text{
    public static void main(String[] args) {
        Zi zi = new Zi();
    }
}
// 输出结果
Fu static code
Zi static code
Fu code
Fu GouZao
Zi code
Zi GouZao

解释:

首先:在编译Text.java时,JVM 先加载了Fu类, 执行静态代码块内容,因此Fu类的静态代码块首先执行,而后加载Zi类,Zi类的静态代码块执行;

其次:然后执行 new Zi() 创建Zi的对象,大家都知道构造代码块优先于构造方法执行,这时候问题来了, 这时应该先看Zi类的构造方法,Zi类里的构造方法里有一句隐式的super()首先被执行, 所以找到Fu类的构造方法, 而Fu类的构造方法中也有一句隐式的super()执行(调用Object类的构造方法), 并没有什么返回结果, 接下来才是在执行Fu类构造方法的方法体前先执行了Fu类的构造代码块(Fu code) ,再执行Fu类构造方法的方法体(也就是Fu GouZao),最后又回到Zi类的构造方法中,这时Zi类的super()已经执行完了,在执行Zi类构造方法的方法体前先执行Zi类的构造代码块(Zi code),再执行Zi类构造方法的方法体(Zi GouZao)。

所以:[JVM触发]{父类静态代码块 - 子类静态代码块 } - [super()触发] 父类构造代码块 - 父类构造方法 - 子类构造代码块 - 子类构造方法;

《2》Socket的获取与TCP的握手

对于TCP/IP网络的7层还是 5 层,我们这里暂定讨论 5 层的;

应用层 -- 传输层(TCP层解决端(PC)与端之间的通信,段)-- 网络层(IP层解决点(PC)与点(交换机)数据的传输,数据包)-- 链路层(解决物理传输中,帧)

首先回顾下TCP的三次握手 和 四次挥手

对于TCP的标识位,也就是位码,主要有

(1)SYN(synchronous建立连接)(2)ACK(acknowledgement确认连接)(3)PSH(push传送)(4)FIN(finish结束)(5)RST(rest重置)(6)URG(urgent紧急)以及两个数字标识:顺序码(sequence number)和确认码(acknowledge number)

十种状态:

  1. SYN-SEND:当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即它会进入到了SYN_SENT状态;
  2. SYN_RECV:与SYN_SEND遥相呼应,当接收到SYN标识时,服务端会同时发送ACK标识和SYN标识服务端随机进入SYN_RECV状态;
  3. ESTABLISHED:当客户端接受并确认服务端发送来的ACK和SYN后,发送ACK标识,随机进入ESTABLISHED状态,当服务端接收到第三次握手的ACK状态后,随机也会进入到ESTABLISHED状态;
  4. FIN_WAIT1:当客户机发送断开标识FIN到服务端时,客户端进入FIN_WAIT1状态,这个状态表示在等待对方的FIN标识;
  5. CLOSE_WAIT:当收到客户端发送的FIN标识后,服务端进入到CLOSE_WAIT状态,接下来会检测自己是否还有数据发送给客户端,没有发送FIN标识进入LAST_AKC状态;
  6. FIN_WAIT2:当服务端发送ACK标识后,客户端确认后进入到FIN_WAIT2状态,此时SOCKET处于半链接状态,也就是不能给服务端写数据了,但可以继续接受服务端发送的数据;
  7. LAST_ACK:服务端发送FIN到客户端,自己进入LAST_ACK状态,等待客户端的ACK标识。
  8. TIME_WAIT:客户端发送ACK自己进入TIME_WAIT状态,等待2MSL后即可回到CLOSED状态;
  9. CLOSED:当服务端收到客户端发送的ACK后自己进入CLOSED状态;
  10. CLOSING

上面的几个状态是从三次握手到四次挥手每个过程中可能出现的情况;结合下图中进行理解更加方便:

代码复习遇到知识点回顾(一)_第1张图片

代码复习遇到知识点回顾(一)_第2张图片

推荐两篇很不错的博文:

(1)Socket简单应用详解  

(2)TCP通信的三次握手和四次撒手的详细流程(顿悟)

《3》十六进制与字节的转换,以及位运算问题

十六进制(Hex):计算机中数据的一种表示方法,它由0-9,A-F(a-f)组成,字母不区分大小写。与10进制的对应关系是:0-9对应0-9;A-F对应10-15。4个二级制(0000~1111)有16种组合刚好对应如下16进制的字符;也就是4个二进制表示一个十六进制字符,一个字节等于8个二进制,等于2个16进制字符;【1byte = 8bit = 2 个16进制code】

0:0000     1:0001     2:0010     3:0011     4:0100     5:0101     6:0110     7:0111     8:1000      9:1001

A:1010     B:1011    C:1100     D:1101    E:1110     F:1111

那么:十六进制 0xF6对应的二进制应为:F:1111 ;6 : 0110 所以对应的二进制为 1111 0110 相应的十进制为:246

byte b = (byte) 5 ;对应的二进制0000 0101对应的16进制为 0x05;

/** 
 * 字节转十六进制 
 */  
public static String byteToHex(byte b){  
    String hex = Integer.toHexString(b & 0xFF);  
    if(hex.length() < 2){  
        hex = "0" + hex;  
    }  
    return hex;  
} 

output: 
0x05 = byteToHex((byte)5);
0xF6 = byteToHex((byte)246);

public static String bytesToHex(byte[] bytes) {  
    StringBuffer sb = new StringBuffer();  
    for(int i = 0; i < bytes.length; i++) {  
	String hex = Integer.toHexString(bytes[i] & 0xFF);  
	if(hex.length() < 2){  
	    sb.append(0);  
	}  
	sb.append(hex);  
    }  
    return sb.toString();  
} 

byte[] bArr = {5,(byte) 246,(byte) 213};
System.out.println(bytesToHex(bArr));
output:
05f6d5
/** 
 * Hex字符串转byte 
 * @param inHex 待转换的Hex字符串 
 * @return  转换后的byte 
 */  
public static byte hexToByte(String inHex){  
   return (byte)Integer.parseInt(inHex,16);  
}  

System.out.println(hexToByte("D"));
output: 
13
/** 
 * hex字符串转byte数组 
 * @param inHex 待转换的Hex字符串 
 * @return  转换后的byte数组结果 
 */  
public static byte[] hexToByteArray(String inHex){  
    int hexlen = inHex.length();  
    byte[] result;  
    if (hexlen % 2 == 1){  
        //奇数  
        hexlen++;  
        result = new byte[(hexlen/2)];  
        inHex="0"+inHex;  
    }else {  
        //偶数  
        result = new byte[(hexlen/2)];  
    }  
    int j=0;  
    for (int i = 0; i < hexlen; i+=2){  
        result[j]=hexToByte(inHex.substring(i,i+2));  
        j++;  
    }  
    return result;   
}  

位运算

  • 按位与运算(&): 两个都是 1 才是 1;
  • 按位或运算(|): 有 1 就得 1;
  • 按位异或运算(^): 不同就是 1 ;
  • 按位取反(~): 0 1 互换
  • 按位左移(<<): 右侧补0,在数字没有溢出的情况下相当于乘以2的移动位次方;
  • 按位右移(>>):左侧补0,在数字没有溢出的情况下相当于除以2的移动位次方;

《4》Thread   Runnalbe   Callable   ExecutorService   ThreadPoolExecutor

Thread 和 ExecutorService 区别:

Thread :

  • Thread 每次new 线程,线程新建和回收性能开销比较大;
  • Thread 缺乏对线程的控制,可以无限的新建线程;难易控制并发线程的数量,容易产生OOM;
  • Thread 的功能单一,如缺乏定时,周期执行等功能;

Executors 创建的线程池:

  • 重用存在的线程,减少对象创建、消亡的开销,性能佳
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能;

ThreadPoolExecutor :

Java多线程复习与巩固(六)--线程池ThreadPoolExecutor详解

ThreadPoolExecutor最佳实践--如何选择线程数

Executor详解

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

下面解释一下第四个构造器中各个参数的含义:

  •  corePoolSize:核心池的大小。

        核心池中的线程会一致保存在线程池中(即使线程空闲),除非调用allowCoreThreadTimeOut方法允许核心线程在空闲后一定时间内销毁,该时间由构造方法中的keepAliveTime和unit参数指定;
        在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads() 或者 prestartCoreThread()方法,从这两个方法的名字就可以看出,是“预创建线程”的意思,即在没有任务到来之前就创建corePoolSize个线程(prestartAllCoreThreads)或者一个线程(prestartCoreThread);

  • maximumPoolSize:线程池允许的最大线程数。它表示在线程池中最多能创建多少个线程。

        默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把新加入的任务放到缓存队列当中,缓存队列由构造方法中的workQueue参数指定,如果入队失败(队列已满)则尝试创建临时线程,但临时线程和核心线程的总数不能超过maximumPoolSize,当线程总数达到maximumPoolSize后会拒绝新任务;所以有两种方式可以让任务绝不被拒绝:

    ① 将maximumPoolSize设置为Integer.MAX_VALUE(线程数不可能达到这个值),CachedThreadPool就是这么做的

    ② 使用无限容量的阻塞队列(比如LinkedBlockingQueue),所有处理不过来的任务全部排队去,FixedThreadPool就是这么做的。

  •  keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。

        默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用——当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会被销毁,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(true)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

  • workQueue:一个阻塞队列(BlockingQueue接口的实现类),用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:
  1. ArrayBlockingQueue    数组实现的阻塞队列,数组不支持自动扩容。所以当阻塞队列已满,线程池会根据handler参数中指定的拒绝任务的策略决定如何处理后面加入的任务
  2. LinkedBlockingQueue  链表实现的阻塞队列,默认容量Integer.MAX_VALUE(不限容),当然也可以通过构造方法限制容量
  3. SynchronousQueue    零容量的同步阻塞队列,添加任务直到有线程接受该任务才返回,用于实现生产者与消费者的同步,所以被叫做同步队列
  4. PriorityBlockingQueue  二叉堆实现的优先级阻塞队列
  5. DelayQueue   延时阻塞队列,该队列中的元素需要实现Delayed接口;底层使用PriorityQueue的二叉堆对Delayed元素排序;ScheduledThreadPoolExecutor底层就用了DelayQueue的变体"DelayWorkQueue";队列中所有的任务都会封装成ScheduledFutureTask对象(该类已实现Delayed接口)      
  • threadFactory:线程工厂,主要用来创建线程;默认情况都会使用Executors工具类中定义的默认工厂类DefaultThreadFactory。可以实现ThreadFactory接口来自己控制创建线程池的过程(比如设置创建线程的名字、优先级或者是否为Deamon守护线程)
  • handler:表示当拒绝处理任务时的策略,有以下四种取值(默认为AbortPolicy):
  1. ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

Executor 可 以 创 建 3 种 类 型 的 ThreadPoolExecutor 线 程 池:

 1. FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) { 
        return new ThreadPoolExecutor(nThreads, nThreads, 0L,
                                      TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue()); 
}  

1. FixedThreadPool的corePoolSize和maxiumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。
2. 0L则表示当线程池中的线程数量操作核心线程的数量时,多余的线程将被立即停止
3. LinkedBlockingQueue作为线程池的做工队列,由于是无界的,当线程池的线程数达到corePoolSize后,
   新任务将在无界队列中等待,因此线程池的线程数量不会超过corePoolSize,同时maxiumPoolSize也就
   变成了一个无效的参数,并且运行中的线程池并不会拒绝任务。

2. SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
}

1.如果当前工作中的线程数量少于corePool的数量,就创建一个新的线程来执行任务。

2.当线程池的工作中的线程数量达到了corePool,则将任务加入LinkedBlockingQueue。

3.线程执行完1中的任务后会从队列中去任务。

注意:由于在线程池中只有一个工作线程,所以任务可以按照添加顺序执行。

3. CachedThreadPool

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
}

1. CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为
   Integer.MAX_VALUE,即maximum是无界的。这里keepAliveTime设置为60秒,意味着空闲的线
   程最多可以等待任务60秒,否则将被回收
2. 如果主线程提交任务的速度高于线程池中处理任务的速度时,CachedThreadPool会不断创建新线程。
   极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源

4. ScheduleExecutorService

public ScheduledFuture scheduleAtFixedRate(Runnable command, 
                                              long initialDelay, 
                                              long period, 
                                              TimeUnit unit); 
1. scheduleAtFixedRate方法,当我们要执行的任务大于我们指定的执行间隔时,
   它并不会在指定间隔时开辟一个新的线程并发执行这个任务。而是等待该线程执行完毕。

2. 当任何一个任务的执行过程中遇到一个异常,剩下的执行将会被抑制,

* Creates and executes a periodic action that becomes enabled first
* after the given initial delay, and subsequently with the given
* period; that is executions will commence after
* initialDelay then initialDelay+period, then
* initialDelay + 2 * period, and so on.
* If any execution of the task
* encounters an exception, subsequent executions are suppressed.
* Otherwise, the task will only terminate via cancellation or
* termination of the executor.  If any execution of this task
* takes longer than its period, then subsequent executions
* may start late, but will not concurrently execute.

public ScheduledFuture scheduleWithFixedDelay(Runnable command, 
                                                 long initialDelay, 
                                                 long delay, 
                                                 TimeUnit unit); 

代码复习遇到知识点回顾(一)_第3张图片

《5》JAVA 类加载机制

参考博文:

(1)JVM系列(一):java类的加载机制

(2)Java 类加载机制(阿里面试题)-何时初始化类

类的生命周期:

Loading(加载)--- Verification(验证)--- Preparation(准备)--- Resolution(解析)--- Initialization(初始化);

加载过程:

第一个问题:加载class的方式有哪些?

  1.  从本地系统中直接加载
  2.  通过网络下载.class文件
  3.  从zip,jar等归档文件中加载.class文件
  4. 从专有数据库中提取.class文件
  5.  将Java源文件动态编译为.class文件

第二个问题:加载class文件中的二进制数据,虚拟机完成了那三件事情:

    1、通过一个类的全限定名来获取其定义的二进制字节流

    2、将这个字节流中所代表的静态存储结构转化为方法区的运行时数据结构

    3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

第三个问题:双亲委派机制是什么?

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器(子加载器和父加载器之间不是继承关系,而是组合关系)去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类:系统类防止内存中出现多份同样的字节码 ;保证Java程序安全稳定运行。

代码复习遇到知识点回顾(一)_第4张图片

也就是说:

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

从启动类加载器 -- 扩展类加载器 -- 应用类加载器 -- 自定义类加载器,它们各自的搜索范围是什么?

  • 启动类加载器,Bootstrap ClassLoader,由C++编写,无法由java程序引用,加载JACA_HOME\jre\lib,或者被-Xbootclasspath参数限定的类
  • 扩展类加载器,Extension ClassLoader,由 该加载器由sun.misc.Launcher$ExtClassLoader实现,加载JAVA_HOME\jre\lib\ext,或者被java.ext.dirs系统变量指定路径下的所有类库;
  • 应用程序类加载器,Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现加载ClassPath中的类库
  • 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

验证过程:

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

准备阶段:

准备阶段是正式为 类变量 分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  •     这时候进行内存分配的仅包括类变量(static),而不包括 实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  •     这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

   假设一个类变量的定义为:public static int value = 3;

   那么变量value在准备阶段过后的初始值为 0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在 初始化阶段 才会执行。

  • 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

   假设上面的类变量value被定义为: public static final int value = 3;

   编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

解析阶段:

将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。

初始化阶段:

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  1.   ①声明类变量是指定初始值
  2.   ②使用静态代码块为类变量指定初始

 JVM初始化步骤

  •  假如这个类还没有被加载和连接,则程序先加载并连接该类
  •  假如该类的直接父类还没有被初始化,则先初始化其直接父类
  •  假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种

  • – 创建类的实例,也就是new的方式
  • – 访问某个类或接口的静态变量,或者对该静态变量赋值
  • – 调用类的静态方法
  • – 反射(如Class.forName(“com.shengsiyuan.Test”))
  • – 初始化某个类的子类,则其父类也会被初始化
  • – Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
public class SSSClass {
    static {
        System.out.println("SSSClass");
    }
} 

public class SSClass extends SSSClass {
    static{
        System.out.println("SSClass init!");
    }
 
    public static int value = 123;
 
    public SSClass(){
        System.out.println("init SSClass");
    }
}

public class SClass extends SSClass{
    static {
        System.out.println("SClass init");
    }
 
    static int a;
 
    public SClass() {
        System.out.println("init SClass");
    }
}
public class NotInitialization
{
    public static void main(String[] args) {
        System.out.println(SClass.value);
    }
}

output:
SSSClass
SSClass init!
123

REASON:
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,
只会触发父类的初始化而不会触发子类的初始化。

《6》BlockingQueue 中 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别

参考博文,一点说的比较好,一个锁,不同锁

存入数据:

  • offer(anObject): 如果BlockingQueue可以容纳,则返回true,否则返回 false.(本方法不阻塞当前执行方法的线程);       
  • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
  • put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻塞.

获取数据:

  • poll() 或 poll(time): 取走BlockingQueue里排在首位的对象, 若不能立即取出, 则可以等time参数规定的时间,取不到时返回 null;
  • poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
  • take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入; 
  • drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

1. ArrayBlockingQueue

  基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

  ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁

  2.LinkedBlockingQueue

  基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

《7》CopyOnWriteArrayList 和 CopyOnWriteArraySet

CopyOnWrite容器即 写时复制 的容器。通俗的理解是:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。但是当我们涉及到更改容器值的操作时(add set remove),都是在lock中进行的,这也保证了多线程写的一致性;所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器;由于更改容器的元素是copy出来的一份,所以在内存中,在某一段时间就会存在两份容器数据,导致非常占内存,其次,我们在修改完以后又把新的容器(copy出来的这份)的引用赋给原来容器的引用,这就导致了刚修改的元素可能不能立即读取到,于此同时频繁的更赋引用也容易导致GC回收问题;因此,CopyOnWrite容器适合于高频度读取操作,低频度修改的操作;

内部源码:

获取操作 get(index)

/**
  * {@inheritDoc}
  *
  * @throws IndexOutOfBoundsException {@inheritDoc}
  */
public E get(int index) {
    return get(getArray(), index);
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

final Object[] getArray() {
    return array;
}

添加操作:add(E e) remove( int index);

/**
  * Appends the specified element to the end of this list.
  *
  * @param e element to be appended to this list
  * @return {@code true} (as specified by {@link Collection#add})
  */
    public boolean add(E e) {
        // 注意 在此先进行lock,也就序列化了多线程增加问题;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 注意, 复制了一份出来, 并且新的长度增加了一个;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            // 将原来数组的引用指向了新的引用;
            setArray(newElements);
            return true;
        } finally {
            // finally 中释放锁
            lock.unlock();
        }
    }

    final void setArray(Object[] a) {
        array = a;
    }
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

注意上面的:

Arrays.copyOf(elements, len + 1);
System.arraycopy(elements, 0, newElements, 0, index);

《8》单例模式

实现思路:

  • 私有化构造方法,让使用者无法通过 new 关键字创建;
  • 提供一个静态方法getInstance(),让用户通过类名直接获取这个唯一的对象;

常用场景:

  • 缓存,线程池,连接池,内嵌FTP,内嵌MQ 。。。。。。

常见写法,多线程模式下:

饿汉式:类在加载和初始化时,就创建,缺点:没满足懒加载, 可能浪费空间,优点 简单

public class Singleton {

    private static fianl Singleton INSTANCE = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance (){
        return INSTANCE;
    }
}

public class Singleton {

    private static Singleton INSTANCE;
    static {
        INSTANCE = new Singleton();
    }
    
    private Singleton(){}

    public static Singleton getInstance (){
        return INSTANCE;
    }
}

懒汉式:优点延迟加载,缺点需要 sychronized 加 双重判断,存在锁竞争问题;

public class Singleton {
    private static Singleton instance;
    
    private Singleton(){}    

    public static sychronized Singleton getInstance(){
        if( null == instance ){
            instance = new Singleton();
        }
        return instance;
    }
}

public class Singleton {
    private static Singleton instance;
    
    private Singleton(){}

    public static Singleton getInstance(){
        if( null == instance ){
            synchronized (Singleton.class) {
                if( null == instance )
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

私有静态内部类: 也可以使用到锁,并且不用使用同步代码块,也可以

public class Singleton {
    
    private Singleton(){}
    
    private static class SingletonInstance{
        private static final Singleton INSTANCE = new Singleton(); 
    }
    public static Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

枚举:简单方便,缺点 不好理解

public enum Singleton{
    INSTANCE;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(JAVA-SE)