JAVA Level: JDK8

1.变量名的组成

变量名由数字、字母大小写、$_组成,但是在JDK9以后,_不能单独作为变量名

2.原码、反码、补码的关系,字符编码

正数原码、反码、补码都是一样的,负数反码是原码除符号位外取反,负数补码是反码再加1

各种类型的字节数

byte(字节): 8bit,即8个二进制位

boolean: 1byte

short: 2byte

char: 2byte,JAVA中所有字符在内存中都以Unicode编码存在(java源文件一般是UTF-8编码,编译后会转成Unicode)

int: 4byte

long: 8byte

float: 4byte

double: 8byte

编码

ASCII码占1个字节

GBK占1到2个字节(英文数字占1个字节,汉字占2个字节),GB2312是在GBK的基础上加入了对繁体的支持

Unicode(又叫UTF-16)占2个字节

UTF-8占1到3个字节(与ASCII重合部分占1个字节,汉字占3个字节)

3.<<>>>>>的区别

<<左移,>>带符号右移(符号位补上原符号位相同的值),>>>无符号右移(符号位补0)

4.JAVA中的自增(++)与C中的自增的区别

int a = 1;,JAVA中a = a++;后,a还是1,但如果是C中a = a++;后,a变成了2,这是因为JAVA中的变量运算是先将变量复制一份到高速缓存中,计算完成再赋值给实际变量的,后++是先把高速缓存中的变量赋值给实际变量,然后高速缓存中的值再加1,所以a还是1;但C中变量运算操作的就是实际变量,是a指向的内存地址中的值加1,无论最后有没有再赋值给a,a都是2

5.类型转换

判断下面程序是否有错

5.1

byte b1=1, b2=2, b;
b = b1 + b2;

5.2

byte b1=1,b;
b = b1 + 3;

5.3

byte b;
b = 3 + 4;

5.4

short s = 15;
s = s + 5;

5.5

short s = 15;
s+=5;

5.1: 错,变量相加自动转型,因为是变量,无法确认其值,所以按int处理

5.2: 错,同5.1

5.3: 对,只要不超过byte的范围,都可以赋值

5.4: 错,同5.1

5.5: 对,+=等符号有自动类型转换

6.判断(输出)结果

6.1 取模运算

5 % 2
5 % -2
-5 % 2
-5 % -2

6.2 整数除法与小数除法

5 / 2
5 / 2.0
3520 / 1000 * 1000

6.3 字符串拼接

System.out.println("5+5=" + 5 +5);
System.out.println("5+5=" + (5+5));
System.out.println('5' + '+' + '5');

6.4 & 和 &&,先++和后++

int a=1,b=2;
if(++a==2 & b++ ==3);
System.out.println("a=" + a + " b=" + b);

a=1,b=2;
if(++a==2 && b++ ==3);
System.out.println("a=" + a + " b=" + b);

a=1,b=2;
if(a++==2 & b++ ==3);
System.out.println("a=" + a + " b=" + b);

a=1,b=2;
if(a++==2 && b++ ==3);
System.out.println("a=" + a + " b=" + b);

6.5 字符串问题

String str1 = "Hello";
String str2 = "World";
String str3 = "HelloWorld";
String str4 = "Hello" + "World";
String str5 = str1 + str2;
String str6 = str1 + "World";
String str7 = str4.intern();
System.out.println(str3 == str4);
System.out.println(str3 == str5);
System.out.println(str3 == str6);
System.out.println(str3 == str7);

6.6 final问题

public static void main(String[] args){
    String str1 = "helloworld";
    String str2 = "hello";
    final String str3 = "hello";
    final String str4 = getHello();

    String str5 = str2 + "world";
    String str6 = str3 + "world";
    String str7 = str4 + "world";
    System.out.println((str1 == str5));
    System.out.println((str1 == str6));
    System.out.println((str1 == str7));
}

public static String getHello() {
    return "hello";
}

6.7 装箱类缓存问题

Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;

System.out.println(i1 == i2);
System.out.println(i3 == i4);

Double d1 = 127.0;
Double d2 = 127.0;
Double d3 = 128.0;
Double d4 = 128.0;

System.out.println(d1 == d2);
System.out.println(d3 == d4);

6.8 参数传递问题

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "world";
    System.out.println(s1 + "---" + s2);
    change(s1, s2);
    System.out.println(s1 + "---" + s2);

    StringBuffer sb1 = new StringBuffer("hello");
    StringBuffer sb2 = new StringBuffer("world");
    System.out.println(sb1 + "---" + sb2);
    change(sb1, sb2);
    System.out.println(sb1 + "---" + sb2);
}

public static void change(String s1,String s2){
    s1 = s2;
    s2 = s1 + s2;
}

public static void change(StringBuffer sb1,StringBuffer sb2){
    sb1 = sb2;
    sb2.append(sb1);
}

答案

6.1:分别为:1、1、-1、-1,取决于被模数

6.2:分别为:2、2.5、3000,含小数自动转成double,否则是整数

6.3:分别为:5+5=55、5+5=10、149,看左边是不是String,是的话加号就是连接字符串的意思

6.4:分别为:a=2 b=3、a=2 b=3、a=2 b=3、a=2 b=2,因为无论如何&两边都执行,而&&如果前面为false,则后面不执行。补充:运算符优先顺序是(从高到低) ()、[]!、+(正)、-(负)、~、++、--*、/、%+(加)、-(减)<<、>>、>>><、<=、>、>=、instanceof==、!=&^|&&||? :(三目运算符)=、+=、-=、*=等一系列赋值运算符

6.5:分别为true、false、false、true,纯字符串存在常量池里,而只要含字符串引用,都存在堆里;对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.intern() == t.intern()才为true。

6.6:分别为false、true、false,final修饰的基本数据类型以及String由于不可更改,在编译时就可以确定其值,所以编译器会优化成编译期常量,在用到该final变量的地方,会直接替换为它的值,相当于6.5中”str4”的情况;但如果final变量是通过函数返回的,则在编译时无法确定其值,不会进行优化

6.7 分别为true、false、false、false,这里用到了自动装箱,相当于调用了对应的valueOf方法(如Integer.valueOf),而ByteShortIntegerLongChar这几个装箱类的valueOf方法是以128位分界线做了缓存的,假如是[-128,127]区间的值是会取缓存里面的引用的,而FloatDouble不会做缓存的原因也很简单,因为整数类型在某个范围内的整数个数是有限的,但是float、double这两个浮点数却不是

6.8:输出hello—world、hello—world和hello—world、hello—worldworld,String类型是值传递的;JAVA中执行方法前,会将要调用的函数的参数值或地址压人栈中,执行完方法后再将之前压入栈的引用数据类型的地址重新赋值给变量,因此JAVA中方法无法改变参数列表中的引用数据类型的指向(但可以改变其成员变量的指向)

7.int[] a、int a[]、int[] a[]的区别

int[] aint a[]是一样的,而int[] a[]是二维数组

8.重裁函数的规则

只有参数列表不一样才构成重裁,而与权限修饰符、返回值类型等无关

9.可变长参数(Varargs)

可变参数函数写法:void func(int i, String... str){},str按数组类型处理

可变长度参数必须作为方法参数列表中的的最后一个参数且方法参数列表中只能有一个可变长度参数

10.private、default、protect、public的区别

它们都可以修饰函数、属性和类,private修饰的函数或属性只能在类内部调用,default能同一个包内可调用,protect在同一个包内及子类都可调用,public所有位置都可调用

一个java文件只能有一个用public修饰的类,且该类的名称必须和java文件名一致(但可以有多个非public修饰的类,类名可以和java文件名不一致)

11.重写函数的规则,Object的equals()和String的equals()的区别

被重写的函数的权限修饰符的范围不能小于父类,抛出的异常不能大于父类,且父类用final修饰的方法不能重写;如果父类构造函数中使用了被子类重写的方法,则父类构造函数调用的是子类的方法,而非父类中的方法,如

class Father{
    String name = "father";
    Father(){
        show();
    }
    void show(){
        System.out.println(name);
    }
}
class Child extends Father{
    String name = "child";
    Child(){
        //子类会自动调用父类的空构造函数
        show();
    }
    void show(){
        System.out.println(name);
    }
}

class Test{
    public static void main(String[] args){
        Child c = new Child();
        c.show();
    }
}

输出结果为

null
child
child

创建子类对象时,自动调用父类的构造函数,此时父类调用的show是子类的show,但这时子类的name属性还没有完成初始化(调用完父类构造函数才开始初始化子类成员变量),所以输出null;调用完父类构造函数后,初始化子类成员变量,然后再调用show,输出child;new完Child对象,此时调show再次输出child

完整的子类初始化顺序是:父类静态变量->父类静态代码块->子类静态变量->子类静态代码块->父类非静态变量->父类非静态代码块->父类构造函数->子类非静态变量->子类非静态代码块->子类构造函数(简单的说就是:父类静态部分->子类静态部分->父类非静态部分+构造函数->子类非静态部分+构造函数)

Object的equals()实际上就是==,比较对象的时候比的是地址,而String的equals()重写了,比的是字符串是否一样

12.成员变量和局部变量有什么区别,代码块和成员变量的执行顺序

作用范围不同,且成员变量有默认的初始值,而局部变量必须显示初始化

代码块和成员变量定义是按顺序执行的,且非static的代码块和成员变量在父类构造函数执行完才开始执行,如果在代码块里初始化成员变量,而该成员变量定义在代码块后面,且静态初始化了,则会覆盖代码块初始化的值

13.多态

用父类类型接收子类的实例就构成多态,这时多态对象调的方法是子类的,但调的属性是父类的(因为方法可以重写,但属性不能)

14.用static和final分别修饰类、属性、方法的作用,用static修饰的代码块

static修饰的内部类不会持有对外部类的隐式引用,静态内部类中的静态成员变量只有在用到的时候才会被加载;static修饰的变量在内存里只有一份,所有实例用的是同一个变量;static修饰的方法可以静态调用(类名.方法),static修饰的代码块只会被执行一次

final修饰的类不可继承;final修饰的属性一旦赋值就不可更改(可以静态赋值,或在代码块、构造函数里赋值),final修饰基本数据类型以及String时,可能会被编译优化,final修饰对象时,不能重新指向别的对象,但对象的属性仍可更改;final修饰的方法不可重写,但可以重裁

15.抽象类和接口

抽象类可以有抽象方法和非抽象方法,而接口全部都是抽象方法,且属性都是final static的(interface即使不声明方法是abstract、属性是final static的,也会默认是这些属性修饰的)。(在JDK8以后,interface内可以有方法的具体实现(需用default或static关键字修饰该方法),而不再是全都是抽象方法)

16.构造函数的调用规则

调用本类的其他构造函数用this(),调用父类构造函数用super(),调用其它构造函数必须放在构造函数的第一行调,且只能调一个,如果不显式调其他构造函数,则默认调用父类参数为空的构造函数(super()),如果父类没有空的构造函数,则一定要显式调用

17.内部类的创建和使用规则(static和非static)

