Java高级篇

Catalogue
  1. 1. 前言
    1. 1.1. 本系列文章将分为三部分:
      1. 1.1.1. Java基础篇
      2. 1.1.2. Java高级篇
      3. 1.1.3. Java实践篇
    2. 1.2. 相关链接
  2. 2. JVM知识
    1. 2.1. 内存模型
      1. 2.1.1. JMM结构
    2. 2.2. JVM相关概念
      1. 2.2.1. 定义
      2. 2.2.2. Java类加载机制
    3. 2.3. JVM的GC
      1. 2.3.1. 垃圾回收算法
      2. 2.3.2. 垃圾回收器
    4. 2.4. 泛型擦除
  3. 3. 多线程
    1. 3.1.
      1. 3.1.1. 内置锁
      2. 3.1.2. 显式锁
      3. 3.1.3. 原子操作
      4. 3.1.4. 协定
    2. 3.2. 同步队列
      1. 3.2.1. 基本操作
      2. 3.2.2. 常用类
      3. 3.2.3. ArrayBlockingQueue VS LinkedBlockingQueue
    3. 3.3. 线程间数据交互
    4. 3.4. 线程池
      1. 3.4.1. 类的介绍
      2. 3.4.2. ThreadPoolExecutor
      3. 3.4.3. 创建线程池的常用方法
      4. 3.4.4. Tips
  4. 4. Java IO
    1. 4.1. 同步与阻塞
      1. 4.1.1. 同步与异步
      2. 4.1.2. 阻塞与非阻塞
    2. 4.2. 多路复用
    3. 4.3. BIO,NIO,AIO
      1. 4.3.1. BIO
      2. 4.3.2. NIO
      3. 4.3.3. Java AIO(NIO.2)
  5. 5. Java安全
  6. 6. 设计模式
    1. 6.1. 分类
    2. 6.2. 几种常见设计模式
      1. 6.2.1. 观察者模式
      2. 6.2.2. 装饰者模式
      3. 6.2.3. 工厂模式
      4. 6.2.4. 单例模式
      5. 6.2.5. 适配器模式

前言

本系列文章将分为三部分:

Java基础篇

  • 语法篇
  • 数据结构篇
  • 集合框架

Java高级篇

  • JVM知识
  • 多线程
  • Java IO
  • Java安全
  • 设计模式

Java实践篇

  • 并发编程相关
  • 分布式相关
  • 框架介绍

有些【疑问】可能会留在笔记中,在学习中补充…

相关链接

JVM知识

内存模型

JMM结构

  • 基本结构
    • 分为class loader;
    • 运行时数据区;
    • 执行引擎;
    • 本地方法接口

基本示意图如下:


此处输入图片的描述
  • 类在JVM中的唯一性
    通过类的全限定名和加载这个类的类加载器ID确定唯一性。

JVM相关概念

