java基础知识一

java系列文章

java基础知识一
Java基础知识二


Java基础知识


前言

记录找实习所学的java知识


一、基础知识

·封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
·多态多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。多态的特点:
·对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
·引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
·多态不能调用“只在子类存在但在父类不存在”的方法;
·如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

·// 变量其实就是可以改变的向量存储
·标识符, // 标识数据的符号,称之为标识符。变量名称就是标识符,标识符主要用来起名。
· 数据存储单位, // 1. 比特(bit位):数据运算的最小单位
// 2. 字节(byte): 计算机数据的最小存储单位
// 1byte = 8bit
// 1024 Byte = 1 kb
// 1024 kb = 1mb
// 1024mb = 1GB
// 1024GB = 1 TB
// java数据类型可以确定数据的范围

·基本数据类型

1.整数类型
     byte: 8位
     short : 16位
     int: 32位
     long : 64位
2. 浮点类型
    float: 单精度, 数据需要使用F(f)结尾
     double : 双精度。 // 默认情况下,小数点会被识别为精度较高的双精度double
    float f = 1.0f;double d = 1.0;
3.字符类型// 就是使用符号标识文字内容
4.布尔类型

·java中基本类型的数据,数值范围小的可以自动转为大的
·5.引用数据类型
·String
·逻辑运算符 & | ! ^。短路运算符&&,||
·三元运算符
变量 = (条件表达式) ? (任意表达式1):(任意表达式2), 条件表达式为TRUE,结果为任意表达式1

·面向对象:其实就是分析问题时,以问题所涉及的事或物为中心的分析方式。
·类和对象,对象表示具体的事物。New对象,引用数据类型。
·引用变量:
对象是将内存地址赋值给了变量,所以变量其实引用了内存中的对象,所以称之为引用变量,而变量的类型称之为引用数据类型。
// 特殊的对象,没有引用的对象叫做:空对象(null),关键字对象。
// 所有引用类型变量的默认值就是null
·属性就是类的对象的相同特征。
·形参,可变参数应该放在所有参数的最后。
· java中方法参数的传递为值传递
// 基本数据类型:数值
// 引用数据类型:引用地址
· 针对于具体对象的属性称之为对象属性,成员属性,实例属性
// 针对于具体对象的方法称之为对象方法,成员方法,实例方法
// 和对象无关,只和类相关的称之为静态。静态属性,静态方法。
// 静态方法就是在属性和方法前添加static关键字
· 成员方法可以访问静态方法,静态属性
· 静态代码块 。static{…}
// 类的信息加载完成后,会自动调用静态代码块,可以完成静态属性的初始化功能
// 对象准备创建时,也会自动调用代码块,但不是静态的
//静态代码块不需要创建对象就可以执行,代码块需要先创建对象才可以执行

·public class able{   
            static int a=0//类变量
            String b="hello world"//实例变量
            public void method(){
                 int c=0//局部变量
        }
    }

·构建对象
构造方法:专门用于构建对象
如果一个类中没有任何的构造方法,那么jvm会自动添加一个公共的构造方法
构造方法没有void关键字
构造方法名和类名完全相同
构造方法也可以传递参数,一般传递参数的目的时用于对象属性的赋值
代码块是在构造对象之前执行的

·继承

    类的继承只能单继承,只能有一个父类,多个子类
    如果父类和子类含有相同的属性,那么可以采用特殊的关键字进行区分
    super, this。默认情况下,this可以不写

    父类对象是在子类对象创建前创造完成的,子类对象在创造时会调用父类的构造方法
    若父类有构造方法,子类应该用super方法构建父类对象

· 多态

    就是一个对象在不同场景下表现出来的不同状态和形态
    多态语法其实就是对对象的使用场景进行了约束
    一个对象可以使用的功能取决于引用变量的类型,即左边

·方法的重载:

相同方法名,和返回值类型无关,但可以用不同形参区分。
构造方法也存在方法的重载
如果在一个构造方法中,想要调用其他的构造方法,需要使用特殊的关键字:this
基本数据类型在匹配方法时,可以在数值不变的情况下,扩大数据的精度。
byte不能和char转换。char没有负数,byte有
若是类,子类找不到,找父类作为形参的方法

· 方法的重写:

子类重写父类的方法
·一个对象能使用什么方法,取决于引用变量的类型。具体的使用(直接,间接)是需要看具体对象的 ,也就是左边看能用什么,右边看实际用什么
· 一个对象能使用什么属性,取决于引用变量的类型。一个对象的属性具体的使用是不需要看具体的对象的,属性在哪里声明在哪里使用。
·也就是: 父类 = new 子类。具体的使用方法是子类里面的,但是如果该方法父类里面没有,子类里面有,使用就会报错。属性是在哪儿声明哪里使用。
·访问权限。
java的源码中,公共类只能有一个,而且必须和源码文件名相同。main方法:由jvm调用的
private, 同一个类中可以用
(default)默认权限,包(路径)权限
protected,同类,同包,子类可以访问
public,任意使用
· java不允许外部类使用private,protected修饰
外部类:就是在源码中直接声明的类
内部类:就是在类中声明的类
· jvm(java虚拟机)默认为类提供构造方法,其实就是公共的无参构造方法。
静态方法不能访问成员属性

·字符串,

java.lang.String
字符串,字符,字节的关系。每个char是一个字节,三个字节表示一个字符
·相等equals. 忽略大小写的相等equalsIgnoreCase
compareTo:比较谁大谁小。结果为正数则 a>b;结果为0,则a=b
忽略大小写的比较: compareToIgnoreCase
· 字符串的截断:s.substring(0, 3); //从0位置截断到3位置。// 两个参数,起始和结束位置。左闭右开。若只传一个参数,表示的是起始位置
· 分解字符串:split。 String[] s4 = s.split(" ");
·trim:去掉字符串的首尾空格
·replace(),纯粹的替换。 s.replace(“world”, “java”);
·replaceAll(),按照指定规则的替换。s.replaceAll(“World | zhagnsan”, “java”);
·toLowerCase:所有的字母都变成小写
·toUpperCase:所有的字母都变成大写
· s.charAt(1);//charAt可以传递索引定位字符串中指定位置的字符
·StringBuilder:构建字符串
·字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
· 在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。
·字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
·String s1 = new String(“abc”);这句话创建了几个字符串对象?
会创建 1 或 2 个字符串对象。
·String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

·深拷贝和浅拷贝

浅拷贝:只复制对象本身,而不复制对象包含的子对象。新旧对象之间共享子对象的引用,即新对象和原始对象中的子对象指向同一个内存地址。
深拷贝:不仅复制对象本身,还要复制对象包含的所有子对象。新对象和原始对象所包含的子对象是相互独立的。
引用拷贝:就是两个不同的引用指向同一个对象。
· 为什么重写 equals() 时必须重写 hashCode() 方法?因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
·String、StringBuffer、StringBuilder 的区别?
·可变性
String 是不可变的。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
·线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
·性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
·String 真正不可变有下面几点原因:
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。 String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
·在 Java 9 之后,String、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。

二、基础知识2

·单例模式

    1.类的创建过程复杂
    2.类的对象消耗资源

·单例模式中,实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节
· final:
数据初始化后不被修改,可以修饰变量。但是修饰属性,jvm无法自动进行初始化。一般将其称为常量,或不可变量。
也可以修饰方法,这个方法不能被子类修改
也可以修饰类,这个类就没有子类了
不可以修饰构造方法
final 可以修饰形参,一旦修饰,参数就无法修改

· 抽象类,

abstract class 类名
abstract无法直接构造对象,但是可以通过子间接构造对象
如果抽象类中有抽象方法,子类继承后需要重写抽闲方法
抽象方法:只有声明,没有实现的方法 abstract 返回值类型 方法名(参数)

·接口 interface

可以理解为规则
interface 接口名称{规则属性,规则的行为}
接口其实是抽象的,规则的属性必须为固定值,不能被修改。
属性和行为必须是公共的,
属性应该是静态的。接口中定义的方法都是抽象的。
行为应该是抽象的。
接口和类是两个层面的东西。接口不可以创建对象
接口乐于继承其他接口
类的对象需要遵循接口,在java中,这个遵循称之为实现,类需要实现接口,而且可以实现多个接口。implements实现
实现了接口的类的对象可以赋值给接口类型的接口变量,接口变量就可以访问实现类的接口方法了

·枚举 enum

    枚举是一个特殊的类,其中包含了一组特定的类,这些对象不会发生改变,一般使用大写的标识符
    枚举类会将对象放置在最前面,那么和后面的语法需要使用分号隔开
    枚举类不可以创建对象,它的对象是在内部自己创建的

·接口和抽象类有什么共同点和区别?
共同点:
都不能被实例化。
都可以包含抽象方法。
都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别:
接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。
抽象类主要用于代码复用,强调的是所属关系。
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,
而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

·匿名类

某些场景下,类的名字不重要,单要使用其中的方法或功能,就可以用匿名类

me.sayHello(new Person24() {
    public String name() {
        return "wangwu22";
    }
});

简化为:

me.sayHello(new Person24() {
    @Override
    public String name() {
        return "wanwu";
    }
});

·接口也可以使用匿名

new Bird2().fly();

简化为:

new Fly(){
    public void fly(){
        System.out.println("使用飞行棋飞翔");
    }
}.fly();

· 类主要用于
1.编写逻辑
2.建立数据模型(Bean)
· bean 类的设计规范:Bean规范
JavaBean是一种符合一定规范的Java类。
1.类必须有无参,公共的构造方法
2.属性必须私有化,然后提供公共的set,get方法

·作用域

    如果属性和(局部)变量的名称相同,访问不加修饰符,优先访问变量
    父类和子类都有,默认是子类
    静态的方法不允许访问对象,static其中子类就不可以用super。Static声明的不存在继承,所以不可以用super,直接用父类.属性

在静态方法中不可以调用成员方法和变量

· 常见类和对象

    ·object
    java.lang.Object : 对象。 Object,超类
   
    将对象转换成字符串
    默认打印的是对象的内存地址,可以进行重写
     System.out.println(s); // 16进制
    // 获取对象的内存地址
    int i =  obj.hashCode();
    System.out.println(i);  // 10进制
    ·判断两个对象是否相等,默认比较内存地址。也可以重写。obj.equals(new Person2());
    // getClass 获取对象的类型信息
    // getSimpleName获取对象的名称
    // getPackageName获取包的名称
    Class<?> aClass = obj.getClass();

·数组
声明 :类型[] 变量;
·数组的创建: new 类型[容量];

· 常见类和对象
// byte short int long
// float double
// char
// boolean

· 包装类

:java构建的类
·数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。

        Byte b = null;
        Short s = null;
        Integer i = null;
        Long lon = null;
        Float f = null;
        Double d = null;
       Character c = null;
       Boolean bln = null;
 ·		int i  = 10;
        // 将基本数据类型转换为包装类型
        Integer i1 = Integer.valueOf(i);
        // 自动装箱
        Integer i2 = i;
        //从包装类型转换为基本数据类型呢
        int i3 = i1.intValue();
        //自动拆箱
        int i4 = i1;

·日期类:Data。在java.util中

    // 时间戳:毫秒
    // y(Y) -> 年 -> yyyy
    // m(M) -> MM:月份, mm:分钟
    // d(D) -> d:一个月中的日期。 D:一年中的日期
    // h(H) -> h:十二进制。H:24进制
    // s(S) -> s:秒。S:毫秒

·SimpleDateFormat。Data -> String用format。String -> Data用parse。
d.setTime(System.currentTimeMillis());//根据时间戳构建指定的日期对象
d.getTime();//获取时间戳
parseDate.before(d);
//before,after判断时间是否早于或者晚于