static修饰的内部类可以通过Outter.Inner a = new Outter.Inner()创建,没有static修饰的内部类要先创建外部类的实例Outter out = new Outter(),再通过Outter.Inner a = new out.Inner()创建(或者写成Outter.Inner a = new Outter().new Inner()

如果内部类定义了静态成员,该内部类也必须是静态的,静态内部类使用外部类成员变量或方法时,要求该外部类的成员变量或方法也是静态的

非静态内部类会持有对外部类的隐式引用(内部类的构造器在编译成字节码后编译器会给构造函数再添加一个参数,该参数就是指向外部类对象的引用)

匿名内部类(局部内部类)使用局部变量时,要求局部变量是final的,因为当方法执行完后,局部内部类还没有被销毁,而局部内部类用到了局部变量,局部变量已经被销毁,无法使用,于是编译器通过复制局部变量的方式解决这一问题,而只有用final修饰的变量才能在编译期间进行复制(如果这个变量的值在编译期间可以确定,则编译器默认会在局部内部类的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中;如果不能在编译期间确定(如通过函数返回的对象,参考5.6),会被当做构造函数参数传入内部类中),也就是说,局部内部类使用的局部变量实际上不是同一个对象,只是一份拷贝,所以在局部内部类中修改其值会出现问题,所以编译器就直接限制要访问局部变量,必须把局部变量声明为final

可以在方法里面定义内部类,但由于在方法外无法访问该内部类,一般不会这样写

18.集合中Collection和Map主要的实现类及其特点

集合是用来保持对象引用的

Collections工具类里面有提供synchronizedXXX方法把ListSetMap变为线程安全的(装饰者模式)

hashCode

字符串hash的计算:

int hash = 0;
for (int i = 0; i < value.length; i++) {
    hash = 31 * hash + val[i];
}

这里使用31的好处是 - 31是素数

但是对于可变的元素,如果每次都实时计算,和集合一起使用时会出现问题:

public class Test {
    public static void main(String[] args) {
        HashMap<Time, String> map = new HashMap<>();
        Time time = new Time(12, 34, 15);
        System.out.println(time.hashCode());

        map.put(time, "11111");

        time.hour = 13;
        System.out.println(time.hashCode());
        System.out.println(map.get(time));
    }
}

class Time {
    int hour, minute, second;

    Time(int hour, int minute, int second) {
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }

    @Override
    public int hashCode() {
        int hash = second + minute * 31 + hour * 31 * 31;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Time))
            return false;
        return ((Time) obj).hour==hour && ((Time) obj).minute==minute && ((Time) obj).second==second;
    }

    @Override
    public String toString() {
        return "Time{hour=" + hour + ", minute=" + minute + ", second=" + second + '}';
    }
}

输出:

12601
13562
null

因为set、map根据hashCodeequals判断是否为同一元素,当修改元素的内容时,hashCode如果发生变化,就被认为不是同一元素,所以一般的做法是

int hash;

@Override
public int hashCode() {
    if (hash == 0)
        hash = second + minute * 31 + hour * 31 * 31;
    return hash;
}

equals

hashCode算法可知,hashCode并不可靠,可能会有两个不同的对象hash却相同的情况,所以还需要equalsequalstrue时,hashCode也必须一致,但hashCode一样时,equals不一定为true

通常我们认为两个对象的内容(而非地址值)一样时,它们就是相等的

@Override
public boolean equals(Object obj) {
    //null或者其他类型返回false
    if (!(obj instanceof Time))
        return false;
    return ((Time) obj).hour==hour && ((Time) obj).minute==minute && ((Time) obj).second==second;
}

但是如果创建一个Time的子类,并拓展了其功能,像上面这样写就会有问题

public class Test {
    public static void main(String[] args) {
        Time time= new Time(12,34,15);
        DayTime dayTime = new DayTime(25,12,34,15);
        System.out.println(time.equals(dayTime));
    }
}
class DayTime extends Time{
    int day;

    DayTime(int day,int hour, int minute, int second) {
        super(hour, minute, second);
        this.day = day;
    }
}

所以非final类的equals是不可靠的,通常如果父类重写了equals的话,子类也必须重写equalshashCode同理)

@Override
public boolean equals(Object obj) {
    if (obj instanceof DayTime)
        return super.equals(obj) && day == ((DayTime) obj).day;
    return false;
}

但是这样又会有time.equals(daytime)truedaytime.equals(time)false的问题,即equals没有传递性,所以我们会考虑复合而非继承

class DayTime {
    Time time;
    int day;

    DayTime(int day, int hour, int minute, int second) {
        this.time = new Time(hour, minute, second);
        this.day = day;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof DayTime)
            return time.equals(((DayTime) obj).time) && day == ((DayTime) obj).day;
        return false;
    }
}

19.泛型、泛型类、泛型函数,泛型擦除

class MyClass<T>{
    public T t;
    public MyClass(T t){
        this.t = t;
    }

    /*
    public static List<T> creatTList(){//不能通过编译
        return new ArrayList<T>();
    }
    */

    //在静态函数使用泛型必须声明为泛型方法,而无论该泛型是否在类中定义过
    public static <T> List<T> creatList(){//可以通过编译
        return new ArrayList<T>();
    }

    //E是该函数特有的泛型,而非类中确定的泛型,此时用<E>声明该函数是一个泛型为E的泛型函数
    public <E> E method1(E e){
        return e;
    }

    //不但K在类中没有声明,而且调用的时候也没有参数指明K是什么,此时编译器会自动判断返回值
    public static <K> List<K> method2(){
        renturn new ArrayList<K>();
    }

    public static void main(String[] args){
        //自动类型推断,右边的String可以省略不写(JDK7)
        List<String> list = new ArrayList<>();

        //在调用像method2这种函数时,可以像下面这样在调用时就指定K的类型(JDK8)
        List<Person> persons1 = MyClass.<Person>method2();
        //上面写法和下面等价,此时编译器自动推断泛型参数的类型
        List<Person> persons2 = MyClass.method2();
    }
}
class Person{  }
List< ? > a1 = null;
List<String> a2 = new ArrayList<String>;
a1=a2;

此时a1中元素的类型实际上是Object,可以遍历集合并调用object.toString()输出,但不能往a1中添加元素,因为不知道a1中具体是什么类型的(思考:如果把?改成Object会怎么样—-如果改成Object,则可以往a1中添加任何类型的数据,就不满足泛型的定义了),除此以外,还可以指定泛型是哪个类的父类或子类,如< ? super MyClass>< ? extends MyClass><T extedns MyClass>

ArrayList<String> a1 = new ArrayList<String>();
ArrayList<Integer> a2 = new ArrayList<Integer>();
System.out.println(a1.getClass()==a2.getClass());//true

而以下代码也可以通过编译(提示Note: xxx.java uses unchecked or unsafe operations.)、运行,JVM并不能检测泛型有没有错误使用。(除此以外,使用反射也能往a1里面添加其他类型)

ArrayList<String> a1 = new ArrayList<String>();
a1.add("abc");
ArrayList a2 = a1;//手动擦除泛型
a2.add(123);//堆污染

System.out.println(a2.get(0));
System.out.println(a2.get(1));

当向一个有泛型的对象写入一个非此泛型的对象时,发生堆污染,所以要注意以下写法:

ArrayList a1 = new ArrayList<String>();//手动泛型擦除
a1.add("abc");
a1.add(123);

以上代码看似使用了泛型,但实际上是可以编译通过的,因为泛型只对对象引用起限制作用,而与构造器无关,如果是ArrayList的引用,构造器用ArrayList<String>()也不能限制只能存入String

public static void main(String[] args){
        Set set = new TreeSet();
        set.add("abc");
        varagMethod(set);

        Iterator<String> iter = set.iterator();
        while (iter.hasNext())
        {
            String str = iter.next();   // ClassCastException
            System.out.println(str);
        }
    }
    public static void varagMethod(Set<Integer> objects) {
        objects.add(new Integer(10));
    }

以上代码也能编译通过,但运行时报错(ClassCastException);当一个可变泛型参数指向一个无泛型参数,或者一个方法既使用泛型的时候也使用可变参数(如Arrays.asList(T… a))时,堆污染(Heap Pollution)就有可能发生

20.枚举

enum MyEnum{ A,B,C,D; }
enum MyEnum{
    A,B,C,D;
    private int id;
    public void setId(int id){
        this.id=id;
    }
    public int getId(){
        return this.id;
    }
}
enum MyEnum{
    A{ public void show(){ System.out.println("A"); }},
    B{ public void show(){ System.out.println("B"); }};
}
enum MyEnum{
    A(1000,"A"),B(1001,"B"),C(1002,"C"),D(1003,"D");
    private int id;
    private String name;
    private MyEnum(int i,String name){
        this.id = id;
        this.name = name;
    }
}

根据枚举名获取枚举对象:

MyEnum A=MyEnum.valueof("A");

要注意的是,枚举的声明要放在enum开始的地方,且枚举之间用,隔开,每个枚举都默认是public static final修饰的,不用显式声明

21.多线程、同步代码块、同步方法,锁

创建线程

创建线程:

class MyThread1 extends Thread{
    public static int i=0;
    public void run(){
        //code
    }
}

class MyThread2 implements Runnable{
    public int i=0;
    public void run(){
        //code
    }
}

以上两种方式创建的区别在于

//继承Thread的话如果要共享数据,则要声明数据为static
MyThread1 mt1 = new MyThread1();
mt1.start();

//实现Runnable的话如果要共享数据,则往Thread构造函数里传入同一个实例就可以了
MyThread mt2 = new MyThread2();
Thread t1 = new Thread(mt2);//t1、t2用同一个i
Thread t2 = new Thread(mt2);
t1.start();
t2.start();

//如果新建一个MyThread2的话,t3与t1、t2不共享一个i
Thread t3 = new Thread(new MyThread2());

如果需要得到线程的返回值,可以使用Callable创建线程

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int result = 0;
        for (int i = 0; i <= 100; i++)
            result += i;
        return result;
    }
};
FutureTask<Integer> result = new FutureTask<>(callable);
new Thread(result).start();
//get方法会阻塞至线程执行完成,FutureTask实现了Future接口,实际上是用了Future模式
System.out.println(result.get());

线程的状态

  1. 初始(NEW): 新创建了一个线程对象,但还没有调用start()方法
  2. 运行(RUNNABLE): Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为”运行” 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)
  3. 阻塞(BLOCKED): 进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
  4. 等待(WAITING): 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断),处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态
  5. 超时等待(TIMED_WAITING): 该状态不同于WAITING,它可以在指定的时间后自行返回
  6. 终止(TERMINATED): 表示该线程已经执行完毕

创建进程

调用cmd或者shell

Process process=Runtime.getRuntime().exec("mkdir aaa");
process.waitFor();

JDK5以后可以使用ProcessBuilder创建,JDK9以后可以使用ProcessHandle来获取进程的信息

final ProcessBuilder processBuilder = new ProcessBuilder("ls")
        .inheritIO();
final ProcessHandle processHandle = processBuilder.start().toHandle();

processHandle.onExit().whenCompleteAsync((handle, throwable) -> {
    if (throwable == null) {
        System.out.println(handle.pid());
    } else {
        throwable.printStackTrace();
    }
});

//通过ProcessHandle.info()可以获得进程的启动时间、执行时间等信息
ZonedDateTime startTime = processHandle.info()
        .startInstant()
        .orElse(Instant.now())
        .atZone(ZoneId.systemDefault());

Duration duration = processHandle.info()
        .totalCpuDuration()
        .orElse(Duration.ZERO);

同步代码块、同步方法

同步代码块、同步方法是为了解决多线程中数据共享的问题

class Test {
    public int count = 100;

    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (test.count > 0) {
                            //暂停1毫秒,增加出现并发问题的几率
                            try {
                                Thread.sleep(1);
                            }catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            test.count--;
                        }
                        else
                            break;
                    }
                }
            }).start();

        }
        try {
            //主程序暂停3秒,以保证上面的程序执行完成
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count=" + test.count);
    }
}

