面向对象
面向过程和面向对象
面向过程:oriented-procedure,执行者思维,关注问题的实现步骤(怎么实现),适合简单、不需要协作、关注如何执行的事务
面向对象:oriented-object,设计者思维,从整体上分析系统,但实现部分依然依靠面向过程处理,适合解决复杂、需要协作的问题
面向对象离不开面向过程
构造器(构造方法)
- 构造器通过new关键字调用,调用的时候用来初始化值,此时对象已经创建好
- 构造器有返回值,但不能定义返回类型,不能在构造器里用return
- 编译器会自动定义一个无参构造器,如果自定义了一个,编译器不会自动添加
- 构造器方法名称必须和类名一致
this关键字
this的本质是创建好的对象的地址
创建对象的步骤:
- 分配对象空间,成员变量初始化为0或空
- 执行构造方法,属性值显式初始化
- 返回对象的地址
this的用法
- 避免二义性,用this指明当前对象;普通方法中this指向调用该方法的对象;构造方法中this指向正要初始化的对象
- 调用重载的构造方法,避免相同的初始化代码,只能在构造方法中用并且必须位于第一句
- this不能用于static方法中,因为static方法不从属对象,this指代的是当前对象
static关键字
静态属性和静态方法属于类,非静态属性属于对象;
静态方法中不能调用非静态方法;
静态初始化块用于类的初始化,在静态初始化块中不能直接访问非静态成员
局部变量、成员变量、静态变量
类型 | 声明位置 | 从属于 | 生命周期 | 内存位置 |
---|---|---|---|---|
局部变量 | 方法或语句块内部 | 方法/语句块 | 声明位置开始到方法或语句块执行完毕 | 方法的栈帧中 |
成员变量(实例变量) | 类内部,方法外部 | 对象 | 对象创建开始到对象消失结束 | 堆里对象的内存位置 |
静态变量 (类变量) | 类内部,static修饰 | 类 | 类加载开始,类卸载结束 | 方法区 |
包机制
通过package解决类同名和类管理问题,package通常是类的第一句非注释语句;包名由域名倒着写加上模块名
注意:com.lqr和com.lqr.test两个包没有包含关系
java常见包
- java.lang:包含核心类,String/Math/Integer/System/Thread,不需要导入可以直接使用
- java.awt:包含抽象窗口工具集的多个类,用来构建和管理GUI
- java.net
- java.io
- java.util:包含实用工具类,定义系统特性、日期
静态导入:导入指定类的静态属性,可以直接使用静态属性
//例如
package com.lqr
import static java.lang.Math.*;
import static java.lang.Math.PI;
public class Test{
public static void main(String args[]){
System.out.println(PI);
System.out.println(random());
}
}
内部类
定义在类内的类
作用:提供更好的封装,只让外部类访问
public class Outer {
int age=10;
private void show(){
System.out.println("lalala");
}
public class Inner{
int age=20;
public void showInner(){
System.out.println(age);//调用的是自己的属性
System.out.println(Outer.this.age);//调用外部类的重名属性
show();//调用外部类的方法
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.showInner();
}
}
- 非静态内部类:必须寄存在外部类对象里,可以直接访问外部类成员,不能有静态属性和静态方法
- 静态内部类:看作外部类的静态成员,只能访问外部类的静态成员
- 匿名内部类:适合只需要使用一次的类,比如键盘监听操作,在安卓开发、swing开发常见
- 局部内部类:定义在方法内
面向对象三大特性
继承
作用:代码复用和类的扩展;对事务建模
注意:
- java只有单继承,c++有多继承
- java的类没有多继承,但接口有多继承
- 子类继承父类的全部属性和除构造器外的所有方法,但不能直接访问父类的私有属性和私有方法
- 定义类如果没有显式extends,则父类为java.lang.Object
instanceof运算符
对象 instanceof 类,判断对象是否是类或子类所创建的
重写override
- ==:方法名、形参列表相同
- <=:返回类型和声明异常类型,子类<=父类
-
=:方法权限修饰符(public/protected/private),子类>=父类
final
作用:
- 修饰变量不可改变
- 修饰方法不可重写(但可以重载)
- 修饰类不能被继承,例如Math/String
组合
实现代码复用的方法:继承和组合,组合更加灵活
组合:将父类对象作为子类属性,子类通过调用属性来获得父类属性和方法
public class Student extends Person{
Person p;
int grade;
Student(int grade, int age, String name){
this.grade = grade;
this.p.age = age;
this.p.name = name;
}
}
继承:is a
组合:has a
Object
toString
IDEA快捷键: 类的结构视图(alt+7)
System.out.println(对象)就是打印一个对象的类别+地址
重写toString()方法可以在类里面自定义打印的内容
@Override
public String toString() {
return "Student{" +
"grade=" + grade +
", name='" + name + '\'' +
", age=" + age +
", money=" + money +
'}';
}
==和equals
Object的equals方法是用==判断两个对象地址是否一样(是不是同一个对象);
重写equals方法,用来判断两个对象是不是逻辑上相等:右键constructer生成
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person p = (Person) o;
return id == p.id;
}
super
super可以看作是直接父类对象的引用,调用super可以访问父类中被子类覆盖的方法或属性
构造方法的第一行若没有显式调用super()或this(),默认调用super来调用父类的无参构造方法
public abstract class Person {
String name;
int age;
int id;
public Person(int id) {
this.id = id;
System.out.println("person constructing");
}
}
public class Student extends Person{
int grade;
public Student(int id) {
super(id);
System.out.println("constructing student");
}
}
public static void main(String[] args) {
new Student(1000);
}
//person constructing
//constructing student
继承树
绘制类的继承树:类文件右键->show Diagram->右键->show implements
封装
高内聚、低耦合
优点:提高代码安全性;提高代码复用性;高内聚(便于修改代码,提高可维护性);低耦合(简化外部调用,便于协作扩展)
封装的实现——访问控制符
修饰符 | 同一个类 | 同一个包 | 子类 | 所有类 |
---|---|---|---|---|
private | * | |||
default | * | * | ||
protected | * | * | * | |
public | * | * | * | * |
同一个包内,子类可以访问父类和父类对象的protected
不同包内,子类可以通过super访问父类的protected,但不能访问父类对象的protected
开发中封装的简单规则——javabean
- 属性一般使用private,并提供public get/set方法访问(boolean类型的get方法是is开头)(IDEA中直接输入set/get可以快捷生成)
- 方法只用于本类的用private修饰,希望其他类调用的用public
多态
同一个方法调用,对象不同可能会有不同行为
注意:
- 多态是方法的多态,与属性无关
- 多态存在的必要条件:继承、重写、父类引用指向子对象并调用子类重写的方法
public class Person {
public void rest(){
System.out.println("person rest");
}
}
public class Teacher extends Person{
@Override
public void rest(){
System.out.println("teacher rest");
}
}
public class Student extends Person{
@Override
public void rest(){
System.out.println("student rest");
}
static void rest(Person p){
p.rest();
}
public static void main(String[] args) {
Person p = new Person();
Student s = new Student();
Teacher t = new Teacher();
p.rest();//person rest
s.rest();//student rest
t.rest();//teacher rest
p = new Student();
p.rest();//student rest
rest(s);//student rest
rest(t);//teacher rest
}
}
对象转型
抽象方法和抽象类
抽象方法:只有声明,没有方法体,只定义规范,非抽象子类必须实现
抽象类:
- 有抽象方法的类必须是抽象类
- 抽象类不能实例化(不能用new)
- 抽象类可以包含属性、方法、构造方法,但构造方法只能被子类调用
- 抽象类只能用来被继承
- 非抽象子类必须实现抽象方法
接口
比抽象还抽象,全面实现了规范和实现的分离
普通类都是具体实现;抽象类既有具体实现也有抽象方法;接口都是抽象方法(JDK8以后也有具体实现)
JDK8以前
接口的方法都是public abstract,属性都是public static final
接口可以多继承
JDK8以后
允许在接口内定义默认方法(扩展方法)和静态方法
default关键字声明默认方法,可以有具体实现,类通过实现接口继承和重写默认方法,通过对象调用
接口中的静态方法从属于接口(接口也是特殊的类),通过接口名调用
若子类中定义了同名的静态方法,则该方法从属于子类,通过子类名调用(不叫重写)
多继承
A接口继承B接口和C接口(extends),D类实现A接口时(implements)需要实现ABC接口的所有方法
JVM
JVM内存分析
不含参数的函数有一个隐式参数为this,在调用时会在虚拟机栈中新建一个栈帧,存储所需的变量
若栈中变量指向对象,对象存储在堆中
若对象的属性不是基本类型,则指向方法区中属于这个类的区域中,方法体也存在这里
方法调用结束后,从栈中退出
创建对象时,调用的构造方法也会新开辟一块栈帧,创建结束后退出
方法区是一种规范,可以有不同实现,jdk7/jdk8之前和之后都是不同的
垃圾回收机制garbage collection
java的内存管理很大程度指的是堆中的对象管理
对象空间的分配:使用new关键字创建对象
对象空间的释放:赋值null,垃圾回收器负责回收所有不可达对象的内存空间
垃圾回收算法
引用计数法
堆中每个对象对应一个引用计数器,引用计数器的值为0时,垃圾回收器认为该对象是无用对象并进行回收
优点:算法简单
**缺点:**无法识别循环引用的无用对象
引用可达法(根搜索算法)
将所有引用关系看作一张图,从根节点开始遍历引用的节点,结束后没有被访问过的节点就是没有被引用的节点,即无用的节点
分代垃圾回收机制
将对象分为年轻代、年老代、永久代,并放到不同区域
JVM将堆内存划分为Eden、Survivor和Tenured/Old空间
- 年轻代:新生成的对象先放在Eden区,由minor GC回收;垃圾回收后依然存在的就将对象存放到survivor1区域,再满就存到survivor2区域,循环回收两个survivor区域
- 年老代:在年轻代中经历了15次垃圾回收后依然存活的对象;由major GC和full GC回收,全面清理年轻代和年老代区域
- 永久代:存放静态类或静态方法;JDK7以前永久代就是一种方法区的实现,JDK8以后没有永久代,使用metaspace元数据空间和堆代替
- minor GC:用于清理年轻代区域,Eden区满了就触发一次minor GC,清理无用对象,将有用对象复制到survivor1和survivor2
- major GC:清理年老代区域
- full GC:清理年轻代、年老代区域;成本高,对系统性能会有影响
JVM调优和full GC
导致full GC的原因
- 年老代tenured被写满
- 永久代perm被写满
- System.gc()被显式调用(只是通知JVM,申请启动full GC,并不是直接调用GC,尽量少用,成本高,影响性能);finaliza方法,释放对象或资源的方法,类似析构函数,尽量少用)
容易造成内存泄露的操作
- 创建大量无用对象
//例子
String str = ""
for(int i=0; i<10000; i++){
str += i;//相当于产生了10000个string对象
}
- 静态集合类的使用,例如HashMap/Vector/List
- 各种连接对象未关闭,例如IO流对象/数据集连接对象/网络连接对象
- 监听器,例如在释放对象时没有删除相应监听器
字符串三兄弟
String
不可变字符序列,位于java.lang包,java默认导入
java字符串是unicode字符序列
java没有内置字符串类型,String只是一个预定义的类,字符串是String的一个实例
创建
//字符串内容存在方法区中的常量池里
String s = new String("aaa");//s指向堆中的对象指向常量池
String ss = "aaa";//ss直接指向方法区内的常量池
System.out.println(s==ss);//false
System.out.println(s.equals(ss));//true
方法
String是不可变字符序列,所有替换、截取、去空格、转换大小写都是生成了新的字符串
private final char value[];
String s1 = "hello"+" java";//编译器在编译时直接拼接字符串
String s2 = "hello java";
System.out.println(s1==s2);//true
String s3 = "hello";
String s4 = " java";
String s5 = s3+s4
System.out.println(s5==s2);//false
StringBuilder
可变字符序列,效率高但线程不安全,最常用
StringBuffer
可变字符序列,效率低但线程安全
使用陷阱
String ss = "";
long num1 = Runtime.getRuntime().freeMemory();
long time1 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
ss+=i;
}
long num2 = Runtime.getRuntime().freeMemory();
long time2 = System.currentTimeMillis();
System.out.println("内存占用:"+(num1-num2));
System.out.println("耗时:"+(time2-time1));
StringBuilder sb = new StringBuilder("");
num1 = Runtime.getRuntime().freeMemory();
time1 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
sb.append(i);
}
num2 = Runtime.getRuntime().freeMemory();
time2 = System.currentTimeMillis();
System.out.println("内存占用:"+(num1-num2));
System.out.println("耗时:"+(time2-time1));
需要拼接字符串时一定使用StringBuilder
数组
特点:
- 长度确定,一旦创建,无法改变大小
- 元素类型相同
- 可以是任意类型
- 数组变量属于引用类型,数组也是对象
初始化类型:默认初始化;静态初始化;动态初始化
数组的长度length是属性,String的长度length()是方法
增强for循环
for(int i:a)
遍历过程不能修改元素值,只适合读取
拷贝
System类中的arraycopy(src, scr_start, targ, targ_start, length)方法
int[] a = {0,8,67};
int[] b = {1,2,3,4};
System.out.println(Arrays.toString(b));
System.arraycopy(a, 0, b, 1, 3);
System.out.println(Arrays.toString(b));
Arrays
打印:System.out.println(Arrays.toString(b));
排序:Arrays.sort(b)
赋值:Arrays.fill(a, start, ends, value)
二分查找:Arrays.binarySearch(a, value)
多维数组
int[][] a =new int[3][];
a[0] = new int[2];
a[1] = new int[3];
a[2] = new int[4];
存储表格数据
Object数组
Object[] o = {100, "li", 2022-09-07, "arefeasdc"}
Object[][] os = new Object[8][];
os[0] = o
javabean和数组
- 定义javabean,包括属性、get/set方法、构造器
- 构造对象
- 创建数组并初始化
comparable接口
public int compareTo(Object obj)
将当前对象和obj对象比较,大于返回1,等于返回0,小于返回-1
包装类
包装类均位于java.lang包中
Number类是抽象类,抽象方法intValue~doubleValue由数字类实现,可以实现数字类型的互相转换
Integer a = new Integer(2);//不推荐
Integer b = Integer.valueOf(2);//基本类型转化为对象
int a = b.intValue();//对象转化为基本类型
Double d = b.doubleValue();//Interger转化为Double
Integer c = Integer.parseInt("123");//String转化为Integer
Integer cc = new Integer("222");
String str = b.toString();//Integer转化为String
System.out.println(Integer.MAX_VALUE);//能表示的最大值
自动装箱和拆箱
JDK1.5以前,Integer a = 2
是错误的
JDK1.5以后,JVM执行了Integer a = Integer.valueOf(2)
,将基本类型转化为包装类,称为自动装箱
int b = a
,JVM执行了int b = a.intValue()
,称为自动拆箱
包装类的缓存问题
Integer a1 = Integer.valueOf(4000);
Integer a2 = Integer.valueOf(4000);
Integer b1 = Integer.valueOf(22);
Integer b2 = Integer.valueOf(22);
System.out.println(a1==a2);//false
System.out.println(b1==b2);//true
System.out.println(a1.equals(a2));//true
当数字在-128~127时,返回的是缓存数组中的某个元素,因此b1和b2指向的同一个对象
常用类
Date类
1970年1月1日00:00:00作为基准时间,单位是毫秒
long类型表示时间
long a = Long.MAX_VALUE/365/24/60/60/1000L;
System.out.println(a);//大约能表示2.9亿年
long time = System.currentTimeMillis();//当前时刻毫秒数
java.util.Date
要点:new Date() getTime()
Date d1 = new Date();//默认当前时刻
System.out.println(d1);//Thu Dec 01 19:51:48 CST 2022
System.out.println(d1.getTime());//1669895508068
Date d1 = new Date(1000L*3600*24*365*30);
System.out.println(d1);//Sat Dec 25 08:00:00 CST 1999
System.out.println(d1.getTime());//946080000000
DataFormat和java.text.SimpleDateFormat
实现时间对象和字符串的互相转化:DateFormat是抽象类,一般用SimpleDateFormat
- 规定时间格式 new SimpleDateFormat()
- 字符串转成Date:Date d = df.parse(str),需要抛出异常
- Date转成字符串:String s = df.format(new Date())
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat df2 = new SimpleDateFormat("yyyy年MM月dd日");
String time = df.format(new Date());
System.out.println(time); //2022-12-01 20:01:03
System.out.println(df2.format(new Date()));//2022年12月01日
System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));//08:01:03
try {
Date d = df.parse("1999-10-30 09:24:35");
System.out.println(d); //Sat Oct 30 09:24:35 CST 1999
} catch (ParseException e) {
throw new RuntimeException(e);
}
SimpleDateFormat df = new SimpleDateFormat("今天是今年的第w周,这个月的第W周," +
"今年的第D天,这个月的第d天,");
System.out.println(df.format(new Date()));//今天是今年的第49周,这个月的第1周,今年的第335天,这个月的第1天
Calendar类
关于日期计算的相关功能
Calendar是抽象类,子类为GregorianCalendar
月份用0~11表示,Calendar中用常量JANUARY/FEBRUARY…表示
- new GregorianCalendar对象,指定年月日时分秒
- get()方法获得Calendar属性
- new GregorianCalendar对象,未指定年月日时分秒
- set()方法设置Calendar属性
- add()方法计算日期
- 日历对象转时间对象:Date d = calendar.getTime()
- 时间对象转日历对象:g.setTime(new Date())
GregorianCalendar g = new GregorianCalendar(1999,10,30,9,30,27);
System.out.println(g.get(Calendar.YEAR));
System.out.println(g.get(Calendar.MONTH));
System.out.println(g.get(Calendar.DAY_OF_MONTH));
System.out.println(g.get(Calendar.DATE)); //与Calendar.DAY_OF_MONTH同义
System.out.println(g.get(Calendar.DAY_OF_WEEK)); //周日-周六依次为1-7
g.set(Calendar.YEAR, 2022);
g.set(Calendar.MONTH, Calendar.DECEMBER);
g.add(Calendar.YEAR, 2);
g.add(Calendar.MONTH, -2);
Math类
Math.abs() | Math.sqrt() | Math.asin() | Math.sin() | Math.pow() | Math.min() | Math.PI | Math.E |
---|---|---|---|---|---|---|---|
Math.ceil() | 比a大的最小整数,返回double | Math.floor() | 比a小的最大整数 | Math.round() | 四舍五入 | Math.random() | 产生[0,1)之间的随机小数 |
Random类
Random r = new Random();
System.out.println(r.nextDouble());//[0,1)
System.out.println(r.nextInt());//int允许范围内
System.out.println(r.nextInt(10));//[0,10)
System.out.println(r.nextInt(10)+9);//[9,19)
System.out.println(r.nextBoolean());
java.io.File类
获得项目所在路径:
System.out.println(System.getProperty("user.dir"));
- new File(“”)创建对象
- createNewFile()、exist()、isDirectory()、isFile()、lastModified()、length()、getName()、getPath()、delete()、mkdir()、mkdirs()、listFiles()
递归打印目录树
public static void main(String[] args) {
File f = new File("D:\\myfiles\\数模");
printf(f, 0);
}
public static void printf(File f, int level){
for (int i = 0; i <= level; i++) {
System.out.print("-");;
}
System.out.println(f.getName());
if(f.isDirectory()){
File[] fs = f.listFiles();
for (File ff:fs) {
printf(ff, level+1);
}
}
}
enum
需要定义一组常量时使用
枚举本质上是类,每个成员就是一个实例,默认public static final修饰
enum Season{SPRING, SUMMER, AUTUMN, WINTER}
System.out.println(Season.SPRING);//SPRING
for(Season s:Season.values()){
System.out.println(s);
}
泛型
数据类型参数化,调用泛型时必须传入实际类型
泛型类,泛型接口,泛型方法
类型擦除:编码时采用泛型写的类型参数,编译器在编译时会去掉,编译后的class不包括泛型定义,会被编译器替换成具体的Object
注意
- 基本类型不能用于泛型,应该使用包装类
- 不能通过泛型T创建对象,即
T test = new T()
是错误的
使用
定义:泛型字符可以是任何标识符,一般采用E,T,K,V,N,?
泛型类
public class 类名<泛型标识符>{}
public class Gener<T> {
private T flag;
public T getFlag() {
return flag;
}
public void setFlag(T flag) {
this.flag = flag;
}
public static void main(String[] args) {
Gener<String> gen = new Gener<>();
gen.setFlag("flag");
}
}
泛型接口
public interface 类名<泛型标识符>{}
public interface Gener<T> {
T getName(T name);
}
public class genrimpl implements Gener<String>{
@Override
public String getName(String name) {
return name;
}
public static void main(String[] args) {
genrimpl g = new genrimpl();
g.getName("name");
Gener<String> gen = new genrimpl();//使用泛型接口时指定类别
gen.getName("name");
}
}
泛型方法
将方法的参数定义成泛型,调用时接收不同类型的参数
非静态泛型方法既可以自己定义,也可以通过泛型类定义
非静态
public <T> void xxx(T xxx){}
public <T> T xxx(T xxx){}
public class Gener{
public <T> void setName(T name){
System.out.println(name);
}
public <T> T getName(T name){
return name;
}
public static void main(String[] args) {
Gener g = new Gener();
g.setName("lqr");
g.setName(123);
Gener g2 = new Gener();
String n = g2.getName("lqr");
System.out.println(n);
Integer nn = g2.getName(123);
System.out.println(nn);
}
}
静态方法
静态方法不能使用类上定义的泛型
public static <T> void xxx(T xxx){}
public class Gener{
public static <T> void test(T name){
System.out.println(name);
}
public static <T> T getFlag(T flag){
return flag;
}
public static void main(String[] args) {
Gener.test(123);
Boolean f = Gener.getFlag(true);
}
}
可变参数
public <T> void xxx(T... args){}
public class Gener{
public <T> void test(T... args){
for(T t:args){
System.out.println(t);
}
}
public static void main(String[] args) {
new Gener().test(123, "lllaaa", 1.2);
}
}
通配符
无界通配符
"?“表示类型通配符,代替具体类型,只能在”<>"中使用,可以解决类型不确定时候的问题
public void xxx(Generic<?> gen){}
public class Gener<T>{
private T flag;
public T getFlag() {
return flag;
}
public void setFlag(T flag) {
this.flag = flag;
}
public void showFlag(Gener<?> gen){
System.out.println(gen.getFlag());
}
public static void main(String[] args) {
Gener<String> g1 = new Gener<>();
g1.setFlag("20");
new Gener<>().showFlag(g1);
Gener<Integer> g2 = new Gener<>();
g2.setFlag(20);
new Gener<>().showFlag(g2);
}
}
上限限定
通配符的类型是T或T的子类,T接口或T接口的子接口,适用于泛型类
public void XXX(Generic<? extends Number> gener){}
下限限定
通配符类型是T或T的父类,T接口或T接口的父接口,不适用于泛型类
public void XXX(Gneric<? super Integer> gen){}
容器
单例集合——Collection接口
数据一个一个地存储
抽象方法:
List接口
存储有序,可重复,动态数组
List接口中增加的方法(因为list是有序的,添加的方法都是跟元素位序有关的):
ArrayList类
ArrayList类是List接口的实现类,底层是用数组实现的存储
特点:查询效率高,增删效率低,线程不安全
增删改查清
//实例化容器 接口=new 实现类
List<String> arrlist = new ArrayList<String>();
boolean flag = arrlist.add("test1");
arrlist.add(0,"test2");//没有返回值,索引数值不能大于元素个数
System.out.println(flag);//true
arrlist.remove(1);//返回移除的值;同arrlist.remove("test1"),返回boolen
String s = arrlist.set(0, "lalala");//替换,不能添加,Index不能超过长度
System.out.println(arrlist.contains("lalala"));
arrlist.clear();
System.out.println(arrlist.isEmpty());
System.out.println(arrlist);
转换为数组
- 转换为Object数组:
Object[] ss = arrlist.toArray();//不能使用String[]强制转换,会出现异常
- 转换为泛型类型数组:
String[] ss = arrlist.toArray(new String[arrlist.size()]);
并集、交集、差集
List<String> A = new ArrayList<String>();
A.add("a");
A.add("b");
A.add("c");
List<String> B = new ArrayList<String>();
B.add("d");
B.add("b");
B.add("c");
A.addAll(B);
System.out.println(A);//[a, b, c, d, b, c]
A.removeAll(B);
System.out.println(A);//[a]
A.retainAll(B);
System.out.println(A);//[]
源码分析
JDK1.7中使用立即加载,初始化时分配默认10个大小的空间(长度为10),JDK1.8之后使用延迟加载,初始化默认为空(长度为0)
private static final int DEFAULT_CAPACITY = 10;//创建时的默认长度
transient Object[] elementData;//元素操作存放的数组
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {//无参构造
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
Vector类
底层用数组实现,线程安全但效率低,大部分与ArrayList类似
源码分析
- 立即初始化的方式,初始化长度为10
public synchronized boolean add(E e) {}
,add()方法有并行化处理,是线程安全的- ArrayList扩容时是1.5倍,Vector是2倍
Stack容器
Vector的一个子类
通过5个方法对Vector进行扩展,实现堆栈操作
empty()、peek()、pop()、push()、search()
Stack<Integer> stack = new Stack<>();//不再是用List定义
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop());
System.out.println(stack.empty());
System.out.println(stack.search(2));
System.out.println(stack.search(9));
System.out.println(stack.peek());
System.out.println(stack);
Stack匹配括号合法性
public static boolean sysm(String s){
Stack<Character> stack = new Stack<>();
for(int i = 0; i<s.length(); ++i){
char c = s.charAt(i);
if(c=='(')
stack.push(')');
if(c=='[')
stack.push(']');
if(c=='{')
stack.push('}');
if(c==')'||c==']'||c=='}'){
if(stack.empty()||c!=stack.peek())
return false;
else
stack.pop();//匹配的字符需要出栈
}
}
return stack.empty();//匹配结束后需要满足栈空
}
LinkedList类
底层使用双向链表实现,查询效率低,增删效率高,线程不安全
使用与ArrayList基本一致,额外添加的方法使用,需要在声明时使用LinkedList变量
方法 | 说明 |
---|---|
void addFirst(Ee) | 将指定元素插入到开头 |
void addLast(E e) | 将指定元素插入到结尾 |
getFirst() | 返回此列表的第一个元素 |
getLast() | 返回此列表的最后一个元素 |
removeFirst() | 移除此列表中的第一个元素,并返回这个元素 |
removeLast() | 移除此列表中的最后一个元素,并返回这个元素 |
E pop() | 从此列表所表示的堆栈处弹出一个元素,等效于removeFirst |
void push(E e) | 将元素推入此列表所表示的堆栈这个等效于addFisrt(E e) |
boolean isEmpty() | 判断列表是否包含元素,如果不包含元素则返回true |
源码分析
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
Eitem;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
指定位置添加元素时寻找元素位置——二分:
寻找索引位置时先对size进行>>1,与index进行大小比较;
若index在前半部分从前往后找,否则从后往前找
Set接口
存储无序,不可重复,“集合”,List接口有相比Collection新增的方法,但Set没有
HashSet类
允许null元素出现,采用哈希算法实现,底层用HashMap实现,查询和增删效率都高
Set<String> set = new HashSet<>();
存储特征
- 不保证元素顺序底层用数组和链表实现,底层数组创建默认长度为16,对元素哈希值进行运算决定元素在数组中的位置
- 没有重复元素:哈希值相同的元素利用equals方法判断是否相同,相同则不添加,不同则用单向链表保存
- 允许null元素、线程不安全
HashSet存储自定义对象
HashSet存储两个属性相同的对象时,无法分别出这是两个相同的元素,因为二者的HashCode值不同;因此需要在类中重写equals方法
源码分析
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
TreeSet类
可以对元素进行排序,实际是用TreeMap实现的,通过key存储Set的元素
排序规则:元素自身比较或通过比较器比较
- 元素自身比较:存储自定义类时实现Comparable接口的CompareTo(o1)方法
- 比较器:新建一个比较器实现Comparator接口,重写compare(o1, o2)方法,new TreeSet时将比较器对象作为参数进行初始化
源码分析
TreeSet是通过实现NavigableMap间接实现TreeMap
private transient NavigableMap<E, Object> m;
private static final Object PRESENT = new Object();
双例集合——Map接口
基于key+value的结构存储数据,类似函数f(x)
不能包含重复的键,可以包含重复的值,每个键只能对应一个值
方法 | 说明 |
---|---|
V put (K key,V value) | 把key与value添加到 Map集合中,若key已存在,则更新对应的value |
void putAll(Map m) | 从指定Map中将所有映射关系复制到此Map中,key相同的value值会被更新 |
V remove (Object key) | 删除 key对应的value |
v get(Object key) | 根据指定的key,获取对应的value |
boolean containsKey(Object key) | 判断容器中是否包含指定的key |
boolean containsValue(Object value) | 判断容器中是否包含指定的value |
Set keySet() | 获取Map集合中所有的 key,存储到Set集合中 |
Set<Map.Entry<K,V>> entrySet() | 返回一个Set基于Map.Entry类型包含Map中所有映射。 |
void clear() | 删除Map中所有的映射 |
HashMap类
Map接口的实现类,采用哈希算法实现,查删改效率都很高
Map<String, Integer> map = new HashMap<>();
- keySet()获取所有元素:
Set<String> keys = map.keySet();
for(String s:keys){
String v = map.get(s);
}
- entrySet()获取元素:
Set<Map.Entry<String, Integer>> entryset = map.entrySet();
for(Masp.Entry<String, Integer> entry:entryset){
String key = entry.getKey();
Integer value = entry.getValue();
}
源码分析
底层采用哈希表,本质是数组+链表
JDK1.8特性:链表长度大于8时,会转换为红黑树来减少查询时间;红黑树节点小于6时转换为链表
static final int DEFAULT_INITIAL_CAPACITY=1<<4;//16,数组初始长度
static final int MAXIMUM_CAPACITY=1<<30;//数组长度上限
static final float DEFAULT_LOAD_FACTOR=0.75f;//扩容负载因子,当数组达到这个长度时进行扩容,13
static final int TREEIFY_THRESHOLD=8;//转换为红黑树的阈值
static final int UNTREEIFY_THRESHOLD=6;//转换为链表的阈值
static final int MIN_TREEIFY_CAPACITY=64;//数组长度达到64才会将链表转为红黑树
transient int size;
transient Node<K,V>[] table;
链表使用Node类,红黑树使用TreeNode类,继承Node类
TreeNode类继承Entry类继承Node类实现Entry接口;
数组初始化采用延迟初始化,等有内容了再调用resize()方法扩容和初始化数组;
计算hash值:
- 调用key.hashCode()获得key的hashcode值
- 根据hashcode值计算hash值,要求在[0, length-1]之间,计算方法:
hashcode%length,但对CPU来说除法效率低;
改进:要求数组长度是2的整数幂,hashcode^(hashcode>>>16)&(length-1)
添加元素:
调用put方法时,实际执行putVal方法,先利用key计算hash值,得到放置位置,再new Node对象,放在table数组对应位置上;若对应位置已有元素,判断是替换value还是添加节点;添加节点时需要判断是否要将链表转化为红黑树,判断是否需要扩容
扩容:
每次扩容数组长度double
TreeMap类
效率低于TreeMap,但可以对键值进行排序,底层基于红黑树实现
与TreeSet相同,可以是类实现Comparable接口或自定义比较器作为TreeMap的初始化参数
firstKey():获得最小key值
lastKey():获得最大key
Iterator迭代器
Collection接口继承了Iterator接口,接口中包括iterator抽象方法,返回一个Iterator对象,包含三个方法用于单例容器的迭代处理
- boolean hasNext():当前游标位置是否含有元素
- Object next():获取当前游标位置元素,并将游标后移
- void remove()
List<String> list = new ArrayList<>();
list.add("123");
list.add("13");
list.add("23");//set容器类似
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String s = iterator.next();
if(s.equals("123"))
iterator.remove();
}
System.out.println(list);
注意:
- 最好不要在循环中删除或添加List的元素,删除或添加后元素会移动,list的大小会改变,结果并不是删除或添加一个元素的结果,而会一直删或一直加
- 最好不要在一个循环中多次调用iterator.next()
Collections工具类
Collections是一个工具类,它提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。该类中所有的方法都为静态方法。
常用方法:
- void sort(List)//对List容器内的元素排序,排序的规则是按照升序进行排序。
- void shuffle(List)//对List容器内的元素进行随机排列。
- void reverse(List)//对List容器内的元素进行逆续排列。
- void fill(List, Object)l/用一个特定的对象重写整个List容器。
- int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。
List<String> list = new ArrayList<>();
list.add("z");
list.add("d");
list.add("a");
list.add("123");
list.add("32");
list.add("2");
Collections.sort(list);
Collections.shuffle(list);
System.out.println(list);
注解 java.annotaion和反射 java.Reflection
annotaion是JDK5.0引入的新技术
作用:对程序作出解释(类似注释);可以被其他程序(如编译器)读取
格式:“@注释名”,可以添加参数值
例如@SuppressWarnings(value="unchecked")
内置注解
元注解
作用:负责注解其他注解
自定义注解
public class annotation {
@myannotaion(age=22)
public void test(){
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface myannotaion{
String name() default "lqr";//注解的参数
int age();
}
静态语言 VS. 动态语言
反射
允许程序在执行期借助reflection API获得任何类的内部消息,并能直接操作任意对象的内部属性及方法
**作用:**在运行时判断任意一个对象所属的类、构造任意一个类的对象、判断类具有的成员变量和方法、获取泛型信息、调用对象的成员变量和方法、处理注解、生成动态代理
**优点:**实现动态创建对象和编译
**缺点:**影响性能
try {
Class c1 = Class.forName("com.lqr.basic.User");
Class c2 = Class.forName("com.lqr.basic.User");
System.out.println(c1);//class com.lqr.basic.User
//一个类在内存中只有一个Class对象
//一个类被加载后,类的整个结构会被封装在Class对象中
System.out.println(c1.hashCode()==c2.hashCode());//true
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
获取Class的方法
//1. 通过对象获得Class
People p = new People("111");
Class pc1 = p.getClass();
//2. forName获得Class
try {
Class pc2 = Class.forName("com.lqr.basic.People");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
//3. 通过类名.class获得
Class pc3 = People.class;
//4. 内置对象的TYPE属性
Class ic = Integer.TYPE;
拥有Class对象的类型: class、interface、数组、enum、annotaion、基本数据类型、void
类加载和内存分析
类的加载过程
类初始化
什么时候会初始化类
类的主动引用——一定会初始化类:
- 虚拟机启动,初始化main方法所在类;
- new一个类的对象;
- 调用类的静态成员和静态方法;
- java.lang.Reflect包的方法对类进行反射调用;
- 初始化类时若父类没有被初始化,则会先初始化它的父类
什么时候不会初始化类
类的被动引用:
- 访问静态域时,只有真正声明这个域的类才会被初始化(例如通过子类引用父类静态变量,只会初始化父类,不会初始化子类)
- 通过数组定义引用,不会初始化数组中的类
- 调用类中的常量
类加载器
作用:将class文件字节码内容加载到内存,在堆中生成这个类的java.lang.Class对象,
类型:根加载器(C++编写、无法直接获取)、扩展加载器、系统类加载器
双亲委派机制:若根加载器中含有的包,则不会调用扩展加载器中的同名包,以此类推
获取类的运行时结构
getName()
:获得包名+类名
getSimpleName()
:获得类名
getFields(); getField(String); getMethods(); getMethod(String, paramTypes)
:获得public属性或本类及父类的所有方法
getDeclaredFields(); getDeclaredField(String); getDeclaredMethods(); getDeclaredMethod(String, paramTypes)
:获得所有属性或本类的方法
getConstructors(); getDeclaredConstructors()
:获得构造器
动态创建对象
//newInstance()创建对象
Class c1 = Class.forName("com.lqr.basic.User");
User u1 = (User)c1.newInstance();//调用类的无参构造器
//通过构造器创建对象
Constructor con = c1.getDeclaredConstructor(String.class, int.class);
User u2 = (User)con.newInstance("lqr", 22);
//通过反射调用方法
User u3 = (User)c1.newInstance();
Method setName = c1.getDeclaredMethod("setName", String.class);
setName.invoke(u3, "lqr");//激活
//通过反射操作属性
User u4 = (User)c1.newInstance();
Field name = c1.getDeclaredField("name");
name.setAccessible(true);//私有属性或方法不能直接操作,需要关闭程序的安全检测
name.set(u4, "lqr");
反射调用多的话,关闭程序的安全检测能提高效率
反射操作泛型
java采用泛型擦除机制引入泛型,编译完成后与泛型相关的类型全部擦除
为了通过反射操作泛型,java新增了几种类型代表不能被归一到Class类中的类型:
- parameterizedType:参数化类型,如Map<String,Integer>
//获得参数化的泛型
Method method = Test.class.getMethod("test01",Map.class,List.class);
Type[] genParamTypes = method.getGenericParameterTypes();
for(Type genpt:genParamTypes){
System.out.println(genpt);
if(genpt instanceof ParameterizedType){
Type[] actualpts = ((ParameterizedType)genpt).getActualTypeArguments();
for(Type actpt:actualpts){
System.out.println(actpt);
}
}
}
//获得返回值中的参数化泛型
Method method = Test.class.getMethod("test02",null);
Type genReType = method.getGenericReturnType();
if(genReType instanceof ParameterizedType){
Type[] actualrts = ((ParameterizedType)genReType).getActualTypeArguments();
for(Type actrt:actualrts){
System.out.println(actrt);
}
}
反射操作注解
Class Stu = Class.forName("com.lqr.reflection.Student");
//获取注解
Annotation[] annos = Stu.getAnnotaions();
for(Annotation ann:annos){
System.out.println(ann);
}
//获取类的注解的参数值
Tablelqr ann = (Tablelqr)Stu.getAnnotation(Tablelqr.class);//参数为注解的class
String value = ann.value();
System.out.println(value);
//获取属性的注解的参数值
Field f = Stu.getDeclaredField("name");
Fieldlqr ann = (Fieldlqr)f.getAnnotation(Fieldlqr.class);
System.out.println(ann.colunm());
System.out.println(ann.len());
System.out.println(ann.type());
多线程 java.Thread
概念
程序:指令和数据的有序集合,是静态概念
进程process:执行程序的一次执行过程,是动态概念,是系统资源分配的单位
线程thread:一个进程可以包含若干线程,是CPU调度和执行的单位
真正的多线程指有多个CPU(多核);模拟出来的多线程是在一个CPU的情况下切换的很快
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
继承Thread类
- 自定义线程类继承Thread类
- 重写run()方法
- 创建线程对象,调用start()启动
public class myThread extends Thread{
@Override
public void run(){
for(int i=0; i<10; ++i){
System.out.println("我的线程 第"+i+"次");
}
}
public static void main(String[] args){
myThread mt = new myThread();
mt.start();//mt.run()则先执行run()方法,调用start()则交替执行main方法和自定义线程
for(int i=0; i<10; ++i){
System.out.println("main线程 第"+i+"次");
}
}
}
注意:创建线程后不一定立即执行,执行时间由CPU调度
实现Runnable接口(推荐使用)
- 自定义类实现Runnable接口
- 重写run()方法
- 创建线程对象,new Thread()并将Runnable接口实现类的对象作为构造器的参数传进去
- 调用thread对象的start()方法
与Thread相比,避免了java单继承的局限性
//抢票模拟,多个线程操作同一个资源,线程不安全
public class myRunnable implements Runnable{
private int ticketNum=10;
@Override
public void run(){
while(true){
if(ticketNum<=0)
break;
try{
Thread.sleep(200);//模拟延时
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNum--+"张票");
}
}
public static void main(String[] args){
myRunnable mr = new myRunnable();
new Thread(mr,"抢票器1").start();
new Thread(mr,"抢票器2").start();
new Thread(mr,"抢票器3").start();
}
}
//龟兔赛跑
public class TurRabitRace implements Runnable{
private static String winner;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//兔子睡觉
if(Thread.currentThread().getName().equals("rabbit")&&i==20) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//比赛结束
if(over(i))
break;
System.out.println(Thread.currentThread().getName()+"跑了"+i+"米");
}
}
private boolean over(int steps){
if(winner!=null)
return true;
if(steps>=100){
winner=Thread.currentThread().getName();
System.out.println(winner+"胜利");
return true;
}
return false;
}
public static void main(String[] args) {
TurRabitRace mr = new TurRabitRace();
new Thread(mr,"turtle").start();
new Thread(mr,"rabbit").start();
}
}
实现Callable接口
- 实现Callable接口
- 重写call()方法
- 创建目标对象
- 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:
Future<Boolean> result = ser.submit();
- 获取结果:
boolean re = result.get();
- 关闭服务:
ser.shutdownNow();
import java.util.concurrent.*;
public class myCallable implements Callable<Boolean>{
@Override
public Boolean call(){
for(int i=0; i<10; ++i){
System.out.println(Thread.currentThread().getName()+" 第"+i+"次");
}
return true;
}
public static void main(String[] args){
myCallable mc1 = new myCallable();
myCallable mc2 = new myCallable();
ExecutorService ser = Executors.newFixedThreadPool(2);
Future<Boolean> result1 = ser.submit(mc1);
Future<Boolean> result2 = ser.submit(mc2);
try {
boolean re1 = result1.get();
boolean re2 = result2.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
ser.shutdown();
}
}
静态代理
- 真实对象和代理对象实现同一接口
- 真实对象作为构造参数传入代理对象
- 代理对象调用真实对象执行方法
public class StaticProxy {
public static void main(String[] args) {
//Thread相当于婚庆代理,Runnable的接口实现类相当于真实对象,start相当于marry方法
new Thread(()->System.out.println("i love u")).start();
new WeddingCompany(new You("wzj")).Marry();
}
}
interface Marry{
void Marry();
}
class You implements Marry{
private String name;
public You(String name) {
this.name = name;
}
@Override
public void Marry() {
System.out.println(this.name+"要结婚了");
}
}
class WeddingCompany implements Marry{
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void Marry() {
System.out.println("婚庆公司帮你准备");
this.target.Marry();
System.out.println("婚庆公司帮你收尾");
}
}
Lamda表达式
作用:避免匿名内部类定义过多;代码简洁;
函数式接口:任何接口如果只包含一个抽象方法,那么它就是函数式接口
函数式接口可以通过lamda表达式创建对象
- 接口必须是函数式接口
- 方法体只有一行代码时才能省略{}
- 多个参数可以省略参数类型,但必须全部省略且加上()
public class Lamda {
public static void main(String[] args) {
new like().lamda("新建对象");//创建实现类对象再调用方法
ilike like = new ilike() {
@Override
public void lamda(String name) {
System.out.println(name+" implements ilike");
}
};
like.lamda("匿名内部类");
//ilike lamdalike = (String name)->{
// System.out.println(name+" implements ilike");
//};
//简化为
ilike lamdalike = name->System.out.println(name+" implements ilike");
lamdalike.lamda("lamda");
}
}
interface ilike{
void lamda(String name);
}
class like implements ilike{
@Override
public void lamda(String name) {
System.out.println(name+" implements ilike");
}
}
线程状态
五大状态
线程方法
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 合并线程,待此线程执行完后再执行其他线程,其他线程阻塞 |
static void yield() | 暂停当前正在执行的线程对象,并重新调度线程 |
void interrupt() | 中断线程,别用这个方式 |
boolean isAlive() | 测试线程是否处于活动状态 |
线程停止
不推荐使用stop()或destroy()方法(已废弃),建议让线程自己停下来
可用使用标志位标志线程是否运行,提供public stop()方法更改标志位,run()方法利用标志位判断是否运行
线程休眠 sleep()
slepp()存在异常interruptedException;
sleep()时间达到后线程进入就绪状态;
slepp()可以模仿网络延时、倒计时等;
每个对象都有一个锁,sleep()不会释放锁
线程礼让yield()
让当前正在执行的线程停止,但不阻塞,将线程从运行状态转为就绪状态;
礼让不一定成功,CPU来负责重新调度线程,可能调度礼让的线程,也可能调度其他线程;
线程强制执行join()
强制执行时其他线程是阻塞状态,类似插队
join()会抛出InterruptedException异常
观测线程状态getState()
Thread.State st = thread.getState();
终止TERMINATED,阻塞TIMED_WARNING,创建NEW,就绪RUNNABLE
线程优先级
线程优先级用数字表示1~10:
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;
getPriority()获取优先级,setPriority()改变优先级
优先级低只是被调度的概率低,不一定不会被调用
守护线程
线程分为用户线程和守护线程,正常的线程都是用户线程
虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕,用户线程全部执行完毕后程序就终止
例如,后台操作日志、监控内存、垃圾回收
thread.setDaomon(true);//设置为守护线程
线程同步
并发:同一个对象被多个线程同时操作
线程同步:并发时,某些线程想修改这个对象时,需要线程同步;线程同步就是一种等待机制,需要队列和锁(每个对象都有一把锁)
锁机制(synchronized)
当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁
问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置﹐引起性能问题.
三大线程不安全案例
- 买票线程,票数出现负数
- 取钱线程,钱数出现负数
- 集合list线程不安全:两个线程操作同一个list
同步方法
synchronized方法:
必须获得调用该方法的对象的锁才能执行,否则线程会阻塞;
方法一旦执行就独占该锁,直到方法返回才释放
缺点:影响效率,只有修改的代码里面需要锁,只读代码添加锁的话会浪费资源
同步块
synchronized (Obj){}
,Obj称之为同步监视器,可以是任何对象﹐但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者是class
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
死锁
两个或多个线程都在等待对方释放资源,都停止执行
同一个同步块有两个以上对象的锁时,可能发生死锁
必要条件
- 互斥:一个资源每次只能被一个进程使用
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
Lock接口
JDK5.0开始,通过显式定义同步锁对象来实现同步
ReentrantLock类实现了Lock接口,拥有与synchronized相同的并发性和内存语义,可以显式加锁和释放锁
public class Lock {
public static void main(String[] args) {
BuyTicket buyer = new BuyTicket();
new Thread(buyer).start();
new Thread(buyer).start();
new Thread(buyer).start();
}
}
class BuyTicket implements Runnable {
private int ticketNum=10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
lock.lock();
if(ticketNum>0){
Thread.sleep(1000);
System.out.println(ticketNum--);
} else {
System.out.println("sold out");
break;
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
}
与synchronized的区别
- lock是显式锁,需要手动开启和关闭;synchronized是隐式锁,出了作用域自动释放
- lock只能锁代码块,synchronized可以锁方法
- 使用lock锁JVM调度线程花费时间较少,性能更好,扩展性更好
- 优先使用顺序:lock>synchronized代码块>synchronized方法
线程协作
生产者消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止.
解决方式
管程法
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿数据
使用wait()
方法和notifyAll()
方法
public class ProduceConsume {
public static void main(String[] args) {
container con = new container();
new producer(con).start();
new consumer(con).start();
}
}
class producer extends Thread{
container con;
public producer(container con) {
this.con = con;
}
public void run(){
for (int i = 0; i < 10; i++) {
con.push(new production(i));
System.out.println("生产了"+i+"个产品");
}
}
}
class consumer extends Thread{
container con;
public consumer(container con) {
this.con = con;
}
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println("消费了第"+con.pop().id+"个产品"); ;
}
}
}
class production {
int id;
public production(int id) {
this.id = id;
}
}
class container{
production[] pro = new production[10];
int size=0;
public synchronized void push(production p){
if(size==pro.length){
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
pro[size++] = p;
this.notifyAll();
}
public synchronized production pop(){
if(size==0){
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
production p = pro[--size];
this.notifyAll();
return p;
}
}
信号灯法
package com.lqr.Thread;
public class ActorAudian {
public static void main(String[] args) {
show ss = new show();
new Actor(ss).start();
new Audian(ss).start();
}
}
class Actor extends Thread{
show ss;
public Actor(show ss) {
this.ss = ss;
}
public void run(){
for (int i = 0; i < 10; i++) {
if(i%2==0){
this.ss.show("新白娘子传奇播放中");
}else{
this.ss.show("孩子咳嗽老不好");
}
}
}
}
class Audian extends Thread{
show ss;
public Audian(show ss) {
this.ss = ss;
}
public void run(){
for (int i = 0; i < 10; i++) {
this.ss.see();
}
}
}
class show{
String name;
boolean flag=true;
public synchronized void show(String name){
if(!flag){
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("演员表演"+name);
this.notifyAll();
this.name = name;
this.flag = !this.flag;
}
public synchronized void see(){
if(flag){
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("观众观看"+name);
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
经常创建销毁或线程使用的资源量大,影响性能
提前创建好多个线程放入线程池,使用时直接获取,使用完放回线程池中
好处:提高响应速度;降低资源消耗;便于线程管理
- 创建线程池:
ExecutorService service = Executors.newFixedThreadPool(10);
- 添加线程并执行:
service.execute(new myThread());
- 关闭连接:
service.shutdown();