·日历类:Calendar 它是抽象类。

静态常量一般用类名去调用

Calendar instance = Calendar.getInstance();
instance.get(Calendar.YEAR);
instance.get(Calendar.MONTH);//注意从0开始的。0就是一月
instance.get(Calendar.DATE);
instance.get(Calendar.DAY_OF_MONTH);
instance.setTime(new Date());
instance.add(Calendar.YEAR, -1);

· 工具类

    StringUtil 字符串工具类
     ==,用于基本数据类型,比较的是数值。引用数据类型,比较的是内存地址

· 类型转换出现了错误
递归导致栈出错:StackOverflow .错误error
访问一个为空对象的成员方法时,出现了错误:java.lang.NullPointerException.异常
索引越界:ArrayIndexOutOfBoundsException
格式化异常:NumberFormatException
类型转化错误:ClassCastException

·java异常:

·java中异常分为两大类:
1.可以通过代码恢复正常逻辑执行的异常,称之为运行期异常:RuntimeException。可以通过代码处理,不用抛出。

NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

2.不可以通过代码恢复正常逻辑执行的异常,称之为编译器异常:Exception

·异常处理语法:

        tyr{
        }catch(抛出的异常对象 对象引用){
        }catch(){
			e.getMessage();//错误的消息
			e.getCause();//错误的原因
        }finally{
        } //先捕捉范围小的异常

· 手动扔异常 使用throw关键字,然后new出异常对象。 此时谁调用这个方法,谁去处理这个异常
·finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:程序所在的线程死亡。关闭 CPU。
·如何使用 try-with-resources 代替try-catch-finally?适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行。

·泛型语法

ArrayList list = new ArrayList(); //当前的集合中只能放Person6类型的数据
//多态是限定对象的使用场景和类型、
// <> 是限定集合中元素的类型
如果没有指定集合能放什么类型的数据,从集合中获取的对象类型为Object。如果想要执行对象的方法,那么需要进行强制类型转换。
· todo 泛型和类型的区别
用于约束外部对象的使用场景,就是类型
用于约束内部对象的使用场景,就是泛型
有时也把泛型称之为类型参数
类型存在多态的使用,泛型没有多态

·集合

:数据的一种容器.对不确定的有关系的数据进行相同的逻辑处理的场合。
·java提供了完整的集合框架
数组在数据量不确定的时候使用起来不方便
java集合分为两大体系
1.单一数据体系:Collection接口
常用的子接口:
List: 按照插入顺序保存数据,数据可以重复
具体的实现类:ArrayList,LinkedList
Set:集,无序保存,数据不能重复
具体的实现类:HashSet
Queue:队列
具体的实现类:ArrayBlockingQueue
2.成对出现的数据体系:Map接口。也称之为键值对数据(类似python的字典)=>(key,value)
具体的实现:HashMap,Hashtable

·Array优缺点:

优点:

1、可以根据索引直接访问,访问速度快
2、数据是安全的,由于数据类型一致性,在存储使用过程中不涉及到装箱拆箱操作
缺点:
1、由于数据是连续存储的,导致插入效率变慢
2、由于数组长度大小固定,那么对预期非固定长度的数字不好处理
·ArrayList优缺点:
优点:
1、长度不固定,在定义是不必担长度溢出
2、可以存储任意数据类型
3、可根据索引查询,查询效率快
缺点:
1、由于长度不固定,执行效率低下,因为超出默认长度(10)后,会自动扩容拷贝数据,牺牲性能
2、由于存储类型是object,所以在存数据时会有装箱操作,在取数据时会有拆箱操作,影响效率
3、线程不安全,因为其内部实现是用size、array来共同控制,在新增操作时是非原子操作,所以非安全线程
·List集合内部还是采用的Array实现,同时在定义时需要指定对应的数据类型。这样级保留了Array集合的优点,同时也避免了ArrayList集合的数据类型不安全和装箱带来的性能牺牲
·List特点:
1、数据长度不固定,自动增加
2、存储相同的数据类型
3、可根据索引查询,查询效率快
优点:
1、长度不固定,在定义是不必担长度溢出
2、存储相同数据类型的数据,避免的数据的装箱拆箱,提高了数据处理效率
3、支持索引查询,查询效率快
缺点:
1、由于长度不固定,执行效率低下,因为超出默认长度(10)后,会自动扩容拷贝数据,牺牲性能
2、线程不安全,因为其内部实现是用size、array来共同控制,在新增操作时是非原子操作,所以非安全线程
·LinkedList:
优点:
1、由于非连续存储,中部插入和删除元素效率高
2、长度非固定,在创建时不用考虑其长度
3、可以冲头部和底部添加元素
4、数据类型是安全的,在创建时需要指定的数据类型
缺点:
1、由于非连续存储,不能通过下标访问,查询效率低

·Collection接口

·Collection - list

·ArrayList
Array + List (array:数组,阵列)。这个集合里的元素是数组。
·1.不传构造参数,底层数组为空数组
2.构造参数为一个int类型的数组,用于设定底层数组的长度。则初始容量为 initialCapacity,之后再次扩容为原容量的1.5倍。
3.构造参数需要传递一个collection集合类型的值,用于将其他集合中的数据放置在当前集合中
·添加数据时,如果集合中没有任何的数据,那么底层会创建长度为10的数组。之后再次扩容为原容量的1.5倍
·如果循环遍历集合时,不关心数据的位置,那么可以采用特殊的for循环,即 for(循环对象:集合)
·list.toArray();//将集合变成数组类型
·LinkedList:
linked(连接)+list //默认尾插法。将数据间用链表的形式连接,有first,last。底层是个链表结构。从查询的角度来看,有索引访问的话,Arraylist更快一点

·Collection -Set

·HashSet集合, hash算法存储。Hash没有修改,只能删除在加入。查询也不能,没有get,只能for循环一个一个遍历

·Collection -Queue

ArrayBlockingQueue: Array + BLocking(阻塞,堵住)
·放数据。·add满了报错;put满了堵住,程序不停止

offer:返回布尔值,满了返回false,没有在里面放数据。
poll,弹数据。take();//拿数据。同样也是如果队列已经空了,会等待
add(element):将指定的元素插入到队尾,如果成功则返回 true,如果队列已满则抛出异常。
offer(element):将指定的元素插入到队尾,如果成功则返回 true,如果队列已满则返回 falseremove():移除并返回队首的元素,如果队列为空则抛出异常。
poll():移除并返回队首的元素,如果队列为空则返回 nullelement():返回队首的元素,但不移除它,如果队列为空则抛出异常。
peek():返回队首的元素,但不移除它,如果队列为空则返回 null

·Map接口

·HashMap

数据存储是无序的,根据key判断两个数据是否相同。

HashMap<String, String> map = new HashMap();
map.put("lisi", "2");//put也可以修改数据,即key相同,value会保存新的那个
map.putIfAbsent("b", "3");//没有就添加,有就不添加
Object b = map.replace("b", "4"); //修改,返回没有修改前的值
//获取map集合中所有的key。map.keySet();
//获取键值对对象
Set<Map.Entry<String, String>> entries = map.entrySet();
for(Map.Entry<String, String> entry : entries){
    System.out.println(entry.getKey() + "," + entry.getValue());
}
·HashTable

1.实现方式不一样。hashtable继承的是Dictionary,hashmap继承的是AbstractMap
2.底层容量不一样。hashtable(11),hashmap(16)
3.HashMap的K,V都可以为null。hashtable不可以
4. HashMap的数据定位采用的是Hash算法,但是HashTable采用的就是Hashcode
5.HashMap的性能较高,线程安全
·HashMap 和 HashTable 都是 Java 中用于存储键值对的数据结构,但它们有一些重要的区别。以下是 HashMap 和 HashTable 的主要区别:
·线程安全性:
HashMap 是非线程安全的。多个线程可以同时访问和修改 HashMap,如果没有适当的同步措施,可能会导致数据不一致或竞争条件。
HashTable 是线程安全的。它的方法都被同步(synchronized)了,可以在多线程环境中使用,但这可能会降低性能。
·允许空键和空值:
HashMap 允许使用空(null)键和空(null)值。这意味着可以将 null 作为键或值存储在 HashMap 中。
HashTable 不允许使用空(null)键或空(null)值。如果尝试存储空(null)键或空(null)值,会抛出 NullPointerException。
·继承关系:
HashMap 继承自 AbstractMap 类,而 HashTable 继承自 Dictionary 类(已过时)。
·效率和性能:
由于 HashMap 不进行同步,适用于单线程环境,因此在性能上通常比 HashTable 更高效。
HashTable 的同步操作可能导致性能下降,特别是在高并发环境下。
·迭代器和枚举:
HashMap 的迭代器是快速失败的(fail-fast iterator),可以检测到在迭代过程中其他线程对 HashMap 的修改。
HashTable 的枚举(Enumeration)不是快速失败的,不能检测到其他线程的修改。
·泛型支持:
HashMap 支持泛型,可以指定键和值的类型。
HashTable 不支持泛型,只能存储 Object 类型。
总的来说,如果在多线程环境下需要使用键值对存储,可以考虑使用 ConcurrentHashMap 来替代 HashTable,因为 ConcurrentHashMap 提供了更好的并发性能。在单线程环境下,通常首选使用 HashMap,因为它的性能更好。
·ConcurrentHashMap,底层采用分段的数组+链表实现,线程安全。
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

·HashMap底层实现原理和扩容机制

HashMap 的底层实现原理是基于哈希表(Hash Table)。它使用哈希函数将键映射到数组索引,然后将值存储在该索引处。当发生哈希碰撞(多个键映射到同一索引位置)时,HashMap 使用链表(或红黑树,自Java 8开始)来存储多个键值对。
·底层数据结构:
HashMap 内部使用一个数组来存储哈希桶(Bucket)。
每个桶可以存储一个或多个键值对。
每个键值对被封装为一个 Node 对象,其中包含键、值以及下一个节点的引用。
·存储流程:
当添加键值对时,HashMap 使用键的哈希码通过哈希函数计算出索引位置。
如果该位置为空,则直接将键值对放入。
如果位置不为空,可能发生哈希碰撞,HashMap 会遍历链表(或树)查找是否存在相同的键。
如果存在相同的键,更新对应的值,否则将新的键值对插入链表(或树)中。
·扩容机制(Rehashing):
HashMap 在添加键值对时,会监测当前元素数量是否超过了负载因子与初始容量的乘积(默认负载因子为0.75)。
如果超过了阈值,HashMap 会进行扩容,将数组大小增加一倍,然后重新计算每个键值对的新位置。
扩容时,所有键值对都需要重新计算哈希码并放入新的位置。这是一个相对耗时的操作。
扩容后,新的容量是原来的两倍,负载因子变得更小,从而减少哈希碰撞的概率,提高了性能。
·红黑树(Red-Black Tree):
在 JDK 8 及以后的版本中,HashMap 在处理哈希碰撞时引入了红黑树来提高性能,这被称为 “树化”(Treeify)操作。当链表长度达到一定阈值时,链表会被转换为红黑树,以减少查找、插入和删除操作的时间复杂度。以下是 HashMap 中红黑树部分的详细解释:
·红黑树是一种自平衡的二叉搜索树,具有以下特性:
每个节点要么是红色,要么是黑色。
根节点是黑色的。
所有叶子节点(NIL 节点,空节点)都是黑色的。
如果一个节点是红色的,则其子节点必须是黑色的。
从根节点到任何叶子节点的路径上,黑色节点的数量相同。
红黑树在 HashMap 中的应用:
当 HashMap 中的链表长度超过一定阈值(默认为8)时,链表会被树化。这是为了防止出现长链表导致查找、插入和删除操作的性能下降。
·以下是树化操作的步骤:
如果链表长度小于等于阈值(8),则不进行树化,继续使用链表存储。
如果链表长度大于阈值,将链表转换为红黑树。
树化后,当链表中元素个数减少到小于等于6时,会将红黑树重新转换为链表,以节省内存。
·优势和注意事项:
红黑树相对于链表在查找、插入和删除操作上具有更好的性能,其时间复杂度为 O(log n),而链表的时间复杂度为 O(n)。这在处理大量数据时可以显著提高性能。
然而,红黑树的创建和维护相对于链表要复杂,占用的内存也更多。因此,红黑树主要用于优化哈希碰撞导致的性能问题,对于较小的链表,仍然使用链表存储。在实际使用中,应注意权衡和合理配置 HashMap 的初始化容量和负载因子,以便获得最佳性能。
红黑树是 HashMap 在解决哈希碰撞问题时的一种优化手段,可以提高大链表情况下的性能。它是一种自平衡的二叉搜索树,用于优化查找、插入和删除操作的性能。
·总结:
HashMap 的底层实现原理基于哈希表,使用数组存储键值对,通过哈希函数映射到数组索引。在哈希碰撞时,使用链表或红黑树来存储多个键值对。为了保持性能,HashMap 会在负载因子达到一定阈值时进行扩容,以减少哈希碰撞的影响。这个底层实现使得 HashMap 能够高效地支持快速的键值对存储和检索。