这时,输出了-1,多次运行甚至有时会输出-2,这是由于当count为1时,在if (test.count > 0)后进行了耗时操作,而这时又刚好有另一个线程抢占了执行权,由于还没有执行test.count--,此时线程仍然可以进入导致多次执行test.count--导致的,这就要用同步代码块或同步方法解决了

同步代码块或同步方法限制使用同一个锁的代码块或方法在同一时间只有一个线程能调用对应的代码(可以是不同方法使用同一个锁,这时另一线程就要等前面获取锁了的线程把方法执行完、释放锁后,另一线程才能调用使用了该锁的其他方法,保证了方法之间不能并发执行,如读写数据时,要等写完才可以读,否则会出现读出来的数据不完整的情况,但实际情况下会使用读写分离(写时复制一份数据,写完再把指针指向写好的数据)来提高效率)

//obj1称为同步监视器(又叫监视器锁,monitor),任何对象都可以用作同步监视器
Object obj1 = new Object();
public void method1(){
    synchronized(obj1){
        //code
    }
}

//同步方法默认用this作同步监视器
public synchronized void method(){
    //code
}

当所有线程用同一个同步监视器时,synchronized才有效,implements Runnable实现多线程时,锁一般声明为全局的,extends Thread实现多线程时,锁一般声明为全局、static的, 更一般的,如果只有一个同步代码块或同步方法时,非静态方法用this做同步监视器,同步代码块可以指定自定义同步监视器,静态方法用class做同步监视器(如:synchronized(MyThread2.class){}

使用同一个锁的同步方法互调是没问题的

synchronized void method1(){
    System.out.println("method1");
    method2();
}

synchronized void method2(){
    System.out.println("method2");
}

public static void main(String[] args){
    Test t = new Test();
    t.method1();
}

子类调父类的同步方法也是没问题的,此时父类和子类用的是同一个锁

class Sup {
    public synchronized void method1() {  }
}

class Sub extends Sup {
    public synchronized void method2() {
        super.method1();
    }

    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.method2();
    }
}

像出现异常等情况,用synchronized加的锁会自动释放,但java.util.concurrent.locks包下的锁要手动释放

使用对象做锁可以使用notifywait等方法,但notify不会释放锁,而wait会释放锁并让线程阻塞

要注意的是,使用notifynotifyAllwait等方法时,应尽量在while循环中使用,否则可能会导致虚假唤醒(Spurious wakeup)

while(条件不满足){
   obj.wait();
}
而不是:
If( 条件不满足 ){
   obj.wait();
}

LockSupport

像以往的waitnotify等方法必须写在synchronized中,而且必须先wait,再调用notify才能结束等待,但实际情况中,我们无法确定哪个线程先执行完成,比如下面的程序:

Object obj = new Object();
Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            //do something...
            Thread.sleep(new Random().nextInt(500));
            synchronized (obj) {
                obj.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            //do something...
            Thread.sleep(new Random().nextInt(500));
            synchronized (obj) {
                obj.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
t1.start();
t2.start();

多运行几次上边的代码,有的时候线程能够正常退出,但有的时候线程却阻塞住了。原因就在于:t2线程调用完notify后,t1线程才进入wait方法,导致t1线程一直阻塞住。由于t1线程不是后台线程,所以整个程序无法退出

LockSupport可以不在synchronized中执行,而且可以先unpark(相当于notify)再park(相当于wait)

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t2 end sleep");
        //LockSupport.park(Object)
        LockSupport.park(this);
        System.out.println("I am alive");
    }
});

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(50L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //LockSupport.unpark(Thread)
        LockSupport.unpark(t2);
        System.out.println("I am done");
    }
});
t1.start();
t2.start();

要注意的是,LockSupport要求unpark的线程已经启动,如果LockSupport.unpark(t2)的时候t2线程还没有启动,那么unpark是无效的

waitnotifyLockSupport相比,waitnotify不需要线程之间相互感知(能获取对方的引用),只需共享一个锁对象即可,而LockSupport需要获取另一线程的引用才能unpark,但是这样的好处在于LockSupport可以唤醒指定的线程。它们各有优势,两者间不能完全取代彼此

LockSupportwaitnotify一样,存在虚假唤醒的情况,也应尽量在while循环中使用

锁的几个概念

锁的性质

内存可见性

如果一个线程修改了一个变量,另一个线程有一定几率是不知道的:

class Test {
    public static int count = 0;

    void method1() {
        while (true) {
            if (count > 5) break;
        }
    }

    void method2() {
        for (int i = 0; i < 10; i++) {
            count++;
        }
        System.out.println("count = " + count);
    }

    public static void main(String[] args) {
        Test t = new Test();
        new Thread(() -> t.method1()).start();
        new Thread(() -> t.method2()).start();
    }
}

一个线程用于修改count,count最后一定大于5,但另一个线程是有几率无法感知(内存不可见)的,这就导致了调用method2的线程有几率不会结束

要保证变量的内存可见性需要使用volatile关键字

如果count使用volatile修饰,则调用method2的线程一定能正常退出:

public static volatile int count = 0;

原子性

volatile能保证内存可见性,但它并不具有原子性:

class Test {
    public static volatile int count = 0;

    public static void main(String[] args) {
        for (int i=0;i<10;i++){
            new Thread(() -> {
                for (int i1 = 0; i1 <100; i1++)
                    Test.count++;
                System.out.println(Test.count);
            }).start();
        }
    }
}

多运行几次会得到不一样的结果

100
200
300
400
501
593
793
693
893
993

上面的中间的返回值不需要看(可能在输出前,其他线程抢到了优先级,先更改了count,导致输出不一定准确),关注最后的值,最后的值不是1000,这是因为volatile没有原子性,不能保证每次只有一个线程能更改count的值,当count在高速缓存中更改,但还没有同步进主内存时,另一个线程更改了它的值,导致自增多次但只有一次生效

原子类型(CAS)

此时可以使用原子类型解决(基本数据类型都有对应的原子类型,其他的对象可以用AtomicReference):

public static volatile AtomicInteger count = new AtomicInteger(0);

原子类型就是一种乐观的锁,使用CAS(compare and swap)实现,它假设当前线程操作变量时没有其他线程干扰,更改变量时先缓存变量,然后对缓存的变量进行操作,得到预期值,然后设置真实值之前和之后都校验一次以确保正确修改,如果校验失败说明有其他线程干预,会再次尝试修改,直至成功为止

//将value声明为volatile的,确保内存可见性
private volatile int value;

//自增的实现原理
public final int getAndIncrement() {
    for (;;) {
        // 当前值
        int current = get();
        // 预期值
        int next = current + 1;
        if (compareAndSet(current, next)) {
            // 如果加成功了, 则返回当前值,此时实际的value已经加1
            return current;
        }
    }
}

public final boolean compareAndSet(int expect, int update) {
    // 调用native方法修改变量的值,该方法是基于CPU的CAS指令来实现, 可以认为无阻塞.
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

但是上面的设计会有ABA的问题:如果线程一准备用CAS将变量的值由A替换为B, 在此之前线程二将变量的值由A替换为C, 线程三又将C替换为A, 然后线程一执行CAS时发现变量的值仍然为A, 所以线程一CAS成功。这种情况下线程一是无法感知的

AtomicStampedReference解决了ABA问题,其原理就是每次对变量进行修改时,都把stamp加1,也就是记录修改的次数,校验时除了校验value是否修改成功,还要校验stamp以确保没有其他线程修改过

补充:

volatile也会在高速缓存中对变量进行操作:

class Test {
    volatile int count = 0;

    public static void main(String[] args) {
        Test t = new Test();
        t.count = t.count++;
        System.out.println(t.count);//输出0
    }
}

ThreadLocal

如果希望每个线程都有变量的副本,对变量副本的操作不影响其他线程,可以用ThreadLocal

ThreadLocal<String> localStr = new ThreadLocal<>() {
    @Override
    protected String initialValue() {
        return "Hello";
    }
};

new Thread(new Runnable() {
    @Override
    public void run() {
        localStr.set("world");
        System.out.println(Thread.currentThread().getName() + "--" + localStr.get());
    }
}).start();

//确保Thread1运行时Thread0已修改localStr
Thread.sleep(500);

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--" + localStr.get());
    }
}).start();

输出

Thread-0--world
Thread-1--Hello

此时Thread-0对localStr的修改并不会影响Thread-1

注意ThreadLocal的remove方法,它可以清空ThreadLocal的值,但是如果之后还调了get方法,就会重新根据初始化方法进行初始化

private static final ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 6);

public static void main(String[] args) {
    System.out.println(num.get());//6
    num.set(num.get() + 1);
    System.out.println(num.get());//7
    num.remove();
    System.out.println(num.get());//6
}

ThreadLocal的内存泄露问题

ThreadLocal底层是通过键值对ThreadLocalMap.Entry<ThreadLocal<?>, Object>的形式存储的(线程私有),而键值对中的key是弱引用、value是强引用,所以key有可能在内存回收的时候被回收掉,但是value却不能被回收(只有当前线程结束后,没有对map的强引用时,才能回收),导致了内存泄露

Java为了最小化减少内存泄露的可能性和影响,在使用ThreadLocal的get、set、remove的时候都会清除线程Map里所有key为null的value。但如果使用线程池,那么线程不会被销毁,而且get、set、remove也不会被使用时,又或者使用了static修饰ThreadLocal,延长了其生命周期,这期间就会发生真正的内存泄露

要避免产生内存泄露,我们需要在每次用完ThreadLocal后手动调用其remove方法,清除数据

为什么使用弱引用

考虑使用强引用的情况:

  1. 如果某个类的内部使用了ThreadLocal,由于ThreadLocalMap存放在线程中,而线程还没有被销毁,当该类的实例被回收时,由于ThreadLocalMap对其内部使用的ThreadLocal存在强引用,所以ThreadLocal无法被回收,就会造成内存泄露
  2. 如果错误使用TreadLocal,比如把ThreadLocal定义为局部变量,当方法执行完后,由于ThreadLocalMap仍持有其强引用,也会造成内存泄露

使用弱引用就不存在上述问题

互斥锁与单例模式

synchronized效率比较低,如果要提高效率一般用互斥锁(双重判空)(以单例模式的懒汉式为例):

class Singleton{
    //单例模式,私有化构造函数
    private Singleton(){}
    private volatile static Singleton singleton = null;//如果不声明为volatile ,会导致singleton分配了内存空间,但还没被初始化时(这时已经不为null),其他线程进入,获取了没有完成初始化的对象

    public static Singleton geInstance(){

        if(singleton != null)
            return singleton;
        synchronized(Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
                //code
            }
            else{
                return singleton;
            }
        }
    }
}

分析: 线程A进入,singleton为null进入锁内,线程B进入singleton为null,但线程A已获取锁,所以线程B阻塞,线程A在锁内,此时singleton仍为null,线程A初始化singleton,然后释放锁,线程B进入此时singleton已经不为null了,直接获取singleton然后返回

lock

如果不需要等待返回,可以使用java.util.concurrent.locks下的一些锁,作用和synchronized相同(wait、notify、notifyAll只能在synchronized下使用,如果用concurrent包下的锁,则要lock.newCondition()取得condition,再使用condition对应的await、signal、signalAll),但concurrent包下的锁引入了condition的概念,使得我们可以根据条件唤醒需要的线程,而不像synchronized那样,要么随机唤醒一个线程,要么唤醒全部线程