定义

  • 程序计数器(Program Counter Register):
    当前线程执行字节码的行号指示器。通过改变这个指示器的值来选取下一条需要执行的字节码指令。由于多线程间切换时要恢复每一个线程的当前执行位置,所以每个线程都有自己的程序计算器。这个内存区域是Java虚拟机唯一一个没有定义OutOfMemeryError情况的区域。
  • Java虚拟机栈(Java Visual Machine Stacks):
    虚拟机栈描述的是Java方法执行的内存模型:每个方法执行是都会创建栈帧(Stack Frame)用于存储局部变量(包括引用变量),操作栈,方法信息,动态链接,方法出口等信息。栈帧由三部分组成:局部变量区(以一个字长为单位,用数组下标实现定位局部变量)、操作数栈(以一个字长为单位,用数组表示,但采用先进后出的策略)、帧数据区(用于常量池解析、正常方法返回以及异常派发机制)。
    >
    在java虚拟机规范中,对于这两个区域规定了两种情况的异常:1)如果线程请求的栈深度大于虚拟机所允许的深度将会抛出StackOverFlowError异常; 2)如果Java虚拟机可以动态扩展,当无法申请到足够的内存时会抛出OutOfMemeryError。

  • 本地方法栈(Native Method Stacks)
    本地方法栈与Java虚拟机栈区别是Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈是虚拟机使用到的Native方法服务。如JNI引用的对象。所以本地方法栈也可能出现两种与Java虚拟机栈相同的异常。

  • Java堆(Java Heap)
    Java堆是Java虚拟机管理的最大的一块内存区域,java堆是被所有Java线程共享的,在Java虚拟机启动时创建,此内存的唯一目的就是存放对象实例。几乎所有的对象实例都要分配在堆中,垃圾回收的主要区域就是这里(还可能有方法区)。(随着JIT编译器的发展,逃逸分析技术的逐渐成熟,栈上分配,标量替换等优化技术,使得部分对象不再分配在堆上。)JNI调用的程序new的空间不受这里管理。
    Java堆的大小通过:

    -Xmx和-Xms两个参数控制。但是当堆的内存再无法扩展时,就会出现OutOfMemeryError。

  • 方法区(Method Area)
    方法区与Java堆一样,是各个线程共享的内存区域,他用于存储类信息,常量,静态变量以及及时编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemeryError.

  • 简图


    这里写图片描述
  • 相关测试方法

    这里写图片描述

Java类加载机制

  • 什么是类的加载
    类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