·迭代器,

当数据变动时,它也会收到通知。
Iterator iterator = keys.iterator();
//hashNext(),用于判断是否还有下一条数据
·迭代器可以用在循环遍历数据时删除数据,但是只能对当前数据进行删除。
·类加载器
类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象。

三、文件处理,IO流

·Java 数据 + 流(转)操作

·文件流 File(文件/文件夹),
File对象封装的是文件或者路径属性,
String[] list = file.list();//文件夹中的文件
file.listFiles();//返回每一个文件对象
·对文本文件我们使用字符流,对应Reader、Writer。对二进制文件 我们使用字节流,对应InputStream、OutPutStream。字符通常由一个或多个字节表示。一个字节是8个2进制位
·文件缓冲流 buffer
·InputStream的用法:
通过InputStream类的read()方法,对文件内容进行读取
注意,我们使用读取完毕后一定要使用close()方法关闭文件,释放资源。
为了避免我们执行代码时,跳过close方法,第一种代码写法通过finally来保证close()一定被执行。读到-1时,代表读完。
·OutputStream的用法:
通过OutputStream的write方法写入内容。flush()方法保证我们缓冲区的数据刷新到设备中
·流按操作数据类型的不同分为两种:字节流与字符流。
· 流按流向分为:输入流,输出流(以程序为参照物,输入到程序,或是从程序输出)
·字节流的抽象基类:
输入流:java.io.InputStream
输出流:java.io.OutputStream
特点:
字节流的抽象基类派生出来的子类名称都是以其父类名作为子类名的后缀。
如:FileInputStream, ByteArrayInputStream等。
·字节流处理的单元是一个字节,用于操作二进制文件(计算机中所有文件都是二进制文件)
·问题1: 使用缓冲(字节数组)拷贝数据,拷贝后的文件大于源文件.
测试该方法,拷贝文本文件,仔细观察发现和源文件不太一致。
打开文件发现拷贝后的文件和拷贝前的源文件不同,拷贝后的文件要比源文件多一些内容问题就在于我们使用的容器,这个容器我们是重复使用的,新的数据会覆盖掉老的数据,显然最后一次读文件的时候,容器并没有装满,出现了新老数据并存的情况。
所以最后一次把容器中数据写入到文件中就出现了问题。
如何避免?使用FileOutputStream 的write(byte[] b, int off, int len)
b 是容器,off是从数组的什么位置开始,len是获取的个数,容器用了多少就写出多少。