class MyClass{
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        if(lock.tryLock()) {//如果获取锁失败,就做其他事情
            try {
               // code
            }
            finally {
               lock.unlock()
            }
        }else{
            //code
        }
    }
}

利用类的加载特性实现单例

互斥锁能保证线程安全,但写起来比较复杂,我们一般使用静态内部类实现单例(外部类加载时并不需要立即加载内部类,静态内部类只有在第一次被使用的时候才会被加载,而类的加载是线程安全的)

class SingleTon {
    // 利用静态内部类特性实现外部类的单例
    private static class SingleTonBuilder {
        private static SingleTon singleTon = new SingleTon();
    }

    // 私有化构造函数
    private SingleTon() {  }

    public static SingleTon getInstance() {
        return SingleTonBuilder.singleTon;
    }
}

更简单的写法是用枚举实现单例(枚举实例创建是线程安全的),但是不常用

public enum SingleTon{
    INSTANCE;
    public void method(){  }
}

死锁

死锁一般出现在有多个锁且交叉使用的情况:

public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args){
    new Thread((new Runnable(){
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);
                        synchronized(obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }).start();

    new Thread((new Runnable(){
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(obj2){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);
                        synchronized(obj1){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }).start();
}

分析:当第一个线程运行到sleep的时候暂停3秒,此时obj1被锁定,由于第一个线程暂停,第二个线程有足够时间运行到sleep,此时obj2被锁定;当两个线程都暂停3秒后继续运行,此时线程1要obj2才能向下运行,从而释放obj1,但obj2被线程2锁定,而线程2又要obj1才能向下运行,从而释放obj2,这就造成了死锁

我们可以通过JConsole等工具对代码进行分析,及时发现死锁

可重入锁

在不可重入锁中,一个线程访问一个加锁方法会获取锁,而当这个线程再次访问该方法时(如递归调用),会再次尝试获取锁,这时会造成死锁

在可重入锁中,如果一个线程访问一个同步方法会获取锁,而且只要它没有释放锁,那么当它再次访问该方法时,是允许再次进入的(所以可以递归调用加锁的方法),synchronized是可重入锁,java.util.concurrent.locks包下也有很多可重入锁,如ReentrantLock

下面是它的实现原理:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    //当同一个线程再次访问时,允许线程进入,且lockedCount++,当lockedCount为0时代表当前线程释放锁,会notify其他线程
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        //这里使用自旋锁
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                lockedBy = null;
                notify();
            }
        }
    }
}

这里使用了自旋锁(采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区)

公平锁与非公平锁(Condition)

像上面的例子中,由于线程的执行是抢占式的,调用notify后,各个线程会抢占锁,这样就是非公平的,而如果按照先后顺序,先运行的线程先获取锁,后面运行的线程在队列中等待,这样就是公平的

ReenTrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,除此以外,ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程

下面是三条线程交替输出A、B、C的例子

public class Main {
    private static volatile int condition = 1;
    private static Lock lock = new ReentrantLock();
    private static Condition lockCondition1 = lock.newCondition();
    private static Condition lockCondition2 = lock.newCondition();
    private static Condition lockCondition3 = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lock.lock();
                    try {
                        while (!(condition == 1)) {
                            lockCondition1.await();
                        }
                        System.out.println(Thread.currentThread().getName());
                        condition = 2;
                        lockCondition2.signal();
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lock.lock();
                    try {
                        while (!(condition == 2)) {
                            lockCondition2.await();
                        }
                        System.out.println(Thread.currentThread().getName());
                        condition = 3;
                        lockCondition3.signal();
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lock.lock();
                    try {
                        while (!(condition == 3)) {
                            lockCondition3.await();
                        }
                        System.out.println(Thread.currentThread().getName());
                        condition = 1;
                        lockCondition1.signal();
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }, "C").start();
    }
}

重量级锁、轻量级锁、偏向锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)

锁在内存中的标志位(32位JDK)

锁状态 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向锁 锁标志位
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 11
偏向锁 线程ID Epoch 对象分代年龄 1 01
无锁 对象的hashCode 对象分代年龄 0 01

重量级锁

class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

当我们使用javap -c来反编译上面代码时,得到的JAVA字节码如下

class SynchronizedDemo {
  SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void method();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String Method 1 start
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}

注意到monitorentermonitorexit两条指令,Synchronized代码块就是通过这两条指定实现的(执行monitorexit的线程必须是objectref所对应的monitor的所有者)

再看看同步方法

class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

使用javap -v反编译,可以看到常量池中的内容

 public synchronized void method();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8

可以看到,synchronized方法没有使用monitor(监视器锁),而是在常量池中加入了ACC_SYNCHRONIZED字段,JVM就是根据该标示符来实现方法的同步的,但这其实只是交由JVM帮我们实现了对monitor的检测而已,内部也是通过monitor实现的

通过monitor(监视器锁)实现的锁称为重量级锁,因为监视器锁是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么synchronized效率低的原因

轻量级锁

JDK6以后,引入了轻量级锁和偏向锁以减少获得锁和释放锁的开销

加锁过程

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

偏向锁

大多数情况下,锁不存在多线程竞争,而是总是由同一线程多次获得时,为了使线程获得锁的代价更低而引入了偏向锁

加锁过程

  1. 如果为可偏向状态,测试对象头Mark Word(默认存储对象的HashCode,分代年龄,锁标记位)里是否存储着指向当前线程的偏向锁

  2. 若测试失败,则测试Mark Word中偏向锁标识是否设置成1(表示当前为偏向锁)

  3. 没有设置则使用CAS竞争,否则尝试使用CAS将对象头的偏向锁指向当前线程

几种锁的区别

重量级锁

优点:线程竞争不使用自旋,不会消耗CPU

缺点:线程阻塞,响应时间缓慢

适用场景:追求吞吐量。同步块执行速度较慢。

轻量级锁

优点:竞争的线程不会阻塞,提高了程序的响应速度

缺点:如果始终得不到锁竞争的线程使用自旋会消耗CPU

适用场景:追求响应时间。同步块执行速度非常快

偏向锁

优点:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距

缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用场景:适用于只有一个线程访问同步块场景

阻塞队列

和普通队列不同的是,取出/加入元素时如果达到数量下限(0)/数量上限(CAPACITY)时会阻塞并等待加入/取出,下面是其实现原理

class BlockingQueue<T> {
    //max size of this queue
    final int CAPACITY;
    LinkedList<T> list = new LinkedList<>();
    Object lock = new Object();

    public BlockingQueue(int capacity) {
        this.CAPACITY = capacity;
    }

    //队列中元素小于0时,阻塞并等待添加数据
    public T take() {
        synchronized (lock) {
            if (list.size() <= 0) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //取出后从队列中删除该元素
            T element = list.poll();
            System.out.println("take "+element);
            lock.notify();
            return element;
        }
    }

    //队列元素大于CAPACITY时,阻塞并等待取出元素
    boolean put(T element) {
        synchronized (lock) {
            if (list.size() > CAPACITY) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.offer(element);
            System.out.println("put "+element);
            lock.notify();
            return true;
        }
    }
}

JDK给我们提供了一些常用的阻塞队列

高性能队列

与阻塞队列相对应的,有高性能队列ConcurrentLinkedQueue、高性能哈希表ConcurrentHashMap,所谓高性能就是不加锁或局部加锁

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它不允许存入null元素。其内部通过使用transientvolatile关键字来提高效率并保证线程安全,而以前的LinkedList通过Collections.synchronizedList转成线程安全后,效率是会下降的,其原因在于synchronizedList函数是将LinkedList的每个方法加锁来保证线程安全的,而加锁会导致效率下降

ConcurrentHashMap是将以往的HashMap分成16段(segment),每次读写是只需锁定需要进行读写操作的那一个segment,其他segment仍然可以并行的读写,这样就提高了并发情况下的效率(JDK8以后,把每个segment改为transient volatile HashEntry<K,V>[] table,table底层使用了红黑树进行优化)

CopyOnWrite

CopyOnWrite(COW)即写时复制的容器,它实现了读写分离:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样的话,当我们需要对数据进行大量读取、少量写入时,CopyOnWrite就在可以并发读取的同时也能保证线程安全。但是当数据量很大又或者有大量写入操作时,CopyOnWrite的效率就会很低。

JAVA给我们提供了CopyOnWrite容器,如CopyOnWriteArrayListCopyOnWriteArraySet

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer(AQS)为java.util.concurrent包下的一些类提供了一套通用的方法,它维护了一个volatile int state(状态值),一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列),并定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。我们自定义同步类容器只需继承它并重写对应方法即可(线程竞争入队列等待的逻辑已经帮我们实现好,我们只需维护state和获取、释放资源即可),如CountDownLatch的源码中的内部类

private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }

    //以共享方式尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    //共享方式。尝试释放资源,成功则返回true,失败则返回false
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c - 1;
            //通过CAS来更新state
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

共享方式访问需重写tryAcquireSharedtryReleaseShared即可

独占方式访问需重写

如果需要使用到Condition,还需重写

CountDownLatch

CountDownLatch又叫闭锁,它可以等待其他线程执行完(或执行到某个状态)再继续执行某一线程。它类似计数器,创建的时候指定总的线程数,线程执行完成后使用countDown函数使计数器减1,当计数器为0时,结束await

比如计算所有线程的执行时间,需阻塞至所有线程执行完成

public class Main {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        MyRunnable runnable = new MyRunnable(countDownLatch);

        Instant start = Instant.now();
        for (int i = 0; i < 5; i++) {
            new Thread(runnable).start();
        }
        //阻塞至所有线程执行完成
        countDownLatch.await();

        Instant end = Instant.now();
        System.out.println(Duration.between(start, end).toMillis());
    }
}

class MyRunnable implements Runnable {
    CountDownLatch countDownLatch;

    MyRunnable(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        int count = 0;
        int sum = 0;
        while (count <= 10000) {
            count++;
            sum += count;
        }
        System.out.println(sum);
        countDownLatch.countDown();
    }
}

CyclicBarrier

CountDownLatch相反,CyclicBarrier可以让一组线程等待至某个状态之后再全部同时执行

public class Main {
    public static void main(String[] args) throws Exception {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            new Thread(new MyRunnable(cyclicBarrier)).start();
            Thread.sleep(1000);
        }
    }
}

class MyRunnable implements Runnable {
    CyclicBarrier cyclicBarrier;

    MyRunnable(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        System.out.println("正在等待其他线程启动完成....");
        try {
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("所有线程均启动完成,开始执行任务");
        //...
    }
}

Semaphore

Semaphore,信号量,它可以控制同时访问的最大线程个数

public class Main {
    public static void main(String[] args) throws Exception {
        Semaphore semaphore = new Semaphore(5);
        for (int i=0;i<8;i++){
            new Thread(new MyRunnable(semaphore)).start();
        }
    }
}

class MyRunnable implements Runnable {
    Semaphore semaphore;

    MyRunnable(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"在使用");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            semaphore.release();
        }
    }
}

ReadWriteLock

多线程时,如果读取数据的同时如果还在写入,会导致脏读,因此读和写是互斥的;但如果是多个线程同时读取,没有写入操作,是完全没有问题的,读操作之间并不互斥,JAVA给我们提供了ReetrantReadWriteLock(基于AQS,AbstractQueuedSynchronizer)实现这种逻辑

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

volatile int count = 0;