此处输入图片的描述
  • 类加载的时机
    类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
    参考:http://www.cnblogs.com/ityouknow/p/5603287.html

  • 类加载流程

    1. 装载:查找和导入Class文件;
    2. 链接:把类的二进制数据合并到JRE中;
      1. 校验:检查载入Class文件数据的正确性;
      2. 准备:给类的静态变量分配存储空间,并将其初始化为默认值,普通static为0,final static为设定值;
      3. 解析:将符号引用转成直接引用;
    3. 初始化:对类的静态变量,静态代码块执行初始化操作

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

  • 三个类加载器

    • Bootstrap ClassLoader
      根类加载器,负责核心类的加载,如System,String等,还有jdk中jre的lib目录的rt.jar,也可以通过指定参数-Xbootclasspath再加;
    • Extension ClassLoader
      扩展类加载器,负责jdk中jre的lib目录下的ext目录,或者通过-Djava.ext.dirs再加;
    • Application ClassLoader
      用于加载用户路径上指定的类库。由-classpath或-cp选项定义,或者是JAR中的Manifest的classpath属性定义。
    • Tip:
      • 用户自定义类加载器,默认情况下父加载器是系统类加载器,也可以指定其它两个。
      • Tomcat 为每个 App 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader。需要加载 WebApp 下的类时,就取出 ClassLoader 来使用
      • JVM在判定两个class是否相同时(相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字的结果),不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
      • 类加载器相关例子:
        1
        2
        3
        4
        5
        Object obj = myClassLoader.loadClass("com.test.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        // return false
        // com.test.ClassLoaderTest类默认使用Application ClassLoader加载,而obj是通过自定义类加载器加载的,类加载不相同,因此不相等
        System.out.println(obj instanceof com.test.ClassLoaderTest);
  • 双亲委托模型
    应用程序都是由以上三种类加载器互相配合进行加载的,还可以加入自己定义的类加载器,称为类加载器的双亲委派模型。

    • 类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的。
    • ClassLoader本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。
      1. 当一个ClassLoader实例需要加载某个类时,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载;
      2. 如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。
    • 为什么要使用双亲委托这种模型呢?

      此处输入图片的描述
      此处输入图片的描述

JVM的GC

垃圾回收算法

  • Mark-Sweep(标记-清除)算法
    分标记和回收两个阶段,内存碎片问题。
  • Copying(复制)算法
    内存空间利用率不高。
  • Mark-Compact(标记-整理)算法
    分标记和整理两个阶段,弥补Copying算法的缺陷,标记后移动,再清除。

以上三种是传统最常见的方法,具体演示参考:
http://www.cnblogs.com/dolphin0520/p/3783345.htm

  • 分代回收算法
    在Hotspot JVM中采用分代回收,现在在各种其它JVM中也比较常用。

    • JVM区域的划分(Java1.7)
      从GC的角度,jvm区域总体分两类,heap区(Java堆)和非heap区(其它)。
      • heap区:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区);eden和survivor默认比例是8:1:1(新生代实际可用的内存空间为 9/10(即90%)的新生代空间)。
      • 非heap区:CodeCache(代码缓存区)、PermGen(永久代)等。
    • 关于永久代(主要存放的是Java类定义信息,生成大量动态类可能溢出)

      • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载,一般需要避免,因此就不会发生GC了;
      • Java8不设永久代,类的元信息等被移到了一个与堆不相连的本地内存区域,这个区域就是元空间(metaspace)。

        a. 对永久代进行调优是很困难的,永久代中的元数据可能会随着每一次FullGC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。b. 同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

    • 如何分代回收


      此处输入图片的描述

      • Minor GC:
        当Eden区满时,触发Minor GC。新生代采用了复制算法,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中(总有一块保持为空作为to,用于复制,满就复制到老年区),然后再把这个Eden区域和Survivor区域清除。
        在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代(年轻代被分为3个部分——Enden区和两个Survivor区(From和to))所有对象的总空间(这一段似乎有误,有说法是说survival from和to区是否够):
        1. 如果大于则进行Minor GC;
        2. 如果小于则看HandlePromotionFailure设置是否允许担保失败。
          1. 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。
          2. 如果不允许,则直接Full GC。
        3. 如何晋升为老年代:多次Minor GC后还存活的对象会晋升。
      • Major GC:
        清理老年代,许多是由 Minor GC 触发的,老年代采用的是Mark-Compact(标记-整理)算法(HotSpot VM,其它的JVM实现并不一定是这样做的)。
      • Full GC:
        老年代空间不足:如上分析,要么是前期的检查,要么是晋升期间。当频繁Full GC时,可通过jstat、GC日志、对象内存工具等查看是什么原因,例如JMap查看。
        据知乎:
        1. fullgc频繁说明old区很快满了,即剩余空间不足(来的多或留的多)。
        2. 如果是一次fullgc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区。
        3. 如果一次fullgc后,old区回收率不大,那么说明old区太小。
  • 哪些对象需要回收
    每个应用程序都包含一组根(root)。每个根包含指向引用类型对象的一个指针,该指针要么引用托管堆中的一个对象,要么为null,GC会收集那些不是GC roots且没有被GC roots引用的对象。
    java中可作为GC Root的对象有:

    • 虚拟机栈中引用的对象(本地变量表);
    • 本地方法栈中引用的对象(Native对象);
    • 方法区中静态属性引用的对象;
    • 方法区中常量引用的对象(对比而言单例可能会被回收);
      参考: http://www.importnew.com/15820.html

垃圾回收器

  • Serial/Serial Old
  • ParNew
  • Parallel Scavenge
  • Parallel Old
  • CMS
    CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法,默认的回收线程数是(CPU个数+3)/4。CMS的缺点也比较明显,CMS会牺牲更多的硬件资源、吞吐量及Heap使用量等,所以在Java1.7之后引入了G1 收集器来全面替代CMS。
  • G1(1.7开始引进)
    G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。整体上是基于标记整理,局部采用复制回收算法。

什么时候触发GC:http://www.tuicool.com/articles/3yuqMzz

泛型擦除

泛型可以说是Java中最常用的语法糖之一,因此虚拟机是不支持这些语法,在编译时转化为Object,继承的时候利用桥方法动态调用,据此应考虑泛型在开发过程中的约束和局限性。具体用法解释可参考【Java基础篇】

多线程

更多内容请查看Java实践篇

内置锁

  • synchronized

对于类而言,静态方法的锁是Test.class,而普通方法的锁是this。如果对类中的两个方法用synchronized进行修饰,要么都是静态的,或都是成员方法,这两个方法才会同步,如下例子,类中的普通成员方法都采用this作为锁对象。