try{...
}catch(IOException e){
	throw new RuntimeException(e);
}finally{
 if (fis != null) {
       try {
           fis.close();
          } catch (IOException e) {
               throw new RuntimeException(e);
         }

·Java其实提供了专门的字节流缓冲来提高效率.
BufferedInputStream和BufferedOutputStream。查看API文档,发现可以指定缓冲区的大小。其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192。显然缓冲区输入流和缓冲区输出流要配合使用。首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时,或者调用flush方法,缓冲输出流会将数据写出。BufferedInputStream是缓存字节输入流,是一个高级流,与其他低级流配合使用。

·字符流

就是:字节流 + 编码表,为了更便于操作文字数据。字符流的抽象基类:
Reader , Writer。
·字节流可以拷贝视频和音频等文件,那么字符流可以拷贝这些吗?
经过验证拷贝图片是不行的。发现丢失了信息,为什么呢?
计算机中的所有信息都是以二进制形式进行的存储(1010)图片中的也都是二进制。在读取文件的时候字符流自动对这些二进制按照码表进行了编码处理,但是图片本来就是二进制文件,不需要进行编码。有一些巧合在码表中有对应,就可以处理,并不是所有的二进制都可以找到对应的。信息就会丢失。所以字符流只能拷贝以字符为单位的文本文件。(以ASCII码为例是127个,并不是所有的二进制都可以找到对应的ASCII,有些对不上的,就会丢失信息。)
·在使用缓冲区对象时,要明确,缓冲的存在是为了增强流的功能而存在,所以在建立缓冲区对象时,要先有流对象存在。缓冲区的出现提高了对流的操作效率。原理:其实就是将数组进行封装。
·BufferedReader,BufferedWriter.
·readLine方法默认没有换行.需要手动换行

·装饰器模式:

使用分层对象来动态透明的向单个对象中添加责任(功能)。
装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。
某些对象是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。
装饰器必须具有和他所装饰的对象相同的接口。

JavaIO中的应用:
Java I/O类库需要多种不同的功能组合,所以使用了装饰器模式。
FilterXxx类是JavaIO提供的装饰器基类,即我们要想实现一个新的装饰器,就要继承这些类。
装饰器与继承:
问题:修饰模式做的增强功能按照继承的特点也是可以实现的,为什么还要提出修饰设计模式呢?
继承实现的增强类和修饰模式实现的增强类有何区别?
继承实现的增强类:
优点:代码结构清晰,而且实现简单
缺点:对于每一个的需要增强的类都要创建具体的子类来帮助其增强,这样会导致继承体系过于庞大。
修饰模式实现的增强类:
优点:内部可以通过多态技术对多个需要增强的类进行增强
缺点:需要内部通过多态技术维护需要增强的类的实例。进而使得代码稍微复杂。
·序列流,也称为合并流。
·SequenceInputStream,序列流,对多个流进行合并。SequenceInputStream表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
·当创建对象时,程序运行时它就会存在,但是程序停止时,对象也就消失了.但是如果希望对象在程序不运行的情况下仍能存在并保存其信息,将会非常有用,对象将被重建并且拥有与程序上次运行时拥有的信息相同。可以使用对象的序列化。
对象的序列化: 将内存中的对象直接写入到文件设备中
对象的反序列化: 将文件设备中持久化的数据转换为内存对象
·基本的序列化由两个方法产生:一个方法用于序列化对象并将它们写入一个流,另一个方法用于读取流并反序列化对象。
·ObjectOutput
writeObject(Object obj)
将对象写入底层存储或流。
ObjectInput
readObject()
读取并返回对象。
·由于上述ObjectOutput和ObjectInput是接口,所以需要使用具体实现类。
ObjectOutputStream被写入的对象必须实现一个接口:Serializable否则会抛出:NotSerializableException
ObjectInputStream 该方法抛出异常:ClassNotFountException
·ObjectOutputStream和ObjectInputStream 对象分别需要字节输出流和字节输入流对象来构建对象。也就是这两个流对象需要操作已有对象将对象进行本地持久化存储。
·类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
所以需要被序列化的类必须是实现Serializable接口,该接口中没有描述任何的属性和方法,称之为标记接口。
如果对象没有实现接口Serializable,在进行序列化时会抛出:NotSerializableException 异常。
·注意:
保存一个对象的真正含义是什么?如果对象的实例变量都是基本数据类型,那么就非常简单。但是如果实例变量是包含对象的引用,会怎么样?保存的会是什么?很显然在Java中保存引用变量的实际值没有任何意义,因为Java引用的值是通过JVM的单一实例的上下文中才有意义。通过序列化后,尝试在JVM的另一个实例中恢复对象,是没有用处的。
·如下:
·首先建立一个Dog对象,也建立了一个Collar对象。Dog中包含了一个Collar(项圈)
·现在想要保存Dog对象,但是Dog中有一个Collar,意味着保存Dog时也应该保存Collar。假如Collar也包含了其他对象的引用,那么会发生什么?意味着保存一个Dog对象需要清楚的知道Dog对象的内部结构。会是一件很麻烦的事情。
Java的序列化机制可以解决该类问题,当序列化一个对象时,Java的序列化机制会负责保存对象的所有关联的对象(就是对象图),反序列化时,也会恢复所有的相关内容。本例中:如果序列化Dog会自动序列化Collar。但是,只有实现了Serializable接口的类才可以序列化。如果只是Dog实现了该接口,而Collar没有实现该接口。会发生什么?
·所以我们也必须将Dog中使用的Collar序列化。但是如果我们无法访问Collar的源代码,或者无法使Collar可序列化,如何处理?
·两种解决方法:
1:继承Collar类,使子类可序列化
但是:如果Collar是final类,就无法继承了。并且,如果Collar引用了其他非序列化对象,也无法解决该问题。
·transient

2:transient

此时就可以使用transient修饰符,可以将Dog类中的成员变量标识为transient,那么在序列化Dog对象时,序列化就会跳过Collar。transient关键字只能修饰变量,而不能修饰方法和类。
·对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。 注意static修饰的静态变量天然就是不可序列化的。

·序列化:

从对象变成字节。java中只有增加了特殊的标记的类,才能再写文件时进行序列化操作。这里的标记就是一个接口。writeObject
·反序列化:从字节变成对象。(从文件中读取数据转换成对象)。readObject()
·序列化协议在TCP/IP 的第四层,应用层。

·常见序列化协议有哪些?JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择

·其他流

·可以和流相关联的集合对象Properties
·PrintStream可以接受文件和其他字节输出流,所以打印流是对普通字节输出流的增强,其中定义了很多的重载的print()和println(),方便输出各种类型的数据。
· print 方法和write方法的却别在于,print提供自动刷新。普通的write方法需要调用flush或者close方法才可以看到数据.
· JDK1.5之后Java对PrintStream进行了扩展,增加了格式化输出方式,可以使用printf()重载方法直接格式化输出。但是在格式化输出的时候需要指定输出的数据类型格式。
·PrintWriter,是一个字符打印流。构造函数可以接收四种类型的值。1,字符串路径。
2,File对象。
对于1,2类型的数据,还可以指定编码表。也就是字符集。
3,OutputStream
4,Writer
对于3,4类型的数据,可以指定自动刷新。
注意:该自动刷新值为true时,只有三个方法可以用:println,printf,format.

·操作数组的流对象
·操作字节数组ByteArrayInputStream。以及ByteArrayOutputStream
toByteArray();
toString();
writeTo(OutputStream);
· 操作字符数组。CharArrayReader。CharArrayWriter
对于这些流,源是内存。目的也是内存。
而且这些流并未调用系统资源。使用的就是内存中的数组。
所以这些在使用的时候不需要close。
操作数组的读取流在构造时,必须要明确一个数据源。所以要传入相对应的数组。
对于操作数组的写入流,在构造函数可以使用空参数。因为它内置了一个可变长度数组作为缓冲区。
·操作基本数据类型的流对象。DataInputStream以及DataOutputStream
·String类的getBytes() 方法进行编码,将字符串,转为对映的二进制,并且这个方法可以指定编码表。如果没有指定码表,该方法会使用操作系统默认码表。

四、进程、线程

·进程 process

,运行的程序
javac 将java源码文件翻译成java虚拟机(jvm)可以执行的文件,javac有时候称为编译指令或者编译器
jps,可以展示当前java的进程
自己的程序就是一个进程,进程的名字就是执行的类的名字
进程理解为多个工厂,线程就是工厂里面的生产线
·创建线程的几种方式:
①继承Thread类创建线程类
·定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
·创建Thread子类的实例,即创建了线程对象。
·调用线程对象的start()方法来启动该线程。
②通过Runnable接口创建线程类
·定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
· 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
·调用线程对象的start()方法来启动该线程。
③通过Callable和Future创建线程
·创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
·创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
·使用FutureTask对象作为Thread对象的target创建并启动新线程。
·调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

④线程池创建线程
·https://blog.csdn.net/qq_38345899/article/details/130146094
比较好的线程,并发等的讲解。
· 线程 ,Thread是线程类
System.out.println(Thread.currentThread().getName()); // currentThread()用于获取当前正在运行的线程,getName获取线程名称
main方法运行在main线程中
Java程序在运行的时候默认会产生一个进程,这个进程会有一个主线程,代码都在主线程中执行。

·创建线程

      Thread t = new Thread();
  		// 启动线程
       t.start();
       t.stop();//关闭线程,不推荐使用了
 	 //·线程间的先后执行顺序不确定
       // 线程的状态:new新建,runnable可运行,terminated终止 ,
       // blocked阻塞,waiting等待,timed_waiting,定时等待

       // · 线程执行方式(串行,并发)
      //  串行执行:多个线程连接成串,然后按照顺序执行
      //  并发执行:多个线程是独立的,谁抢到了CPU的执行权,谁就能执行
    t1.join();//·将线程连接成串。
    Thread.sleep(3000);//休眠3秒钟,sleep是个静态方法,线程就变成定时等待状态
//·构建线程对象时,可以只把逻辑传递给这个对象。传递逻辑时,需要遵循规则: ()->{逻辑}。lambda表达式。
    Thread t5 = new Thread(()->{  // 箭头是lambda表达式
            System.out.println("线程执行");
        });
//·构建线程对象时,可以传递实现了Runnable接口的类的对象,一般使用匿名类
       Thread t6 = new Thread(new Runnable() {
            public void run() {
                System.out.println("线程执行");
            }
        });

·声明自定义线程类,需要继承父类Thread,然后重写run方法

·线程池,

就是线程对象的容器,可以根据需要,在启动时,创建一个或者多个线程对象。
·java有4种比较常见的线程池
1.创建固定数量的线程对象。(ExecutorsService是线程服务对象)。
newFixedThreadPool()
2.根据需求动态的创建线程
newCachedThreadPool()
3.单一线程
newSingleThreadExecutor()
4.定时调度线程
newScheduledThreadPool()
·线程同步,异步
·线程 - 同步 synchronized:同步关键字
多个线程访问同步方法时,只能一个一个访问,同步操作
new HashTable<>(); // 比HashMap慢,HashMap是异步的
synchroized 关键字还可以修饰代码块,称之为同步代码块
synchroized(用于同步的对象){
处理逻辑
}
·notifyAll(); // 一旦执行此方法,就会唤醒所有被wait()阻塞的线程
·线程 阻塞
wait & sleep
1.wait:等待,来自object,成员方法,每个对象都会有这个方法
2.sleep:休眠,来自Thread,静态方法,
// wait:不能直接使用,只能使用在同步代码种。而sleep可以在任意地方使用
// wait:超时时间 会发生错误,sleep休眠时间 不会
// wait:如果执行wait方法,那么其他线程有机会执行当前的同步操作
// sleep:如果执行sleep方法,那么其他线程没有机会执行当前的同步操作

· 线程安全

:所谓的线程安全问题,其实就是多个线程在并发执行时,修改了共享内存中的共享对象的属性,导致的数据冲突问题。
· java会对每一个线程创建栈内存,但是堆内存只有一个。
·线程间通信的模型有两种:共享内存和消息传递
①第一种方式,就是内存共享, 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式的进行通信。
·使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式。
②第二种方式,就是消息传递。顾名思义,就是通过明确的发送消息来显式的进行通信。这里又经常会涉及到等待唤醒机制:wait() 和 notify() 等相关问题了。
·使用JUC工具类 CountDownLatch
·使用 ReentrantLock 结合 Condition
·基本 LockSupport 实现线程间的阻塞和唤醒
·在 java 程序中怎么保证多线程的运行安全?
·线程切换导致的原子性问题
一个或者多个操作在 CPU 执行的过程中不被中断的特性。
解决方式:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题。
·缓存导致的可见性问题
一个线程对共享变量的修改,另外一个线程能够立刻看到。
解决方式:
synchronized、volatile、LOCK,可以解决可见性问题
·编译优化导致的有序性问题
程序执行的顺序按照代码的先后顺序执行。
解决方式:
Happens-Before 规则可以解决有序性问题。
·使用安全类,比如 Java.util.concurrent 下的类。
·使用自动锁 synchronized。
·使用手动锁 Lock。
·锁的级别从低到高:
无锁——偏向锁——轻量级锁——重量级锁,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
·尽量降低锁的使用粒度,尽量不要几个功能用同一把锁,能锁块不锁方法。
·Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫将变化的值重新写入共享内存(这边要注意一下,虽然是将变化的值写入到共享内存,但是写这个过程却不是同步的,也就是说这个写-set 值过程是不安全的),这样两个不同的线程在访问同一个共享变量的值时,始终看到的是同一个值(但由于前边写的是不安全的,所以只能保证多线程取-get 的是同一份,但不是说这个属性就是线程安全的,要想这个属性不仅仅取-get是安全的,而且set也是安全的话,就必须在属性的set方法上加synchronize锁才能真正意义上保证这个属性是线程安全的)。
·在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
·volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

· sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。
·synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
·synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。
·ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
·AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。AQS 就是一个抽象类,主要用来构建锁和同步器。
·基于 AQS 的常见同步工具类。
·Semaphore(信号量)
·CountDownLatch (倒计时器)
·CyclicBarrier(循环栅栏)

·JMM(Java 内存模型)(Java Memory Model)

主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。其主要目的是为了简化多线程编程,增强程序可移植性的。
·Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
·对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。

·内存屏障

(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。

·ThreadLocal

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
·最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

·强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候。
·软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收。
·弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。
·虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

·ConcurrentHashMap

JDK1.8 的 ConcurrentHashMap 不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时,链表会转换成红黑树。

·ReentrantLock

实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock 里面有一个内部类 Sync,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。ReentrantLock 默认使用非公平锁。
·synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
·ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
·相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法.

·ReentrantReadWriteLock 是什么?

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。由于 ReentrantReadWriteLock 既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下使用。
·StampedLock 不是直接实现 Lock或 ReadWriteLock接口,而是基于 CLH 锁 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。StampedLock 通过 CLH 队列进行线程的管理,通过同步状态值 state 来表示锁的状态和类型。

·AQS

全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。
CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 线程等待/等待队列 来完成获取资源线程的排队工作。state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。
·以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。
·AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

·什么是钩子方法呢? 钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。
·AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

·并发编程

五、反射

·反射:Java 程序在运行期间可以获取到一个对象的全部信息。
·反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。
·优点:可以动态地创建和使用对象,反射机制是 Java 框架的底层核心,其使用灵活,没有反射机制,底层框架就失去支撑。为各种框架提供开箱即用的功能提供了便利。
缺点:使用反射基本是解释执行,对程序执行速度有影响。也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
·当我们获取到某个Class类对象时,实际上就获取到了一个类的类型:

Class cls = String.class; // 获取到 String 的 Class类对象
String s = "";
Class cls = s.getClass(); // s是String,因此获取到String的Class
Class s = Class.forName("java.lang.String");

·这三种方式获取的Class类对象都是同一个对象,因为 JVM 对每个加载的Class只创建一个Class类对象来表示它的类型。
·反射机制是 Java实现动态语言的关键,也就是通过反射实现类的动态加载。
·静态加载:编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。
·动态加载:运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。
·如果获取到了一个Class类对象,我们就可以通过该Class类对象来创建其对应类的实例对象:

Class cls = String.class;// 获取 String 的 Class 类对象
String s = (String) cls.newInstance();// 通过 String 的 Class 类对象创建一个 String 类的实例对象
上述代码相当于new String()。通过Class.newInstance()可以创建类的实例对象,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。

·为了调用任意的构造方法,Java 的反射 API 提供了Constructor类对象,它包含一个构造方法的所有信息,通过Constructor类对象可以创建一个类的实例对象。Constructor类对象和Method类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象。
·在方法名中加Declared的是返回所有的构造方法,不加Declared的只返回public访问权限的构造器
·通过反射可以直接修改指定对象的字段的值。设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的对象,第二个Object参数是待修改的值。
·通过反射读写字段是一种非常规的方法,它会破坏对象的封装。
·还可以获取父类的Class。获取interface:getInterfaces()方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。获取继承关系。
·通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

· Class<? extends User> aClass  = user.getClass(); //Class对应java种编码后字节码。编译后的字节码文件也可以当做对象
    // javap,反编译。java -v Chlid执行

·获取权限(修饰符):多个修饰符会融合成一个Int值
·getFiled:获取字段。
·getMethod:获取方法。
·注解也使用了反射。
·注解只有被解析之后才会生效,常见的解析方法有两种:
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。

·何谓 SPI?

·SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

·一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
·SPI 的优缺点?通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
当多个 ServiceLoader 同时 load 时,会有并发问题。

·动态代理

· 不能实例化接口,所有接口interface类型的变量总是通过某个实现了接口的类的对象向上转型再赋值给接口类型的变量。
·有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。
· 还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()方法创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理。JDK 提供的动态创建接口对象的方式,就叫动态代理。
·动态代理提供了一种灵活且非侵入式的方式,可以对对象的行为进行定制和扩展。它在代码重用、解耦和业务逻辑分离、性能优化以及系统架构中起到了重要的作用。
·实现AOP编程:动态代理是实现面向切面编程(AOP)的基础。通过代理对象,可以将横切关注点(如日志、事务、安全性)与业务逻辑进行解耦,提供更高层次的模块化和可重用性。
·动态代理实现方式主要有两种:

·基于JDK动态代理

使用Java的反射机制来实现动态代理。通过Proxy.newProxyInstance(目标对象的类加载器,目标对象所实现的接口,代理对象的调用处理程序)方法来获取代理对象;
通过InvocationHandle中的invoke方法实现增强和调用目标对象方法;
invoke(目标对象,对应于在代理对象上调用的接口方法的Method实例,代理对象调用接口方法时传递的实际参数);
通过method.invoke(目标对象,代理对象调用接口方法时传递的实际参数)来执行真实对象

·CGLIB动态代理:

利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
·区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。
1、JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高;
2、JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法,CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。

·实际:
// 创建目标对象
// 创建InvocationHandler实例
// 创建动态代理对象
// 通过代理对象调用方法

·相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

· 反射 类加载器

    java种的类主要分为3种:
         1.java核心类库中的类:String.Object
         2.JVM软件平台开发商
         3.自己写的类
     类加载器也有3种:
         1.BootClassLoader:启动类加载器(加载类时,采用操作系统平台语言实现)
         2.PlatformClassLoader:平台类加载器
         3.AppClassLoader:应用类加载器

加载java核心类库 > 平台类 > 自己写的类
·之后的java版本都是先获得构造方法,在通过构造方法的对象将这个类实例化

六、垃圾回收

·垃圾回收(Garbage Collection,简称GC)是内存管理的核心组成部分,它负责自动回收不再使用的内存空间。在Java中,程序员不需要手动释放对象占用的内存,一旦对象不再被引用,垃圾回收器就会在适当的时机回收它们所占用的内存。这样可以避免内存泄漏和野指针,从而大大减轻了程序员的负担,也使得Java成为一个相对安全、易于开发的编程语言。
·防止内存泄漏:手动管理内存容易导致内存泄漏,而GC可以自动回收不再使用的对象,防止内存泄漏的发生。
·提高开发效率:程序员不再需要关心内存释放的问题,可以更加集中精力在业务逻辑的实现上。
·系统性能和稳定性:通过有效的垃圾回收策略,可以保证系统的性能和稳定性。
垃圾回收的基本步骤分两步:
·查找内存中不再使用的对象(GC判断策略)
·释放这些对象占用的内存(GC收集算法)
·Java中有四种类型的引用,它们对垃圾回收的影响不同:
·1)强引用 (Strong Reference): 最常见的引用类型,只要对象有强引用指向,它就不会被垃圾回收。
·2)软引用 (Soft Reference): 软引用可以帮助垃圾回收器回收内存,只有在内存不足时,软引用指向的对象才会被回收。
·3)弱引用 (Weak Reference): 弱引用指向的对象在下一次垃圾回收时会被回收,不管内存是否足够。
·4)虚引用 (Phantom Reference): 虚引用的主要用途是跟踪对象被垃圾回收的状态,虚引用指向的对象总是可以被垃圾回收。

