JAVA Level: JDK8
变量名由数字、字母大小写、$
、_
组成,但是在JDK9以后,_
不能单独作为变量名
正数原码、反码、补码都是一样的,负数反码是原码除符号位外取反,负数补码是反码再加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个字节)
<<
、>>
、>>>
的区别<<
左移,>>
带符号右移(符号位补上原符号位相同的值),>>>
无符号右移(符号位补0)
int a = 1;
,JAVA中a = a++;
后,a还是1,但如果是C中a = a++;
后,a变成了2,这是因为JAVA中的变量运算是先将变量复制一份到高速缓存中,计算完成再赋值给实际变量的,后++是先把高速缓存中的变量赋值给实际变量,然后高速缓存中的值再加1,所以a还是1;但C中变量运算操作的就是实际变量,是a指向的内存地址中的值加1,无论最后有没有再赋值给a,a都是2
判断下面程序是否有错
5.1
byte b1=1, b2=2, b;
= b1 + b2; b
5.2
byte b1=1,b;
= b1 + 3; b
5.3
byte b;
= 3 + 4; b
5.4
short s = 15;
= s + 5; s
5.5
short s = 15;
+=5; s
5.1: 错,变量相加自动转型,因为是变量,无法确认其值,所以按int处理
5.2: 错,同5.1
5.3: 对,只要不超过byte的范围,都可以赋值
5.4: 错,同5.1
5.5: 对,+=等符号有自动类型转换
5 % 2
5 % -2
-5 % 2
-5 % -2
5 / 2
5 / 2.0
3520 / 1000 * 1000
System.out.println("5+5=" + 5 +5);
System.out.println("5+5=" + (5+5));
System.out.println('5' + '+' + '5');
int a=1,b=2;
if(++a==2 & b++ ==3);
System.out.println("a=" + a + " b=" + b);
=1,b=2;
aif(++a==2 && b++ ==3);
System.out.println("a=" + a + " b=" + b);
=1,b=2;
aif(a++==2 & b++ ==3);
System.out.println("a=" + a + " b=" + b);
=1,b=2;
aif(a++==2 && b++ ==3);
System.out.println("a=" + a + " b=" + b);
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);
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";
}
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);
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){
= s2;
s1 = s1 + s2;
s2 }
public static void change(StringBuffer sb1,StringBuffer sb2){
= sb2;
sb1 .append(sb1);
sb2}
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
),而Byte
、Short
、Integer
、Long
、Char
这几个装箱类的valueOf方法是以128位分界线做了缓存的,假如是[-128,127]区间的值是会取缓存里面的引用的,而Float
、Double
不会做缓存的原因也很简单,因为整数类型在某个范围内的整数个数是有限的,但是float、double这两个浮点数却不是6.8:输出hello—world、hello—world和hello—world、hello—worldworld,String类型是值传递的;JAVA中执行方法前,会将要调用的函数的参数值或地址压人栈中,执行完方法后再将之前压入栈的引用数据类型的地址重新赋值给变量,因此JAVA中方法无法改变参数列表中的引用数据类型的指向(但可以改变其成员变量的指向)
int[] a
和int a[]
是一样的,而int[] a[]
是二维数组
只有参数列表不一样才构成重裁,而与权限修饰符、返回值类型等无关
可变参数函数写法:void func(int i, String... str){}
,str按数组类型处理
可变长度参数必须作为方法参数列表中的的最后一个参数且方法参数列表中只能有一个可变长度参数
它们都可以修饰函数、属性和类,private修饰的函数或属性只能在类内部调用,default能同一个包内可调用,protect在同一个包内及子类都可调用,public所有位置都可调用
一个java文件只能有一个用public修饰的类,且该类的名称必须和java文件名一致(但可以有多个非public修饰的类,类名可以和java文件名不一致)
被重写的函数的权限修饰符的范围不能小于父类,抛出的异常不能大于父类,且父类用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){
= new Child();
Child c .show();
c}
}
输出结果为
null
child child
创建子类对象时,自动调用父类的构造函数,此时父类调用的show是子类的show,但这时子类的name属性还没有完成初始化(调用完父类构造函数才开始初始化子类成员变量),所以输出null;调用完父类构造函数后,初始化子类成员变量,然后再调用show,输出child;new完Child对象,此时调show再次输出child
完整的子类初始化顺序是:父类静态变量->父类静态代码块->子类静态变量->子类静态代码块->父类非静态变量->父类非静态代码块->父类构造函数->子类非静态变量->子类非静态代码块->子类构造函数(简单的说就是:父类静态部分->子类静态部分->父类非静态部分+构造函数->子类非静态部分+构造函数)
Object的equals()实际上就是==,比较对象的时候比的是地址,而String的equals()重写了,比的是字符串是否一样
作用范围不同,且成员变量有默认的初始值,而局部变量必须显示初始化
代码块和成员变量定义是按顺序执行的,且非static的代码块和成员变量在父类构造函数执行完才开始执行,如果在代码块里初始化成员变量,而该成员变量定义在代码块后面,且静态初始化了,则会覆盖代码块初始化的值
用父类类型接收子类的实例就构成多态,这时多态对象调的方法是子类的,但调的属性是父类的(因为方法可以重写,但属性不能)
static修饰的内部类不会持有对外部类的隐式引用,静态内部类中的静态成员变量只有在用到的时候才会被加载;static修饰的变量在内存里只有一份,所有实例用的是同一个变量;static修饰的方法可以静态调用(类名.方法),static修饰的代码块只会被执行一次
final修饰的类不可继承;final修饰的属性一旦赋值就不可更改(可以静态赋值,或在代码块、构造函数里赋值),final修饰基本数据类型以及String时,可能会被编译优化,final修饰对象时,不能重新指向别的对象,但对象的属性仍可更改;final修饰的方法不可重写,但可以重裁
抽象类可以有抽象方法和非抽象方法,而接口全部都是抽象方法,且属性都是final static的(interface即使不声明方法是abstract、属性是final static的,也会默认是这些属性修饰的)。(在JDK8以后,interface内可以有方法的具体实现(需用default或static关键字修饰该方法),而不再是全都是抽象方法)
调用本类的其他构造函数用this(),调用父类构造函数用super(),调用其它构造函数必须放在构造函数的第一行调,且只能调一个,如果不显式调其他构造函数,则默认调用父类参数为空的构造函数(super()),如果父类没有空的构造函数,则一定要显式调用
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
可以在方法里面定义内部类,但由于在方法外无法访问该内部类,一般不会这样写
集合是用来保持对象引用的
Collection
分为List
和Set
两大类,List
中元素是有序、可重复的,而Set
中元素的无序、不可重复的;
List
常用的有ArrayList
(底层是数组,查找快)、LinkedList
(底层是链表,插入、删除快,迭代时要使用迭代器,如果使用for循环,每次都要从头节点开始访问,直到找到该元素,速度慢)、Vector(线程安全),List
要求存入的类型重写equals
方法
Set
常用的有HashSet
、LinkedHashSet
、TreeSet
,Set
要求存入的类型重写hashCode
和equals
方法,因为Set
是根据这两个方法判断存入的元素是否重复的,如果不重写,则默认根据地址值判断,这样相同属性的两个对象不会认为是重复的,而且TreeSet
还要求存入的类型实现Comparable
接口,或者在TreeSet
的构造函数里传入实现Comparetor
的实例,并以此确定排序方法
Map
的key是用Set
来存储的,value是用Collection
来存储的,所以要求key不能重复,且key要重写hashCode
和equals
方法;Map中一对key-value称为Entry
,遍历时可用entrySet()
来得到Set<Map.Entry<K,V>>
从而实现遍历,常用的有HashMap
(线程不安全)、LinkedHashMap
、Properties
Collections
工具类里面有提供synchronizedXXX方法把List
、Set
、Map
变为线程安全的(装饰者模式)
字符串hash的计算:
int hash = 0;
for (int i = 0; i < value.length; i++) {
= 31 * hash + val[i];
hash }
这里使用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());
.put(time, "11111");
map
.hour = 13;
timeSystem.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根据hashCode
和equals
判断是否为同一元素,当修改元素的内容时,hashCode
如果发生变化,就被认为不是同一元素,所以一般的做法是
int hash;
@Override
public int hashCode() {
if (hash == 0)
= second + minute * 31 + hour * 31 * 31;
hash return hash;
}
从hashCode
算法可知,hashCode
并不可靠,可能会有两个不同的对象hash却相同的情况,所以还需要equals
(equals
为true
时,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);
= new DayTime(25,12,34,15);
DayTime dayTime 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
的话,子类也必须重写equals
(hashCode
同理)
@Override
public boolean equals(Object obj) {
if (obj instanceof DayTime)
return super.equals(obj) && day == ((DayTime) obj).day;
return false;
}
但是这样又会有time.equals(daytime)
为true
但daytime.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;
}
}
像ArrayList<String> array = new ArrayList<String>()
就是泛型
泛型类:class MyClass<T>{ }
泛型函数:
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(){
new ArrayList<K>();
renturn }
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>;
=a2; a1
此时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>();
.add("abc");
a1ArrayList a2 = a1;//手动擦除泛型
.add(123);//堆污染
a2
System.out.println(a2.get(0));
System.out.println(a2.get(1));
当向一个有泛型的对象写入一个非此泛型的对象时,发生堆污染,所以要注意以下写法:
ArrayList a1 = new ArrayList<String>();//手动泛型擦除
.add("abc");
a1.add(123); a1
以上代码看似使用了泛型,但实际上是可以编译通过的,因为泛型只对对象引用起限制作用,而与构造器无关,如果是ArrayList的引用,构造器用ArrayList<String>()
也不能限制只能存入String
public static void main(String[] args){
Set set = new TreeSet();
.add("abc");
setvaragMethod(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) {
.add(new Integer(10));
objects}
以上代码也能编译通过,但运行时报错(ClassCastException);当一个可变泛型参数指向一个无泛型参数,或者一个方法既使用泛型的时候也使用可变参数(如Arrays.asList(T… a))时,堆污染(Heap Pollution)就有可能发生
enum MyEnum{ A,B,C,D; }
enum MyEnum{
,B,C,D;
Aprivate int id;
public void setId(int id){
this.id=id;
}
public int getId(){
return this.id;
}
}
enum MyEnum{
{ public void show(){ System.out.println("A"); }},
A{ public void show(){ System.out.println("B"); }};
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.valueof("A"); MyEnum A
要注意的是,枚举的声明要放在enum开始的地方,且枚举之间用,
隔开,每个枚举都默认是public
static final修饰的,不用显式声明
创建线程:
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
= new MyThread1();
MyThread1 mt1 .start();
mt1
//实现Runnable的话如果要共享数据,则往Thread构造函数里传入同一个实例就可以了
= new MyThread2();
MyThread mt2 Thread t1 = new Thread(mt2);//t1、t2用同一个i
Thread t2 = new Thread(mt2);
.start();
t1.start();
t2
//如果新建一个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++)
+= i;
result return result;
}
};
FutureTask<Integer> result = new FutureTask<>(callable);
new Thread(result).start();
//get方法会阻塞至线程执行完成,FutureTask实现了Future接口,实际上是用了Future模式
System.out.println(result.get());
调用cmd或者shell
Process process=Runtime.getRuntime().exec("mkdir aaa");
.waitFor(); process
JDK5以后可以使用ProcessBuilder
创建,JDK9以后可以使用ProcessHandle
来获取进程的信息
final ProcessBuilder processBuilder = new ProcessBuilder("ls")
.inheritIO();
final ProcessHandle processHandle = processBuilder.start().toHandle();
.onExit().whenCompleteAsync((handle, throwable) -> {
processHandleif (throwable == null) {
System.out.println(handle.pid());
} else {
.printStackTrace();
throwable}
});
//通过ProcessHandle.info()可以获得进程的启动时间、执行时间等信息
= processHandle.info()
ZonedDateTime startTime .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) {
= new Test();
Test 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) {
.printStackTrace();
e}
.count--;
test}
else
break;
}
}
}).start();
}
try {
//主程序暂停3秒,以保证上面的程序执行完成
Thread.sleep(3000);
} catch (InterruptedException e) {
.printStackTrace();
e}
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){
= new Test();
Test t .method1();
t}
子类调父类的同步方法也是没问题的,此时父类和子类用的是同一个锁
class Sup {
public synchronized void method1() { }
}
class Sub extends Sup {
public synchronized void method2() {
super.method1();
}
public static void main(String[] args) {
= new Sub();
Sub sub .method2();
sub}
}
像出现异常等情况,用synchronized
加的锁会自动释放,但java.util.concurrent.locks
包下的锁要手动释放
使用对象做锁可以使用notify
、wait
等方法,但notify
不会释放锁,而wait
会释放锁并让线程阻塞
要注意的是,使用notify
、notifyAll
、wait
等方法时,应尽量在while循环中使用,否则可能会导致虚假唤醒
(Spurious
wakeup)
while(条件不满足){
obj.wait();
}
而不是:
If( 条件不满足 ){
obj.wait();
}
像以往的wait
、notify
等方法必须写在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) {
.wait();
obj}
} catch (InterruptedException e) {
.printStackTrace();
e}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//do something...
Thread.sleep(new Random().nextInt(500));
synchronized (obj) {
.notify();
obj}
} catch (InterruptedException e) {
.printStackTrace();
e}
}
});
.start();
t1.start(); t2
多运行几次上边的代码,有的时候线程能够正常退出,但有的时候线程却阻塞住了。原因就在于: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) {
.printStackTrace();
e}
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) {
.printStackTrace();
e}
//LockSupport.unpark(Thread)
LockSupport.unpark(t2);
System.out.println("I am done");
}
});
.start();
t1.start(); t2
要注意的是,LockSupport
要求unpark
的线程已经启动,如果LockSupport.unpark(t2)
的时候t2线程还没有启动,那么unpark
是无效的
wait
、notify
和LockSupport
相比,wait
、notify
不需要线程之间相互感知(能获取对方的引用),只需共享一个锁对象即可,而LockSupport
需要获取另一线程的引用才能unpark
,但是这样的好处在于LockSupport
可以唤醒指定的线程。它们各有优势,两者间不能完全取代彼此
LockSupport
和wait
、notify
一样,存在虚假唤醒
的情况,也应尽量在while循环中使用
synchronized
块、synchronized
方法),换句话说,就是一次操作,是一个连续不可中断的过程,数据不会执行的一半的时候被其他线程所修改volatile
关键字设置;JVM会为每个线程创建一个工作内存(TLAB,Thread
Local Allocation
Buffer,线程本地分配缓存区),对共享变量进行修改时,会先缓存到工作内存中再修改,最后再同步回主内存,但是同步的时机是不确定的,这就导致了如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改(脏读),此时使用volatile
可以避免这种情况,volatile
修饰的变量每次写数据时都会加锁(锁总线,后来采用效率更高的锁缓存替代),而且会让其他线程的缓存失效,线程每次读写时都会检测当前数据是否过期,如果过期了就从主内存中获取最新数据volatile
关键字防止重排后出现的不确定结果如果一个线程修改了一个变量,另一个线程有一定几率是不知道的:
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) {
= new Test();
Test t 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++)
.count++;
TestSystem.out.println(Test.count);
}).start();
}
}
}
多运行几次会得到不一样的结果
100
200
300
400
501
593
793
693
893
993
上面的中间的返回值不需要看(可能在输出前,其他线程抢到了优先级,先更改了count,导致输出不一定准确),关注最后的值,最后的值不是1000,这是因为volatile
没有原子性,不能保证每次只有一个线程能更改count的值,当count在高速缓存中更改,但还没有同步进主内存时,另一个线程更改了它的值,导致自增多次但只有一次生效
此时可以使用原子类型解决(基本数据类型都有对应的原子类型,其他的对象可以用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) {
= new Test();
Test t .count = t.count++;
tSystem.out.println(t.count);//输出0
}
}
如果希望每个线程都有变量的副本,对变量副本的操作不影响其他线程,可以用ThreadLocal
ThreadLocal<String> localStr = new ThreadLocal<>() {
@Override
protected String initialValue() {
return "Hello";
}
};
new Thread(new Runnable() {
@Override
public void run() {
.set("world");
localStrSystem.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
.set(num.get() + 1);
numSystem.out.println(num.get());//7
.remove();
numSystem.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方法,清除数据
为什么使用弱引用
考虑使用强引用的情况:
ThreadLocal
,由于ThreadLocalMap
存放在线程中,而线程还没有被销毁,当该类的实例被回收时,由于ThreadLocalMap
对其内部使用的ThreadLocal
存在强引用,所以ThreadLocal
无法被回收,就会造成内存泄露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){
= new Singleton();
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 {
.unlock()
lock}
}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{
;
INSTANCEpublic 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){
.printStackTrace();
e}
}
}).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){
.printStackTrace();
e}
}
}).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();
}
= true;
isLocked ++;
lockedCount= thread;
lockedBy }
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
--;
lockedCountif(lockedCount == 0){
= false;
isLocked = null;
lockedBy notify();
}
}
}
}
这里使用了自旋锁
(采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区)
像上面的例子中,由于线程的执行是抢占式的,调用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();
locktry {
while (!(condition == 1)) {
.await();
lockCondition1}
System.out.println(Thread.currentThread().getName());
= 2;
condition .signal();
lockCondition2Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
.unlock();
lock}
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
.lock();
locktry {
while (!(condition == 2)) {
.await();
lockCondition2}
System.out.println(Thread.currentThread().getName());
= 3;
condition .signal();
lockCondition3Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
.unlock();
lock}
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
.lock();
locktry {
while (!(condition == 3)) {
.await();
lockCondition3}
System.out.println(Thread.currentThread().getName());
= 1;
condition .signal();
lockCondition1Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
.unlock();
lock}
}
}
}, "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
}
注意到monitorenter
和monitorexit
两条指令,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以后,引入了轻量级锁和偏向锁以减少获得锁和释放锁的开销
加锁过程
大多数情况下,锁不存在多线程竞争,而是总是由同一线程多次获得时,为了使线程获得锁的代价更低而引入了偏向锁
加锁过程
如果为可偏向状态,测试对象头Mark Word(默认存储对象的HashCode,分代年龄,锁标记位)里是否存储着指向当前线程的偏向锁
若测试失败,则测试Mark Word中偏向锁标识是否设置成1(表示当前为偏向锁)
没有设置则使用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 {
.wait();
lock} catch (InterruptedException e) {
.printStackTrace();
e}
}
//取出后从队列中删除该元素
= list.poll();
T element System.out.println("take "+element);
.notify();
lockreturn element;
}
}
//队列元素大于CAPACITY时,阻塞并等待取出元素
boolean put(T element) {
synchronized (lock) {
if (list.size() > CAPACITY) {
try {
.wait();
lock} catch (InterruptedException e) {
.printStackTrace();
e}
}
.offer(element);
listSystem.out.println("put "+element);
.notify();
lockreturn true;
}
}
}
JDK给我们提供了一些常用的阻塞队列
与阻塞队列相对应的,有高性能队列ConcurrentLinkedQueue
、高性能哈希表ConcurrentHashMap
,所谓高性能就是不加锁或局部加锁
ConcurrentLinkedQueue
是一个基于链接节点的无界线程安全队列,它不允许存入null
元素。其内部通过使用transient
、volatile
关键字来提高效率并保证线程安全,而以前的LinkedList
通过Collections.synchronizedList
转成线程安全后,效率是会下降的,其原因在于synchronizedList
函数是将LinkedList
的每个方法加锁来保证线程安全的,而加锁会导致效率下降
ConcurrentHashMap
是将以往的HashMap
分成16段(segment),每次读写是只需锁定需要进行读写操作的那一个segment,其他segment仍然可以并行的读写,这样就提高了并发情况下的效率(JDK8以后,把每个segment改为transient volatile HashEntry<K,V>[] table
,table底层使用了红黑树进行优化)
CopyOnWrite
(COW)即写时复制的容器,它实现了读写分离:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样的话,当我们需要对数据进行大量读取、少量写入时,CopyOnWrite
就在可以并发读取的同时也能保证线程安全。但是当数据量很大又或者有大量写入操作时,CopyOnWrite
的效率就会很低。
JAVA给我们提供了CopyOnWrite
容器,如CopyOnWriteArrayList
、CopyOnWriteArraySet
等
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;
}
}
}
共享方式访问需重写tryAcquireShared
、tryReleaseShared
即可
独占方式访问需重写
tryAcquire
:
以独占方式尝试获取资源,成功则返回true,失败则返回falsetryRelease
:
以独占方式尝试释放资源,成功则返回true,失败则返回false如果需要使用到Condition
,还需重写
isHeldExclusively
: 该线程是否正在独占资源CountDownLatch
又叫闭锁,它可以等待其他线程执行完(或执行到某个状态)再继续执行某一线程。它类似计数器,创建的时候指定总的线程数,线程执行完成后使用countDown
函数使计数器减1,当计数器为0时,结束await
比如计算所有线程的执行时间,需阻塞至所有线程执行完成
public class Main {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
= new MyRunnable(countDownLatch);
MyRunnable runnable
= Instant.now();
Instant start for (int i = 0; i < 5; i++) {
new Thread(runnable).start();
}
//阻塞至所有线程执行完成
.await();
countDownLatch
= Instant.now();
Instant end 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+= count;
sum }
System.out.println(sum);
.countDown();
countDownLatch}
}
与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 {
.await();
cyclicBarrier} catch (Exception e) {
.printStackTrace();
e}
System.out.println("所有线程均启动完成,开始执行任务");
//...
}
}
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 {
.acquire();
semaphoreSystem.out.println(Thread.currentThread().getName()+"在使用");
Thread.sleep(500);
} catch (InterruptedException e) {
.printStackTrace();
e}finally {
.release();
semaphore}
}
}
多线程时,如果读取数据的同时如果还在写入,会导致脏读,因此读和写是互斥的;但如果是多个线程同时读取,没有写入操作,是完全没有问题的,读操作之间并不互斥,JAVA给我们提供了ReetrantReadWriteLock
(基于AQS,AbstractQueuedSynchronizer
)实现这种逻辑
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
volatile int count = 0;
public int getCount() {
.readLock().lock();
lockint tmp;
try {
= count;
tmp } finally {
//锁的释放最好放在finally中
.readLock().unlock();
lock}
return tmp;
}
public void setCount(int count) {
.writeLock().lock();
locktry {
this.count = count;
} finally {
.writeLock().unlock();
lock}
}
有时候我们希望把一个大任务拆分成许多的小任务,分别交由多个线程去执行,最后再把结果整合起来,这时就需要用到ForkJoinPool
public class Main {
public static void main(String[] args){
= new CalculateTask(1, 1000000000);
CalculateTask task
= new ForkJoinPool();
ForkJoinPool pool Long sum = pool.invoke(task);
System.out.println(sum);
.shutdown();
pool}
}
//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;
= new CalculateTask(start, middle);
CalculateTask left = new CalculateTask(middle + 1, end);
CalculateTask right //fork就是拆分的意思
.fork();
left.fork();
right//整合结果
return left.join() + right.join();
} else {
for (long i = start; i <= end; i++) {
+= i;
sum }
return sum;
}
}
}
JAVA给我们提供了Executors
工厂类来创建一些常用的线程池,它有如下方法
newCachedThreadPool: 创建一个可缓存线程池,根据需要创建线程,当线程空闲超过60秒时,会自动回收线程
newFixedThreadPool: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
newSingleThreadExecutor: 创建一个只有一个线程的线程池,它可以保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
如果上述线程池都无法满足需求,我们也可以通过指定ThreadPoolExecutor
的参数来自定义线程池,其构造函数如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);//后两个参数为可选参数
参数说明
corePoolSize:
核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务,即使线程池中的其他线程是空闲的;当运行的线程数超过corePoolSize时,任务会加入到BlockingQueue
中;设置allowCoreThreadTimeout=true
(默认false)时,核心线程会超时关闭。该参数一般设置为CUP数量-1
maximumPoolSize:
最大线程数,当BlockingQueue
(是有界队列时)满了以后,如果线程数小于maximumPoolSize,则新加入的任务会创建线程并执行;如果线程数超过maximumPoolSize,则会执行拒绝策略RejectedExecutionHandler
.该参数一般设置为CUP数量*2+1
keepAliveTime: 线程空闲时的可存活时间,超过时间后会被回收
unit: 时间的单位,可以指定keepAliveTime的单位是时、分、秒、毫秒等
workQueue: 用于在线程数大于corePoolSize时保存任务的队列,如果是有界队列,当队列满了且线程数小于maximumPoolSize时会新建线程并执行任务;如果是无界队列,则可以一直添加任务,直至内存溢出,此时maximumPoolSize参数一般和corePoolSize保持一致,且此时拒绝策略无效。使用无界队列有可能导致服务器崩溃(OOM),不建议使用
threadFactory: 使用ThreadFactory创建新线程,默认使用defaultThreadFactory创建线程,参数一般不设置,使用默认的即可
handler:
拒绝策略,当workQueue满了且线程数大于maximumPoolSize时,执行拒绝策略,默认的拒绝策略是抛RejectExecutorException
异常,JAVA给我们提供了其他的拒绝策略,比如DiscardPolicy
,抛弃任务;DiscardOldestPolicy
,抛弃下一个将要执行的任务;CallerRunsPolicy
,把任务交给调用execute方法的线程运行。这些都不是好的拒绝策略,会导致用户数据丢失或者阻塞线程,我们尽量自己实现拒绝策略,比如将任务分发给另一台服务器运行
参数调优
定义如下参数
tasks: 每秒的任务数
taskcost: 每个任务花费时间
responsetime: 统允许容忍的最大响应时间
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
当客户端请求数据时,先返回一个空的数据(wrapper),这时服务器端新建一个线程去获取真正的数据,当数据获取完了之后填入wrapper中,这就要求wrapper和真实数据的类型都实现或继承相同的类或接口,且在用到数据的地方都要先判断真实数据是否为空,如果为空,则要阻塞线程,等待真实数据填入。
master负责接收客户端的请求并把任务分发给worker,worker负责真正的处理任务。它能将大任务分解成若干个小任务,并发执行,提高系统性能,并且实现了调用和执行的解耦
传统的IO操作、socket都是基于BIO(Blocking IO)实现的
UDP
//服务器端
new Thread(new Runnable() {
@Override
public void run() {
DatagramSocket udpSocket = null;
try {
= new DatagramSocket(new InetSocketAddress(8080));
udpSocket
DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
.receive(receive);
udpSocketSystem.out.println("Server: "+new String(receive.getData(),0,receive.getLength()));
byte[] data = "Hello client".getBytes();
.send(new DatagramPacket(data, data.length,receive.getAddress(),receive.getPort()));
udpSocket} catch (IOException e) {
.printStackTrace();
e} finally {
.close();
udpSocket}
}
}).start();
//客户端
new Thread(new Runnable() {
@Override
public void run() {
DatagramSocket udpSocket = null;
try {
= new DatagramSocket();
udpSocket
byte[] data = "Hello server".getBytes();
.send(new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 8080));
udpSocket
DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
.receive(receive);
udpSocketSystem.out.println("Client: "+new String(receive.getData(),0,receive.getLength()));
} catch (IOException e) {
.printStackTrace();
e} finally {
.close();
udpSocket}
}
}).start();
TCP
//服务器端
new Thread(new Runnable() {
@Override
public void run() {
ServerSocket serverSocket = null;//1024-65535的某个端口
try {
= new ServerSocket(8080);
serverSocket } catch (IOException e) {
.printStackTrace();
eSystem.exit(0);
}
Socket socket = null;
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
OutputStream os = null;
while (true) {
try {
= serverSocket.accept();
socket //读取客户端信息
= socket.getInputStream();
is = new InputStreamReader(is);
isr = new BufferedReader(isr);
br String data;
while ((data = br.readLine()) != null)
System.out.println("收到客户端请求:" + data);
.shutdownInput();//关闭输入流
socket
//响应客户端
= socket.getOutputStream();
os .write("Hello client".getBytes());
os.flush();
os.shutdownOutput();
socket} catch (Exception e) {
.printStackTrace();
e} finally {
try {
.close();
br.close();
isr.close();
is.close();
os.close();
socket} catch (IOException e) {
.printStackTrace();
e}
}
}
}
}).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 {
= new Socket(InetAddress.getByName("127.0.0.1"), 8080);
clientSocket //向服务器端发数据
= clientSocket.getOutputStream();
os .write("Hello Server".getBytes());
os.flush();
os.shutdownOutput();
clientSocket
//读取服务器端信息
= clientSocket.getInputStream();
is = new InputStreamReader(is);
isr = new BufferedReader(isr);
br String data;
while ((data = br.readLine()) != null)
System.out.println("收到服务器端响应:" + data);
.shutdownInput();
clientSocket} catch (IOException e) {
.printStackTrace();
e} finally {
try {
.close();
os.close();
is.close();
isr.close();
br.close();
clientSocket} catch (IOException e) {
.printStackTrace();
e}
}
}
}
}).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
}
}
这样做有如下缺点
getInputStream
等方法是阻塞式的,只有当客户端把所有数据都写完了服务器才能接收到NIO的Selector
可以有效的解决线程利用率低的问题
NIO全称为Non-Blocking IO,即(同步)非阻塞IO,它是基于Reactor模式的
以前的IO操作是由CPU直接负责的,这样的缺点是当有大量的IO操作时,CPU无法进行其他操作,性能损耗太大
后来改为使用DMA
(Direct Memory Access
)复制IO操作,当应用使用操作系统的IO接口的时候,由DMA
向CPU申请权限,获得权限后,IO操作由DMA
全权负责,但是如果有大量的IO请求,还是会造成DMA
的走线过多,也会影响性能
再后来改为使用Channel
,Channel
为完全独立的单元,不需要向CPU申请权限,专门用于IO
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
的几个概念
flip
函数后才有效mark
函数时会记录当前的position,调用reset
时通过mark恢复IntBuffer buffer = IntBuffer.allocate(10);
.put(1);
buffer.put(2);
buffer.put(3);
buffer//把第0号元素改为4,此时不会影响position的值
.put(0,4);
buffer//如果index所在的位置没有被不通过index的方式put过,put之后,该位置仍然被看作是无效的
.put(5,90);
bufferSystem.out.println(buffer);//java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
.mark();
buffer
//filp后,position归0,limit为有效元素的个数,capacity不变,mark重新置为-1
.flip();
bufferSystem.out.println(buffer);//java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
//get、set也可以直接操作数组,此时position也会增加对应长度
int[] tmp = new int[buffer.remaining()];
.get(tmp);
bufferSystem.out.println(buffer);//java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
//清空Buffer
.clear(); buffer
我们可以通过Charset
将ByteBuffer
和CharBuffer
相互转换
Charset charset = Charset.forName("UTF-8");
CharBuffer origin = CharBuffer.allocate(20);
.put("Hello world");
origin.flip();
origin
//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);
= new String(charBuffer.array());
data System.out.println(data);
和IO一样,有用于操作本地文件的Channel
、也有分别对应TCP、UDP的Channel
:FileChannel
、SocketChannel
、ServerSocketChannel
、DatagramChannel
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
.flip();
buffer.write(buffer);
outChannel//用完buffer一定要clear,以备下次使用
.clear();
buffer}
.close();
inChannel.close();
outChannel.close();
fis.close(); fos
除此以外,还可以
//获取文件大小
long size = inChannel.size();
//指定position
long pos = inChannel.position();
.position(pos +123);
inChannel
//截取文件的前1024个字节,后面的内容会被删掉
.truncate(4);
outChannel
//出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法
.force(true);
outChannel
//分散读取(Scatter)、聚集写入(Gather),即读到多个Buffer中,再从多个Buffer中写入
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(200);
ByteBuffer[] buffers = new ByteBuffer[]{buffer1, buffer2};
.read(buffers);
inChannelfor (Buffer buffer : buffers)
.flip();
buffer.write(buffers); outChannel
Channel
也可以使用内存映射文件来提高效率,MappedByteBuffer
和allocateDirect
创建的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()];
.get(tmp);
inBuffer.put(tmp);
outBuffer
.close();
inChannel.close(); outChannel
还有一个更快捷的方式,也是使用了直接缓冲区
FileChannel inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
.transferTo(0,inChannel.size(),outChannel);
inChannel
.close();
inChannel.close(); outChannel
UDP的 Channel
public static void main(String[] args) throws Exception {
//服务器端
new Thread(new Runnable() {
@Override
public void run() {
DatagramChannel channel = null;
try {
= DatagramChannel.open().bind(new InetSocketAddress(8080));
channel
ByteBuffer buf = ByteBuffer.allocate(1024);
.clear();
bufSocketAddress clientInfo = channel.receive(buf);
printBuffer("receive from client: ",buf);
.clear();
buf.put("Hello client".getBytes());
buf.flip();
buf
int bytesSent = channel.send(buf, clientInfo);
System.out.println("Server: send " + bytesSent + " bytes");
}catch (Exception e){e.printStackTrace();}
finally {
try {
.close();
channel} catch (IOException e) {
.printStackTrace();
e}
}
}
}).start();
//保证服务器先启动
Thread.sleep(500);
//客户端
new Thread(new Runnable() {
@Override
public void run() {
DatagramChannel channel = null;
try {
= DatagramChannel.open();
channel
ByteBuffer buf = ByteBuffer.allocate(1024);
//向服务器端发数据
.clear();
buf.put("Hello server".getBytes());
buf.flip();
bufint bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1", 8080));
System.out.println("Client: send " + bytesSent + " bytes");
//接收服务器的响应
.clear();
buf.receive(buf);
channelprintBuffer("receive from server: ",buf);
}catch (Exception e){e.printStackTrace();}
finally {
try {
.close();
channel} catch (IOException e) {
.printStackTrace();
e}
}
}
}).start();
}
static void printBuffer(String tag,ByteBuffer buffer) {
.flip();
bufferSystem.out.print(tag);
while (buffer.hasRemaining())
System.out.print((char) buffer.get());
System.out.println();
.flip();
buffer}
TCP的Channel
和UDP差距不大(注意shutdownOutput
、shutdownInput
),这里不在赘述
除此以外,还有Pipe
负责多线程的通信
final Pipe pipe = Pipe.open();
new Thread(new Runnable() {
@Override
public void run() {
Pipe.SinkChannel sink = pipe.sink();
ByteBuffer buffer = ByteBuffer.allocate(48);
.put(new Date().toString().getBytes());
buffer.flip();
buffertry {
.write(buffer);
sink} catch (IOException e) {
.printStackTrace();
e}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Pipe.SourceChannel source = pipe.source();
ByteBuffer buffer = ByteBuffer.allocate(48);
try {
.read(buffer);
source.flip();
bufferSystem.out.println(new String(buffer.array(),0,buffer.limit()));
} catch (IOException e) {
.printStackTrace();
e}
}
}).start();
值得注意的一点是,Channel
可以指定为阻塞式还是非阻塞式的,在非阻塞模式下,accept()
方法会立刻返回,如果还没有新进来的连接,返回的将是null,所以我们要判断返回的channel是否为null
,一般非阻塞式都会和Selector
配合使用
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
serverSocketChannel
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
//...
}
}
NIO把客户端、服务器端的连接放在通道(Channel
)中,通过多路复用器(Selector
)轮询每个注册了的channel
,当channel
准备就绪时,就进行相应的处理,这样就不需要等待所有数据都准备好之后才能开始处理,而且一个线程可以通过selector
同时处理多个channel
,大大地提升了每个线程的利用率
//服务器端
new Thread(new Runnable() {
@Override
public void run() {
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
//设置为非阻塞式
.configureBlocking(false);
serverChannel
Selector selector = Selector.open();
//把ServerSocketChannel注册到Selector上,并监听accept事件
.register(selector, SelectionKey.OP_ACCEPT);
serverChannel
//轮询
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
.remove();
iterator
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//accept就绪,开始accept客户端
SocketChannel clientChannel = server.accept();
//非阻塞式时,accept有可能返回null
if (clientChannel != null) {
//客户端也要设置为非阻塞式
.configureBlocking(false);
clientChannel//把客户端注册到Selector上,并监听read事件
.register(selector, SelectionKey.OP_READ);
clientChannel
//向客户端应答数据
byte[] data = "Hello client".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(data.length);
.put(data);
buffer.flip();
buffer.write(buffer);
clientChannel.shutdownOutput();
clientChannel}
} else if (key.isReadable()) {
//接收客户端发过来的数据
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (clientChannel.read(buffer) != -1) {
.flip();
bufferSystem.out.println("Server: revecie from" + clientChannel.getLocalAddress() + ": " + new String(buffer.array(), 0, buffer.limit()));
.clear();
buffer}
} else if (key.isWritable()) {
//...
} else if (key.isConnectable()) {
//...
}
}
}
} catch (IOException e) {
.printStackTrace();
e}
}
}).start();
//确保服务器先启动
Thread.sleep(500);
for (int i = 0; i < 10; i++)
//客户端
new Thread(new Runnable() {
@Override
public void run() {
try {
SocketChannel socketChannel = SocketChannel.open();
.connect(new InetSocketAddress("127.0.0.1", 8080));
socketChannel.configureBlocking(false);
socketChannelByteBuffer buffer = ByteBuffer.allocate(20);
.put("Hello server".getBytes());
buffer.flip();
buffer.write(buffer);
socketChannel//告知服务器我们已经写完
.shutdownOutput();
socketChannel
.clear();
bufferwhile (socketChannel.read(buffer) != -1) {
.flip();
buffer//如果不是空数据
if (buffer.limit() > 0)
System.out.println("Client: revecie from" + socketChannel.getLocalAddress() + ": " + new String(buffer.array(), 0, buffer.limit()));
.clear();
buffer}
.close();
socketChannel} catch (IOException e) {
.printStackTrace();
e}
}
}).start();
NIO也有一些缺点
selector
,如果业务很复杂,写起来很麻烦channel
和selector
,代码过于复杂channel
,当channel
数量过多时,程序效率低下虽然1、2条缺点可以通过使用NIO的框架来解决,但是我们仍希望可以通过回调的方式异步的实现非阻塞IO
AIO全称为Asynchronous IO,即异步(非阻塞)IO,它的读和写都是异步的,可以同时写多个数据,它是基于Proactor模式的
NIO使用了select/poll
的方式,将channel
注册到selector
上,通过轮询channel
是否就绪,将就绪的channel
返回并进行处理,这样当channel
数量过多时,轮询的效率会很低
AIO使用epoll
的方式,将channel
注册到selector
上,基于回调的方式(类似监听者模式),告知selector
哪些channel
已经就绪,然后将就绪的channel
返回,这样效率高,但需要操作系统层面的支持
下面是一个使用AIO,客户端往服务器端发数据、服务器端接收数据的例子
客户端:
= AsynchronousSocketChannel.open();
AsynchronousSocketChannel channel .connect(new InetSocketAddress("127.0.0.1",8888)).get();
channelByteBuffer 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));
.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
channel@Override
public void completed(final AsynchronousSocketChannel client, Void attachment) {
.accept(null, this);//我们没有使用while循环不断的accept,而是当接收到一个请求后,再准备下一次accept,因为这里的accept是异步的,不会阻塞线程
channel
ByteBuffer buffer = ByteBuffer.allocate(1024);
.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
client@Override
public void completed(Integer result_num, ByteBuffer attachment) {
//attachment就是客户端发过来的数据
//Buffer需要手动重置position
.flip();
attachmentCharBuffer charBuffer = CharBuffer.allocate(1024);
CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
.decode(attachment,charBuffer,false);
decoder.flip();
charBufferString data = new String(charBuffer.array(),0, charBuffer.limit());
System.out.println("read data:" + data);
try{
.close();
client}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 pid
、System.exit()
、Ctrl+C
、正常关机、程序正常退出都会调用ShutdownHook
,但是如果使用kill -9 pid
强制杀死进程不会调用ShutdownHook
Java中有三个类加载器
-Xbootclasspath
和路径来改变Bootstrap ClassLoader
的加载目录,被指定的文件追加到默认的bootstrap路径中-D java.ext.dirs
选项指定的目录SystemAppClass
,负责加载当前应用的classpath的所有类每个类加载器都有一个父加载器,直至最顶层的类加载器:
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) {
= path;
mLibPath }
@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) {
.write(len);
bos}
} catch (IOException e) {
.printStackTrace();
e}
byte[] data = bos.toByteArray();
.close();
is.close();
bosreturn defineClass(name,data,0,data.length);
} catch (IOException e) {
.printStackTrace();
e}
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();
= new Test("小明");
Test testObj
//这里getMethod和getDeclaredMethod的区别和上面是一样的
//根据名字获取并执行public方法
Method showInfo = Test.class.getDeclaredMethod("showInfo");
.invoke(testObj);
showInfo
//根据名字获取private方法,执行前需设置访问权限
Method reset = Test.class.getDeclaredMethod("reset");
.setAccessible(true);
reset.invoke(testObj);
resetSystem.out.println(testObj);
//同样有int.class、double.class、Void.class等
//调用静态方法把obj设为null即可
Method print = Test.class.getMethod("print",String.class);
.invoke(null,"小红"); print
//获取所有的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();
.setAccessible(true);
constructorSystem.out.println(constructor.newInstance());
//下面方法和之前的getMethod、getDeclaredMethod类似
.class.getFields();
Test.class.getDeclaredFields();
Test
= new Test("小明");
Test testObj
//操作非public变量的时候也要使用setAccessible设置访问权限
Field field = Test.class.getField("name");
System.out.println(field.get(testObj));
.set(testObj,"小红");
fieldSystem.out.println(testObj);
class Father<T>{}
class Child extends Father<String>{}
class TestReflact {
public static void main(String[] args) throws Exception {
= new Child();
Child 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 {
<String> child = new Child<>();
ChildParameterizedType type = (ParameterizedType) child.getClass().getGenericSuperclass();
System.out.println(type.getActualTypeArguments()[0].getTypeName());
}
}
会输出T
,因为运行时设置的泛型会被擦除,是无法获取的
内省是针对JavaBean的,只有类中有getXXX和setXXX,无论是否有XXX这个属性,都能获取/设置
= new Test("小明");
Test 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方法
.invoke(test, "小红");
setNameSystem.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 {
= new MyClass();
MyClass obj = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, new MyDynamicProxyHandler(obj));
MyInterface proxy
.doSomething();
proxy.somethingElse("aaa");
proxy}
}
输出
代理工作了.
doSomething
代理工作了.
somethingElse
基本注解(annotation)有:
@Override
:限定重写父类方法@Deprecated
:示某个程序元素(类、方法等)已过时@SuppressWarnings
:忽略编译器警告,且可以指定忽略的类型,常用的有@SuppressWarnings({"unchecked","unused"})
@SafeVarargs
:忽略堆污染警告(JDK7)@FunctionalIterface
:指定某个接口必须是函数式接口(JDK8)(如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口)元注解(meta-annotation)是用来注释注解用的注解,有:
@Retention
:指定注解可以保留多长时间(生命周期)@Target
:指定该注解可用于修饰哪些程序元素@Documented
:使用javadoc后保留对该注解的说明,需Retention为RetentionPolicy.RUNTIME@Inherited
:指定注解具有继承性,使用该注解的类的子类默认有该注解@Repeatable
(JDK8):允许把同一个类型的注解使用多次自定义注解使用@interface
,它的用法和class
、enum
是一样的,它声明的是一个注解类型:
@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() default Status.FIXED;
Status
//布尔类型
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)){
= field.getAnnotation(AnnotationElementDemo.class);
AnnotationElementDemo demo 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 {
.parse(MyClass.class);
AnnotationUtils}
}
注解是不支持继承的,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation
接口
当数字过长时,可以加下划线以提高阅读性,但只能在数字之间添加下划线
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只支持整形及其包装类型(或byte、short、char等可以隐式转换成int的类型)或枚举,JDK7以后switch中可以使用字串
在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<>(){};
只要使用的类实现了AutoCloseable
接口就可以在try
语句块退出的时候自动调用close()
方法关闭流资源,而不用在finally
里面调用close()
public class Demo {
public static void main(String[] args) {
//需要自动关闭的资源要在try后的括号中声明,多个资源用分号隔开
try(Resource res = new Resource()) {
.doSome();
res} catch(Exception ex) {
.printStackTrace();
ex}
//或者
Resource res = new Resource();
try(Resource res1 = res) {
.doSome();
res1} catch(Exception ex) {
.printStackTrace();
ex}
//多个需要自动关闭的资源
try(Resource res1 = new Resource(); Resource res2 = new Resource()) {
.doSome();
res1.doSome();
res2} catch(Exception ex) {
.printStackTrace();
ex}
}
}
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();
}
}
catch (InterruptedException | IOException e)
,多个异常用|
隔开即可,不用写多个catch,且此时e是final的
可以直接通过Files实现文件读写
List<String> lines = Files.readAllLines(Paths.get("a.txt"));
.write(Paths.get("b.txt"), "Hello JDK7!".getBytes()); Files
Object添加了nonNull
和isNull
两个静态方法,一般配合Stream
和方法引用使用
//获取一个流的所有不为null的对象
.of("a", "c", null, "d")
Stream.filter(Objects::nonNull)
.forEach(System.out::println);
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的判断
<String> userName = Optional.ofNullable(user).map(User::getName);
Optionalreturn userName.orElse(null);
}
}
允许把函数作为一个方法的参数(且该参数为函数式接口),如Collections.sort(list, (s1, s2) -> {return s1.compareTo(s2);})
,不用再实现Comparetor而可以直接用Lambda指定排序方式
方法引用是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));
方法引用有:
Class<T>::new
;要求类有一个无参的构造器,且对应的函数式接口无参并且返回一个对象interface MyInterface< T extends List<?> >{
getInstance();
T }
class MyClass{
public <T> List<T> getList(MyInterface< List<T> > interface,T... array){
List<T> list = interface.getInstance();
for(T t : array)
.add(t);
listreturn list;
}
public static void main(String[] args){
List<T> list = getList(LinkedList::new,1,2,3,4,5);
//LinkedList::new相当于()->{return new LinkedList();}
.forEach(System.out::println);
list}
}
Class::static_method
,如list.forEach(System.out::println);
Class::method
,如list.forEach(l::toString);
instance::method
;方法引用中的this和Lambda表达式一样,表示的是所在类的对象,而非实现的函数式接口接口里可以有实现了的方法,且该方法用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的方法
}
如果子类继承的两个接口中有重名的默认方法,则子类必须实现冲突的方法(直接指定调用哪个父接口的方法:接口名.super.方法名()
)
如果继承的父类的方法和接口的默认方法有重名,则调用父类的方法
接口的默认方法无法重写Object中的方法,因为所有类都继承自Object,所以调的也是Object中的方法,而非接口重写的Object的方法
在JDK9中,接口中还可以有private
方法,以解决接口中的代码复用问题
interface MyInterface{
private static byte[] encode(byte[] data) {
//...
}
default byte[] decode(byte[] data) {
= encode(data);
data for(int i=0; i<data.length; i++)
[i] = data[i] ^ 'a'
data}
default boolean check(byte[] data) {
if(decode(data)=="pwd")
return true;
else
return false;
}
}
现在有如下目录结构
|-module1
|-src
|-com.test1
User.java
|-module2
|-src
|-com.test2
Main.java
如果module2想使用module1中的User类,在JDK9中可以通过在module
的src
目录中添加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 .test1;
exports com}
module2中的module-info.java
:
{
module module2 ;
requires module1}
HttpClient
支持HTTP1.1
及HTTP/2
,在JDK9中,是以孵化器模块交付的
使用前需导入模块
{
module mymodule .incubator.httpclient;
requires jdk}
创建HttpClient
:
= HttpClient.newHttpClient(); HttpClient client
或者使用HTTPS:
Authenticator authenticator = new Authenticator() { };
= HttpClient.newBuilder()
HttpClient client .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请求:
<Path> response = client.send(
HttpResponse.newBuilder(new URI("https://www.baidu.com/img/bd_logo1.png"))
HttpRequest.headers("Content-Type", "image/png","Accept-Ranges","bytes")
.GET()
.build(),
.BodyHandler.asFile(Paths.get("./logo.png"))
HttpResponse);
System.out.println(response.statusCode());
System.out.println(response.headers());
System.out.println(response.body());
发送POST请求:
<String> response = client.send(
HttpResponse.newBuilder(new URI("https://www.baidu.com/s"))
HttpRequest.headers("Content-Type", "text/html", "Connection", "Keep-Alive")
.POST(HttpRequest.BodyPublisher.fromString("wd=aaa"))
.build(),
.BodyHandler.asString()
HttpResponse);
Response也可以使用Lambda表达式创建匿名内部类
<Path> response = client.send(
HttpResponse.newBuilder(new URI("https://www.baidu.com/img/bd_logo1.png2"))
HttpRequest.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"))
);
类似反射
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) {
+= i + " ";
str }
+= "] }";
str return str;
}
}
public class Main {
public static void main(String[] args) throws Exception {
= new Demo();
Demo instance = MethodHandles.lookup()
VarHandle countHandle .in(Demo.class)
.findVarHandle(Demo.class, "count", int.class);
.set(instance, 99);
countHandle
//访问private属性
= MethodHandles.privateLookupIn(Demo.class, MethodHandles.lookup())
VarHandle nameHandle .findVarHandle(Demo.class, "name", String.class);
.set(instance, "bbb");
nameHandle
//访问数组
= MethodHandles.arrayElementVarHandle(int[].class);
VarHandle arrayHandle //如果instance.array[0]是1,则改为4
.compareAndExchange(instance.array, 0, 1, 4);
arrayHandle
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 {
= new Demo();
Demo instance = MethodHandles.lookup()
VarHandle count1Handle .in(Demo.class)
.findVarHandle(Demo.class, "count1", int.class);
//确保在此访问之前不会重新排序后续加载和存储
int previousValue = (int) count1Handle.getAndSetAcquire(instance, 20);
System.out.println(previousValue);
//确保在此访问后不会重新排序先前的加载和存储
= (int) count1Handle.getAndSetRelease(instance, 23);
previousValue System.out.println(previousValue);
//用于读取volatile修饰的变量
= MethodHandles.lookup()
VarHandle count2Handle .in(Demo.class)
.findVarHandle(Demo.class, "count2", int.class);
.getVolatile(instance);
count2Handle.setVolatile(instance, 21);
count2HandleSystem.out.println(instance);
//按程序顺序访问,但不保证相对于其他线程的内存排序效果
.getOpaque(instance);
count1Handle.setOpaque(instance, 22);
count1HandleSystem.out.println(instance);
}
}
VarHandle
可以取代java.util.concurrent.atomic
包和sun.misc.Unsafe
包的功能
只能用于局部变量初始化、for循环局部变量
var list = new ArrayList<String>();
var steam = getStream();
GC全称为Garbage Collected,即垃圾回收,GC会每隔一段时间执行,GC执行时程序会被暂停以防止新的垃圾产生
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) {
= new TestFinalize();
TestFinalize testFinalize = null;
testFinalize System.gc();
}
}
class TestFinalize {
ArrayList<FileInputStream> list = new ArrayList();
TestFinalize() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
.add(new FileInputStream(new File(("in.txt"))));
list} catch (FileNotFoundException e) {
.printStackTrace();
e}
}
}
}).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();
.close();
stream.remove(stream);
list}
}
//控制台不会打印错误信息
int num = 1 / 0;
}
}
finalize
在JDK9中已经被弃用,改为用AutoCloseable
接口实现资源的回收
主内存分为
native
方法用的栈方法区和堆是线程共享的,虚拟机栈和本地方法栈和程序计数器是线程私有的
工作内存:
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
工作内存中有一个叫TLAB(Thread Local Allocation Buffer)的区域。在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配。如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在TLAB上。TLAB仅作用于新生代的Eden区,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效
除此以外,还有直接内存:
NIO
通过调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,NIO
使用直接内存以提高效率。直接内存不属于JMM,它不存储在JAVA内存中,而是在计算机内存中
溢出
堆溢出java.lang.OutOfMemoryError: Java heap space
List<byte[]> list = new ArrayList<>();
int i = 0;
while (true) {
.add(new byte[5 * 1024 * 1024]);
listSystem.out.println("分配次数:" + (++i));
}
栈溢出java.lang.StackOverflowError
int num = 0;
public void testStackOF() {
++;
numtestStackOF();
}
栈空间不足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) {
.add(UUID.randomUUID().toString().intern());
list++;
i}
永久代溢出——方法区溢出(使用CGLib动态生成大量的代理类)
int i=0;
try {
while(true){
= new Enhancer();
Enhancer enhancer .setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
enhancer@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
.create();
enhancer++;
i}
} finally{
System.out.println("运行次数:"+i);
}
直接内存(DirectMemory)溢出(使用GC参数-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m
)
int i=0;
try {
Field field = Unsafe.class.getDeclaredFields()[0];
.setAccessible(true);
field= (Unsafe) field.get(null);
Unsafe unsafe while(true){
//通过unsafe分配物理内存
.allocateMemory(1024*1024);
unsafe++;
i}
} catch (Exception e) {
.printStackTrace();
e}finally {
System.out.println("分配次数:"+i);
}
在分代垃圾回收方式中,非堆为持久代(持久代又叫永久代,从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);//软引用
= null;//删除强引用软引用才能生效
obj .get();//可能会返回null sf
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
= null;
obj .get();//可能会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾 wf
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
=null;
obj.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除 pf
可以在javac
时指定GC的参数,(+
代表启用,如果想禁用,把+
改为-
即可)
日志相关:
-XX:+PrintGC
: 打印GC日志-XX:+PrintGCDetails
: 打印GC详细日志-XX:+PrintGCTimeStamps
:
以基准时间的形式打印GC的时间戳-XX:+PrintGCDateStamps
: 以日期的形式打印GC的时间戳-XX:+PrintHeapAtGC
: 在GC前后打印出堆的信息-Xloggc:../logs/gc.log
: 日志文件的输出路径内存大小相关:
-Xms
:
堆区内存初始内存分配的大小,通常为操作系统可用内存的1/64大小即可(如:-Xms1600m)-Xmx
:
堆区内存可被分配的最大上限,通常为操作系统可用内存的1/4大小,但是开发过程中,通常会将-Xms
与-Xmx
两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源-XX:newSize
: 新生代初始内存的大小-XX:MaxnewSize
: 新生代可被分配的内存的最大上限-Xmn
:
同时设置-XX:newSize
和-XX:MaxnewSize
并使它们的值相等-XX:OldSize
: 老年代初始内存的大小(老年代的内存=堆内存
- 新生代内存,老年代的最大内存 = 堆内存 -
新生代最大内存,一般设置堆内存和新生代即可)-XX:PermSize
: 非堆区初始内存分配大小(permanent
size,持久化内存)-XX:MaxPermSize
: 非堆区分配的内存的最大上限-XX:SurvivorRatio
:
设置新生代中Eden区和两个Survivor区的大小比值(比如-XX:SurvivorRatio=8,则表示Survivor:Eden=2:8)-XX:NewRatio
:
指定老年代/新生代的堆内存比例(比如-XX:NewRatio=4,则表示年轻代与年老代所占比值为1:4),设置了-XX:MaxNewSize时,-XX:NewRatio的值会被忽略-XX:PretenureSizeThreshold
:
设置对象超过多大时,创建的时候直接进入老年代,不经过新生代收集器相关:
GC有serial
收集器、serial old
收集器、parnew
收集器、parallel old
收集器、parallel scavenge
收集器、CMS
收集器(Concurrent
Mark-Sweep)、G1
收集器(Garbage First)
-XX:MaxGCPauseMills
:
设置GC时最大的停顿时间,单位为毫秒,但不一定有效
-XX:GCTimeRatio
:
垃圾回收时间占总运行时间的比值,公式为1/(1+N)
,默认我99,即1%的时间用于垃圾回收
-XX:+UseAdaptiveSizePolicy
:
并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开
-XX:+UseSerialGC
:
使用串行收集器,串行收集器最稳定,但可能会产生较长的停顿,因为它只使用一个线程去回收
-XX:+UseParNewGC
:
在新生代使用并行收集器,不影响老年代,可以通过-XX:ParallelGCThreads
指定并行的线程数,可与CMS收集同时使用。在serial基础上实现的多线程收集器。更关注响应时间
-XX:+UseParallelGC
:
在新生代使用并行收集器,不影响老年代,可以同时并行多个垃圾收集线程,但此时用户线程必须停止。更关注吞吐量
-XX:+UseParallelOldGC
:
在老年代使用并行收集器,不影响新生代
-XX:+UseConcMarkSweepGC
: 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection
:
设置CMS收集器经过Full GC后,进行一次整理
-XX:+CMSFullGCsBeforeCompaction
:
设置CMS收集器经过几次Full GC后进行一次碎片整理
-XX:ParallelCMSThreads
: 设置CMS收集器线程数
下载压缩包
地址
点击左侧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
中