1
2
3
4
5
class Test{
private in value;
synchronized int get(){return value;}
sychronized void set(int value){this.value = value;}
}

显式锁

java.util.concurrent.locks包中定义了两个锁类,ReentrantLock和ReentrantReadWriteLock类。

  • ReentrantReadWriteLock
    在读多于写的情况下,利用读写锁分离访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private Lock readLock = rwl.readLock();
    private Lock writeLock = rwl.writeLock();
    public double getTotalBalance(){
    readLock.lock();
    try{...}
    finally{readLock.unlock();}
    }
    public double setBalance(){
    writeLock.lock();
    try{...}
    finally{writeLock.unlock();}
    }
  • ReentantLock
    类似的,也是需要lock(),并在finally中unlock(),而一般常用的方法,是将其与Condition配合使用,使线程在某些情况下,挂起等待,某些情况下,被唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private Lock suspendLock = new ReentranLock();
    private Condition suspendCondition = suspendLock.newCondition();
    // 在某个安全操作下
    public double getTotalBalance(){
    suspendLock.lock();
    try{
    while(a==0){
    suspendCondition.await(); // 暂时放弃锁
    }
    }
    finally{suspendLock.unlock();}
    }
    public double setBalance(){
    suspendLock.lock(); // 尝试获得锁,假如某个操作线程放弃锁就继续执行
    try{
    // do someting
    a++;
    suspendCondition.signalAll(); // 唤醒其它线程
    }
    finally{suspendLock.unlock();}
    }

原子操作

  • 除了long型字段和double型字段外,Java内存模型确保访问任意类型字段所对应的内存单元都是原子的。此外,volatile long 和volatile double也具有原子性。Java编程规范的文档说明

    对于64big的环境来说,单次操作可以操作64bit的数据,但在32bit操作系统下,指令单次操作能处理的最长长度为32bit,因此在读写的时候,JVM分成两次操作,每次读写32位。因为采用了这种策略,所以64位的long和double的读与写都不是原子操作。

  • 原子性不能确保获得的是任意线程写入之后的最新值,需要额外编程实现同步(原子和同步是不同的概念)。因此,原子性保证通常对并发程序设计的影响很小。

协定

每个共享的和可变的变量都应该只由一个锁来保护,从而维护人员知道是哪一个锁。

一种常见的加锁规定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。

同步队列

基本操作

  • peek:返回头元素,不删除,队列空为null
    poll:返回头元素,删除,队列空为null
    take:返回头元素,删除,队列空阻塞

  • offer:添加元素,队列满为false
    put:添加元素,队列满阻塞

常用类

每一个BlockingQueue都有一个容量,让容量满时往BlockingQueue中添加数据时会造成阻塞,当容量为空时取元素操作会阻塞。

  • LinkedBockingQueue
    默认容量没有上限,也可以指定最大容量
  • ArrayBlockingQueue
    构造时需要指定容量
  • PriorityBlockingQueue
    堆的实现可利用PriorityQueue,这里加了Blocking,可以认为是多线程的版本。

ArrayBlockingQueue VS LinkedBlockingQueue

  • 队列中锁的实现不同
    ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
    LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock,而在遍历以及删除元素则要两把锁都锁住。
  • 在生产或消费时操作不同
    ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
    LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
  • 队列大小初始化方式不同
    ArrayBlockingQueue实现的队列中必须指定队列的大小;
    LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE(如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽)

线程间数据交互

  • Exchanger
    同步的主要作用是保证一致性,或者说逻辑上的顺序性。但某些场景我们只是想两个线程共享数据,利用锁保证读写分离是没问题的,但还有更优雅的方式。
    可在两个线程之间交换数据,只能是2个线程,他不支持更多的线程之间互换数据。实际上,一个线程向缓冲区填入数据,另一个线程消耗这些数据,一方线程完成操作,通过交换缓冲区的方式让另一个线程消费数据。如下,我们不必关系同步不同步的问题。
    1
    2
    3
    4
    5
    Exchanger<List<Integer>> exchanger = new Exchanger<>();
    new Consumer(exchanger).start();
    new Producer(exchanger).start();
    // 内部操作
    list = exchanger.exchange(list);