public int getCount() {
    lock.readLock().lock();
    int tmp;
    try {
        tmp = count;
    } finally {
        //锁的释放最好放在finally中
        lock.readLock().unlock();
    }
    return tmp;
}

public void setCount(int count) {
    lock.writeLock().lock();
    try {
        this.count = count;
    } finally {
        lock.writeLock().unlock();
    }
}

ForkJoinPool

有时候我们希望把一个大任务拆分成许多的小任务,分别交由多个线程去执行,最后再把结果整合起来,这时就需要用到ForkJoinPool

public class Main {
    public static void main(String[] args){
        CalculateTask task = new CalculateTask(1, 1000000000);

        ForkJoinPool pool = new ForkJoinPool();
        Long sum = pool.invoke(task);
        System.out.println(sum);
        pool.shutdown();
    }
}

//RecursiveTask和RecursiveAction是一样的,只不过一个有返回值,一个没有返回值
class CalculateTask extends RecursiveTask<Long> {
    final long THRESHOLD = 10000;
    long start;
    long end;

    CalculateTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    //使用fork的时候,会自动调用compute函数
    @Override
    protected Long compute() {
        long sum = 0;
        long block = end - start;
        if (block > THRESHOLD) {
            //如果计算量大于阈值,则拆分成两个小任务
            long middle = (start + end) / 2;
            CalculateTask left = new CalculateTask(start, middle);
            CalculateTask right = new CalculateTask(middle + 1, end);
            //fork就是拆分的意思
            left.fork();
            right.fork();
            //整合结果
            return left.join() + right.join();
        } else {
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }
    }
}

Executors

JAVA给我们提供了Executors工厂类来创建一些常用的线程池,它有如下方法

如果上述线程池都无法满足需求,我们也可以通过指定ThreadPoolExecutor的参数来自定义线程池,其构造函数如下

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);//后两个参数为可选参数

参数说明

参数调优

定义如下参数

threadcount = tasks/(1/taskcost) = tasks*taskcost

假设每秒要执行5001000个任务,平均每个任务要执行0.1秒,那么threadcount为50100,所以corePoolSize要大于50,如果80%的情况下每秒任务数小于800,那么corePoolSize就设置为80

queueCapacity = (coreSizePool/taskcost)*responsetime

计算可得 queueCapacity = 80/0.1*1 = 80,意思是队列里的线程可以等待1s,超过了的需要新开线程来执行

maxPoolSize = (max(tasks) - queueCapacity)/(1/taskcost)

计算可得 maxPoolSize = (1000-80)/10 = 92

Future模式

当客户端请求数据时,先返回一个空的数据(wrapper),这时服务器端新建一个线程去获取真正的数据,当数据获取完了之后填入wrapper中,这就要求wrapper和真实数据的类型都实现或继承相同的类或接口,且在用到数据的地方都要先判断真实数据是否为空,如果为空,则要阻塞线程,等待真实数据填入。

Master-Worker模式

master负责接收客户端的请求并把任务分发给worker,worker负责真正的处理任务。它能将大任务分解成若干个小任务,并发执行,提高系统性能,并且实现了调用和执行的解耦

22.BIO、NIO、AIO

BIO

传统的IO操作、socket都是基于BIO(Blocking IO)实现的

UDP

//服务器端
new Thread(new Runnable() {
    @Override
    public void run() {
        DatagramSocket udpSocket = null;
        try {
            udpSocket = new DatagramSocket(new InetSocketAddress(8080));

            DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
            udpSocket.receive(receive);
            System.out.println("Server: "+new String(receive.getData(),0,receive.getLength()));

            byte[] data = "Hello client".getBytes();
            udpSocket.send(new DatagramPacket(data, data.length,receive.getAddress(),receive.getPort()));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            udpSocket.close();
        }
    }
}).start();

//客户端
new Thread(new Runnable() {
    @Override
    public void run() {
        DatagramSocket udpSocket = null;
        try {
            udpSocket = new DatagramSocket();

            byte[] data = "Hello server".getBytes();
            udpSocket.send(new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 8080));

            DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
            udpSocket.receive(receive);
            System.out.println("Client: "+new String(receive.getData(),0,receive.getLength()));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            udpSocket.close();
        }
    }
}).start();

TCP

//服务器端
new Thread(new Runnable() {
    @Override
    public void run() {
        ServerSocket serverSocket = null;//1024-65535的某个端口
        try {
            serverSocket = new ServerSocket(8080);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(0);
        }
        Socket socket = null;
        InputStream is = null;
        InputStreamReader isr = null;
        BufferedReader br = null;
        OutputStream os = null;
        while (true) {
            try {
                socket = serverSocket.accept();
                //读取客户端信息
                is = socket.getInputStream();
                isr = new InputStreamReader(is);
                br = new BufferedReader(isr);
                String data;
                while ((data = br.readLine()) != null)
                    System.out.println("收到客户端请求:" + data);
                socket.shutdownInput();//关闭输入流

                //响应客户端
                os = socket.getOutputStream();
                os.write("Hello client".getBytes());
                os.flush();
                socket.shutdownOutput();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    br.close();
                    isr.close();
                    is.close();
                    os.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}).start();

//客户端
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            Socket clientSocket = null;
            OutputStream os = null;
            InputStream is = null;
            InputStreamReader isr = null;
            BufferedReader br = null;
            try {
                clientSocket = new Socket(InetAddress.getByName("127.0.0.1"), 8080);
                //向服务器端发数据
                os = clientSocket.getOutputStream();
                os.write("Hello Server".getBytes());
                os.flush();
                clientSocket.shutdownOutput();

                //读取服务器端信息
                is = clientSocket.getInputStream();
                isr = new InputStreamReader(is);
                br = new BufferedReader(isr);
                String data;
                while ((data = br.readLine()) != null)
                    System.out.println("收到服务器端响应:" + data);
                clientSocket.shutdownInput();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    os.close();
                    is.close();
                    isr.close();
                    br.close();
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}).start();

这样每一次请求都会阻塞服务器,如果在客户端请求时服务器端在处理另一客户端的数据,就会导致连接失败,好一点的设计是为每个请求新开线程处理

while(true){
    Socket socket = serverSocket.accept();
    new Thread(new MyRunnable(socket)).start();
}

class MyRunnable extends Runnable{
    Socket socket;
    public MyRunnable(Socket socket){
        this.socket = socket;
    }
    public void run(){
        //处理socket
    }
}

这样做有如下缺点

NIO的Selector可以有效的解决线程利用率低的问题

NIO

NIO全称为Non-Blocking IO,即(同步)非阻塞IO,它是基于Reactor模式的

以前的IO操作是由CPU直接负责的,这样的缺点是当有大量的IO操作时,CPU无法进行其他操作,性能损耗太大

后来改为使用DMADirect Memory Access)复制IO操作,当应用使用操作系统的IO接口的时候,由DMA向CPU申请权限,获得权限后,IO操作由DMA全权负责,但是如果有大量的IO请求,还是会造成DMA的走线过多,也会影响性能

再后来改为使用ChannelChannel为完全独立的单元,不需要向CPU申请权限,专门用于IO

Buffer

Channel只是用于控制IO请求,无法直接操作文件,要操作文件,需要使用Buffer

基本数据类型都有对应的Buffer,比如ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer

传统的IO操作是调用操作系统的IO接口,让操作系统获取数据,然后把数据复制到系统内存中,然后JVM把操作系统内存中的数据复制到JVM的内存中,这样做是出于安全的考虑,但是两次的复制会导致不必要的开销,于是在不需要考虑安全问题的情况下,我们可以使用Buffer直接在操作系统内存中创建空间(这时我们无法手动释放资源,这块内存空间必须由GC回收,而GC的时间是不确定的,可能会导致程序无法及时退出)

//在JVM内存中开辟空间(非直接缓冲区)
ByteBuffer buffer1 = ByteBuffer.allocate(1024);

//在操作系统内存中开辟空间(直接缓冲区)
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);

Buffer的几个概念

IntBuffer buffer = IntBuffer.allocate(10);

buffer.put(1);
buffer.put(2);
buffer.put(3);
//把第0号元素改为4,此时不会影响position的值
buffer.put(0,4);
//如果index所在的位置没有被不通过index的方式put过,put之后,该位置仍然被看作是无效的
buffer.put(5,90);
System.out.println(buffer);//java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]

buffer.mark();

//filp后,position归0,limit为有效元素的个数,capacity不变,mark重新置为-1
buffer.flip();
System.out.println(buffer);//java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]

//get、set也可以直接操作数组,此时position也会增加对应长度
int[] tmp = new int[buffer.remaining()];
buffer.get(tmp);
System.out.println(buffer);//java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]

//清空Buffer
buffer.clear();

我们可以通过CharsetByteBufferCharBuffer相互转换

Charset charset = Charset.forName("UTF-8");

CharBuffer origin = CharBuffer.allocate(20);
origin.put("Hello world");
origin.flip();

//CharBuffer -> ByteBuffer
ByteBuffer byteBuffer = charset.encode(origin);
String data = new String(byteBuffer.array(), 0, byteBuffer.limit(), "UTF-8");
System.out.println(data);

//ByteBuffer -> CharBuffer
//可以用charset.decode,也可以用charset.newDecoder
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(byteBuffer);
data = new String(charBuffer.array());
System.out.println(data);

Channel

和IO一样,有用于操作本地文件的Channel、也有分别对应TCP、UDP的ChannelFileChannelSocketChannelServerSocketChannelDatagramChannel

IO流一般都有getChannel方法来获取Channel

FileInputStream fis = new FileInputStream("1.txt");
FileOutputStream fos = new FileOutputStream("2.txt");

FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(1024);

//我们不需要处理length,因为buffer内部有limit属性
while(inChannel.read(buffer)!=-1){
    //buffer使用前一定要flip
    buffer.flip();
    outChannel.write(buffer);
    //用完buffer一定要clear,以备下次使用
    buffer.clear();
}

inChannel.close();
outChannel.close();
fis.close();
fos.close();

除此以外,还可以

//获取文件大小
long size = inChannel.size();

//指定position
long pos = inChannel.position();
inChannel.position(pos +123);

//截取文件的前1024个字节,后面的内容会被删掉
outChannel.truncate(4);

//出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法
outChannel.force(true);

//分散读取(Scatter)、聚集写入(Gather),即读到多个Buffer中,再从多个Buffer中写入
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(200);
ByteBuffer[] buffers = new ByteBuffer[]{buffer1, buffer2};
inChannel.read(buffers);
for (Buffer buffer : buffers)
    buffer.flip();
outChannel.write(buffers);

Channel也可以使用内存映射文件来提高效率,MappedByteBufferallocateDirect创建的Buffer是一样的,都是使用直接缓冲区

//通过FileChannel的静态方法获取Channel
//第二个参数为打开的方式,是可变参数,可以有多个打开方式
FileChannel inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

//注意这里的MapMode要和上面的OpenOption对应
MappedByteBuffer inBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());

byte[] tmp = new byte[inBuffer.remaining()];
inBuffer.get(tmp);
outBuffer.put(tmp);

inChannel.close();
outChannel.close();

还有一个更快捷的方式,也是使用了直接缓冲区

FileChannel inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

inChannel.transferTo(0,inChannel.size(),outChannel);

inChannel.close();
outChannel.close();

UDP的 Channel