·GC判断策略

·1. 引用计数算法
·2. 可达性分析算法
·垃圾回收算法

·1 标记-清除 (Mark-Sweep)

标记阶段: 在标记阶段,垃圾回收器会从GC Roots开始,遍历所有可达的对象,并标记它们为活动对象。
清除阶段: 在清除阶段,垃圾回收器会遍历整个堆,回收所有未被标记的对象的内存。
该算法有两个问题:
效率问题:标记和清除过程的效率都不高;
空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

·2复制 (Copying)

复制算法将堆内存分为两个相等的区域,只使用其中一个区域。当这个区域的内存用完时,垃圾回收器会将所有活动对象复制到另一个区域,并回收原区域的所有内存。
优点: 减少内存碎片,提高空间利用率。
缺点: 减半了可用的堆内存,可能增加垃圾回收的频率。

·3 标记-整理 (Mark-Compact)

标记整理算法是标记清除算法的改进版本。它在标记和清除的基础上增加了整理阶段,将所有活动对象向一端移动,从而消除内存碎片。
优点: 解决了内存碎片化问题,提高了空间利用率。
缺点: 移动对象增加了额外的开销。

·4 分代收集 (Generational Collection)

·新生代(Young Generation)
的回收算法(以复制算法为主)
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
·老年代(Tenured Generation)
的回收算法(以标记-清除、标记-整理为主)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC,Major GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
·永久代(Permanet Generation)
的回收算法
用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面3个条件:
·该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
·加载该类的ClassLoader已经被回收;
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

·JVM 具有四种类型的 GC 实现:

1、串行垃圾收集器
2、并行垃圾收集器
3、CMS 垃圾收集器
4、G1 垃圾收集器
·CMS(Concurrent Mark Sweep)是Java虚拟机中一种常用的垃圾回收器,它以低停顿时间为目标,在垃圾回收过程中减少应用程序的停顿时间。  CMS全称Concurrent marke sweep,中文是并发标记清除算法。
·初始标记阶段:在这个阶段中,收集器会暂停程序的执行,标记出直接被根对象引用的对象。
·并发标记阶段:在这个阶段中,收集器可以并发标记所有可达的对象,不需要暂停应用程序。
·重新标记阶段:在应用程序运行期间,收集器继续标记新生成的对象,直到所有对象都被标记为可达或不可达。
·并发清除阶段:在这个阶段中,收集器可以并发清除不可达对象,不需要暂停应用程序。
相比于其他垃圾回收算法,CMS具有较短的回收时间和较小的暂停时间。但是,CMS也存在某些缺点,如在运行过程中需要占用一定的CPU资源,可能会导致部分应用程序性能下降。

**·G1 **(Garbage-First)垃圾回收器是一种基于分代的简单垃圾回收器,它将整个Java堆划分为许多大小相等的区域,称为“区域”。它 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
·并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
·分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
·空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
·可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。G1 收集器的运作大致分为以下几个步骤:
G1的回收过程分为以下几个步骤:
·1)初始标记阶段:和CMS一样,这个阶段需要暂停程序的执行,标记出直接被根对象引用的对象。
·2)并发标记阶段:和CMS一样,这个阶段中,G1收集器并发地标记所有可达的对象。
·3)最终标记阶段:在这个阶段中,收集器扫描堆并且标记所有未被标记的存活对象。
·4)筛选回收阶段:在这个阶段中,收集器根据区域的回收价值进行排序,以便选择价值最低的区域进行垃圾回收,这样可以保证回收效率。
相较于CMS,G1的优势在于它可以控制暂停时间,避免应用程序停顿过长时间,同时还具有高回收效率、低内存使用和更好的可预测性等优点。
·G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
·CMS(Concurrent Mark-Sweep)和G1(Garbage-First)是Java虚拟机(JVM)中两种不同的垃圾回收器,它们在设计、行为和适用场景上有一些显著的区别。
并发度:
CMS:CMS是一种并发垃圾回收器,它在大部分工作时间与应用程序线程并发执行,目的是最小化垃圾收集期间的停顿时间。它通过标记-清除(Mark-Sweep)算法来执行垃圾收集,其中标记阶段和清除阶段与应用程序线程并发执行。
G1:G1也是一种并发垃圾回收器,但它采用了不同的工作方式。G1使用了分代收集的概念,但它不像传统的分代收集器那样将堆划分为固定大小的年轻代和老年代,而是将堆划分为多个相等大小的区域。G1的并发收集主要体现在其标记阶段,通过并发标记来减少垃圾收集期间的停顿时间。
内存分配:
CMS:CMS不会进行全堆的整理,因此在清除阶段会产生内存碎片。这可能导致在分配大对象时出现内存不足的情况,从而触发Full GC。
G1:G1通过在标记阶段记录每个区域的存活对象信息,以及在垃圾收集阶段将存活对象移动到空闲的区域来避免内存碎片的问题。这使得G1能够更好地应对大量分配和回收对象的场景,减少Full GC的频率。
停顿时间:
CMS:CMS致力于减少垃圾收集期间的停顿时间,因此适用于对延迟敏感的应用程序。但是,随着应用程序的负载增加,CMS可能会出现“Concurrent Mode Failure”,导致Full GC停顿时间较长。
G1:G1也致力于减少垃圾收集期间的停顿时间,但它通过在标记阶段与应用程序线程并发执行来实现这一目标,从而避免了CMS中可能出现的“Concurrent Mode Failure”问题。G1在调优配置良好的情况下通常能够提供更稳定和可控的停顿时间。
可预测性:
G1:G1的设计目标之一是提供更稳定和可预测的暂停时间,它通过动态确定每次垃圾收集的目标暂停时间来实现这一目标。
CMS:CMS虽然也致力于减少停顿时间,但在面对不可预测的负载和应用程序行为时,其停顿时间可能会不稳定。

七、注解

·注解(Annotation)是Java语言中的一种特殊语法结构,它可以在代码中嵌入元数据(metadata),用于一些特殊的标记和说明。注解可以在编译时被读取,并在运行时使用。
·基本语法:

@注解名称(属性名1=属性值1, 属性名2=属性值2, ...)

·元数据(metadata)指的是描述数据的数据,是关于数据的信息。在Java语言中,元数据指的是描述Java程序中各种元素的信息,比如类、方法、变量等的信息。元数据可以被注解(Annotation)所表示和使用,相当于是对程序的一些补充说明和标记。简单来说,元数据是用来描述程序中各种元素的信息的,而注解则是用来表示这些元数据的。
·元注解(meta-annotation)是指用来注解其他注解的注解。Java语言中提供了4种元注解,分别是@Retention、@Target、@Inherited和@Documented。它们的作用如下:
①@Retention:用于指定注解的保留策略。它有一个属性value,可以设置为RetentionPolicy.SOURCE、RetentionPolicy.CLASS或RetentionPolicy.RUNTIME,分别表示注解保留在源代码中、保留在class文件中或保留在运行时。
②@Target:用于指定注解的作用目标。它有一个属性value,可以设置为ElementType.TYPE、ElementType.FIELD、ElementType.METHOD等,表示注解可以作用于类、字段、方法等不同的目标上。
③@Inherited:用于指定注解是否可以被继承。如果一个注解被@Inherited注解,则它被用来注解的类的子类也会继承这个注解。
④@Documented:用于指定注解是否包含在JavaDoc中。如果一个注解被@Documented注解,则它会被包含在JavaDoc中,方便开发者查看。
·Java语言提供了一些内置注解,这些注解可以用来标记特定的场景,从而帮助编译器和开发者更好地理解代码的含义。以下是一些常见的内置注解及其使用场景和用法:
@Override:用来标记方法覆盖父类的方法。如果一个方法使用了该注解,但是却没有覆盖父类中的任何方法,编译器就会报错。
@Deprecated:用来标记已经过时的方法或类。如果一个方法或类使用了该注解,编译器会给出警告信息,提示开发者不要再使用该方法或类。
@SuppressWarnings:用来抑制编译器的警告信息。如果一个方法或类使用了该注解,编译器就不会报出被抑制的警告信息。
@SafeVarargs:用来标记可变参数的方法。如果一个方法使用了该注解,并且方法中使用了可变参数,编译器会给出警告信息,提示开发者要确保该方法是类型安全的。
@FunctionalInterface:用来标记函数式接口。如果一个接口使用了该注解,编译器会检查该接口是否符合函数式接口的定义,即是否只包含一个抽象方法。
·自定义注解
是Java语言的一个重要特性,可以通过注解来标记代码,方便开发者在编写代码时进行一些特殊的处理。以下是自定义注解的创建和使用方法:
自定义注解需要使用关键字@interface来定义,例如:
public @interface MyAnnotation {
//注解属性定义
}
在这个例子中,@interface表示创建一个注解,MyAnnotation是注解的名称,可以在代码中使用该注解。
·注解和反射结合使用是Java语言的一个高级应用,可以通过注解来标记代码,在运行时通过反射机制来获取和处理这些注解,从而实现一些特定的功能。以下是注解和反射结合使用的一些高级应用:

·运行时注解处理
运行时注解处理是指在程序运行时,通过反射机制来获取和处理注解。例如,我们可以定义一个注解,用来标记某个方法是否需要进行性能监控,例如:
·注解处理器
注解处理器是指一种程序,用来在编译时处理注解。例如,我们可以定义一个注解,用来生成序列化代码,例如:

八、IO,BIO,NIO,AIO

·IO:

·IO模型主要分类:
同步(synchronous) IO和异步(asynchronous) IO
阻塞(blocking) IO和非阻塞(non-blocking)IO
同步阻塞(blocking-IO)简称BIO
同步非阻塞(non-blocking-IO)简称NIO
异步非阻塞(asynchronous-non-blocking-IO)简称AIO
·BIO:
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
·NIO:
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
·AIO:
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。