具体例子参考:http://blog.csdn.net/andycpp/article/details/8854593

  • 障栅
    例如我们想所有线程运行到某个点,再做合并操作(类似MapReduce),可以考虑用CyclicBarrier类,与内存栅障类似:运算线程集内的线程必须等待该集合中所有其他线程都执行完某个运算后,才能继续向下执行。确保在所有线程全部到达某个逻辑执行点前,任何线程都不能越过该逻辑点。当一个线程运行完它那部分,则让其运行到障栅出,一旦所有线程都到达了这个障栅,障栅自动撤销,所有线程继续运行
    1
    2
    3
    4
    5
    // 表示至少三个线程到障栅才释放
    CyclicBarrier barrier = new CyclicBarrier(3);
    // 传到每个线程的run方法中
    doWork();
    barrier.await();

对比CountDownLatch:闭锁用来等待事件,而栅栏用于等待其他线程,大家相互等待,只要有一个没完成,所有人都得等着。即闭锁用来等待的事件就是countDown事件,只有该countDown事件执行后所有之前在等待的线程才有可能继续执行;

  • 信号量(Semaphores)
    当一个线程想要访问某个共享资源,首先,它必须获得semaphore。如果semaphore的内部计数器的值大于0,那么semaphore减少计数器的值并允许访问共享的资源。计数器的值大于0表示,有可以自由使用的资源,所以线程可以访问并使用它们。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private final Semaphore semaphore;
    public Class(){
    semaphore = new Semaphore(1);
    }
    // in function
    try {
    semaphore.acquire();
    // do something
    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally{
    semaphore.release();
    }

线程池

常用例子:

1
2
3
4
5
6
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService executor = Executors.newFixedThreadPool(3);
// 实现Runnable接口
Runnable runner = new ExecutorThread();
executor.execute(runner);

类的介绍

  • Executor
    接口,该 接口 只包含一个方法:

    1
    void execute(Runnable command);
  • ExecutorService
    也是一个接口,继承Executor接口。

    1
    2
    public interface ExecutorService extends Executor {
    }
  • Executors
    Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

ThreadPoolExecutor

继承ExecutorService接口,Executors工厂类中提供的线程池的底层实现,所以自定义灵活度高,通常建议用Executors已经配置好了的。继承关系简单如下所示:


此处输入图片的描述

如下最常见的newFixedThreadPool方法源码:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

参数:

  • corePoolSize
    线程池的基本大小,如果当前线程数目小于该配置,无论是否其中有空闲的线程,都会给新的任务产生新的线程。
    如果刚好等于或大于,会尝试将新任务添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • maximumPoolSize
    线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。如果线程数目达到maximumPoolSize,一般意味着队列也已经满了,则会采取任务拒绝策略进行处理。形象一点可看下图;
  • keepAliveTime
    线程空闲时间超过keepAliveTime,线程将被终止。
  • poolSize
    线程池中当前线程的数量。

此处输入图片的描述

线程池中的线程初始化:

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法做到:

  • prestartCoreThread()
    初始化一个核心线程;
  • prestartAllCoreThreads()
    初始化所有核心线程

重要的方法:

  • execute()
    无返回值。
  • submit()
    利用了Future来获取任务执行结果。
  • shutdown()
    关闭线程池。
  • shutdownNow()
    关闭线程池。

创建线程池的常用方法

正如例子所述,可利用Executors工厂类快速创建线程池。

  • newCachedThreadPool
    创建一个无界可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。corePoolSize和maximumPoolSize的大小是一样的,而queue是无界的
  • newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

Tips

  • 如果你提交任务时,线程池队列已满。会时发会生什么?
    许多人可能会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor的submit()方法将会抛出一个RejectedExecutionException异常。
  • Java线程池中submit() 和 execute()方法有什么区别?
    两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

Java IO

同步与阻塞

参考:http://www.cnblogs.com/dolphin0520/p/3916526.html

IO请求分两个阶段:

  • 查看数据是否就绪;
  • 进行数据拷贝(内核将数据拷贝到用户线程);

同步与异步

关键:内核发送通知

多个任务和事件发生时,一个事件的发生或执行是否会导致整个流程的暂时等待。

  • 对于同步而言,是需要用户线程或内核去轮询,当数据就绪时,再将数据从内核拷贝到用户空间。
  • 对于异步而言,IO两个阶段都由内核完成,然后发送通知告知用户,由内核主动拷贝数据到用户线程,可见异步需要操作系统底层的支持;

阻塞与非阻塞

关键:操作返回标志位

  • 对于阻塞而言,用户线程一直等待数据,直到就绪;
  • 对于非阻塞而言,用户线程读取数据,立即返回标志位告知用户线程当前IO状态,下次再读取数据了,一旦就绪,内核就将数据拷贝到用户线程。

多路复用

  • select

    • 遍历,效率线性下降;
    • 支持的文件描述符数量太小,为1024;
  • poll
    使用pollfd结构而不是select的fd_set结构,其它方面非常相似

  • epoll

    • 水平触发
      LT(level triggered)水平触发:是缺省的工作方式,并且同时支持block和no-block socket.当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果你不作任何操作或没读写完,那么下次调用 epoll_wait()时,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
    • 边缘触发
      ET(edge-triggered)边缘触发:是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
      参考 http://www.tuicool.com/articles/VvU7fum

在Netty权威指南中写道:

  1. socket描述符数量受限
  2. 需要扫描所有的socket集合,在网络较差(如Wide Area Network-WAN),epoll效率远在前者之上,得益于epoll只有在活跃的socket上才会调用callback函数,而不是轮询。
  3. epoll是将内核和用户空间通过mmap(内存映射)同一块内存实现FD消息在内核和用户空间的交互,从而避免不必要的内存拷贝。

BIO,NIO,AIO

推荐阅读:java-nio tutorials,更多补充及应用场景参考【Java实践篇】。

BIO

同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

NIO

  • 同步非阻塞IO(Java NIO)
    服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。

  • 异步阻塞IO(Java NIO)
    应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性。

Java AIO(NIO.2)

  • 异步非阻塞IO
    在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。

此处输入图片的描述

Java安全

待补充

设计模式

分类

共23种设计模式可分为三类:

  • 创建型模式
    简单工厂模式(Simple Factory)(不属于23种设计模式)
    工厂方法模式(Factory Method)
    抽象工厂模式(Abstract Factory)
    创建者模式(Builder)
    原型模式(Prototype)
    单例模式(Singleton)

  • 结构型模式
    外观模式/门面模式(Facade门面模式)
    适配器模式(Adapter)
    代理模式(Proxy)
    装饰模式(Decorator)
    桥梁模式/桥接模式(Bridge)
    组合模式(Composite)
    享元模式(Flyweight)

  • 行为型模式
    模板方法模式(Template Method)
    观察者模式(Observer)
    状态模式(State)
    策略模式(Strategy)
    职责链模式(Chain of Responsibility)
    命令模式(Command)
    访问者模式(Visitor)
    调停者模式(Mediator)
    备忘录模式(Memento)
    迭代器模式(Iterator)
    解释器模式(Interpreter)

几种常见设计模式

观察者模式

一个实现主题接口Observable的类subject,可以往里面注册多个观察者Observer(List维护),名义上由观察者传入(订阅)subject,实际上观察者中还要调用subject的方法register:

1
2
subject s = new subject();
new observer(s); // 其中调用register(this),s.get等

s 做其它事情,并触发其中的方法把List中的对象进行逐一调用。

装饰者模式

(如Java Stream各种类)
本质上通过使用虚类(强制实现方法,例如cost),不断继承形成“种类”,装饰器是继承虚类最终的方法类,组合“种类”和“装饰器”:构造函数传入“种类”对象即可构造新种类对象,配合该被装饰对象实现cost方法。

工厂模式

(包括简单工厂,工厂方法,抽象工厂)

  • 本质上通过定义接口,将new对象封装在工厂中,而不是暴露在外面具体方法,通过传入type判断需要new什么对象(工厂方法对这些类实现共同接口),缺点就是对于新加类需要修改工厂创建对象的方法;
  • 工厂方法就是直接在外面new一个对象,这个对象实现某个接口,然后通过固定方法返回特定类型对象;
  • 抽象工厂方法侧重表示通过顶级接口、接口中工厂方法,来强制所有具体工厂需要实现某些方法。

单例模式

  • 优点:
    1. 节省内存资源(只有在用这个实例调用方法时,方法才被加入到内存中,当对象不用的时候,gc会将方法回收,效率高了很多,而static常驻内存,但话不能这么说,单例需要在堆中申请资源,static不用);
    2. 提供了对唯一实例的受控访问,因此便于控制;
  • 主要缺点:
    1. 由于单例模式中没有抽象层(个人认为Java中的通过接口,虚类等实现的逻辑就是抽象层的表现),因此单例类的扩展有很大的困难。
    2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。(记住数据库不要用,否则效率)
  • 例子
  1. 饱汉式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 线程安全的懒汉式=饱汉式(lazy loading):
    public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static synchronized Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }

特点:加锁会影响效率,一般不用这种方法。不加线程同步可能get的时候会生成多个。

  1. 饿汉式
    1
    2
    3
    4
    5
    6
    7
    8
    // 饿汉式(not lazy loading占内存):
    public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
    return instance;
    }
    }

特点:常用,线程安全;容易产生垃圾对象,可能存在多个类加载器的情况,如一些servlet容器对每个servlet使用完全不同的类装载器。

  1. 静态内部类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 静态内部类
    public class Singleton {
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    }

区别:这种达到lazy loading的效果,Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。

想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比就显得很合理。

  1. 双重锁形式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 可能是单例模式最优方案(双重锁形式):
    public class SingletonTest {
    private SingletonTest() { }
    //定义一个静态私有变量(不初始化,不使用final关键字,使用volatile保证了多线程访问时instance变量的可见性,避免了instance初始化时其他变量属性还没赋值完时,被另外线程调用)
    private static volatile SingletonTest instance;
    //定义一个共有的静态方法,返回该类型实例
    public static SingletonTest getIstance() {
    // 对象实例化时与否判断(不使用同步代码块,instance不等于null时,直接返回对象,提高运行效率)
    if (instance == null) {
    //同步代码块(对象未初始化时,使用同步代码块,保证多线程访问时对象在第一次创建后,不再重复被创建)
    synchronized (SingletonTest.class) {
    //未初始化,则初始instance变量
    if (instance == null) {
    instance = new SingletonTest();
    }
    }
    }
    return instance;
    }
    }