public static void main(String[] args) throws Exception {
    //服务器端
    new Thread(new Runnable() {
        @Override
        public void run() {
            DatagramChannel channel = null;
            try {
                channel = DatagramChannel.open().bind(new InetSocketAddress(8080));

                ByteBuffer buf = ByteBuffer.allocate(1024);

                buf.clear();
                SocketAddress clientInfo = channel.receive(buf);
                printBuffer("receive from client: ",buf);

                buf.clear();
                buf.put("Hello client".getBytes());
                buf.flip();

                int bytesSent = channel.send(buf, clientInfo);
                System.out.println("Server: send " + bytesSent + " bytes");
            }catch (Exception e){e.printStackTrace();}
            finally {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    //保证服务器先启动
    Thread.sleep(500);

    //客户端
    new Thread(new Runnable() {
        @Override
        public void run() {
            DatagramChannel channel = null;
            try {
                channel = DatagramChannel.open();

                ByteBuffer buf = ByteBuffer.allocate(1024);

                //向服务器端发数据
                buf.clear();
                buf.put("Hello server".getBytes());
                buf.flip();
                int bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1", 8080));
                System.out.println("Client: send " + bytesSent + " bytes");

                //接收服务器的响应
                buf.clear();
                channel.receive(buf);
                printBuffer("receive from server: ",buf);
            }catch (Exception e){e.printStackTrace();}
            finally {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

static void printBuffer(String tag,ByteBuffer buffer) {
    buffer.flip();
    System.out.print(tag);
    while (buffer.hasRemaining())
        System.out.print((char) buffer.get());
    System.out.println();
    buffer.flip();
}

TCP的Channel和UDP差距不大(注意shutdownOutputshutdownInput),这里不在赘述

除此以外,还有Pipe负责多线程的通信

final Pipe pipe = Pipe.open();
new Thread(new Runnable() {
    @Override
    public void run() {
        Pipe.SinkChannel sink = pipe.sink();
        ByteBuffer buffer = ByteBuffer.allocate(48);
        buffer.put(new Date().toString().getBytes());
        buffer.flip();
        try {
            sink.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

new Thread(new Runnable() {
    @Override
    public void run() {
        Pipe.SourceChannel source = pipe.source();
        ByteBuffer buffer = ByteBuffer.allocate(48);
        try {
            source.read(buffer);
            buffer.flip();
            System.out.println(new String(buffer.array(),0,buffer.limit()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

值得注意的一点是,Channel可以指定为阻塞式还是非阻塞式的,在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null,所以我们要判断返回的channel是否为null,一般非阻塞式都会和Selector配合使用

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    if(socketChannel != null){
        //...
    }
}

Selector

NIO把客户端、服务器端的连接放在通道(Channel)中,通过多路复用器(Selector)轮询每个注册了的channel,当channel准备就绪时,就进行相应的处理,这样就不需要等待所有数据都准备好之后才能开始处理,而且一个线程可以通过selector同时处理多个channel,大大地提升了每个线程的利用率

//服务器端
new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            ServerSocketChannel serverChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
            //设置为非阻塞式
            serverChannel.configureBlocking(false);

            Selector selector = Selector.open();
            //把ServerSocketChannel注册到Selector上,并监听accept事件
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            //轮询
            while (true) {
                //selector.select(timeout)得到已注册并且准备就绪的Channel的个数,select函数是阻塞的
                if (selector.select(500) <= 0)
                    continue;
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    //处理完channel,channel需要再次准备才能就绪,需要把channel从就绪的列表中删除,由于是非阻塞式的,我们应该在一开始就remove
                    iterator.remove();

                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        //accept就绪,开始accept客户端
                        SocketChannel clientChannel = server.accept();
                        //非阻塞式时,accept有可能返回null
                        if (clientChannel != null) {
                            //客户端也要设置为非阻塞式
                            clientChannel.configureBlocking(false);
                            //把客户端注册到Selector上,并监听read事件
                            clientChannel.register(selector, SelectionKey.OP_READ);

                            //向客户端应答数据
                            byte[] data = "Hello client".getBytes();
                            ByteBuffer buffer = ByteBuffer.allocate(data.length);
                            buffer.put(data);
                            buffer.flip();
                            clientChannel.write(buffer);
                            clientChannel.shutdownOutput();
                        }
                    } else if (key.isReadable()) {
                        //接收客户端发过来的数据
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        while (clientChannel.read(buffer) != -1) {
                            buffer.flip();
                            System.out.println("Server: revecie from" + clientChannel.getLocalAddress() + ": " + new String(buffer.array(), 0, buffer.limit()));
                            buffer.clear();
                        }
                    } else if (key.isWritable()) {
                        //...
                    } else if (key.isConnectable()) {
                        //...
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

//确保服务器先启动
Thread.sleep(500);

for (int i = 0; i < 10; i++)
    //客户端
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                SocketChannel socketChannel = SocketChannel.open();
                socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
                socketChannel.configureBlocking(false);
                ByteBuffer buffer = ByteBuffer.allocate(20);
                buffer.put("Hello server".getBytes());
                buffer.flip();
                socketChannel.write(buffer);
                //告知服务器我们已经写完
                socketChannel.shutdownOutput();

                buffer.clear();
                while (socketChannel.read(buffer) != -1) {
                    buffer.flip();
                    //如果不是空数据
                    if (buffer.limit() > 0)
                    System.out.println("Client: revecie from" + socketChannel.getLocalAddress() + ": " + new String(buffer.array(), 0, buffer.limit()));
                    buffer.clear();
                }

                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();

NIO也有一些缺点

虽然1、2条缺点可以通过使用NIO的框架来解决,但是我们仍希望可以通过回调的方式异步的实现非阻塞IO

AIO

AIO全称为Asynchronous IO,即异步(非阻塞)IO,它的读和写都是异步的,可以同时写多个数据,它是基于Proactor模式的

NIO使用了select/poll的方式,将channel注册到selector上,通过轮询channel是否就绪,将就绪的channel返回并进行处理,这样当channel数量过多时,轮询的效率会很低

AIO使用epoll的方式,将channel注册到selector上,基于回调的方式(类似监听者模式),告知selector哪些channel已经就绪,然后将就绪的channel返回,这样效率高,但需要操作系统层面的支持

下面是一个使用AIO,客户端往服务器端发数据、服务器端接收数据的例子

客户端:

AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1",8888)).get();
ByteBuffer buffer = ByteBuffer.wrap("中文,你好".getBytes());
//这里使用了Future模式,返回的是一个空的数据(wrapper)
//这里也可以用回调的方式write(ByteBuffer,Object,CompletionHandler)
Future<Integer> future = channel.write(buffer);
//操作数据前需使用get方法阻塞至获取完真实数据,这里get方法返回的是写出的字节数
int count = future.get();
System.out.println("send" + count + "bytes");

服务器端:

final AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel
            .open()
            .bind(new InetSocketAddress("127.0.0.1",8080));
channel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    @Override
    public void completed(final AsynchronousSocketChannel client, Void attachment) {
        channel.accept(null, this);//我们没有使用while循环不断的accept,而是当接收到一个请求后,再准备下一次accept,因为这里的accept是异步的,不会阻塞线程

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result_num, ByteBuffer attachment) {
                //attachment就是客户端发过来的数据
                //Buffer需要手动重置position
                attachment.flip();
                CharBuffer charBuffer = CharBuffer.allocate(1024);
                CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
                decoder.decode(attachment,charBuffer,false);
                charBuffer.flip();
                String data = new String(charBuffer.array(),0, charBuffer.limit());
                System.out.println("read data:" + data);
                try{
                    client.close();
                }catch (Exception e){}
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println("read error");
            }
        });
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        System.out.println("accept error");
    }
});
//accept不会阻塞线程,想要让服务器不关闭,必须一直阻塞进程
while (true){
    Thread.sleep(Integer.MAX_VALUE);
}

这样selector就由系统帮我们完成,我们只需要处理客户端准备好了的数据即可

但是这样会有一个问题,当我们进行业务升级,需要暂停服务器,进行文件的替换,如果强制结束服务器进程,会导致有的业务没有处理完成,这时就需要使用ShutdownHook来平滑退出

Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
        //暂停接收请求
        //阻塞至当前业务处理完成
    }
});

使用kill pidSystem.exit()Ctrl+C、正常关机、程序正常退出都会调用ShutdownHook,但是如果使用kill -9 pid强制杀死进程不会调用ShutdownHook

23.反射和内省

ClassLoader

Java中有三个类加载器

每个类加载器都有一个父加载器,直至最顶层的类加载器:

class Test {
    public static void main(String[] args){
        ClassLoader classLoader1 = Test.class.getClassLoader();
        System.out.println(classLoader1.toString());
        //获取父加载器
        ClassLoader classLoader2 = classLoader1.getParent();
        System.out.println(classLoader2.toString());
        //返回null,最顶层的类加载器不可见,其实是Bootstrap ClassLoader
        ClassLoader classLoader3 = classLoader2.getParent();
        //System.out.println(classLoader3.toString());//java.lang.NullPointerException
    }
}

输出

jdk.internal.loader.ClassLoaders$AppClassLoader@5a2e4553
jdk.internal.loader.ClassLoaders$PlatformClassLoader@224aed64

双亲委托

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后由自身去查找这些对象

自定义类加载器(继承ClassLoader,重写findClass获取class文件的字节码,然后使用defineClass根据字节码创建class):

public class DiskClassLoader extends ClassLoader {

    private String mLibPath;

    public DiskClassLoader(String path) {
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(mLibPath,fileName);
        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    //获取要加载的class文件名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){ 
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

通过自定义类加载器,可以把加密的class文件解析出来

反射

先定义这样一个类

class Test {
    public String name;

    private Test(){
        
    }

    public Test(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private void reset() {
        name = "";
    }

    protected void showInfo() {
        System.out.println("name = " + name);
    }

    public static void print(String name) {
        System.out.println("name = " + name);
    }

    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                '}';
    }
}

获取、调用方法

//获取所有public方法,包括父类的public方法
Method[] methods1 = Test.class.getMethods();
for (Method m : methods1)
    System.out.println(m);

System.out.println();

//获取该类自己声明的方法,不受访问控制符影响,不包括父类的方法
Method[] methods2 = Test.class.getDeclaredMethods();
for (Method m : methods2)
    System.out.println(m);

System.out.println();

Test testObj = new Test("小明");

//这里getMethod和getDeclaredMethod的区别和上面是一样的
//根据名字获取并执行public方法
Method showInfo = Test.class.getDeclaredMethod("showInfo");
showInfo.invoke(testObj);

//根据名字获取private方法,执行前需设置访问权限
Method reset = Test.class.getDeclaredMethod("reset");
reset.setAccessible(true);
reset.invoke(testObj);
System.out.println(testObj);

//同样有int.class、double.class、Void.class等
//调用静态方法把obj设为null即可
Method print = Test.class.getMethod("print",String.class);
print.invoke(null,"小红");

获取、使用构造器

//获取所有的public构造器
Constructor<?>[] constructors1 = Test.class.getConstructors();
for (Constructor c : constructors1)
    System.out.println(c);

System.out.println();

//获取所有的构造器,不受访问控制符影响
Constructor<?>[] constructors2 = Test.class.getDeclaredConstructors();
for (Constructor c : constructors2)
    System.out.println(c);

System.out.println();

//使用非public的构造器newInstance时,必须设置访问权限
Constructor<Test> constructor = Test.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance());

获取、设置成员变量

//下面方法和之前的getMethod、getDeclaredMethod类似
Test.class.getFields();
Test.class.getDeclaredFields();

Test testObj = new Test("小明");

//操作非public变量的时候也要使用setAccessible设置访问权限
Field field = Test.class.getField("name");
System.out.println(field.get(testObj));
field.set(testObj,"小红");
System.out.println(testObj);

获取父类泛型

class Father<T>{}

class Child extends Father<String>{}

class TestReflact {
    public static void main(String[] args) throws Exception {
        Child child = new Child();
        ParameterizedType type = (ParameterizedType) child.getClass().getGenericSuperclass();
        System.out.println(type.getActualTypeArguments()[0].getTypeName());
    }
}

此时输出java.lang.String

如果是

class Father<T>{}

class Child<T> extends Father<T>{}

class TestReflact {
    public static void main(String[] args) throws Exception {
        Child<String> child = new Child<>();
        ParameterizedType type = (ParameterizedType) child.getClass().getGenericSuperclass();
        System.out.println(type.getActualTypeArguments()[0].getTypeName());
    }
}

会输出T,因为运行时设置的泛型会被擦除,是无法获取的

内省

内省是针对JavaBean的,只有类中有getXXX和setXXX,无论是否有XXX这个属性,都能获取/设置

Test test = new Test("小明");
//操作单个属性
PropertyDescriptor descriptor = new PropertyDescriptor("name", Test.class);

Method getName = descriptor.getReadMethod();//获取属性的getter方法
String name = (String) getName.invoke(test, null);
System.out.println(name);

Method setName = descriptor.getWriteMethod();//获取属性的setter方法
setName.invoke(test, "小红");
System.out.println(test.name);

//操作所有属性
BeanInfo info = Introspector.getBeanInfo(Test.class);
PropertyDescriptor[] pds = info.getPropertyDescriptors();
for (PropertyDescriptor p : pds) {
    System.out.println(p.getReadMethod().invoke(test, null));
}

动态代理

通过动态代理,可以实现对函数的Hook

JAVA通过的动态代理是基于接口的,要求要代理的类必须实现一个接口;我们也可以通过CGlib可以使用基于子类的动态代理

定义一个接口和一个实现该接口的类

interface MyInterface {
    void doSomething();
    void somethingElse(String arg);
}

class MyClass implements MyInterface {
    @Override
    public void doSomething() {
        System.out.println("doSomething");
    }

    @Override
    public void somethingElse(String arg) {
        System.out.println("somethingElse");
    }
}

定义一个实现了InvocationHandler接口的代理类

class MyDynamicProxyHandler implements InvocationHandler {
    private Object proxyed;

    public MyDynamicProxyHandler(Object proxyed) {
        this.proxyed = proxyed;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        System.out.println("代理工作了.");
        return method.invoke(proxyed, args);
    }
}

测试

class TestReflact {
    public static void main(String[] args) throws Exception {
        MyClass obj = new MyClass();
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, new MyDynamicProxyHandler(obj));

        proxy.doSomething();
        proxy.somethingElse("aaa");
    }
}

输出

代理工作了.
doSomething
代理工作了.
somethingElse

24.注解

注解(annotation)

基本注解(annotation)有:

元注解(meta-annotation)

元注解(meta-annotation)是用来注释注解用的注解,有:

自定义注解

自定义注解使用@interface,它的用法和classenum是一样的,它声明的是一个注解类型:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
    boolean next() default false;
}

@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationElementDemo {
    //枚举类型
    enum Status {FIXED,NORMAL};

    //声明枚举
    Status status() default Status.FIXED;

    //布尔类型
    boolean showSupport() default false;

    //String类型
    String name() default "";

    //class类型
    Class<?> testCase() default Void.class;

    //注解嵌套
    Reference reference() default @Reference(next=true);

    //long数组类型
    long[] value();
}

注解只能声明包含哪些变量及变量的可选值,但注解本身是没有作用的,如果想让注解实现特定功能,需要使用反射来解析注解

public class AnnotationUtils{
    public static void parse(Class<?> clazz){
        Field[] fields = clazz.getDeclaredFields();
        for(Field field :fields){
            if(field.isAnnotationPresent(AnnotationElementDemo.class)){
                AnnotationElementDemo demo = field.getAnnotation(AnnotationElementDemo.class);
                System.out.println(demo.status()+"--"+demo.name()+"--"+demo.reference());
                for (long l :demo.value())
                    System.out.println(l);
            }
        }
    }
}

使用

class MyClass {
    @AnnotationElementDemo(status = AnnotationElementDemo.Status.NORMAL,name = "User1", value = {1l,2L})
    private String name;
}

public class Main {
    public static void main(String[] args) throws Exception {
        AnnotationUtils.parse(MyClass.class);
    }
}

注解是不支持继承的,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口

25.JDK7、8、9语法的新特性:

数字可加下划线(JDK7)

当数字过长时,可以加下划线以提高阅读性,但只能在数字之间添加下划线

int num1 = 1_000_000_000;
int num2 = 0x53_42//十六进制数
double num3 = 1.414_213;
long num4 = 999_999_999L;
int num5 = 0b1001_1001;//二进制数

switch强化(JDK7)

在以前,switch只支持整形及其包装类型(或byte、short、char等可以隐式转换成int的类型)或枚举,JDK7以后switch中可以使用字串

泛型类型推断(JDK7)

在JDK7以前,创建一个带泛型的变量,两边的泛型都要写全

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

JDK7以后,可以省略右边的泛型,但是如果是匿名内部类,则不能省略

Map<String, List<String>> myMap = new HashMap<>();

//不能省略,因为是创建匿名内部类
List<String> map = new ArrayList<String>(){};

JDK9以后,匿名内部类也可以省略右边的泛型了

List<String> map = new ArrayList<>(){};

try-with-resources(JDK7 & JDK9)

只要使用的类实现了AutoCloseable接口就可以在try语句块退出的时候自动调用close()方法关闭流资源,而不用在finally里面调用close()

public class Demo {    
    public static void main(String[] args) {
        //需要自动关闭的资源要在try后的括号中声明,多个资源用分号隔开
        try(Resource res = new Resource()) {
            res.doSome();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
        //或者
        Resource res = new Resource();
        try(Resource res1 = res) {
            res1.doSome();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
        //多个需要自动关闭的资源
        try(Resource res1 = new Resource(); Resource res2 = new Resource()) {
            res1.doSome();
            res2.doSome();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

class Resource implements AutoCloseable {
    void doSome() {
        System.out.println("do something");
    }
    @Override
    public void close() throws Exception {
        System.out.println("resource is closed");
    }
}

在JDK9中,如果变量是final或等效于final ,可以不在try中声明,可以直接使用

static String readData(String message) throws IOException {
    Reader inputString = new StringReader(message);
    BufferedReader br = new BufferedReader(inputString);
    try (br) {
        return br.readLine();
    }
}

捕获多个Exception(JDK7)

catch (InterruptedException | IOException e),多个异常用|隔开即可,不用写多个catch,且此时e是final的

读写文件简化(JDK8)

可以直接通过Files实现文件读写

List<String> lines = Files.readAllLines(Paths.get("a.txt"));
Files.write(Paths.get("b.txt"), "Hello JDK7!".getBytes());

Stream(JDK8)

null检查(JDK8)

Object添加了nonNullisNull两个静态方法,一般配合Stream和方法引用使用

//获取一个流的所有不为null的对象
Stream.of("a", "c", null, "d")
        .filter(Objects::nonNull)
        .forEach(System.out::println);

Optional类(JDK8)

class User {
    String name;
    public String getName() {
        return name;
    }
}
class UserProxy{
    /*
    public static String getUserName(User user){
        return user.getName();//这样写可能会有空指针异常
    }
    */
    public static String getUserName(User user){
        if(user != null){
            return user.getName();
        }
        return null;
    }
    public static String getUserNameByOptional(User user) {
        //和上面代码等价,Optional可以省去if else的判断
        Optional<String> userName = Optional.ofNullable(user).map(User::getName);
        return userName.orElse(null);
    }
}

Lambda 表达式(JDK8)

允许把函数作为一个方法的参数(且该参数为函数式接口),如Collections.sort(list, (s1, s2) -> {return s1.compareTo(s2);}),不用再实现Comparetor而可以直接用Lambda指定排序方式

方法引用(JDK8)

方法引用是Lambda表达式的一种简化,当Lambda表达式的参数原封不动地传入Lambda表达式的方法体(只有一个语句时)中调用的函数时,可用方法引用替代,如

Integer[] in1 = new Integer[](){2,4,3,6,5};
Arrays.sort(in1,(x,y)->Integer.compare(x,y));
System.out.println(Arrays.toString(in1));
//等价于
Integer[] in2 = new Integer[](){2,4,3,6,5};
Arrays.sort(in2,Integer::compare);
System.out.println(Arrays.toString(in2));

方法引用有:

interface MyInterface< T extends List<?> >{
    T getInstance();
}
class MyClass{
    public <T> List<T> getList(MyInterface< List<T> > interface,T... array){
        List<T> list = interface.getInstance();
        for(T t : array)
            list.add(t);
        return list;
    }
    public static void main(String[] args){
        List<T> list = getList(LinkedList::new,1,2,3,4,5);
        //LinkedList::new相当于()->{return new LinkedList();}
        list.forEach(System.out::println);
    }
}

接口的默认方法(JDK8)、私有方法(JDK9)

接口里可以有实现了的方法,且该方法用default或static修饰

public interface Person {
   default void print(){//default默认方法
      System.out.println("person");
   }
   static void work(){//静态方法
        System.out.println("work");
   }
   void walk();//抽象函数,当接口只有一个抽象方法(除Object中已声明的方法外)时,该接口称为函数式接口
   boolean equals(Object o);//如果有与Object中相同的方法,且为抽象方法,该接口仍是函数式接口,因为这样写没有意义,默认方法无法重写Object的方法
}

在JDK9中,接口中还可以有private方法,以解决接口中的代码复用问题

interface MyInterface{
    private static byte[] encode(byte[] data) {
        //...
    }

    default byte[] decode(byte[] data) {
        data = encode(data);
        for(int i=0; i<data.length; i++)
            data[i] = data[i] ^ 'a'
    }

    default boolean check(byte[] data) {
        if(decode(data)=="pwd")
            return true;
        else
            return false;
    }
}

Module(JDK9)

现在有如下目录结构

|-module1
    |-src
        |-com.test1
            User.java
|-module2
    |-src
        |-com.test2
            Main.java

如果module2想使用module1中的User类,在JDK9中可以通过在modulesrc目录中添加module-info.java来指定导出和导入的module

|-module1
    |-src
        |-com.test1
            User.java
        module-info.java
|-module2
    |-src
        |-com.test2
            Main.java
        module-info.java

module1中的module-info.java

module module1 {
    exports com.test1;
}

module2中的module-info.java

module module2 {
    requires module1;
}

HttpClient(JDK9)

HttpClient支持HTTP1.1HTTP/2,在JDK9中,是以孵化器模块交付的

使用前需导入模块

module mymodule {
    requires jdk.incubator.httpclient;
}

创建HttpClient

HttpClient client = HttpClient.newHttpClient();

或者使用HTTPS:

Authenticator authenticator = new Authenticator() {  };
HttpClient client = HttpClient.newBuilder()
        .authenticator(authenticator)//配置authenticator
        .sslContext(SSLContext.getDefault())//配置 sslContext
        .sslParameters(new SSLParameters())//配置 sslParameters
        .proxy(ProxySelector.getDefault())//配置 proxy
        .executor(Executors.newCachedThreadPool())//配置 executor
        .followRedirects(HttpClient.Redirect.ALWAYS)//配置 followRedirects
        .cookieHandler(new CookieManager())//配置 cookieHandler
        .version(HttpClient.Version.HTTP_2)//配置 version
        .build();

发送GET请求:

HttpResponse<Path> response = client.send(
        HttpRequest.newBuilder(new URI("https://www.baidu.com/img/bd_logo1.png"))
                .headers("Content-Type", "image/png","Accept-Ranges","bytes")
                .GET()
                .build(),
        HttpResponse.BodyHandler.asFile(Paths.get("./logo.png"))
);

System.out.println(response.statusCode());
System.out.println(response.headers());
System.out.println(response.body());

发送POST请求:

HttpResponse<String> response = client.send(
        HttpRequest.newBuilder(new URI("https://www.baidu.com/s"))
                .headers("Content-Type", "text/html", "Connection", "Keep-Alive")
                .POST(HttpRequest.BodyPublisher.fromString("wd=aaa"))
                .build(),
        HttpResponse.BodyHandler.asString()
);

Response也可以使用Lambda表达式创建匿名内部类

HttpResponse<Path> response = client.send(
        HttpRequest.newBuilder(new URI("https://www.baidu.com/img/bd_logo1.png2"))
                .headers("Content-Type", "image/png","Accept-Ranges","bytes")
                .GET()
                .build(),
        (status, headers) -> status == 200 ? HttpResponse.BodySubscriber.asFile(Paths.get("./logo.png")) : HttpResponse.BodySubscriber.discard(Paths.get("/NULL"))
);

VarHandle(JDK9)

类似反射

class Demo {
    public int count = 1;
    private String name = "aaa";
    public int[] array = new int[]{1, 2, 3};

    @Override
    public String toString() {
        String str = "Demo{ count=" + count + ", name='" + name + "', array=[ ";
        for (int i : array) {
            str += i + " ";
        }
        str += "] }";
        return str;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Demo instance = new Demo();
        VarHandle countHandle = MethodHandles.lookup()
                .in(Demo.class)
                .findVarHandle(Demo.class, "count", int.class);
        countHandle.set(instance, 99);

        //访问private属性
        VarHandle nameHandle = MethodHandles.privateLookupIn(Demo.class, MethodHandles.lookup())
                .findVarHandle(Demo.class, "name", String.class);
        nameHandle.set(instance, "bbb");

        //访问数组
        VarHandle arrayHandle = MethodHandles.arrayElementVarHandle(int[].class);
        //如果instance.array[0]是1,则改为4
        arrayHandle.compareAndExchange(instance.array, 0, 1, 4);
        
        System.out.println(instance);

    }
}

不同于反射的是,它可以指定访问变量的方式

class Demo {
    public int count1 = 1;
    public volatile int count2 = 2;

    @Override
    public String toString() {
        return "Demo{ count1=" + count1 + ", count2=" + count2 + '}';
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Demo instance = new Demo();
        VarHandle count1Handle = MethodHandles.lookup()
                .in(Demo.class)
                .findVarHandle(Demo.class, "count1", int.class);

        //确保在此访问之前不会重新排序后续加载和存储
        int previousValue = (int) count1Handle.getAndSetAcquire(instance, 20);
        System.out.println(previousValue);

        //确保在此访问后不会重新排序先前的加载和存储
        previousValue = (int) count1Handle.getAndSetRelease(instance, 23);
        System.out.println(previousValue);

        //用于读取volatile修饰的变量
        VarHandle count2Handle = MethodHandles.lookup()
                .in(Demo.class)
                .findVarHandle(Demo.class, "count2", int.class);
        count2Handle.getVolatile(instance);
        count2Handle.setVolatile(instance, 21);
        System.out.println(instance);

        //按程序顺序访问,但不保证相对于其他线程的内存排序效果
        count1Handle.getOpaque(instance);
        count1Handle.setOpaque(instance, 22);
        System.out.println(instance);
    }
}

VarHandle可以取代java.util.concurrent.atomic包和sun.misc.Unsafe包的功能

局部变量类型推断(JDK10)

只能用于局部变量初始化、for循环局部变量

var list = new ArrayList<String>();
var steam = getStream();

26.GC

GC全称为Garbage Collected,即垃圾回收,GC会每隔一段时间执行,GC执行时程序会被暂停以防止新的垃圾产生

finalize函数

finalize是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

当对象不可达(通过GC Roots无法搜索到)时,如果重写了finalize函数,GC会把对象放入F-Queue队列,然后开启一低优先级线程执行该队列中对象的finalize方法,执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”

finalize不同于C中的析构函数,它调用的时机不确定(你可以通过System.gc()建议系统执行gc,但仍不一定执行gc);由于对象已不可达,如果在finalize出现异常,如果我们没有try-catch,向外抛的异常是不会有任何提示的;由于执行gc的线程比其他线程的优先级要低,如果在finalize中回收资源(如关闭流资源)如果主线程在不断地创建资源,finalize总是无法及时回收的

public class Test {
    public static void main(String[] args) {
        TestFinalize testFinalize = new TestFinalize();
        testFinalize = null;
        System.gc();
    }
}

class TestFinalize {
    ArrayList<FileInputStream> list = new ArrayList();

    TestFinalize() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        list.add(new FileInputStream(new File(("in.txt"))));
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        //finalize无法及时回收资源,会导致内存溢出
        while (list.size() > 0) {
            Iterator<FileInputStream> it = list.iterator();
            while (it.hasNext()) {
                FileInputStream stream = it.next();
                stream.close();
                list.remove(stream);
            }
        }
        //控制台不会打印错误信息
        int num = 1 / 0;
    }
}

finalize在JDK9中已经被弃用,改为用AutoCloseable接口实现资源的回收

内存模型(JMM)

主内存分为

方法区和堆是线程共享的,虚拟机栈和本地方法栈和程序计数器是线程私有的

工作内存:

除此以外,还有直接内存: NIO通过调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,NIO使用直接内存以提高效率。直接内存不属于JMM,它不存储在JAVA内存中,而是在计算机内存中

溢出

堆溢出java.lang.OutOfMemoryError: Java heap space

List<byte[]> list = new ArrayList<>();
int i = 0;
while (true) {
    list.add(new byte[5 * 1024 * 1024]);
    System.out.println("分配次数:" + (++i));
}

栈溢出java.lang.StackOverflowError

int num = 0;

public void testStackOF() {
    num++;
    testStackOF();
}

栈空间不足OutOfMemberError

public void testOutOfMember() {
    while (true) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                loop();
            }
        }).start();
    }
}

private void loop() {
    while (true) ;
}

永久代溢出——常量池溢出java.lang.OutOfMemoryError: Java heap space(使用GC参数-XX:PermSize=10m -XX:MaxPermSize=10m

List<String> list = new ArrayList<>();
int i = 1;
while (true) {
    list.add(UUID.randomUUID().toString().intern());
    i++;
}

永久代溢出——方法区溢出(使用CGLib动态生成大量的代理类)

int i=0;
try {
    while(true){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OOMObject.class);
        enhancer.setUseCache(false);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                return proxy.invokeSuper(obj, args);
            }
        });
        enhancer.create();
        i++;
    }
} finally{
    System.out.println("运行次数:"+i);
}

直接内存(DirectMemory)溢出(使用GC参数-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m

int i=0;
try {
    Field field = Unsafe.class.getDeclaredFields()[0];
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
    while(true){
        //通过unsafe分配物理内存
        unsafe.allocateMemory(1024*1024);
        i++;
    }
} catch (Exception e) {
    e.printStackTrace();
}finally {
    System.out.println("分配次数:"+i);
}

堆中的GC算法

在分代垃圾回收方式中,非堆为持久代(持久代又叫永久代,从JAVA8开始,持久代被替换成了元空间,Metaspace,元空间作用和持久代一样,但元空间并不在虚拟机中,而是在本地内存中),堆又分为新生代和老年代,它们可以选择不同的回收算法(在新生代执行的gc叫minor gc,在老年代执行的gc叫major gc,清理整个堆空间(包括新生代和老年代)叫full gc

新生代 老年代
Eden From/s0 To/s1

Eden:From:To默认是8:1:1(因为Eden区的对象大多很快就被销毁,大部分不会进入Survivor区),新生代:老年代默认是1:2。新创建的对象会放到新生代中的Eden中,经过一次GC后,如果对象还在被引用,会把它放在From或To(又叫Survivor区,分为s0和s1)中,From和To可以相互转换,其大小是一样的,而且只会使用其中一个,当执行复制算法时,会把被引用的对象复制到一边(比如From),然后把另一边清空(比如To)(因为如果仅删除没有被引用的对象对应的内存区域,有可能会清除不干净,会产生内存碎片,所以采用整块内存清除的策略,而新生代中的对象消亡较快,所以复制被引用的对象会快一些)

如果经过15次GC后对象仍然被引用着(或者对象超过一定的大小,可能会直接进入老年区,不会经过新生代,这个和GC机制和JVM参数有关),这时就会把该对象从新生代移动到老年代,在老年代中执行标记-压缩算法时,会移动所有存活的对象,且按照内存地址次序依次排列(即将所有的存活对象压缩到内存的一端),然后将末端内存地址以后的内存全部回收,这样就不会产生内存碎片了

不同的GC收集器对栈的划分不一样,使用的收集算法也不一样,上面只是其中的一种情况,至于具体使用哪种算法,根据参数配置及GC收集器决定

引用

引用分为

Object obj = new Object();//强引用
SoftReference<Object> sf = new SoftReference<Object>(obj);//软引用
obj = null;//删除强引用软引用才能生效
sf.get();//可能会返回null
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//可能会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除

GC参数

可以在javac时指定GC的参数,(+代表启用,如果想禁用,把+改为-即可)

日志相关:

内存大小相关:

收集器相关:

GC有serial收集器、serial old收集器、parnew收集器、parallel old收集器、parallel scavenge收集器、CMS收集器(Concurrent Mark-Sweep)、G1收集器(Garbage First)

27.编译OpenJDK

下载源码

下载压缩包

地址

JDK8

JDK10

点击左侧zip下载压缩包

在线下载源码

地址(JDK8): https://download.java.net/openjdk/jdk8

向导(JDK8): http://hg.openjdk.java.net/jdk8/jdk8/raw-file/tip/README-builds.html

在线下载JDK10

sudo apt install mercurial
hg clone http://hg.openjdk.java.net/jdk10/jdk10
cd jdk10
sudo chmod +x get_source.sh
./get_source.sh

配置、编译

依赖安装

sudo apt-get install systemtap-sdt-dev libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev libcups2-dev libfreetype6-dev libasound2-dev

安装ccache

tar -zxvf ccache-3.5.tar.gz 
cd ccache-3.5/
./configure
make
sudo make install

执行cofigure脚本(boot-jdk要求JDK版本为9或10,但10没有javah,所以只能使用JDK9

bash configure --with-debug-level=slowdebug --enable-dtrace --with-jvm-variants=server --with-target-bits=64 --enable-ccache --with-num-cores=8 --with-memory-size=8000  --disable-warnings-as-errors --with-boot-jdk=/usr/lib/jvm/jdk-9/

执行make images开始编译

编译完成显示

Finished building target 'images' in configuration 'linux-x86_64-normal-server-slowdebug'

此时编译的JDK目录在build/linux-x86_64-normal-server-slowdebug