·NIO
· Channel(通道)既可以用来进行读操作,又可以用来进行写操作。NIO中常用的Channel有FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
·Buffer缓冲区用来发送和接受数据。
·Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。在javaNIO中使用Selector往往是将Channel注册到Selector中。
·NIO通过一个Selector,负责监听各种IO事件的发生,然后交给后端的线程去处理。NIO相比与BIO而言,非阻塞体现在轮询处理上。BIO后端线程需要阻塞等待客户端写数据,如果客户端不写数据就一直处于阻塞状态。而NIO通过Selector进行轮询已注册的客户端,当有事件发生时才会交给后端去处理,后端线程不需要等待。

·AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。

·BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
·NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
·AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
·Java的NIO,在Linux上底层是使用epoll实现的。epoll是一个高性能的多路复用I/O工具,改进了select和poll等工具的一些功能。
epoll的数据结构是直接在内核上进行支持的。通过epoll_create和epoll_ctl等函数的操作,可以构造描述符(fd)相关的事件组合(event)。
·这里有两个比较重要的概念:
Fd(file descriptor),是进程独有的文件描述符表的索引。 每条连接、每个文件,都对应着一个描述符,比如端口号。内核在定位到这些连接的时候,就是通过fd进行寻址的。
event 当fd对应的资源,有状态或者数据变动,就会更新epoll_item结构。在没有事件变更的时候,epoll就阻塞等待,也不会占用系统资源;一旦有新的事件到来,epoll就会被激活,将事件通知到应用方。
·关于epoll还会有一个面试题:相对于select,epoll有哪些改进?
·epoll不再需要像select一样对fd集合进行轮询,也不需要在调用时将fd集合在用户态和内核态进行交换。
·应用程序获得就绪fd的事件复杂度,epoll时O(1),select是O(n)。
·select最大支持约1024个fd,epoll支持65535个。
·select使用轮询模式检测就绪事件,epoll采用通知方式,更加高效。
·多路复用在Linux内核代码迭代过程中依次支持了三种调用:
·SELECT 系统调用
可以把1024个文件描述符的IO事件轮询,简化为一次轮询,轮询发生在内核空间。使用select的核心步骤:
·先准备了一个数组 fds,让 fds 存放着所有需要监视的 socket。

·然后调用 select,如果 fds 中的所有 socket 都没有数据,select 会阻塞,直到有一个 socket 接收到数据,select 返回,唤醒进程。
·用户可以遍历 fds,通过 FD_ISSET 判断具体哪个 socket 收到数据,然后做出处理。
这里的fd_set,从一个文件描述符数组,优化为一个BitMap结构。在内核遍历完fd数组后,发现有IO就绪的fd,则会将该fd对应的BitMap中的值设置为1,内核处理完成之后,将修改后的fd数组,返回给用户线程。在用户线程中需要重新遍历fd数组,找出IO就绪的fd出来,然后发起真正的读写调用。
select是操作系统内核提供给我们使用的一个系统调用,它解决了在非阻塞IO模型中需要不断的发起系统IO调用去轮询各个连接上的Socket接收缓冲区所带来的用户空间与内核空间不断切换的系统开销。
select系统调用将轮询的操作交给了内核来帮助我们完成,从而避免了在用户空间不断的发起轮询所带来的的系统性能开销。
首先用户线程在发起select系统调用的时候会阻塞在select系统调用上。此时,用户线程从用户态切换到了内核态完成了一次上下文切换
用户线程将需要监听的Socket对应的文件描述符fd数组通过select系统调用传递给内核。此时,用户线程将用户空间中的文件描述符fd数组拷贝到内核空间。
select的不足:
·每次调用 select 都需要这两步操作:添加进程到socket的等待队列,阻塞进程。
所以,需要socket列表两次遍历开销:
第1次:进程加入socket的等待队列时,需要遍历所有socket。
第2次:当进程A被唤醒后,唤醒后需要从所有的socket的等待队列中移除。
正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
·另外就是fd的两次复制开销:
每次调用select都需要将fds列表传递给内核,需要进行一次复制,有一定的开销。
每次调用select完成后,都必须把fd集合从内核空间拷贝到用户空间,这也有一定的开销,这个开销在fd很多时会很大;
·还有:用户线程依然要遍历文件描述符集合去查找具体IO就绪的Socket
虽然由原来在用户空间发起轮询,优化成了在内核空间发起轮询,但select不会告诉用户线程到底是哪些Socket上发生了IO就绪事件,只是对IO就绪的Socket作了标记,用户线程依然要遍历文件描述符集合,去查找具体IO就绪的Socket。时间复杂度依然为O(n)。总之,select也不能解决C10K问题。以上select的不足所产生的性能开销都会随着并发量的增大而线性增长。

·POLL 系统调用

1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。poll其实内部实现基本跟select一样,区别在于它们底层组织fd[]的数据结构不太一样,从而实现了poll的最大文件句柄数量限制去除了。poll的描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,poll管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll只是改进了select只能监听1024个文件描述符的数量限制,但是并没有在性能方面做出改进。
·EPOLL 系统调用
select将“事件注册”和“事件查询”两个步骤合二为一,紧密耦合 ,select/poll,需要两次socket列表遍历:
第1次:每次调用select都需要将fds列表传递给内核,有一定的开销。进程加入socket的等待队列时,需要遍历所有socket。
第2次:当进程A被唤醒后,唤醒后需要从所有的socket的等待队列中移除。
·epoll则“事件注册”和“事件查询”两个步骤进行解耦,一分为二,
如何优化:
epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见的,不需要再每次查询的时候, 进行大量的数据复制, 效率就能得到提升。
select 低效的另一个原因在于程序不知道哪些 socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”rdlist,引用收到数据的 socket ,就能避免遍历。
epoll 的优化措施:
epoll 是在 select 、poll出现 N 多年后才被发明的,是 select 和 poll 的增强版本。epoll 通过以下一些措施来改进效率。
1)功能解耦:把则“事件注册”和“事件查询”两个步骤进行解耦,一分为二
2)空间换时间: 引入了就绪列表rdlist ,存储已经发生了io 事件的文件描述符
epoll 的三个方法
epoll_create:内核会创建一个 eventpoll 对象(专用的文件描述符,也就是程序中 epfd 所代表的对象)
eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。
epoll_ctl:事件注册, 添加待监控的socket
如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将三个 socket 添加到 eventpoll 监听队列。
epoll_wait:事件查询,阻塞等待
进程 A 运行到了 epoll_wait 语句之后,进程A会等待eventpoll 的等待队列。

·Reactor

Reactor模型是对事件处理流程的一种模式抽象,是对IO多路复用模式的一种封装,Reactor又叫反应器,在这里特指的是对各种事件的反应处理。Reactor模型有2个重要的组件:

  • Reactor:专门用于监听和响应各种IO事件,例如连接事件、读事件、写事件等,当检测到有一个新的事件发生时,就会交给相应的Handler去处理。
  • Handler:专门用来处理特定的事件的执行者。
    ·Reactor模式的优点:
    它把阻塞式的I/O转变为事件驱动的非阻塞式I/O,极大提升了重I/O场景下CPU的效率,让一颗CPU核心可以轻松同时处理大量的网络连接。
    但它也存在弊端:
    难以有效利用多颗CPU核心(单线程Reactor)
    每个Handler都必须经过精细的设计,一旦某个Handler中出现阻塞,或Handler执行耗时过长,整个程序都会被阻塞(通过外挂Worker线程池缓解)
    相比于传统的流程式代码,Reactor模式下一个流程被其中的I/O操作切分成了数个Handler,代码难以读懂
    代码调试难度高对外部资源(比如数据库等)的访问不能使用传统的SDK,也需要以Reactor方式实现。虽然外挂Worker线程池可以缓解,但对于大量依赖外部资源的场景仍然不可接受
    ·正因为如此,Reactor模式更多被应用在中间件领域和一些功能相对收敛且对性能有较高要求的程序中,在复杂的业务系统中则较少使用。

·单Reactor单线程模型

这是最基本的Reactor模式,请求连接的接收和请求的处理都由一个Reactor在一个线程内完成。JDK的NIO的Selector选择器就是最简单的单Reactor单线程模型。
优点:
无上下文切换,性能好
无需考虑多线程带来的线程安全和线程通信问题
缺点:
无法有效利用多个CPU核心
任何一个Handler出现性能问题都会阻塞住整个软件程序
Handler中不可以出现阻塞式调用(比如传统的阻塞式I/O)
Redis6.0之前就是典型的单Reactor单线程模型,虽然6.0以后引入了多线程,但是它的多线程只是用来处理耗时的网络IO操作上,实际执行命令的Handler仍然是单线程。

· 单Reactor多线程模型

这种模式给单线程Reactor外挂了一个worker线程池,当Handler中包含耗时较长的计算或阻塞式操作时,可以将Handler委派给worker线程池中的线程执行,在worker线程完成了工作后,通过回调将完成事件推入Event Queue。
这样可以使运行时间较长的Handler不阻塞Reactor线程,一定程度上缓解了Reactor模式中Handler不可以执行阻塞式操作的问题。
优点:
允许预期内的长耗时Handler存在
缺点:
引入了多线程模式的问题,如上下文切换,线程安全等
仍然无法有效利用多颗CPU核心,worker线程虽然在一定程度上使用了多核心,但交给worker线程的工作通常是阻塞式I/O操作,真正繁忙的Reactor线程仍然还是只能利用一颗CPU核心。
这种模式相当于一种向传统多线程模式的折中,毕竟复杂业务系统中有大量的阻塞式操作在所难免。对于开放式的Reactor开发框架来说,要求开发者的每一行代码都不可以阻塞是不现实的。Netty(一个著名的Java网络应用程序开发框架)就支持开发者把特定Handler委派给单独的EventExecutorGroup执行。

· 多Reactor多线程模型(主从Reactor模型)

为了优化单Reactor模型的性能瓶颈,将原来单独的Reactor的功能进行分解为连接处理器和通信处理器,由多个不同的Reactor共同完成网络通信任务。
多Reactor模式拥有多个Reactor线程,其中主Reactor线程负责监听连接,连接创建后便将针对该连接的请求处理委派给一个从Reactor线程。Worker线程池对于多Reactor模式属于可选组件,可以没有Worker线程池,或是从Reactor共享一个Worker线程池,亦或是有多个Worker线程池。
·主Reactor拥有自己的Selector,通过select监控连接事件,事件发生后交给acceptor组件处理,然后acceptor组件将连接分配给某一个从Reactor。
·从Reactor也有自己的Selector,从Reactor监听并执行读、写等事件。
·线程池的任务没有变化,负责处理非IO的事件任务,例如编解码序列化、计算等。
·主从Reactor模型中,主Reactor和从Reactor都可以存在多个,每个Reactor都有自己的Selector,都是独立的线程工作,这样充分利用了多核CPU的优势。
但是多Reactor多线程架构仍然不能根治IO操作对其他Client的效能影响,毕竟有可能某个从Reactor可能有多个client的连接。所以诞生了异步IO模型的Proactor模型来实现真正的异步IO。
Netty、Memcached、Nginx都是采用的多Reactor多线程的模型。不过Netty支持多种Reactor模型的配置。
优点:
可以有效利用多颗CPU核心
多Reactor模式是在实际应用场景中使用最多的:
Nginx的master进程即是此模式中的主Reactor,而每个worker进程则都是从Reactor。master进程负责监听连接,具体的请求处理则交由其中一个worker进程。
Redis 6.0之后的多线程I/O能力也采用了类似的方式,主Reactor负责处理内存读写,每个I/O线程对应一个从Reactor线程。
Netty的bossGroup是主Reactor,workerGroup则是从Reactor

九、并发编程,琐