注意,反射可以调用private构造方法,然而我们不管它。

  • 双重锁为什么使用volatile,而C++不用
    答案:禁止指令重排序。
    参考:
    http://www.cnblogs.com/dolphin0520/p/3920373.html
    http://blog.csdn.net/dl88250/article/details/5439024
    volatile可以保证Java虚拟机对代码执行的指令重排序,也会保证它的正确性。 synchronized虽然保证了原子性,但却没有保证指令重排序的正确性。因此,线程A对单例中的对象进行初始化过程中,可能出现构造函数里面的操作太多了(在 Java 中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的),经过重排序之后,A线程单例实例还没有造出来,但已经被赋值了。而B线程这时过来了,错以为单例被实例化出来,一用才发现其中的功能并不完善(出现不可预知的结果),尚未被初始化。
    我们的线程虽然可以保证原子性,但程序可能是在多核CPU上执行,JVM进行指令重排序的可能性就更大。

适配器模式

适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。

  • 类适配器模式:


    这里写图片描述
    • 目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
    • 源(Adapee)角色:现在需要适配的接口。
    • 适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
      本质上:继承Adaptee父类同时实现Target接口;
  • 对象适配器模式:
    实现接口,构造函数中传入源,装饰为Adapter类,实现更多方法以适配,与装饰者模式类似;

Comments