java基础
本文收集的各种网站结合自己的理解做的笔记看看就可以了!
一、 java概述
1.java之父:詹姆斯·高斯林
2.java体系
1、javaSE,标准版,各应用平台的基础,桌面开发和低端商务应用的解决方案。
2、javaEE,企业版,以企业为环境而开发应用程序的解决方案。
3、javaME,微型版,致力于消费产品和嵌入式设备的最佳方案。
3.java特性
1、一种面向对象的编程语言。
2、一种与平台无关的语言(根据JVM实现的)。
3、一种健壮性语言。
4、具有较高的安全性。
4.javac命令和java命令
javac:负责的是编译的部分,当执行javac时,会启动java的编译器程序。对指定扩展名的.java文件进行编译。 生成了jvm可以识别的字节码文件。也就是class文件,也就是java的运行程序。
java: 负责运行的部分.会启动jvm.加载运行时所需的类库,并对class文件进行执行.一个文件要被执行,必须要有一个执行的起始点,这个起始点就是main函数。
5.JDK和JRE
JDK:java开发工具包
先编译(编译器javec),后运行(解释器java)
JRE:java运行环境
加载代码(加载器),校验代码(校验器),执行代码(解释器)
6.java虚拟机
java虚拟机实际上只是一层接口,一层Java程序和操作系统通讯的接口。java文件编译生成class文件,
而java虚拟机就是这些class文件能够在上面运行的一个平台,你把class文件看成一个软件,java虚拟机就是这个软件可以运行的操作系统。
7.注释
1、单行注释//
2、多行注释/* */
3、java文档注释/** */
二、java语法基础
1.标识符
1、命名规则:由字母、下划线、数字和美元符号组成,不能以数字开头,区分大小写,不能是关键字和保留字(goto、const),长度一般不超过15个字符。
2、驼峰式命名:
类名:单个单词,首字母大写,多个单词,首字母都大写。
方法名、参数名、变量名:单个单词,首字母小写,多个单词,第一单词首字母小写,其他单词首字母大写。
包名:全部小写。
2.java数据类型划分
1、基本数据类型:
数值型:byte 1字节 8位 -128~127
short 2字节 16位 -32768~32767
int 4字节 32位 -2^31~2^31-1
long 8字节 64位 2^63~2^63-1
浮点类型:
float 4字节 32位
double 8字节 64位
字符型:char 2字节 16位 0~65535
布尔型:boolean true false
2、引用类型:
字符串 String、 类 class 、枚举 enum、接口 interface
3.基础数据类型转换
自动类型转换:范围小→范围大
byte→short→int→long→float→double;
char→int→long→float→double
强制类型转换:范围大→范围小
级别从低到高为:byte,char,short(这三个平级)——>int——>float——>long——>double
需要加强制转换符
3.转义字符
\n 换行 \r 回车 \t 水平制表 ' 单引号 " 双引号 \斜杠
4.变量
其实就是内存中的一个存储空间,用于存储常量数据。
作用:方便于运算。因为有些数据不确定。所以确定该数据的名词和存储空间。
特点:变量空间可以重复使用。
变量的作用域和生存期
变量的作用域
作用域从变量定义的位置开始,到该变量所在的那对大括号结束;
生命周期:
变量从定义的位置开始就在内存中活了;
变量到达它所在的作用域的时候就在内存中消失了;
5.运算符
算术运算符:+ 、 - 、 * 、 / 、 % 、 ++ 、 --
赋值运算符:= 、 += 、 -= 、 *= 、 /= 、 %=
关系运算符:> 、 < 、 >= 、 <= 、 == 、 !=
逻辑运算符:! 、 & (只要有一个false 最终结果就是false) 、
| (但凡有一个true 最终结果就是true) 、
^ (如果两边一样 最终结果为false 如果两边不同 最终结果为true)、
&&(如果第一个是false 那第二个不执行 最终结果是false)、
||(如果第一个表达式的结果是true 那第二个表达式 就不去计算了 ,最终结果是true)
位运算符: ~ 、 >> 、 << 、 >>>
字符串连接运算符:+
三目运算符:X ? Y : Z
X为boolean类型表达式,先计算x的值,若为true,整个三目运算的结果为表达式Y的值,否则整个运算结果为表达式Z的值。
6.if语句
1、if(){}
2、if(){}else{}
3、if(){}else if(){}
4、if(){if(){}else()}
5、if()执行语句 esle 执行语句 注意:执行语句只有一条语句的时候.可以将if esle 的大括号省略
注意:()内是boolean类型表达式,{}是语句块
比较字符串用equals,比较内容。比较数值用==,比较地址。
基本数据类型:变量名、变量值在栈中。
引用数据类型:变量名在栈中,变量值在常量池中。
7.switch语句
switch(表达式expr){
case const1:
statement1;
break;
… …
case constN:
statementN;
break;
[default:
statement_dafault;
break;]
}
注意:1、表达式必须是int、byte、char、short、enmu、String类型
2、constN必须是常量或者finall变量,不能是范围
3、所有的case语句的值不能相同,否则编译会报错
4、default可要可不要
5、break用来执行完一个分支后使程序跳出switch语句块,否则会一直会执行下去。
8.while
while( 条件表达式语句){
循环体语句;
}
[初始条件]
do{
循环体;
[迭代]
}while( 循环条件判断);
注意:1、当第一次执行时,若表达式=false时,则while语句不执行,而do/while语句执行一次后面的语句
2、一定要切记在switch循环中,如果没有break跳出语句,每一个case都要执行一遍,在计算最终结果。
break和continue区别:
break:跳出某个循环
continue:跳过某个循环
注意:if外有循环可以用break、continue,单纯if不可以用。
9.方法
1、为什么使用方法?
减少重复代码,提供代码复用性
使用方法将功能提炼出来
写在类内
2、声明格式
[修饰符] 返回值类型 方法名([形式参数列表]){
程序代码;
[return 返回值;]
}
注意:1、方法是给外界提供内容的位置,形式参数是外界提供的
2、方法调用的时候写的是实际参数
3、实际参数的类型、顺序和形参要对应上
4、支持自动转换类型,不支持强制类型转换
三、面向对象编程
1、static调用格式:
1、同一个类中:
静态的:
方法名 属性名
类名.方法名 类名.属性名
对象名.方法名 对象名.属性名
非静态的:
对象名.属性名 对象名.方法名
2、不同类中:
静态的:
对象名.方法名 对象名.属性名
类名.方法名 类名.属性名
非静态的:
对象名.属性名 类名.方法名
注意:1、static可以修饰属性、方法、代码块,不可以修饰类和构造方法。
2、静态方法随着类的加载而加载。
3、在静态方法区内的东西只有一份,所有的对象共享这一份空间,只要有一个对象对属性进行修改,所有的对象调用都是修改后的数据。
4、代码块的执行顺序:静态代码块(只被调用一次)>构造代码块{}>构造方法>普通方法(需调用)
2、this关键字
1、可以调用属性和方法。
this.属性名(全局变量)
this.方法名();
2、在构造方法中:
1、this();括号内的参数个数、顺序、类型根据调用的方法来决定。
2、必须放在第一行,只能调用一次。
3、调用构造方法时只能在构造方法中调用,调用属性和方法时可以在构造方法中可以在普通方法中。
4、当全局变量和局部变量有重名字的时候,用this来区分。
3、super关键字
1、super指代父类对象。
2、super可以调用属性、方法、构造方法。
3、super调用父类的构造方法。
4、super调用构造方法时候必须放在第一行。
4、final最终的
1、可以修饰全局变量,声明的时候必须赋值,只能赋值一次。
2、可以修饰局部变量,声明时候可以不赋值,但也只能赋值一次。
3、可以修饰方法,可以正常使用,不能被重写。
4、可以修饰类,可以正常使用,不能被继承。
5、用final修饰的属性通常叫常量。
6、static final 全局变量。每个字母都要大写。
5、this和super的区别
1、this指的是本类创建的对象。 super指代的是父类的对象
2、this可以调用属性、方法、构造方法。 super也可以调用属性、方法、构造方法。
3、this调用属性和方法的时候,调用自己本类的属性和方法。 如果本类没有,那就用super去父类中找
4、this调用构造方法调用,调用本类的其他构造方法。 super调用父类的构造方法。
5、this和super在调用构造方法的时候必须放在第一行。
6、this和super不能同时存在
6、最小作用域最强原则:
局域变量在此方法中,比全局变量在此方法中的作用强。
1.面向过程(POP)与面向对象(OOP)
二者都是一种思想,面向对象是相对于面向过程而言。面向过程强调的是功能行为,以函数为最小单位。面向对象,将功能封装进对象中,强调具备了功能的对象,以类为最小单位。
面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等
面向对象则是把解决的问题按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合来实现应用程序的功能,这样当应用程序功能发生变动时,只需要修改个别的对象就可以了,从而使代码更容易得到维护。
2.java类以及类的成员
- 对象是实际存在的该实物的实例
- 万物皆对象
类和对象的内存分析
如果创建了一个类的多个对象,则每个对象都独立拥有一套类的属性(非static),意味着:如果修改一个对象的属性a,则不会影响另外一个对象的属性a的值。
堆(heap)存放对象实例,几乎所有对象实例都在此处分配内存。Java虚拟机规范中描述:所有对象实例及数组都在堆上分配内存。
栈(stack)(指虚拟机栈):存储局部变量等。局部变量表存放了编译器可知长度的各种数据类型(Boolean、byte、char、short、int、float、long、double)、对象的引用。方法执行完自动释放。
方法区:用于存储以被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
3.属性(成员变量)vs 局部变量
相同点
- 定义变量的格式:数据类型 变量名=变量值
- 先声明,后使用
- 变量都有其对应的作用域
1.不同点 - 在类中声明的位置不同
- 属性直接定义在对象{}中
- 局部变量:声明在方法内,方法形参,代码块内,构造器形参,构造器内部
2.权限修饰的不同
属性:可以在声明的时候指定权限修饰符,常用的权限修饰符:privte,public,protected
局部变量:不能使用权限修饰符。
3.默认初始化值不同:
属性:整形(byte、short、int、long):0
浮点型(float、double):0.0
字符型(char):0或(’\u0000’)
布尔型(boolean):false
引用数据类型(类、数组、接口):null
局部变量:没有默认初始化值,在调用之前要显试赋值;形参在调用赋值即可。
4.内存加载的位置不同
属性:加载到堆空间(非static)
局部变量:加载到栈空间。
4.方法
对应类中的成员方法(函数method)
调用方法:“对象.方法”
方法的声明
权限修饰符 返回值类型 方法名 (形参列表){
方法体
}
权限修饰符
可以使用访问控制符来保护对类,变量,方法和构造方法的访问。
方法的重载(overload)
1.在同一个类中,允许存在一个以上的同名方法,只要他们的参数个数或者参数类型不同即可
2.特点
与返回值类型无关,只与参数列表有关,调用时根据方法的参数列表不同来区分。
可变个数的形参
1.允许直接定义能和多个实参相匹配的形参,从而可以用一种更简单的方法来传递个数可变的形参。
2.可变个数形参格式:数据类型 . . . 变量名
当调用可变个数形参时,传入的参数可以是0个、1个、2个、、、
可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载。
可变个数形参的方法与本类中方法名相同,形参类型也相同的数组之间不构成重载,即不能同时出现
可变个数形参在方法的形参中,必须声明在形参列表的末尾
//可变个数形参
public void show(String ... str){
for (String s : str) {
System.out.print(s+" ");
}
}
public void show(String[] str){
}
可变个数形参只能声明在形参列表末尾
public void show(String ... str,int n){
}
方法参数的值传递机制
概述
如果变量是基本数据类型,此时赋值为变量所保存的数据值
如果变量是引用数据类型,此时赋值的是变量的地址值
方法形参的传递机制:值传递
形参:方法定义时,声明的小括号内的参数
实参:方法调用时,实际传递给形参的参数
四种引用
https://blog.csdn.net/weixin_32233909/article/details/114611852
5.堆栈溢出
public class Heap
{
public static void main(String[] args)
{
ArrayList list=new ArrayList();
while(true)
{
list.add(new Heap());
}
}
}
堆内存溢出
public class Stack
{
public static void main(String[] args)
{
new Stack().test();
}
public void test()
{
test();
}
}
栈内存溢出
6.构造器
1.作用
- 用于创建类的对象
- 初始化对象的属性
- 如果没有定义构造器,则系统提供默认空参构造器
2.定义
权限修饰符 类名(形参列表){
}
public Person(int i,String str){
this.i=i;
this.str=str;
}
多个构造器彼此构成重载
当类中显示的定义了构造器,则类不再提供空参构造器
属性赋值的先后顺序:
默认初始化值
显式初始化/代码块中赋值
构造器
通过"对象.方法"或"对象.属性"
7.代码块
{
需要执行的逻辑
}
通常用于初始化类、对象。
用static修饰的称为静态代码块。
静态代码块随着类的加载而执行,只执行一次,只能调用静态结构。
非静态代码块,随着对象创建而执行,每创建一个对象,就会执行一次非静态代码块。
8.面向对象三大特征
1.封装
隐藏对象内部的复杂性,只对外提供简单的接口,便于外界调用,从而提高系统的课扩展性、可维护性
将类属性私有化
类外部不能直接访问
提供公共的方法来获取和设置该属性的值,即提供get和set方法
提高了数据的安全性
别人不能够通过 变量名.属性名 的方式来修改某个私有的成员属性
操作简单
封装后,多个调用者在使用的时候,只需调用方法即可,调用者不需要再进行判断
隐藏了实现
实现过程对调用者是不可见的,调用者只需调用方法即可,不知道具体实现过程
修改属性的可见性 ——> 设为private
2)、创建共有的 getter / setter方法 ——> 用于属性的读写
3)、在getter / setter方法中加入属性控制语句
2.继承
使用extends关键字
减少了代码冗余,提高了代码的复用性
便于功能扩展
为多态性的使用,提供了前提
A:子类、派生类
B:父类、超类、基类
A继承了父类B,则A中就获取了父类中的结构
子类继承父类之后可以声明自己特有的方法,即功能的扩展
一个类可以被多个子类继承
一个类只能有一个父类:单继承
所有类都继承于Object类。
重写与重载之间的区别
方法重载:
1、同一个类中
2、方法名相同,参数列表不同(参数顺序、个数、类型)
3、方法返回值、访问修饰符任意
4、与方法的参数名无关
方法重写:
1、有继承关系的子类中
2、方法名相同,参数列表相同(参数顺序、个数、类型),方法返回值相同
3、访问修饰符,访问范围需要大于等于父类的访问范围
4、与方法的参数名无关
执行顺序:
父类属性初始化----->父类构造器初始化----->子类属性初始化---->子类构造器初始化
在继承的关系中:代码块的执行顺序:父静---->子静----->父构造方法------>子构造代码块------>子构造方法
3.多态
封装和继承几乎都是为多态而准备的。
多态存在的三个必要条件:
1.要有继承
2.要有重写
3.父类引用指向子类对象
java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类进行方法重载。
多态:
动态绑定(多态):在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
父类引用指向子类对象,但是父类引用所能看到的只属于父类那部分属性和方法,此过程还存在指针指向变化情况(因为重写),从指向原来自己方法变化到指向new出来对象的方法。
class A {
int m;
public int getM() {
return m;
}
public void setM(int m) {
this.m = m;
}
class B extends A {
int m;
public int getM() {
return m + 100;
}
}
public class E {
public static void main(String[] args) {
B b = new B();
b.m = 20;
System.out.println(b.getM()); // 120
A a = b;
a.m = -100;
System.out.println(a.getM()); // 120
}
}
b.m = 20; 修改的是B类的属性,
b.getM(); 是取到B对象自己的方法,输出20+100A a = b; 此时a指向B对象,但是所能看到的只是父类那部分属性和方法。此过程还发生上 面知识点2中的指针指向变化,如图绿色变变化指向。
a.m = -100; 因为只能看到父类那部分属性和方法,所以修改的是A对象的属性, 因为此时指向方法的指针发生给变,所以指向的还是B对象的方法(自然用B的属性),输出还是20+100
https://www.xp.cn/b.php/83846.html 多态的理解
可以理解为一个事物的多种形态
父类引用指向子类对象
编译看左边,运行看右边
使用前提
需要类的继承关系
需要有方法的重写
对象多态性只适用于方法,不适用于属性(编译运行都看左边)
当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法
有了对象的多态性后,内存中实际上加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用
如果要调用子类特有的属性和方法,则需要将父类强转为子类类型,即向下转型(子类 子类对象=(子类)父类对象)
instanceof关键字:a instanceof A:判断对象a是否是类A的实例,如果是则返回true,否则返回false
编译时多态:在编译过程中察觉的多态,重载,向上转型。
运行时多态:在运行过程中察觉的多态,向下转型。
2、向上转型、向下转型是在继承关系中,向下转型必须在向上转型的基之上。
3、在继承关系中,父类的对象可以指向子类的实例,父类引用实体方法的时候,是调用子类重写以后的方法。
4、向上转型
父类的引用指向子类的实体
父类类名 对象名=new 子类类();
优点:减少重复代码,提高代码的复用性
缺点:父类的引用无法调用子类特有的属性和方法
解决方案:向下转型
5、向下转型:
子类对象的父类引用赋给子类
子类类名 对象名=(子类类名)父类对象;
6、 instanceof 判断左边的对象是否属于右边的类 对象名 instanceof 类名(子类类名)
7、匿名对象
new 类名() 只有堆空间,没有栈空间,只能属于一次,为了节省代码。
9.抽象类和接口
1.抽象类
abstract:抽象的
可以修饰类和方法,则说明该类是抽象类或抽象方法
abstract修饰类时,则该类不可以实例化
抽象类中有构造器,便于子类对象实例化
开发过程中,一般提供抽象类的子类,让子类实例化,完成相应的操作
抽象方法只有方法的声明,没有方法体
包含抽象方法的类一定是抽象类。反之,抽象类中可以没有抽象方法
若子类重写了父类中所有抽象方法后,则子类可以实例化
子类没有重写父类的抽象方法,则子类也必须是抽象的
注意点:
abstract不能修饰属性、构造器等结构
不能用来修饰私有方法、静态方法、final的方法
2.接口
interface:接口
Java中接口和类是并列结构
接口中的成员:
JDK7及以前:只能定义全局常量(public static final)和抽象方法
JDK8:除了定义全局常量和抽象方法外,还可以定义静态方法、默认方法
接口中不能定义构造器,即接口不可以实例化
Java开发中让类实现接口,从而使用相应的功能
如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化,若实现类没有覆盖接口中的所有抽象方法,则此实现类仍为一个抽象类
接口和接口之间是继承关系,可以多继承
1、访问修饰符:只能是public 或者默认
2、extends:接口支持多继承
3、接口中的属性只能是常量,默认就是public static finnal
4、接口中的方法默认是public abstract
10.内部类
分类:成员内部类、静态内部类、局部内部类、匿名内部类
1、成员内部类:
1、可以用四种访问权限修饰符修饰
2、可以有自己的属性和方法,除了静态的。
3、可以使用外部类的所有属性和方法,包括私有的。
4、创建对象
1、通过创建外部类对象的方式创建对象
外部类 外部类对象=new 外部类();
内部类 对象名=外部类对象.new 内部类();
2、内部类 对象名=new 外部类.new 内部类();
2、静态内部类
1、格式:static class 类名{}
2、可以声明静态的属性和方法
3、可以使用外部的静态的属性和方法
4、创建对象
内类名 对象名=new 内类名();(可以直接创建)
外部类名.内部类 对象名=new 外部类.内部类();
包名.外部类名.内部类 对象名=new 包名.外部类.内部类();
5、外部类与内部类同名时,默认是使用内部类对象调用外部类属性
this代表内部类对象
6、要想使用外部类属性,需要使用外部类对象调用
3、局部内部类
1、在方法中声明
2、只能用default修饰
3、可以声明属性和方法,但不能是静态的
4、创建对象,必须在声明内部类的方法内创建
5、调用方法的时候,局部内部类才会被执行
4、匿名内部类
1、匿名内部类只是用一次
2、格式:
父类或接口名 对象名=new 父类或接口名(参数列表){
重写抽象方法
}
调用抽象方法:对象名.方法名
四、数组
1、数组是基本数据类型和字符串类型的容器(引用数据类型),而集合是类数据类型的容器;
2、数组定义的格式:
(1)一般格式:
元素类型[] 数组名 = new 元素类型[元素个数或者数组长度];
其中,元素类型[] 数组名是一个引用数据类型,存放在栈中;
new 元素类型[元素个数或者数组长度]存放在堆中数组对象,继承自Object。
(2)定义和静态初始化
元素类型[] 数组名 = {,};
元素类型[] 数组名 = new 元素类型[元素个数或者数组长度]{
,};
注意:别忘了末尾的;
3、未初始化的数组的默认值问题,就是保存的基本类型的默认初始值
int[] rfInit = new int[3];
System.out.println(rfInit[0]);//0
String[] strInit = new String[3];
System.out.println(strInit[0]);//null
4、数组使用中常见问题
(1)数组越界:编译没有问题,但运行时报错。
int[] arr = new int[3];
System.out.println(arr[3]);
//ArrayIndexOfBoundsException3:操作数组时,访问到了数组中不存在的角标。
注:角标的最大值是长度减1;
(2)数组指向错误
String[] strNull = null;
System.out.println(strNull[0]);
//NullPointException :空指针异常,当引用没有任何指向为
null的情况,该引用还在用于操作实体。
5、数组引用值:
int[] a = {1,2,3,4};
System.out.println(a);
//结果: [I@1db9742 ,这个就是数组的引用值;其中,
[ 表示为数组,I 表示存放类型,1db9742哈希值表示在内存中的存放的十六进制地址。
6、数组操作(算法)
遍历、最大/最小值、排序(冒泡、选择和直接插入)、查找(折半)、删除数组元素操作和进制转化 (练习一遍)
(1)遍历
int[] arr = new int[3];
通过数组的属性arr.length可以获取数组的长度。
(2)最大/最小值:
a、存放值
(注:不能把存放最大值的变量赋值为0,因为数组中有负数,就会产生问题。)
b、存放角标
(注:存放角标的值可以为0,因为它代表数组的0角标数据)
(3)排序:从小到大
a、选择:内循环一次比较,选择出最小的放在最前面,所以减少前面
比较完的数量;(注:暂时保存最大或者最小值下标,最后再交换是一个
优化的方法。)
b、冒泡:内循环一次比较,最小值向前移动,最大值的沉底;所以减少
后面比较完的数量,
-x:让每一次比较的元素减少,-1:避免角标越界。
c、直接插入排序:就是拿出一个空位置,保存起来,接下来把这个位置前面
的元素和这个位置的值比较;如果大于,就一种元素,如果小于,就结束比较,
保存元素在现在的位置上。
注:java中,用Arrays.sort()排序;java中已经定义好的一种排序方式。
开发中,对数组排序。要使用该句代码。
(4)查找:折半查找
先排序,后折半比较;得到相应的数组的下标值。
数组总结:http://t.zoukankan.com/1020182600HENG-p-6560511.html
五、集合
前言
java的集合机制,本质上是一个数据容器,像之前学过的数组,字符串都是一种数据容器。但是集合它提供的功能更加强大,便捷。里面提供的方法的底层源码采用的也是最优秀的算法。
1.Iterator接口
Iterator接口,这是一个用于遍历集合中元素的接口,主要包含hashNext(),next(),remove()三种方法。它的一个子接口LinkedIterator在它的基础上又添加了三种方法,分别是add(),previous(),hasPrevious()。也就是说如果是先Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会再遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap;而那些元素有序的集合,实现的一般都是LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。
2.Collection (集合的最大接口)继承关系
——List 可以存放重复的内容
——Set 不能存放重复的内容,所以的重复内容靠hashCode()和equals()两个方法区分
——Queue 队列接口
——SortedSet 可以对集合中的数据进行排序
Collection定义了集合框架的共性功能。
2. list的常用子类
特有方法。凡是可以操作角标的方法都是该体系特有的方法。
——ArrayList 线程不安全,查询速度快
——Vector 线程安全,但速度慢,已被ArrayList替代
——LinkedList 链表结果,增删速度快
3.set接口
Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。
——HashSet:底层数据结构是哈希表。是线程不安全的。不同步。
HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode和equals来完成。
如果元素的HashCode值相同,才会判断equals是否为true。
如果元素的hashcode值不同,不会调用equals。
注意,对于判断元素是否存在,以及删除等操作,依赖的方法是元素的hashcode和equals方法。
——TreeSet:
有序的存放:TreeSet线程不安全,可以对Set集合中的元素进行排序。
通过compareTo或者compare方法来保证元素的唯一性,元素以二叉树的形式存放。
4.map接口
Correction、Set、List接口都属于单值的操作,而Map中的每个元素都使用key——>value的形式存储在集合中。
Map集合:该集合存储键值对。一对一对往里存。而且要保证键的唯一性。
map接口的常用子类
Map
——HashMap:底层是哈希表数据结构,允许使用 null 值和 null 键,该集合是不同步的。将hashtable替代,jdk1.2.效率高。
——TreeMap:底层是二叉树数据结构。线程不同步。可以用于给map集合中的键进行排序。
5.集合工具类
Collections:集合框架的工具类。里面定义的都是静态方法。
Collections和Collection有什么区别?
Collection是集合框架中的一个顶层接口,它里面定义了单列集合的共性方法。
它有两个常用的子接口,
——List:对元素都有定义索引。有序的。可以重复元素。
——Set:不可以重复元素。无序。
Collections是集合框架中的一个工具类。该类中的方法都是静态的。
提供的方法中有可以对list集合进行排序,二分查找等方法。
通常常用的集合都是线程不安全的。因为要提高效率。
如果多线程操作这些集合时,可以通过该工具类中的同步方法,将线程不安全的集合,转换成安全的。
集合总结
List:add/remove/get/set。
1,ArrayList:其实就是数组,容量一大,频繁增删就是噩梦,适合随机查找;
2,LinkedList:增加了push/[pop|remove|pull],其实都是removeFirst;
3,Vector:历史遗留产物,同步版的ArrayList,代码和ArrayList太像;
4,Stack:继承自Vector。Java里其实没有纯粹的Stack,可以自己实现,用组合的方式,封装一下LinkedList即可;
5,Queue:本来是单独的一类,不过在SUN的JDK里就是用LinkedList来提供这个功能的,主要方法是offer/pull/peek,因此归到这里呢。
Set:add/remove。可以用迭代器或者转换成list。
1,HashSet:内部采用HashMap实现的;
2,LinkedHashSet:采用LinkedHashMap实现;
3,TreeSet:TreeMap。
Map:put/get/remove。
1,HashMap/HashTable:散列表,和ArrayList一样采用数组实现,超过初始容量会对性能有损耗;
2,LinkedHashMap:继承自HashMap,但通过重写嵌套类HashMap.Entry实现了链表结构,同样有容量的问题;
3,Properties:是继承的HashTable。
顺便说一下Arrays.asList,这个方法的实现依赖一个嵌套类,这个嵌套类也叫ArrayList!
5.集合工具类Collections的使用
注意了,Collections多了一个s,是一个工具类,像之前使用过的Arrays,Math工具类一样,用来操作集合。关于工具类的制作方法,首先是构造方法私有化,防止被创造对象。接着是成员(包括成员变量和成员方法)添加静态,可以直接使用"类名.成员"的格式调用。
1.Collections用来操作集合。具体有以下常用方法:
1).void sort(List<T> list):根据集合中的自然元素排序(从小到大);
2).int binarySearch(List<T> list,T key):在排序完成的前提上, 用二分查找的算法在集合list中,根据关键字key查找它的索引值。
3).void reserve(List<T> list): 反转集合的元素
4).void shuffle(List<T> list): 打乱一次集合的元素原有的顺序。像洗牌一样的道理
5).void swap(List<T> list,int i,int j): 在指定的集合中,交换两个元素的位置
6).void copy(List<T> dest,List<T> src) 复制集合。
6.深入进行分析集合
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
7.LinkedList
(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。
LinkedList 底层的数据结构是双向链表结构,且头节点不存放数据。
添加
```html
LinkedList<String> emps = new LinkedList<>();
emps.add("Zhang San");
emps.add("Li Si");
emps.add("Wang Wu");
emps.add(2,"Zhao Liu"); //在指定位置(索引)添加元素
emps.add(null); //可以包含空值
emps.addFirst("Ma Yi"); //在begin添加元素
emps.addLast("Ye gu"); //在end添加元素
System.out.println(emps); //[Ma Yi, Zhang San, Li Si, Zhao Liu, Wang Wu, null, Ye gu]`
访问
LinkedList<String> emps = new LinkedList<>();
emps.add("Zhang San");
emps.add("Li Si");
emps.add("Li Si");
emps.add("Wang Wu");
emps.add("Zhao Liu1");
emps.add("Zhao Liu2");
System.out.println(emps.indexOf("Li Si")); // 1
System.out.println(emps.lastIndexOf("Liu ray")); //-1 找不到返回-1
//根据索引获取元素
System.out.println(emps.get(1)); //Li Si
//获取第1个元素
System.out.println(emps.getFirst()); //Zhang San
//获取最后1个元素
System.out.println(emps.getLast()); //Zhao Liu2
//根据索引删除元素
String name = emps.remove(1);
//删除并返回第1个元素
String firstName = emps.removeFirst();
//删除并返回最后1个元素
String firstName = emps.removeLast();
//根据对象删除元素,返回boolean
boolean flag1 = emps.remove("Li Si");
//根据条件删除元素,返回boolean
boolean flag2 = emps.removeIf(name->name.contains("W"));
//清空List
emps.clear();
添加操作:
就是添加一个新的节点,然后保存该节点和前节点的引用关系。
删除操作
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
如果前一个节点prev为null,即第一个节点元素,则链表first = x下一个节点;
如果前一个节点prev不为null,即不是第一个节点元素,则将当前节点的next赋值给prev.next,x.prev置为null,也就是当前节点x的prev和next和当前链表中的其他元素不存在任何联系;
如果下一个节点next为null,即为最后一个元素,则链表last = x前一个节点;
如果下一个节点next不为null,即不为最后一个元素,则将当前节点的prev赋值给next.prev,同样当前节点x的prev和next和当前链表中的其他元素不存在任何联系;
查询的话:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
根据传入的index去判断是否为LinkedList中的元素,判断逻辑为index是否在0和size之间,如果在则调用node(index)方法,否则抛出IndexOutOfBoundsException;
调用node(index)方法,将size右移1位,即size/2,判断传入的size在LinkedList的前半部分还是后半部分
如果在前半部分,即index < size/2,则从fisrt节点开始遍历匹配
如果在后半部分,即index > size/2,则从last节点开始遍历匹配
可以看出,如果LinkedList链表size越大,则遍历的时间越长,查询所需的时间也越长。
8.ArrayList
特点:
随机访问速度快,插入和移除的性能差
支持null元素
有顺序
元素可以重复
线程不安全
按照下标访问元素最快
ArrayList底层是动态数组,
当使用不带参数的构造方法生成ArrayList对象时,实际上会在底层生成一个长度为10的Object类型数组。
-
如果增加的元素个数超过了10个,那么ArrayList底层会新生成一个数组,长度为原数组的1.5倍+1,然后将原数组的内容复制到新数组中,并且后续增加的内容都会放到新数组中。当新数组无法容纳增加的元素时,重复该过程。
-
对于ArrayList元素的删除操作,需要将被删除元素的后续元素向前移动,代价比较高
public class MyArrayList /*implements List<E>*/{
private transient Object[] elementData;
private int size; //元素个数
public MyArrayList(){
this(10);
}
public MyArrayList(int initialCapacity) {
if (initialCapacity<0) {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
elementData = new Object[initialCapacity];
}
public int size() {
return size;
}
public boolean isEmpty(){
return size == 0;
}
//根据index删掉对象
public void remove(int index) throws Exception {
rangeCheck(index);
int numMoved = size-index-1;
if (numMoved > 0) {
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
elementData[--size] = null;
}
//删掉对象
public boolean remove(Object obj) throws Exception {
for (int i = 0; i < size; i++) {
if (get(i).equals(obj)) {
remove(i);
}
return true;
}
return true;
}
//修改元素
public Object set(int index , Object obj ) throws Exception{
rangeCheck(index);
Object oldValue = elementData[index];
elementData[index] = obj;
return oldValue;
}
//在指定位置插入元素
public void add(int index,Object obj) throws Exception {
rangeCheck(index);
ensureCapacity();
System.arraycopy(elementData, index, elementData, index+1, size-index);
elementData[index] = obj;
size ++;
}
public void add(Object object) {
ensureCapacity();
/*elementData[size] = object;
size ++;*/
elementData[size++] = object; //先赋值,后自增
}
public Object get(int index) throws Exception {
rangeCheck(index);
return elementData[index];
}
public void rangeCheck(int index) throws Exception {
if (index<0 || index >=size) {
throw new Exception();
}
}
//扩容
public void ensureCapacity() {
//数组扩容和内容拷贝
if (size==elementData.length) {
//elementData = new Object[size*2+1]; 这么写原来数组里的内容丢失
Object[] newArray = new Object[size*2+1];
//拷贝数组里的内容
/*for (int i = 0; i < newArray.length; i++) {
newArray[i] = elementData[i];
}*/
System.arraycopy(elementData, 0, newArray, 0, elementData.length);
elementData = newArray;
}
}
// 测试
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList(3);
myArrayList.add("111");
myArrayList.add("222");
myArrayList.add("333");
myArrayList.add("444");
myArrayList.add("555");
try {
myArrayList.remove(2);
myArrayList.add(3, "新值");
myArrayList.set(1, "修改");
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(myArrayList.size());
for (int i = 0; i < myArrayList.size(); i++) {
try {
System.out.println(myArrayList.get(i));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
增删会带来元素的移动,增加数据会向后移动,删除数据会向前移动,所以影响效率。”
ArrayList从原理上就是数据结构中的数组,也就是内存中一片连续的空间,这意味着,当我get(index)的时候,我可以根据数组的(首地址+偏移量),直接计算出我想访问的第index个元素在内存中的位置。写过C的话,可以很容易的理解。
LinkedList可以简单理解为数据结构中的链表(说简单理解,因为其实是双向循环链表),在内存中开辟的不是一段连续的空间,而是每个元素有一个[元素|下一元素地址]这样的内存结构。当get(index)时,只能从首元素开始,依次获得下一个元素的地址。
用时间复杂度表示的话,ArrayList的get(n)是o(1),而LinkedList是o(n)。
9.Set-HashSet与LinkedHashSet
set:
唯一(不可重复),无序(但可做内部排序)。
HashSet:
底层结构是哈希表,(是一个元素为链表的数组)
interface Collection {
...
}
interface Set extends Collection {
...
}
class HashSet implements Set {
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
//1.从这一步我们可以看出来,HashSet()其实使用HashMap()实现的
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) { //e=hello,world
//2.add()方法的内部也是HashMap的实例对象调用方法,这里e是被添加的对象,而PRESENT是一个
private static final Object PRESENT = new Object();对象。
return map.put(e, PRESENT)==null;
}
}
class HashMap implements Map {
//3.来到HashMap实现的put方法
public V put(K key, V value) { //key=e=hello,world
//4.看哈希表是否为空,如果空,就开辟空间
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//5.判断对象是否为null
if (key == null)
return putForNullKey(value);
//6_1.调用hash()方法,通过查看这个方法我们知道这个方法的返回值和对象的hashCode()方法相关
int hash = hash(key);
//7.在哈希表中查找hash值
int i = indexFor(hash, table.length);
//8.这里for循环的的初始条件是把table[i]赋值给e,如果在哈希表中找不到这个hash值得话,
就不会进入for循环比较,如果有的话,就会进入比较
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//9.如果hash值一样(说实话,这一步每太看懂,既然能够查询到,说明两者的hash值必然相等),
并且地址值或者equls一样的话,不会添加进来
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
//走这里其实是没有添加元素
}
}
modCount++;
//9.把元素添加
addEntry(hash, key, value, i);
return null;
}
transient int hashSeed = 0;
//6_2.这是HashMap中的hash方法,从这个方法的实现我们可以看出,这个方法的唯一变量是
hashCode()
final int hash(Object k) { //k=key=e=hello,
int h = hashSeed;//这个值默认是0
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode(); //这里调用的是对象的hashCode()方法
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
HashSet其实是用HashMap()来实现的,HashMap()是Map接口的实现类
调用HashSet的add()方法其实就是调用HashMap()中的put(),put()中主要涉及到两个方面
1).对象的hash值,通过调用hash()得到,这个方法是由hashCode()经过操作实现的,由hashCode()的值控制
2).创建了哈希表,哈希表会将每一个hash值收入
然后比较的方式是
A.先在hash表中查找看是否有这个hash值(第一次比较,看hash表中是否有当前元素的hash值(这个值有hashcode操作的得到)),如果没有,直接将这个hash值对应的对象添加到HashSet中,如果有还要进行第二次比较
B.如果hash表中有这个hash值,那么获取表中的这个hash对应的对象,如果这两个对象的地址值(e.key == key)或者key.equal(e.key)。(第二次比较,如果两个对象的hash值相同,还是不能认为是同一个对象,还要比较两个对象的地址值,或者是equals(),这里是一个||,只要有一个满足相等,就可以认为是同一个元素,不添加
而String类重写了hashCode()和equals()方法,所以他就可以把相同的字符串去掉,之保留其中一个
LinkedHashSet
底层数据结构是hash表和链表
哈希表保证元素唯一
链表保证元素有序。(存储和取出顺序一致)
9.TreeSet
底层数据结构是红黑二叉树
真正的比较是依赖于元素的compareTo()方法,而这个方法是定义在 Comparable里面的。
所以,你要想重写该方法,就必须是先 Comparable接口。这个接口表示的就是自然排序。
a:自然排序(元素具备比较性)
让元素所属的类实现Comparable接口,重写其中的compareTo方法
在这里插入代码片
package cn.itcast_03;
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Student s) {
int num = 0;
num = this.age - s.age;
int num2 = num == 0 ? this.name.compareTo(s.name) : num;
return num2;
}
public TreeSet(Comperator comparator)//比较器排序
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
public int compare(Student s1,Student s2) {
//注意这个三目比较,赋值的优先级最小,==最大,三目中间
int num = s1.getName().length() - s2.getName().length();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
return num3;
}
});
根据对源码和底层数据结构红黑树的理解,我们知道无论是通过自然排序还是通过比较器排序,数据结构核心是通过将根节点和子节点比较,在红黑树的默认方法中,小的为左儿子,大的为右儿子,实现一颗平衡二叉树,而代码的体现是将成员变量进行某种比较得到一个int值,如果值大于1,就放右边,如果值小于1就放左边,如果值相等就不放入,通过这个int值来完成元素的收集
2)我们在考虑这个值的时候,对于大于,小于,相等的条件设定常常使用三目运算符来做判断,熟练使用这一三目运算符
//如果按照正常的二叉排序,那么会先输出小的,而我们要求先输出分高的,所以,可以将全部的s1,s2交换顺序
//就可以达到总分大的放左边(先输出),然后单科权重是语文——数学——英语
int num = s2.getSum() - s1.getSum();
int num2 = num ==0 ? s2.getChineseScore() - s1.getChineseScore() : num;
int num3 = num2 == 0 ? s2.getMathScore() - s1.getMathScore() : num2;
int num4 = num3 == 0 ? s2.getEnglishScore() - s1.getEnglishScore() : num3;
return num4;
10.Collection总结
|–List 有序,可重复
|–ArrayList
底层数据结构是数组,查询快,增删慢。
线程不安全,效率高
|–Vector
底层数据结构是数组,查询快,增删慢。
线程安全,效率低
|–LinkedList
底层数据结构是链表,查询慢,增删快。
线程不安全,效率高
|–Set 无序,唯一
|–HashSet
底层数据结构是哈希表。
如何保证元素唯一性的呢?
依赖两个方法:hashCode()和equals()
开发中自动生成这两个方法即可
|–LinkedHashSet
底层数据结构是链表和哈希表
由链表保证元素有序
由哈希表保证元素唯一
|–TreeSet
底层数据结构是红黑树。
如何保证元素排序的呢?
自然排序
比较器排序
如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定
ArrayXxx:底层数据结构是数组,查询快,增删慢 LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
11.Map
HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
数组特点
存储区间是连续,且占用内存严重,空间复杂也很大,时间复杂为O(1)。
优点:是随机读取效率很高,原因数组是连续(随机访问性强,查找速度快)。
缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中要往后移的,且大小固定不易动态扩展。
链表特点
区间离散,占用内存宽松,空间复杂度小,时间复杂度O(N)。
优点:插入删除速度快,内存利用率高,没有大小固定,扩展灵活。
缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)。
map.put(k,v)实现原理
第一步首先将k,v封装到Node对象当中(节点)。第二步它的底层会调用K的hashCode()方法得出hash值。第三步通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
我的理解:
数组存放着key,完了链表存放着key,value,完了如果有链表则,数组key去和链表key进行对比,如果相同,则覆盖。
map.get(k)实现原理
第一步:先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。第二步:通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
HashMap:内部为hash表实现的,使用散列算法实现的Map,是当今查询速度最快的数据结构,
没有之一,不受数据量的多少影响查询效率。
TreeMap:内部为排序二叉树实现的
LinkedHashMap:继承自HashMap,实现了Map接口:允许遍历与put元素时的顺序一致,虽然map是无序的,且顺序于map而言没有什么太大意义,但是如果需要进行有序的话,可以用LinkedHashMap类
HashMap:内部为hash表实现的,使用散列算法实现的Map,是当今查询速度最快的数据结构,
没有之一,不受数据量的多少影响查询效率。
TreeMap:内部为排序二叉树实现的
LinkedHashMap:继承自HashMap,实现了Map接口:允许遍历与put元素时的顺序一致,虽然map是无序的,且顺序于map而言没有什么太大意义,但是如果需要进行有序的话,可以用LinkedHashMap类
HashMap的实现原理
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单说下HashMap的实现原理:
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中
JDK1.8中的涉及到的数据结构
1.位桶数组
2.红黑树
加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率
HasMap的扩容机制resize();
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时
在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)
红黑二叉树(平衡的)
TreeMap特点(类似于TreeSet)
对于新节点的插入有如下三个关键地方:
1、插入新节点总是红色节点 。
2、如果插入节点的父节点是黑色, 能维持性质 。
3、如果插入节点的父节点是红色, 破坏了性质. 故插入算法就是通过重新着色或旋转, 来维持性质 。
为了保证下面的阐述更加清晰和根据便于参考,我这里将红黑树的五点规定再贴一遍:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
TreeMap put()方法实现分析
在TreeMap的put()的实现方法中主要分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树。
对于排序二叉树的创建,其添加节点的过程如下:
1、以根节点为初始节点进行检索。
2、与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点。
3、循环递归2步骤知道检索出合适的叶子节点为止。
4、将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。
集合的总结比较
https://zhuanlan.zhihu.com/p/74699628
六、IO流
概念:
数据在两设备见的传输称为流
流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作
根据处理数据类型的不同分为:字符流和字节流
根据数据流向不同分为:输入流和输出流
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。 字节流和字符流的区别:
读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
对输入流只能进行读操作,对输出流只能进行写操作
1.输入字节流InputStream
InputStream 是所有的输入字节流的父类,它是一个抽象类。
ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream 是从与其它线程共用的管道中读取数据,与Piped 相关的知识后续单独介绍。
ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)。
2.输出字节流OutputStream
OutputStream 是所有的输出字节流的父类,它是一个抽象类。
ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,
ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。
3.字符输入流Reader
Reader 是所有的输入字符流的父类,它是一个抽象类。
CharReader、StringReader 是两种基本的介质流,它们分别将Char 数组、String中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。
FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。
InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。我们可以从这个类中得到一定的技巧。Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。后面会有Reader 与InputStream 的对应关系。
4.字符输出流Writer
Writer 是所有的输出字符流的父类,它是一个抽象类。
CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据,
BufferedWriter 是一个装饰器为Writer 提供缓冲功能。
PrintWriter 和PrintStream 极其类似,功能和使用也非常相似。
OutputStreamWriter 是OutputStream 到Writer 转换的桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类(具体可以研究一SourceCode)。功能和使用和OutputStream 极其类似,后面会有它们的对应图。
InputStreamReader:字节到字符的桥梁
OutputStreamWriter:字符到字节的桥梁
5.file类
1、File类
创建:File file = new File(路径字符串);
.exists();文件是否存在
.delete();删除文件
.getPath();路径
.isFile());是文件吗
.isDirectory();是目录吗
.getName();名称
.getAbsolutePath();绝对路径
.lastModified();最后修改时间
.length();文件大小,不能判断目录/文件夹
.getParent();父路径,得到指定目录的前一级目录
.getAbsoluteFile();得到文件绝对路径文件
创建:
.createNewFile();创建文件
.mkdir();创建一层文件夹
.mkdirs();创建多层文件夹
File(String path) 通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例
File(String path,String child) 根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
File(File file,String child)
list() 返回一个字符串数组,这些字符串目录中的文件和目录。
list(FileNameFilter) 返回一个字符串数组,这些字符串指满足指定过滤器的文件和目录。
listFiles 返回一个抽象路径名数组,这些路径名目录中的文件。
listFiles(FileNameFilter) 返回抽象路径名数组,这些路径名满足指定过滤器的文件和目录。
6.缓冲流
在这里插入代码片BufferedInoutStream 字节流 输入流 处理流 (byte[]接受数据,返回值!=-1)
BufferedOutputStream 字节流 输出流
BufferedReader 字符流 输入流 (String接受数据,返回值!=null)resdLine();
BufferedWriter 字符流 输出流
readLine();读取一行,返回值String 循环读取判断 !=null
newLine();换行
\r\n换行
BufferedInputStream bis=new BufferedInputStream(new FileInputStream(new File("E:\ceshi.wmv")));
BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(new File("E:\aa\ceshi.wmv")));
byte []bs=new byte[1024];
int num=0;
long time=System.currentTimeMillis();
while((num=bis.read(bs))!=-1){
bos.write(bs,0,num);
}
bos.flush();
关闭流
转换流
字节转字符 InputStreamReader 继承自Reader (String接收数据,返回值!=null)
字符转字节 OutputStreamWriter 继承自Wriiter
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(new FileInputStream( new File("E:\ceshi.wmv"))));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File("E:\aa\demo\ceshi.wmv"))));
String string=null;
while((string=bufferedReader.readLine())!=null){
bufferedWriter.write(string);
}
bufferedWriter.flush();
关闭流
对象流以及数据包装流
bjectInputStream readObject();
ObjectOutputStream writeObject(Object obj);
1、用对象流去存储对象的前提是 将对象序列化 实现接口Serializable
2、序列化和反序列化(将对象持久化储存)
将对象转成字节叫序列化
将字节转成对象叫反序列化
3、序列化ID,帮助区分版本,可写可不写。
4、transient修饰不想被序列化的属性,存储默认值
Person person =new Person("秋秋", 22);
Person person2=new Person("菜菜", 21);
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream(new File("a.txt")));
objectOutputStream.writeObject(person);
objectOutputStream.writeObject(person2);
objectOutputStream.flush();
if (objectOutputStream!=null) {
objectOutputStream.close();
}
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(new File("a.txt")));
Person person3=(Person) objectInputStream.readObject();
System.out.println(person3);
Person person4=(Person) objectInputStream.readObject();
System.out.println(person4);
if (objectInputStream!=null) {
objectInputStream.close();
}
数据包装流(了解)字节流,读取字节
DataInputStream
DataOutputStream
1、新增了很多读取和写入基本数据类型的方法
2、读取的顺序和写入的顺序一样,否则数据内容会和存入时的不同
DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(new File("b.txt")));
dataOutputStream.writeInt(22);
dataOutputStream.writeLong(34567);
dataOutputStream.writeBoolean(true);
dataOutputStream.flush();
if (dataOutputStream != null) {
dataOutputStream.close();
}
DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File("b.txt")));
System.out.println(dataInputStream.readInt());
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readByte());
if (dataInputStream != null) {
dataInputStream.close();
}
七、线程
一个程序同时执行多个任务,通常每个任务称为一个线程。
进程和线程区别
进程拥有自己的一整套变量体系,而线程则是共享。线程是轻量的,创建撤销的开销小。
线程的简单应用
public class ThreadTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
runnable.runThread();
Thread thread = new Thread(runnable);
thread.start();
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("启动线程");
}
public void runThread() {
Thread thread = new Thread(){
@Override
public void run () {
System.out.println("线程2启动!!!");
}
};
thread.start();
}
}
class MyThread extends Thread{
@Override
public void run () {
for (int i = 0; i < 5; i++) {
System.out.println("线程2启动!!!" + i);
}
}
}
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
主线程:main方法产生的线程,也叫作UI线程。
子线程:除了主线程以外的,也叫工作线程。
线程调度
etPriority();分配优先级,默认5,最低1,最高10
.join();插队,阻塞指定的线程等到另一个线程完成以后再继续执行
.sleep();需要设置睡眠时间
.yield();礼让,当执行到这个方法时,会让出cpu时间,立马变成可执行状态
sleep和pield的区别:
sleep yeild
线程进入被阻塞的状态 线程转入暂停执行的状态
打断线程的终止方式
1、用标记,当终止线程时,会执行完run方法
2、stop()方法,不建议使用,会执行不到特定的代码
3、interrupt(),只能中断正在休眠的线程,通过抛异常的方法中断线程的终止。
InputStream inputStream=System.in;
int m=inputStream.read();
myThread2.interrupt();//通过外界输入打断
线程的状态
新建 就绪 执行 阻塞(等待) 死亡
void join()//等待终止指定线程,让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作
void join(long millis) //等待指定的线程死亡或者经过指定的毫秒数,如果超过这个时间,则停止等待,变为可运行状态
void stop()//停止该线程。这一方法已过时
void suspend() //暂停这一线程的执行 已过时
void resume() //恢复线程,和suspend一起使用,也过时
void sleep(long millis)// 等待休眠millis 毫秒,执行这个方法,线程进入计时等待
void yield()//暂停当前正在执行的线程对象,并执行其他线程。理论上,yield意味着放手,放弃,投降。
//一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。
//注意,这仅是一个暗示,并不能保证不会产生任何影响。
线程同步问题
public class SynchronizedThreadTest {
public static void main(String[] args) {
MyThreadTest myThreadTest1 = new MyThreadTest();
MyThreadTest myThreadTest2 = new MyThreadTest();
myThreadTest1.start();
myThreadTest2.start();
}
}
class MyThreadTest extends Thread{
private static int count;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i<50000000; i++) {
count++;
}
try
{
Thread.sleep(200);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(count);
}
}
}
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:
wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
八、TCP编程Socket
客户端:
1、创建socket对象,指定ip地址和端口号
2、需要从socket中得到OutputStream
聊天配合字符流输入流使用,直接转换输入输出即可
文件配合字节流使用,字节流读,socket输出。
3、将想要发送的数据写入到OutputStream中。flush
4、关闭流关闭socket
服务器:
1、创建ServerSocket对象,指定端口号
2、serversocket.accep(),返回一个Socket对象(客户端发过来的socket)
接收客户端发送的数据,如果没有接收到,阻塞程序的运行
3、从Socket中得到InputStream流,将数据从流中读取出来
聊天配合字符输出流使用。
文件配合字节输出流使用,socket读,字节流输出。
4、展示/写入到另外的地方
5、关闭流,关闭Socket
聊天:
聊天客户端:
Socket socket=new Socket("127.0.0.1", 65499);
OutputStream outputStream=socket.getOutputStream();
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
InputStream inputStream=socket.getInputStream();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
Scanner scanner=new Scanner(System.in);
while (true) {
System.out.println("客户端:");
String string=scanner.next();
bufferedWriter.write(string);
bufferedWriter.newLine();
bufferedWriter.flush();
if (string.equals("拜拜")) {
break;
}
//接收数据
String string2=null;
string2=bufferedReader.readLine() ;
System.out.println("服务器回复:"+string2);
}
//关闭流和socket
聊天服务器:
ServerSocket serverSocket=new ServerSocket(65499);
System.out.println("服务器等待中。。。");
Socket socket=serverSocket.accept();
InputStream inputStream=socket.getInputStream();
Scanner scanner=new Scanner(System.in);
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
OutputStream outputStream=socket.getOutputStream();
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
String string=null;
while (true) {
string=bufferedReader.readLine();
System.out.println("客户端说"+string);
if (string.equals("拜拜")) {
break;
}
System.out.println("服务器回复:");
String string2=scanner.next();
bufferedWriter.write(string2);
bufferedWriter.newLine();
bufferedWriter.flush();
}
//关闭各种流和socket等
九、 JUC
链接:https://blog.csdn.net/qq_43417581/article/details/127217919
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
1. 实现多线程的四种方式
继承Thread类
实现Runnable接口
实现Callable接口
线程池
线程池ExecutorService和工具类Executors优点:可以根据实际情况创建线程数量,且只需要创建一个线程池即可,也能够通过Callable和Future接口得到线程的返回值,程序的执行时间与线程的数量紧密相关。缺点:需要手动销毁该线程池(调用shutdown方法)。
尽量不要使用 继承Thread类 和 实现Runnable接口;尽量使用线程池。否则项目导出都是线程。
package com.swain.programmingpearls.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**
* thread 的几种实现
*/
public class threadTest {
public static void main (String[] args) {
//继承thread
ExtendsThread extendsThread = new ExtendsThread();
extendsThread.start();
//实现runnable
Thread runThread = new Thread(new AchieveRunnable());
runThread.start();
//调用callable 可以有返回值 可以捕获异常
Callable<String> tc = new TestCallable();
FutureTask<String> task = new FutureTask<String>(tc);
new Thread(task).start();
try {
System.out.println(task.get());//获取返回值
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//runable 匿名内部类方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现Runnable 匿名内部类方式:" + Thread.currentThread().getName());
}
}).start();
//runnable Lamda表达式
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Lamda表达式:" + i);
}
}).start();
System.out.println("主线程");
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for(int i = 0; i<5; i++)
{
AchieveRunnable achieveRunnable = new AchieveRunnable();
try {Thread.sleep(1000);} catch (InterruptedException e) {}
executorService.execute(achieveRunnable);
}
//关闭线程池
executorService.shutdown();
}
/**
* 继承thread类
*/
public static class ExtendsThread extends Thread {
public void run(){
System.out.println("方法一 继承Thread线程:" + Thread.currentThread().getName());
}
}
/**
* 实现runnable
*/
public static class AchieveRunnable implements Runnable {
@Override
public void run() {
System.out.println("方法二 实现Runnable:" + Thread.currentThread().getName());
}
}
/**
* 通过Callable和FutureTask创建线程
*/
public static class TestCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("方法三 实现callable:" + Thread.currentThread().getName());
return "我是callable的返回";
}
}
}
运行结果:
方法一 继承Thread线程:Thread-0
方法二 实现Runnable:Thread-1
方法三 实现callable:Thread-2
我是callable的返回
实现Runnable 匿名内部类方式:Thread-3
主线程
Lamda表达式:0
Lamda表达式:1
Lamda表达式:2
Lamda表达式:3
Lamda表达式:4
方法二 实现Runnable:pool-1-thread-1
方法二 实现Runnable:pool-1-thread-2
方法二 实现Runnable:pool-1-thread-1
方法二 实现Runnable:pool-1-thread-2
方法二 实现Runnable:pool-1-thread-1
2. 线程和进程
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单元。
线程:进程之内独立执行的一个单元执行流。线程——程序执行的最小单元。
进程:是一个程序,一个进程包含多个线程,且至少包含一个线程。
Java默认有两个线程:main 和 GC。
Java能开启线程吗? start方法开启线程
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//本地方法,底层C++,Java无法操作硬件,由操作系统决定是否创建线程,是否立即创建线程
private native void start0();
Java是不能开启线程的,底层是调用start0()是一个native方法,由底层的C++方法编写。java无法直接操作硬件。
2.1 线程的几种状态
public enum State {
NEW,//新建
RUNNABLE,//准备就绪
BLOCKED, //阻塞
WAITING,//一直等待
TIMED_WAITING,//超时等待,过时不候
TERMINATED;//终止
sleep和wait的区别
1、sleep是线程中的方法,但是wait是Object中的方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
Thread中
join(等待线程结束),yield(当前线程让出CPU,重新去竞争)、sleep(暂停,不交出锁)
2.3 并发和并行
并发:同一个时刻多个线程访问多个资源(多线程共享资源)例如:春运抢票、电商秒杀
并行: 多项工作一起执行,之后再汇总
例如:泡方便面,电水壶烧水的同时,拆开泡面调料倒入桶中
System.out.println(Runtime.getRuntime().availableProcessors());//获取cpu的核数
并发编程的本质:充分利用CPU资源
管程
Monitor 监视器(就是平常说的锁)
是一种同步机制,保证同一时间内,只有一个线程访问被保护的数据或者代码
jvm同步基于进入和退出,使用管程对象实现的
用户线程和守护线程
用户线程:自定义线程(new Thread())
守护线程:后台中一种特殊的线程,比如垃圾回收
3. lock锁**
案例:三个售票员同时卖30张票。
传统 synchronized
public class SaleTicketDemo1 {
public static void main(String[] args) {
//并发:多个线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
//Runnable接口 -》 函数式接口,lambda表达式:函数式接口的实例
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"C").start();
}
}
/**
* 资源类
*/
class Ticket {
//属性
private int num = 30;
//synchronized 本质:线程串行化,排队,锁
public synchronized void sale() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (num--) + "张票,剩余" + num + "张");
}
}
}
Lock接口
实现类
公平锁:十分公平,先来后到(阳光普照,效率相对低一些)
非公平锁:十分不公平,可以插队(默认,可能会造成线程饿死,但是效率高)
public class SaleTicketDemo2 {
public static void main(String[] args) {
//并发:多个线程操作同一个资源类,把资源类丢入线程
Ticket2 ticket = new Ticket2();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"A").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"B").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"C").start();
}
}
/**
* 资源类
*
* Lock三部曲
* 1.new ReentrantLock();
* 2.lock.lock(); //加锁
* 3.finally -》 lock.unlock();//解锁
*
*/
class Ticket2 {
//属性
private int num = 30;
// 创建可重入锁
Lock lock = new ReentrantLock();
public void sale() {
lock.lock(); //加锁
try {
//业务代码
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (num--) + "张票,剩余" + num + "张");
}
} finally {
lock.unlock();//解锁
}
}
}
synchronized 和 Lock区别
1.synchronized 是内置的Java关键字,Lock是Java的一个接口
2.synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
3.synchronized 会自动释放锁,Lock必须手动释放锁!如果不释放锁,会造成死锁
4.synchronized 线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着;Lock就不一定会等待下去
5.synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平/公平(可以自己设置,默认非公平锁)
6.synchronized 适合锁少量的同步代码;Lock适合锁大量同步代码!
7.Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者性能是差不多的,而当竞争资源非常激烈时(即大量线程同时竞争),此时Lock的性能要远远优于synchronized
4. 线程间通信
生产者和消费者问题
面试:单例模式、排序算法、生产者消费者问题、死锁
/**
* 线程之间通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程之间交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class ProducerAndConsumer {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
/**
* 资源类
*
* 判断等待,业务,通知
*/
class Data {
private int num = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (num != 0) {
//等待
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我+1 完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (num == 0) {
//等待
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我-1 完毕了
this.notifyAll();
}
}
问题存在, A B C D 四个线程 虚假唤醒
public class ProducerAndConsumer {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
/**
* 资源类
* <p>
* 判断等待,业务,通知
*/
class Data {
private int num = 0;
/**
* +1
*/
public synchronized void increment() throws InterruptedException {
if (num != 0) {
//等待
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我+1 完毕了
this.notifyAll();
}
/**
* -1
*/
public synchronized void decrement() throws InterruptedException {
if (num == 0) {
//等待
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我-1 完毕了
this.notifyAll();
}
}
为什么会出现虚假唤醒问题呢?
主要是因为wait方法是从哪里等待就从哪里唤醒(而且wait是不会持有锁的,别的线程直接能拿到锁),if只会判断一次,只要第一次满足条件,等待唤醒就会直接执行下面+1/-1操作,所以才会出现>1或者<0的数甚至出现死锁现象,这些都是虚假唤醒导致的。
防止虚假唤醒问题:if 改为 while
/**
* 线程之间通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程之间交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class ProducerAndConsumer {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
/**
* 资源类
*
* 判断等待,业务,通知
*/
class Data {
private int num = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (num != 0) {
//等待
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我+1 完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (num == 0) {
//等待
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我-1 完毕了
this.notifyAll();
}
}
通过Lock找到Condition(官方文档)
public class ProducerAndConsumer2 {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
/**
* 资源类
*
* 判断等待,业务,通知
*/
class Data2 {
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); // 等待
//condition.signalAll(); // 唤醒全部
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
//业务代码
while (num != 0) {
//等待
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我+1 完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
//等待
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
//通知其他线程我-1 完毕了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
多线程编程完整步骤
第一步:创建资源类,在资源类中创建属性和操作方法
第二步:资源类操作方法
(1)判断等待
(2)业务逻辑
(3)通知唤醒
第三步:创建多个线程,调用资源类的操作方法
第四步:防止虚假唤醒
线程间定制通信
线程按约定顺序执行
任何一个新的技术,绝对不是仅仅只覆盖了原来的技术,有其优势和补充!
Condition精准的通知和唤醒线程
代码实现:三个线程顺序执行
/**
* A B C三个线程顺序执行
*/
public class C {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printC();
}
}, "C").start();
}
}
/**
* 资源类 Lock
*/
class Resource {
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA() {
lock.lock();
try {
//业务 判断 -》 执行 -》 通知
while (num != 1) {
//等待
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName() + "=>AAA");
//唤醒,唤醒指定线程B
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
//业务 判断 -》 执行 -》 通知
while (num != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "=>BBB");
num = 3;
//唤醒线程C
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
//业务 判断 -》 执行 -》 通知
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>CCC");
num = 1;
//唤醒线程A
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5. 八锁现象
锁是什么?如何判断锁是谁?
同步方法,谁先拿到锁谁先执行,同一把锁顺序执行。
普通同步方法锁的是 this 当前对象(即方法的调用者)。
静态同步方法锁的是类对象,类只会加载一次,所以静态同步方法永远锁的都是类对象(XXX.class)。
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为一下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象。
6.集合类
//java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
public static void main(String[] args) {
//并发下 ArrayList不安全
/*
解决方案
1、List<String> list = new Vector<>();
2、List<String> list = Collections.synchronizedList(new ArrayList<>());
3、List<String> list = new CopyOnWriteArrayList<>();
*/
//CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
//多个线程操作的时候 list 读取的时候 固定的 写入(覆盖)
//再写入的时候避免覆盖,造成数据问题!
//读写分离
//CopyOnWriteArrayList 比 Vector 好在哪里?
//Vector 底层是synchronized实现效率较低 ; CopyOnWriteArrayList 底层是ReentrantLock实现 效率更高 灵活性也更高
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
//java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
//HashSet<String> set = new HashSet<>();
//解决方案一:
//Set<String> set = Collections.synchronizedSet(new HashSet<>());
//解决方案二:
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashSet底层是HashMap
public HashSet() {
map = new HashMap<>();
}
// add() set 本质就是map 中的key, key是不可重复的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// PRESENT 不变的值!
private static final Object PRESENT = new Object();
Map不安全
public class MapTest {
public static void main(String[] args) {
/*
并发下 HashMap线程不安全
解决方案:
1.Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());
2.Map<String, Object> map = new ConcurrentHashMap<>();
*/
//HashMap<String, Object> map = new HashMap<>();
//Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());
Map<String, Object> map = new ConcurrentHashMap<>();
//加载因子,初始容量
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
7.Callable
1.可以有返回值
2.可以抛出异常
3.方法不同, run() -> call()
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(new MyThread()).start();
/*
本身Callable接口和Runnable接口毫无关系
通过一个Runnable接口的实现类FutureTask,Callable接口与Runnable接口构建了关系,便可以启动线程
*/
//适配类 FutureTask 是 Runnable接口的实现类 构造器 FutureTask(Callable<V> callable)
MyThread1 t1 = new MyThread1();
FutureTask<Integer> futureTask = new FutureTask<>(t1); //泛型是线程返回值类型
/*
启动两个线程,只会打印一个`call()...`
*/
new Thread(futureTask,"A").start(); //怎么启动Callable
new Thread(futureTask,"B").start(); //结果会被缓存,效率高
Integer i = futureTask.get(); //获取线程返回值 get()可能会产生阻塞!把他放到最后 或者 使用异步通信来处理!
System.out.println(i);
}
}
class MyThread implements Runnable{
@Override
public void run() {
}
}
/**
* 泛型是返回值类型
*/
class MyThread1 implements Callable<Integer>{
@Override
public Integer call(){
System.out.println("call()...");
//耗时的操作
return 1024;
}
}
注意:
1.有缓存
2.获取结果可能需要等待,会阻塞!
8.常用的辅助类(必会)
8.1 CountDownLatch(减法计数器)
/**
* 计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//倒计时 起始为6 必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "--->Go out");
countDownLatch.countDown(); // -1
}, String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归零在向下执行
System.out.println("Close Door!");
}
}
原理:
countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归0,再向下执行
每次有线程调用 countDown() 数量-1,当计数器变为0,countDownLatch.await()就会被唤醒,继续往下执行!
8.2 CyclicBarrier(加法计数器)
public class CyclicBarrierDemo {
public static void main(String[] args) {
/*
集齐7颗龙珠召唤神龙
*/
//召唤神龙的线程
CyclicBarrier barrier = new CyclicBarrier(7, () -> System.out.println(“成功召唤神龙!”));
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集了" + temp + "个龙珠");
try {
barrier.await(); //等待
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3 Semaphore(信号量)
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//acquire() 获得
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); //release() 释放
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire(); //获得,如果已经满了,等待,等待被释放为止!
semaphore.release(); // 释放,会将当前的信号量释放+1,然后唤醒等待的线程!
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数!
9.读写锁
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int temp = i;
//写入
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
//读取
new Thread(() -> {
System.out.println("获取" + temp + "缓存数据-> " + myCache.get(temp + ""));
}, String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
/**
* 存 写
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
/**
* 取 读
*/
public Object get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
return o;
}
}
结果分析:由于是多线程操作,多个线程同时开启。写线程写入数据可能花费一些时间,此时数据还没写入完成,读线程就开始读数据导致读取不到任何数据,这种情况需要加锁控制。
加读写锁
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
*
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不可共存!
* 写-写 不可共存!
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int temp = i;
//写入
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
//读取
new Thread(() -> {
System.out.println("获取" + temp + "缓存数据-> " + myCache.get(temp + ""));
}, String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 存 写
*/
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写入" + key);
try {
TimeUnit.MILLISECONDS.sleep(200);
map.put(key, value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
System.out.println(Thread.currentThread().getName() + "写入OK");
}
/**
* 取 读
*/
public Object get(String key) {
readWriteLock.readLock().lock();
Object o = null;
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
return o;
}
}
结果分析:由于加了读写锁,写锁是独占锁,读锁是共享锁。因此写的过程中不允许有任何操作,当写操作写完之后,可以多个线程共享读。
锁降级:写锁会降级为读锁,读锁不能升级为写锁。
10.阻塞队列
什么情况下我们会使用 阻塞队列:多线程并发处理,线程池!
学会使用队列
添加、移除
四组API
/**
* 抛出异常
*/
public static void test1() {
//队列的大小
ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.add("a"));
System.out.println(arrayBlockingQueue.add("b"));
System.out.println(arrayBlockingQueue.add("c"));
//java.lang.IllegalStateException: Queue full 抛出异常
//System.out.println(arrayBlockingQueue.add("d"));
System.out.println("**************");
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
//java.util.NoSuchElementException 抛出异常
//System.out.println(arrayBlockingQueue.remove());
}
/**
*有返回值,不抛出异常
*/
public static void test2(){
ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.offer("a"));
System.out.println(arrayBlockingQueue.offer("b"));
System.out.println(arrayBlockingQueue.offer("c"));
//System.out.println(arrayBlockingQueue.offer("d")); // false 不抛出异常!
System.out.println("*****");
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll()); // null 不抛出异常!
}
/**
*等待,阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
//一直阻塞
arrayBlockingQueue.put("a");
arrayBlockingQueue.put("b");
arrayBlockingQueue.put("c");
//arrayBlockingQueue.put("d"); //队列没有位置了,一直阻塞
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take()); //没有数据了,一直阻塞
}
/**
*等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.offer("a");
arrayBlockingQueue.offer("b");
arrayBlockingQueue.offer("c");
arrayBlockingQueue.offer("d",2, TimeUnit.SECONDS); //等待两秒,超时退出
System.out.println("******");
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
arrayBlockingQueue.poll(2, TimeUnit.SECONDS); //等待两秒,超时退出
}
SynchronousQueue 同步队列
进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
put()、take()
/**
* 同步队列
* SynchronousQueue 和 BlockingQueue 不一样,SynchronousQueue 不存储元素
* put一个元素,必须从里面take取出来,否则不能再put进去值!(存一个,取一个,循环)
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
//存一个元素,取一个元素 循环
SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "->put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + "->put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName() + "->put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->take " + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2").start();
}
}
11.线程池**
线程池:三大方式、七大参数、四种拒绝策略
占用系统资源!优化资源的使用!
线程池的好处
1.降低资源的消耗
2.提高响应的速度
3.方便线程管理
线程复用、可以控制最大并发数、管理线程
线程池:三大方法
/**
* Executors 工具类:创建线程池 3大方法
*/
public class Demo1 {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//创建单个线程的线程池
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建固定线程的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();//创建可伸缩线程池
try {
for (int i = 0; i < 10; i++) {
//使用了线程池之后,用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
七大参数
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//本质:ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂:创建线程,一般不用动
RejectedExecutionHandler handler) { //拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
手动创建一个线程池
/**
* Executors 工具类:创建线程池 3大方法
*
* 4大拒绝策略:
* new ThreadPoolExecutor.AbortPolicy() //默认拒绝策略 银行满了,还有人进来,不处理这个人,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() //哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,也不会抛出异常!
*
*/
public class Demo2 {
public static void main(String[] args) {
//工具类创建
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//创建单个线程的线程池
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建固定线程的线程池
//ExecutorService threadPool = Executors.newCachedThreadPool();//创建可伸缩线程池
//手动创建线程池 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常!
try {
//最大承载:Queue + max
//超出最大承载抛出RejectedExecutionException 异常 (默认拒绝策略)
for (int i = 0; i < 9; i++) {
//使用了线程池之后,用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
四种拒绝策略
4大拒绝策略:
new ThreadPoolExecutor.AbortPolicy() //默认拒绝策略 银行满了,还有人进来,不处理这个人,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() //哪来的去哪里!
*new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,也不会抛出异常!
线程池最大线程数如何设置?
了解:IO密集型,CPU密集型(调优)
public class Demo1 {
public static void main(String[] args) {
//手动创建线程池 ThreadPoolExecutor
//最大线程到底如何定义?
//1、CPU 密集型 电脑处理器数是几,就是几,可以保证CPU的效率最高!
//2、IO 密集型 大于 程序中十分耗IO的线程数 ---> 程序中 15个大型任务 io十分占用资源! =》 30
//获取CPU核数 电脑处理器数
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常!
try {
//最大承载:Queue + max
//超出最大承载 RejectedExecutionException (默认拒绝策略)
for (int i = 0; i < 9; i++) {
//使用了线程池之后,用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
12.四大函数式接口
新时代程序员:lambda表达式(本质就是函数式接口的实例)、链式编程、函数式接口、Stream流式计算
//此注解用来判断该接口是否是函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//简化编程模型,在新版本的框架底层大量应用!
/**
* Function 函数型接口 有一个输入,有一个输出
* 只要是 函数式接口 可以用 lambda表达式简化
*/
public class Demo01 {
public static void main(String[] args) {
//输出输入的值
// Function<String ,String > fun = new Function<String ,String >() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
Function<String ,String > fun = (str) -> str; //lambda表达式
System.out.println(fun.apply("123"));
}
}
/**
* Predicate 断定型接口 有一个输入值 返回值是布尔值!
*/
public class Demo02 {
public static void main(String[] args) {
//判断字符串是否为空
// Predicate<String> predicate = new Predicate<String>() {
// @Override
// public boolean test(String str) {
// return "".equals(str);
// }
// };
Predicate<String> predicate = str -> "".equals(str);
System.out.println(predicate.test(""));
System.out.println(predicate.test("123"));
}
}
/**
* Consumer 消费型接口 只接收参数,不返回值
*/
public class Demo03 {
public static void main(String[] args) {
//接收参数,将其打印出来
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("hello");
}
}
/**
* Supplier 供给型接口 不需参数,有返回值
*/
public class Demo04 {
public static void main(String[] args) {
// Supplier<String> supplier = new Supplier<String>() {
// @Override
// public String get() {
// return "world";
// }
// };
Supplier<String> supplier = () -> "world";
System.out.println(supplier.get());
}
}
13. stream流式计算
大数据:存储 + 计算
存储:集合、MySQL
计算:流式计算~
public class Test {
public static void main(String[] args) {
User user1 = new User(1, "a", 21);
User user2 = new User(2, "b", 22);
User user3 = new User(3, "c", 23);
User user4 = new User(4, "d", 24);
User user5 = new User(5, "e", 25);
User user6 = new User(6, "f", 26);
//存储交给集合
List<User> list = Arrays.asList(user1, user2, user3, user4, user5, user6);
//计算交给Stream流
//lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
.filter(user -> user.getId() % 2 == 0) //找出id为偶数的用户
.filter(user -> user.getAge() > 23) //年龄大于23岁
.map(user -> user.getName().toUpperCase()) //用户名 转为大写
.sorted((u1, u2) -> -u1.compareTo(u2)) //用户名字母到着排序
.limit(1) //只输出一个用户
.forEach(System.out::println);
}
}
14.ForkJoin
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
ForkJoin 特点: 工作窃取
/**
* 求和计算的任务
*
* 如何使用 ForkJoin?
* 1.ForkJoinPool 通过它来执行
* 2.计算任务 forkJoinPool.execute(ForkJoinTask<?> task)
* 3.计算类要继承ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
if ((end - start) < temp) {
long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
//分支合并计算
Long middle = (start + end) / 2;//中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); //拆分任务,把线程任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle, end);
task2.fork(); //拆分任务,把线程任务压入线程队列
//结果汇总
return task1.join() + task2.join();
}
}
}
/**
* 三六九等 三 六(ForkJoin) 九(Stream并行流)
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//test1(); // 419
//test2();
test3();//234
}
//普通程序员
public static void test1() {
long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 0L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + " 时间:" + (end - start));
}
//会使用forkJoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);//提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + "时间:" + (end - start));
}
public static void test3() {
long start = System.currentTimeMillis();
//Stream并行流计算 []
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + " 时间:" + (end - start));
}
}
.parallel().reduce(0, Long::sum) 使用一个并行流去计算,提高效率。(并行计算归约求和)
15.异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
线程异步调用通常使用CompletableFuture类
1)没有返回值的runAsync异步回调
//没有返回值的 runAsync 异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " runAsync => Void");
});
System.out.println("11111");
completableFuture.get(); //阻塞,获取执行结果
(2)有返回值的 supplyAsync 异步回调
/**
* 类似异步调用:Ajax
*
* 异步调用:CompletableFuture
* 成功回调
* 失败回调
*/
public class Demo02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//有返回值的 supplyAsync 异步回调
//成功和失败回调
//返回的是错误信息
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " supplyAsync => Integer");
int i = 10 / 0;
return 1024;
});
//成功回调
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); //正常的返回结果
System.out.println("u=>" + u); //错误信息 java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).exceptionally((e -> {//失败回调
System.out.println(e.getMessage());
return 233; // 可以获取到错误的返回结果
})).get());
}
}
16.JMM
volatile 是Java虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
JMM:Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
线程中分为 工作内存、主内存
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁
8种操作
read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
JMM对这8种操作给了相应的规定:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
17. 关键字volatile
volatile用来修饰会被不同线程访问和修改的变量
1.保证可见性
public class JMMDemo {
// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的,volatile保证一旦数据被修改,其它线程立马能够感知到
private volatile static int num = 0;
public static void main(String[] args) { // main 线程
new Thread(()->{ // 线程1 不知道主内存中的值发生了变化
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
public class JMMDemo {
// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的,volatile保证一旦数据被修改,其它线程立马能够感知到
private volatile static int num = 0;
public static void main(String[] args) { // main 线程
new Thread(()->{ // 线程1 不知道主内存中的值发生了变化
while (num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
2.不保证原子性
原子性:不可分割
线程A在执行任务的时候,是不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
/**
* 不保证原子性
*/
public class VDemo2 {
// volatile 不保证原子性
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
// 20个线程,每个线程调用100次 理论值 2万
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
如果不加lock和synchronized ,怎么样保证原子性?
使用原子类,解决原子性问题
public class VDemo2 {
// volatile 不保证原子性
// 原子类的 Integer
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
//num++; //不是原子性操作
num.getAndIncrement(); // +1 操作 底层是CAS保证的原子性
}
public static void main(String[] args) {
// 20个线程,每个线程调用100次 理论值 2万
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
原子类的底层都直接和操作系统挂钩!在内存中修改值!
Unsafe类是一个很特殊的存在
3.禁止指令重排
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的。
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1234 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
内存屏障:CPU指令。作用:
1、保证特定的操作的执行顺序;
2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
总结:
volatile 可以保证可见性;不能保证原子性;由于内存屏障,可以保证避免指令重排现像产生!
面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式
18.单例模式
1.饿汉式
/**
* 饿汉式单例
* 核心思想:构造器私有化
*/
public class Hungry {
// 可能浪费内存空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private static final Hungry HUNGRY = new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return HUNGRY;
}
}
2.懒汉式
/**
* 懒汉式单例
* 道高一尺,魔高一丈!
*/
public class LazyMan {
private volatile static LazyMan lazyMan;
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class) {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏异常!");
}
}
}
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getInstance() {
if (null == lazyMan) {
synchronized (LazyMan.class) {
if (null == lazyMan) {
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
//不加 synchronized 多线程情况下,不一定是单例
public static void main(String[] args) throws Exception {
// for (int i = 0; i < 10; i++) {
// new Thread(() -> {
// LazyMan.getInstance();
// }).start();
// }
//反射!
//LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance = constructor.newInstance();
flag.set(instance,false);
LazyMan instance1 = constructor.newInstance();
System.out.println(instance == instance1);
}
/*
创建对象的步骤:
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
123
132 线程A
线程B // 此时lazyMan还没有完成构造
*/
}
3.静态内部类
/**
* 静态内部类
*/
public class Holder {
private Holder(){
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
}
单例不安全, 因为反射
4.枚举类
/**
* enum 是什么? 本身也是一个Class类
*/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
//EnumSingle instance2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle instance2 = constructor.newInstance(); //java.lang.NoSuchMethodException: com.lkl.singleton.EnumSingle.
System.out.println(instance1);
System.out.println(instance2);
}
}
使用枚举,我们就可以防止反射破坏了
19.深入理解CAS
public class CASDemo {
//CAS compareAndSet:比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2021);
//期望、更新
//public final boolean compareAndSet(int expect, int update)
//如果我期望的值达到了,就更新,否则,就不更新,CAS 是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2021, 2022));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2022));
System.out.println(atomicInteger.get());
}
}
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行操作!如果不是就一直循环,使用的是自旋锁。
缺点:
循环会耗时;
一次性只能保证一个共享变量的原子性;
它会存在ABA问题
CAS:ABA(狸猫换太子)
主内存中 A=1
线程1:期望值是1,要变成2;
线程2:两个操作:
期望值是1,变成3
期望是3,变成1
所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;线程1不知道A的值发生了修改!
public class CASDemo2 {
//CAS compareAndSet:比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2021);
//期望、更新
//public final boolean compareAndSet(int expect, int update)
//如果我期望的值达到了,就更新,否则,就不更新,CAS 是CPU的并发原语!
// ======================= 捣乱的线程 ==============================
System.out.println(atomicInteger.compareAndSet(2021, 2022));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2022, 2021));
System.out.println(atomicInteger.get());
// ======================= 捣乱的线程 ==============================
//======================== 期望的线程 ==============================
System.out.println(atomicInteger.compareAndSet(2021, 6666));
System.out.println(atomicInteger.get());
}
}
20.原子引用(AtomicReference)
解决ABA问题,引入原子引用!对应的思想:乐观锁!
public class CASDemo3 {
//CAS compareAndSet:比较并交换!
public static void main(String[] args) {
//AtomicStampedReference 泛型如果使用包装类,注意对象引用问题
//正常在业务操作,这里泛型都是一个个对象
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int stamp = stampedReference.getStamp(); //获得版本号
System.out.println(Thread.currentThread().getName() + " 1 -> " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 2,
stampedReference.getStamp(), stampedReference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 2 -> " + stampedReference.getStamp());
System.out.println(stampedReference.compareAndSet(2, 1,
stampedReference.getStamp(), stampedReference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 3 -> " + stampedReference.getStamp());
}, "a").start();
new Thread(() -> {
int stamp = stampedReference.getStamp(); //获得版本号
System.out.println(Thread.currentThread().getName() + " 1 -> " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 6,
stampedReference.getStamp(), stampedReference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 2 -> " + stampedReference.getStamp());
}, "b").start();
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("**************");
System.out.println(stampedReference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意:
Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间
21.各种锁的理解
1、公平锁、非公平锁
公平锁:非常公平,不能插队,必须先来后到!(效率可能较低)
非公平锁:非常不公平,可以插队(默认都是非公平,效率较高)
public ReentrantLock() {
sync = new NonfairSync();
}
1
2
3
带参构造器,可以修改公平状态
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、可重入锁
可重入锁(递归锁):拿到外边的锁后,会自动拿到里面的锁(synchronized【隐式】和Lock【显式】都是可重入锁)
synchronized版
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName() + " sms");
call();
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName() + " call");
}
}
Lock版
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone2{
private final Lock lock = new ReentrantLock();
public void sms(){
lock.lock(); //Lock锁必须配对,有加锁就必须有解锁! 否则就会死锁!
try {
System.out.println(Thread.currentThread().getName() + " sms");
call();
} finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " call");
} finally {
lock.unlock();
}
}
}
3.自旋锁
spinlock
自定义自旋锁:
public class spinlock {
// int 0
// Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " ==> myLock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " ==> myUnLock");
atomicReference.compareAndSet(thread,null);
}
}
测试自定义自旋锁:
public class TestSpinLock {
public static void main(String[] args) {
// Lock lock = new ReentrantLock();
// lock.lock();
// lock.unlock();
// 底层使用的自旋锁CAS
spinlock spinlock = new spinlock();
new Thread(() -> {
spinlock.myLock();
try {
TimeUnit.SECONDS.sleep(4);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlock.myUnLock();
}
}, "T1").start();
new Thread(() -> {
spinlock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlock.myUnLock();
}
}, "T2").start();
}
}
结果分析:
两个线程共同操作一把锁,谁先拿到锁谁先执行。T1线程先拿到锁加锁,其次是T2线程,先输入第一行再输出第二行;T1线程4s后释放锁,随之T2线程拿到锁加锁进行操作,3s后释放锁。故:先输入第一二行,4s后输出第三行,3s后输出第四行。
4.死锁
什么是死锁?两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法在执行下去。
两个线程拿着自己锁不放的同时,试图获取对方的锁,就会造成死锁
/**
* 死锁样例
*/
public class DeadLockDemo {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 获取到锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("获取到锁b");
}
}
}, "A").start();
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 获取到锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println("获取到锁a");
}
}
}, "B").start();
}
}
产生死锁的原因:
第一:系统资源不足
第二:进程运行推进顺序不合适
第三:资源分配不当
如何定位死锁,解决问题?
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
2、使用jstack 进程进程号 找到死锁信息(jstack是jvm中自带的堆栈跟踪工具)
命令:jstack 进程号
22.CAS理解
乐观锁:在并发下对数据进行修改时保持乐观的态度,认为在自己修改数据的过程中,其他线程不会对同一个数据进行修改,所以不对数据加锁,但是会在最终更新数据前,判断一下这个数据有没有被修改,若没有被修改,才将它更新为自己修改的值;
版本号机制
悲观锁:在并发下对数据进行修改时保持悲观的态度,认为在自己修改数据的过程中,其他线程也会对数据进行修改,所以在操作前会对数据加锁,在操作完成后才将锁释放,而在释放锁之前,其他线程无法操作数据;
CAS其实就是乐观锁的一种实现方式,而悲观锁比较典型的就是Java中的synchronized。下面我就来详细介绍一下CAS的相关概念。
1.什么cas
CAS全称compare and swap——比较并替换,它是并发条件下修改数据的一种机制,包含三个操作数:
需要修改的数据的内存地址(V);
对这个数据的旧预期值(A);
需要将它修改为的值(B);
CAS的操作步骤如下:
修改前记录数据的内存地址V;
读取数据的当前的值,记录为A;
修改数据的值变为B;
查看地址V下的值是否仍然为A,若为A,则用B替换它;若地址V下的值不为A,表示在自己修改的过程中,其他的线程对数据进行了修改,则不更新变量的值,而是重新从步骤2开始执行,这被称为自旋;
通过以上四个步骤对内存中的数据进行修改,就可以保证数据修改的原子性。CAS是乐观锁的一种实现,所以这里介绍的步骤和乐观锁的定义差不多,还是很好理解的。
2.java的使用
Java中大量使用的CAS,比如,在java.util.concurrent.atomic包下有很多的原子类,如AtomicInteger、AtomicBoolean…这些类提供对int、boolean等类型的原子操作,而底层就是通过CAS机制实现的。比如AtomicInteger类有一个实例方法,叫做incrementAndGet,这个方法就是将AtomicInteger对象记录的值+1并返回,与i++类似。但是这是一个原子操作,不会像i++一样,存在线程不一致问题,因为i++不是原子操作。比如如下代码,最终一定能够保证num的值为200:
// 声明一个AtomicInteger对象
AtomicInteger num = new AtomicInteger(0);
// 线程1
new Thread(()->{
for (int i = 0; i < 100; i++) {
// num++
num.incrementAndGet();
}
}).start();
// 线程2
new Thread(()->{
for (int i = 0; i < 100; i++) {
// num++
num.incrementAndGet();
}
}).start();
Thread.sleep(1000);
System.out.println(num);
我们看看incrementAndGet方法的源码:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
这里使用了一个unsafe对象,而unsafe对象是什么呢?我们知道,Java并不能像C或C++一样,直接操作内存,但是JVM为我们提供了一个后门,就是sun.misc.Unsafe类,这个类为我们实现了很多硬件级别的原子方法,当然,这些方法都是native方法,使用其他语言实现,而不是Java方法。而上面的另外一个变量valueOffset就是我们需要修改的变量在内存中的偏移量。也许上面这个方法并不能让你感觉使用了CAS,那再看看下面这个方法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSet是AtomicInteger的另一个方法,它的作用就是给定一个预期的旧值expect,以及需要更新为的值update,若当前变量的值是expect,就将其修改为update,否则不修改(这不就是CAS的思想吗)。而它底层调用了unsafe对象的compareAndSwapInt方法,从这个名字可以看出,它的实现使用的就是CAS。compareAndSwapInt的三个参数valueOffset、expect以及update,刚好对应了CAS操作的三个操作数。
3.CAS机制的ABA问题
CAS机制虽然简单,但是也存在一些缺陷,其中比较典型的就是ABA问题。什么是ABA问题,我简单介绍一下:
假设有三个线程T1、T2和T3,它们都要对一个变量num的值进行修改,且使用的都是CAS机制进行同步,假设num的初始值为100;
线程T1首先读取了num的值,将它记录为旧预期A1 = 100,然后它想要将num的值修改为80,记录B2 = 80,在执行num = B2前,线程发生了切换,切换到线程T2;
假设T2毫无阻碍地修改了num的值,将它从100修改为80,然后线程再度切换,T3开始执行;
T3也是毫无阻碍地修改了num,将它从80重新修改为100,线程再次切换回T1;
T1从上次运行的断点恢复,也就是准备用B1的值覆盖num,但是由于CAS机制,它需要先检测num的值是否等于它记录的预期值A1,然后它发现A1 = num = 100,认为num没有被修改过,于是用B1覆盖了num;
上面这种情况就是CAS的ABA问题:一个变量被修改,但是又被改了回去,在CAS机制中,将无法察觉这种错误的现象。在线程T1被中断的过程中,num的值被修改,按照CAS的原则,T1应该放弃对num的修改,从头开始执行。有人可能想问,修改回去之后,不就和没修改一样吗,有什么影响呢?其实我也不知道有什么影响…找遍网上的博客,举的例子都是在扯淡,误人子弟(看的最多的例子就是那个银行取钱的,一个人乱写,其他人乱copy,真心服了)。如果有知道ABA问题影响的朋友,麻烦告知一下。
对于ABA问题的解决方案也非常简单,那就是再添加一个变量——版本号。每个变量都加上一个版本号,在它被修改时,也同步修改版本号,而CAS操作在修改前记录版本号,若在最后更新变量时,记录的版本号与当前版本号一致,表示没有被修改,可直接更新。
(1)优点
前面也提到过,CAS是一种乐观锁,其优点就是不需要加锁就能进行原子操作;
(2)缺点
CAS的缺点主有两点:
CAS机制只能用在对某一个变量进行原子操作,无法用来保证多个变量或语句的原子性(synchronized可以);
假设在修改数据的过程中经常与其他线程修改冲突,将导致需要多次的重新尝试;
(3)适用场景
由上面分析的优缺点可以看出,CAS适用于并发冲突发生频率较低的场合,而对于并发冲突较频繁的场合,CAS由于不断重试,反倒会降低效率。
23.TheadLocal原理
ThreadLocal是什么?
线程本地变量如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题
//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
由结构图是可以看出:
Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
如何「解决内存泄漏问题」?使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal的应用场景
数据库连接池
会话管理中使用