·轻量级阻塞,能被中断的阻塞,通过wait进行阻塞等待,可以被interrupt唤醒;而不能被中断的阻塞被称为重量级阻塞,比如synchronized修饰的同步块,状态为blocked。
·并发系统实现同步机制
信号量:一种用于控制对一个或者多个单位资源进行访问的机制,他有一个用于存放可用资源数量的变量。互斥就是一种特殊的信号量,他只有两种状态忙和空闲,当空闲就可以被争抢,当忙就只能被持有线程释放,通过保护临界段避免条件竞争。
监视器:一种在共享资源上实现互斥的机制,他有一个互斥、一个条件变量、两种操作即等待条件和通报条件,一旦你通报了该条件,在等待他的任务中只有一个会继续执行。
·如果共享数据受同步机制保护,那么代码就是线程安全的。
·CAS是乐观锁的实现方式之一,先拿到值,不加锁,最后修改的时候再获取当前值与之前值比较来判断是否能同步。
·并发多线程之间的两种通信方式:
1)共享内存:同一台计算机运行多个任务,在相同的内存区域读或写,对该内存区域访问采用同步机制的保护临界段
2)消息传递:不同计算机运行多个任务,多任务之间执行消息传递,需要遵循预定义的协议,而且分两种情况同步和异步,发送方发送消息后阻塞并等待响应就是同步,发送方发送消息后继续执行自己流程就是异步。比如分布式集群,在不同机器部署,又需要互相之间消息通讯。
·重排序类型:
1) 编译器重排序。没有依赖关系的语句,编译器可以调整顺序。
2) CPU指令重排序。指令级别执行,两条指令依赖关系就可以不按顺序执行。
3) CPU内存重排序。指令执行顺序与写入执行顺序不一致,比如通过异步调用,造成缓存不一致就是内存重排序

·内存屏障

为了禁止编译器重排序和CPU指令重排序,在编译器和CPU层面都有相应指令防止重排序,也就是内存屏障。他是JMM和happen-before规则的底层实现原理。
编译器的内存屏障是告诉编译器在编译过程中不要进行指令重排,而CPU的内存屏障是指令,给开发者调用,比如volatile关键字。
·Happen-before
他是JMM内存模型的规范,从字面理解就得到结果了先行发生,用于描述两个操作之间的内存可见性。
主要干两个事,
第一:编译器和CPU可以灵活重排序
第二:开发者能知道的那些重排序和不应该知道的重排序,比如开发者知道可以用volitile和synchronized等线程同步机制来禁止重排序。比如A happen-before B,那么A执行结果必须对B可见,保证跨线程之间的内存可见性。

·Condition避免了wait/notify的生产者通知生产者、消费者通知消费者的问题。

·异步编程(CompletableFuture)

·DK8中,在Concurrent包中提供了一个强大的异步编程工具CompletableFuture,CompletableFuture实现了Future接口,具有Future的特性,在此之前,异步编程可以通过线程池和Future来实现。

·多线程并行计算框架(ForkJoinPool)

ForkJoinPool就是JDK7提供的一种“分治算法”的多线程并行计算框架。Fork意为分叉,Join意为合并,一分一合,相互配合,形成分治算法。相比于ThreadPoolExecutor,ForkJoinPool可以更好地实现计算的负载均衡,提高资源利用率。

·语法糖(Syntactic sugar)

代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。举个例子,Java 中的 for-each 就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
·Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
·你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 native 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 本地代码。
·为什么要使用本地方法呢?需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用。对于其他语言已经完成的一些现成功能,可以使用 Java 直接调用。程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。
·编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。

·Comparable 和 Comparator 的区别?Comparable 接口和 Comparator 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:Comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort().Comparator 定制排序。

·比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

·synchronized 底层原理了解吗?

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。上面的字节码中包含一个 monitorenter 指令以及两个 monitorexit 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitoropen in new window实现的。每个对象中都内置了一个 ObjectMonitor对象。另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

·synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。总结synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。

· ReentrantLock 是什么?

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。

· synchronized 和 ReentrantLock 有什么区别?
两者都是可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。

·ReentrantLock 比 synchronized 增加了一些高级功能相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

**·可中断锁和不可中断锁有什么区别?**可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

·共享锁和独占锁有什么区别?
共享锁:一把锁可以被多个线程同时获得。
独占锁:一把锁只能被一个线程获得。

·线程持有读锁还能获取写锁吗?在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

·读锁为什么不能升级为写锁?
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
·通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

·ThreadPoolExecutor 3 个最重要的参数:corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

·给线程池里的线程命名通常有下面两种方式:
1、利用 guava 的 ThreadFactoryBuilder
2、自己实现 ThreadFactory

·假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列(ThreadPoolExecutor 的构造函数有一个 workQueue 参数可以传入任务队列)。

·Future 类

是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。

·AQS

全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。AQS 就是一个抽象类,主要用来构建锁和同步器。AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。

·AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
·CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

·Semaphore 有什么用?
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。

·Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。

·CountDownLatch 有什么用?CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
·CyclicBarrier 有什么用?

· 零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: mmap+write、sendfile和 sendfile + DMA gather copy 。

· 对象的创建

Java 对象的创建过程我建议最好是能默写出来,并且要掌握每一步在做什么。
Step1:类加载检查虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式 (补充内容,需要掌握):指针碰撞: 适用场合:堆内存规整(即没有内存碎片)的情况下。原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。使用该分配方式的 GC 收集器:Serial, ParNew空闲列表: 适用场合:堆内存不规整的情况下。原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。使用该分配方式的 GC 收集器:CMS选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
内存分配并发问题(补充内容,需要掌握)在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
Step3:初始化零值内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。对象的内存布局

·JMM

Java 内存模型(JMM)和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
JMM 为共享变量提供了可见性的保障。

**·为什么需要 happens-before 原则? **
happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。
happens-before 原则的设计思想其实非常简单:为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
·在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

·Java 常见并发容器总结

JDK 提供的这些容器大部分在 java.util.concurrent 包中。
·ConcurrentHashMap : 线程安全的HashMap
**·CopyOnWriteArrayList **: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。它更进一步地实现了这一思想。为了将读操作性能发挥到极致,它的读取操作是完全无需加锁的。写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。它线程安全的核心在于其采用了 写时复制的策略。
·ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的LinkedList,这是一个非阻塞队列。非阻塞队列。它主要使用 CAS 非阻塞算法来实现线程安全就好了。
·BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。阻塞队列。它提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。它 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。
·ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。跳表的本质是同时维护了多个链表,并且链表是分层的。

· 3 个常见的 BlockingQueue 的实现类:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue

·Java 8 才被引入的 CompletableFuture 可以帮助我们来做多个任务的编排,功能非常强大。
·Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。这其实就是多线程中经典的 Future 模式,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。
·在 Java 中,Future 类只是一个泛型接口,位于 java.util.concurrent 包下,其中定义了 5 个方法,主要包括下面这 4 个功能:
1)取消任务;
2)判断任务是否被取消;
3)判断任务是否已经执行完成;
4)获取任务执行结果。
·Future 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。Java 8 才被引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
·虚拟线程在 Java 21 正式发布,这是一项重量级的更新。什么是虚拟线程?
虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。
·虚拟线程有什么优点和缺点?
·优点:
1)非常轻量级:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。
2)简化异步编程: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。
3)减少资源开销: 相比于操作系统线程,虚拟线程的资源开销更小。本质上是提高了线程的执行效率,从而减少线程资源的创建和上下文切换。
·缺点:
1)不适用于计算密集型任务: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。
2)依赖于语言或库的支持: 协程需要编程语言或库提供支持。不是所有编程语言都原生支持协程。比如 Java 实现的虚拟线程。

·和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
·方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

·当永久保存区域的空间耗尽时OutOfMemoryError: PermGen space就会发生,这个错误一般是由于内存泄漏导致的。所谓内存泄漏,是指java类和类加载器在被取消部署后不能被垃圾回收。怎么会发生这种情况呢?
  举个例子:假如我们有一个Student类,这个类是Web应用程序jar包的一部分,同时在Web服务器的lib文件夹中包含了某种日志框架,其中有一个Log类提供register方法调用,从而使得别的类通过注册就可以使用日志功能。如果Student类被注册了,那么Log类就开始拥有了一个对Student对象的引用(reference)。当Student类取消部署时,它仍然是注册Log类的,Log类仍然拥有对Student对象的引用,因此,Student对象永远不会被垃圾回收。此外,由于Student对象拥有一个对它的ClassLoader的引用,所以ClassLoader本身永远也不会被垃圾回收,从而导致由它加载的所有类都不会被回收。
一个更为典型的例子是使用代理对象。Spring和Hibernate常常为某些类生成代理类,这些代理类也是通过类加载器加载的,并且存储在永久保存区域的堆空间,它们永远不会被丢弃,从而会导致永久保存区域的堆空间被填满。
·如何避免永久保存区域内存不足
  ·增加PermGen堆的最大尺寸
·避免使用静态字段
  确保在编写Java类时,不要使用静态变量作为对其他对象的引用。
·使用JDK动态代理,而不是CGLIB代理
  一些第三方的框架,如CGLIB会吞食大量的PermGen。因此,当遇到PermGen错误时,应尽快升级cglib到最新版;改用JDK动态代理,也是一个不错的选择。
·更新到最新版本Hibernate3.2
  此外,新版本的Hibernate不再使用CGLIB作为字节码提供者了,所以及时升级Hibernate,会大大降低出错的机会。
·共用的jar文件放到共享目录下

·JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
·直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

·双亲委派
·加载流程

十.设计模式

·设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
·设计模式分为三大类:

创建型模式:

用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式

结构型模式:

用于描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。共7种:适配器模式、装饰器模式、代理模式、桥接模式、外观模式、组合模式、享元模式

行为型模式:

用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。共11种:策略模式、模板方法模式、观察者模式、责任链模式、访问者模式、中介者模式、迭代器模式、命令模式、状态模式、备忘录模式、解释器模式

·2、设计模式的六大原则:

(1)开闭原则 (Open Close Principle) :
开闭原则指的是对扩展开放,对修改关闭。在对程序进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,我们就需要使用接口或者抽象类
(2)依赖倒转原则 (Dependence Inversion Principle):
依赖倒置原则是开闭原则的基础,指的是针对接口编程,依赖于抽象而不依赖于具体。高层模块不依赖于低层模块。对抽象进行编程。
(3)里氏替换原则 (Liskov Substitution Principle) :
里氏替换原则是继承与复用的基石,只有当子类可以替换掉基类,且系统的功能不受影响时,基类才能被复用,而子类也能够在基础类上增加新的行为。所以里氏替换原则指的是任何基类可以出现的地方,子类一定可以出现。尽量不要重写父类的方法。
里氏替换原则是对 “开闭原则” 的补充,实现 “开闭原则” 的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
(4)接口隔离原则 (Interface Segregation Principle):
使用多个隔离的接口,比使用单个接口要好,降低接口之间的耦合度与依赖,方便升级和维护方便。客户端不应该被迫依赖于它不适用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
(5)迪米特原则 (Demeter Principle):
迪米特原则,也叫最少知道原则,指的是一个类应当尽量减少与其他实体进行相互作用,使得系统功能模块相对独立,降低耦合关系。该原则的初衷是降低类的耦合,虽然可以避免与非直接的类通信,但是要通信,就必然会通过一个“中介”来发生关系,过分的使用迪米特原则,会产生大量的中介和传递类,导致系统复杂度变大,所以采用迪米特法则时要反复权衡,既要做到结构清晰,又要高内聚低耦合。
(6)合成复用原则 (Composite Reuse Principle):
尽量使用组合/聚合的方式,而不是使用继承。

·java常用的设计模式:

1、单例模式;

保证一个类仅有一个实例,并提供一个访问它的全局访问点。可以直接访问,不需要实例化类的对象。单例模式的写法有好几种,主要有三种:懒汉式单例、饿汉式单例、登记式单例。延迟加载,线程安全(java中class加载时互斥的),也减少了内存消耗,推荐使用内部类方式。

2、工厂模式;

为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
这三种模式从上到下逐步抽象,并且更具一般性。分为三类:
·简单工厂模式Simple Factory:不利于产生系列产品;
简单工厂模式又称静态工厂方法模式。它存在的目的:定义一个用于创建对象的接口。
在简单工厂模式中,一个工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, 如同一个交通警察站在来往的车辆流中,决定放行那一个方向的车辆向那一个方向流动一样。
组成:
工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。提供了创建产品的方法。
抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。
具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。
优:把对象的创建和业务逻辑层分开,更加容易扩展。
缺:增加新产品需要修改工厂类的代码,违背“开闭”原则。
·工厂方法模式Factory Method:又称为多形性工厂;
遵循了开闭原则。工厂方法模式是简单工厂模式的进一步抽象化和推广,工厂方法模式里不再只由一个工厂类决定那一个产品类应当被实例化,这个决定被交给抽象工厂的子类去做。工厂也抽象化。
组成:
抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现。
具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象
抽象产品角色:它是具体产品继承的父类或者是实现的接口。在java中一般有抽象类或者接口来实现。
具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在java中由具体的类来实现。
工厂方法模式使用继承自抽象工厂角色的多个子类来代替简单工厂模式中的“上帝类”。正如上面所说,这样便分担了对象承受的压力;而且这样使得结构变得灵活 起来——当有新的产品(即暴发户的汽车)产生时,只要按照抽象产品角色、抽象工厂角色提供的合同来生成,那么就可以被客户使用,而不必去修改任何已有的代 码。可以看出工厂角色的结构也是符合开闭原则的!
优点:新增加具体产品类和对应的具体工厂类,无需对原工厂进行修改。
缺:每增加一个具体产品类和一个对应的具体工厂类,增加系统复杂度。
简单工厂和工厂方法模式的比较:
工厂方法模式的核心是一个抽象工厂类,而不像简单工厂模式, 把核心放在一个实类上。工厂方法模式可以允许很多实的工厂类从抽象工厂类继承下来, 从而可以在实际上成为多个简单工厂模式的综合,从而推广了简单工厂模式。
·抽象工厂模式Abstract Factory:又称为工具箱,产生产品族,但不利于产生新的产品;
在抽象工厂模式中,抽象产品 (AbstractProduct) 可能是一个或多个,从而构成一个或多个产品族(Product Family)。 在只有一个产品族的情况下,抽象工厂模式实际上退化到工厂方法模式。
·总结
·简单工厂模式是由一个具体的类去创建其他类的实例,父类是相同的,父类是具体的。
·工厂方法模式是有一个抽象的父类定义公共接口,子类负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成。
·抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。它针对的是有多个产品的等级结构。而工厂方法模式针对的是一个产品的等级结构。
缺点:产品族新增加一个产品,所有的工厂类都需要进行修改。

3、建造(builder)模式;

是一种对象构建的设计模式,它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
Builder模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们。用户不知道内部的具体构建细节。Builder模式是非常类似抽象工厂模式,细微的区别大概只有在反复使用中才能体会到。为何使用?
是为了将构建复杂对象的过程和它的部件解耦

4、观察者模式;

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。观察者模式又叫发布-订阅(Publish/Subscribe)模式。

5、适配器(adapter)模式;

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。前者应用较少。

6、代理模式;

为其他对象提供一种代理以控制对这个对象的访问。

7、装饰模式。

装饰模式(Decorator),在不改变现有对象结构的情况下,动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
·原型模式
通过将一个对象作为原型,对其进行复制克隆,产生一个与源对象类似的新对象。
·建造者模式:
将复杂产品的创建步骤分解在在不同的方法中,使得创建过程更加清晰,从而更精确控制复杂对象的产生过程;
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
并且每个具体建造者都相互独立,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
·桥接模式
将系统的抽象部分与实现部分分离解耦,使他们可以独立的变化。为了达到让抽象部分和实现部分独立变化的目的,桥接模式使用组合关系来代替继承关系,抽象部分拥有实现部分的接口对象,从而能够通过这个接口对象来调用具体实现部分的功能。也就是说,桥接模式中的桥接是一个单方向的关系,只能够抽象部分去使用实现部分的对象,而不能反过来。
桥接模式符合“开闭原则”,提高了系统的可拓展性,在两个变化维度中任意扩展一个维度,都不需要修改原来的系统;并且实现细节对客户不透明,可以隐藏实现细节。但是由于聚合关系建立在抽象层,要求开发者针对抽象进行编程,这增加系统的理解和设计难度。
·外观模式
又叫门面模式,是一种通过多个复杂的子系统提供一个一致接口,而使这些子系统更加容易被访问的模式。缺点:不符合开闭原则,修改麻烦。
·组合模式
将叶子对象和容器对象进行递归组合,形成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性,能够像处理叶子对象一样来处理组合对象,无需进行区分,从而使用户程序能够与复杂元素的内部结构进行解耦。
·享元模式
通过共享技术有效地支持细粒度、状态变化小的对象复用,当系统中存在有多个相同的对象,那么只共享一份,不必每个都去实例化一个对象,极大地减少系统中对象的数量,从而节省资源。

·模板方法
基于继承实现的,在抽象父类中声明一个模板方法,并在模板方法中定义算法的执行步骤(即算法骨架)。在模板方法模式中,可以将子类共性的部分放在父类中实现,而特性的部分延迟到子类中实现,只需将特性部分在父类中声明成抽象方法即可,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤,不同的子类可以以不同的方式来实现这些逻辑。
模板方法模式的优点在于符合“开闭原则”,也能够实现代码复用,将不变的行为转移到父类,去除子类中的重复代码。但是缺点是不同的实现都需要定义一个子类,导致类的个数的增加使得系统更加庞大,设计更加抽象。
·策略模式
将类中经常改变或者可能改变的部分提取为作为一个抽象策略接口类,然后在类中包含这个对象的实例,这样类实例在运行时就可以随意调用实现了这个接口的类的行为。比如定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换,使得算法可独立于使用它的客户而变化,这就是策略模式。
·命令模式
本质是将请求封装成对象,将发出命令与执行命令的责任分开,命令的发送者和接收者完全解耦,发送者只需知道如何发送命令,不需要关心命令是如何实现的,甚至是否执行成功都不需要理会。命令模式的关键在于引入了抽象命令接口,发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。

使用命令模式的优势在于降低了系统的耦合度,而且新命令可以很方便添加到系统中,也容易设计一个组合命令。但缺点在于会导致某些系统有过多的具体命令类,因为针对每一个命令都需要设计一个具体命令类。
·责任链模式
将请求的处理者组织成一条链,并将请求沿着链传递,如果某个处理者能够处理请求则处理,否则将该请求交由上级处理。
·状态模式
允许对象在内部状态发生改变时改变它的行为,对象看起来就好像修改了它的类,也就是说以状态为原子来改变它的行为,而不是通过行为来改变状态。
·中介者模式
中介者对象/中介角色,来封装一系列的对象交互,将对象间复杂的关系网状结构变成结构简单的以中介者为核心的星形结构,对象间一对多的关联转变为一对一的关联,简化对象间的关系,便于理解;各个对象之间的关系被解耦,每个对象不再和它关联的对象直接发生相互作用,而是通过中介者对象来与关联的对象进行通讯,使得对象可以相对独立地使用,提高了对象的可复用和系统的可扩展性。
·迭代器模式
提供一种访问集合中的各个元素,而不暴露其内部表示的方法。将在元素之间游走的职责交给迭代器,而不是集合对象,从而简化集合容器的实现,让集合容器专注于在它所应该专注的事情上,更加符合单一职责原则,避免在集合容器的抽象接口层中充斥着各种不同的遍历操作。
·访问者模式
访问者模式是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作。
特征:可以在不改变数据结构的前提下定义作用于这些元素的新的操作。为不同类型的数据结构提供多种访问操作方式,这样是访问者模式的设计动机。
·备忘录模式
又称为快照模式(Snapshot Pattern)或令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
·解释器模式
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
特征:为了解释一种语言,而为语言创建的解释器。
·面向对象编程中,组合(Composition)是一种关系,表示一个类包含另一个类的对象,而这种关系通常表达了 “有一个” 的关联。这是一种对象关联的形式,与继承相比,组合更注重 “包含” 关系而不是 “是一个” 关系。
继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。

十一、类加载

·类加载过程

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析
·类加载过程的第一步,主要完成下面 3 件事情:
通过全类名获取定义此类的二进制字节流。
将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
加载这一步主要是通过我们后面要讲到的 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。
·验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。
不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。验证阶段主要由四个检验阶段组成:
文件格式验证(Class 文件格式检查)
元数据验证(字节码语义检查)
字节码验证(程序语义检查)
符号引用验证(类的正确性检查)。
·准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
·解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
·初始化阶段是执行初始化方法 ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

·卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
该类没有在其他任何地方被引用
该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

·类加载器

类加载器赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。
·JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
·JVM 中内置了三个重要的 ClassLoader:
·BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
·ExtensionClassLoader(扩展类加载器):jdk1.9后,平台类加载器,主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
·AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

·双亲委派模型

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
ClassLoader 类使用委托模型来搜索类和资源。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。
·简单总结一下双亲委派模型的执行流程:
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
·JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

双亲委派模型的好处:
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
·打破双亲委派模型方法
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
双亲委派模型的执行流程已经解释了:类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。重写 loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程。
例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。

·线程上下文类加载器

拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。
线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。

·十二、Java网络编程

·Socket概述

Java的网络编程主要涉及到的内容是Socket编程。Socket,套接字,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
  应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
  Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。socket是对端口通信开发的工具,它要更底层一些。

·InetAddress
Java提供了InetAddress类来代表IP地址,它有两个子类,分别为Inet4Address类和Inet6Address类,分别代表IPv4和IPv6的地址。InetAddress类没有提供构造方法,提供了5个静态方法来获取InetAddress实例。
·UDP(User Datagram Protoclo)和TCP(Transmission Control Protoclo),分别被称为用户数据报协议和传输控制协议。

·UDP通信

·在java.net包中还有一个DatagramSocket类,它是一个数据报套接字,包含了源IP地址和目的IP地址以及源端口号和目的端口号的组合,用于发送和接收UDP数据。
· UDP在发送数据时,先将数据封装成数据包,在java.net包中有一个DatagramPacket类,它就表示存放数据的数据包,DatagramPacket类的构造方法如表所示。
先运行接收方,在运行发送方。
·UDP的三种通信方式
·单播
·组播
组播地址:224.0.0.0 -239.255.255.255,其中224.0.0.0-224.0.0.225为预留的组播地址。此时创建的应该是MulticastSocket()。加入组播:joinGroup()。
·广播
广播地址:255.255.255.255

·TCP通信

·在java.net包中有一个ServerSocket类,它可以实现一个服务器端的程序,ServerSocket类的构造方法如表所示。
·在java.net包中还有一个Socket类,它是一个数据报套接字,包含了源IP地址和目的IP地址以及源端口号和目的端口号的组合,用于发送和接收UDP数据。Socket类的常用构造方法如表所示。

·重点理解网络编程的核心是IP(internet protocol设备在网络中的地址,是唯一的标识)、端口(应用程序在设备中的唯一标识)、协议(数据在网络中传输的规则)三大元素,网络编程的本质是进程间通信,网络编程的两个主要问题:一是定位主机,二是数据传输。


总结

暂未整理完整


部分内容参考链接:https://javaguide.cn/

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值