前言
java的一些特性、集合框架概要、线程安全的可用性说明、NIO网络编程
一、特性
1.抽象类和接口的区别
- 定义
- 抽象类:用abstract修饰的类,一个类没有包含足够多的信息来描述一个具体的对象
- 有构造器
- 接口方法可以有public、protectd、default修饰
- 单个继承
- 接口:抽象类型,抽象方法的集合。
- 无构造器
- 接口方法只能由public修饰
- 多重实现
抽象类可以有实例变量,而接口不能拥有实例变量,接口中的变量都是静态(static)的常量(final)
- 默认的方法实现
- 抽象类:可以有方法实现
- 接口: 没有(java8后可以使用default关键字添加非抽象方法)
- 实现
- 抽象类:使用extends继承,并且如果不是抽象类,则需要实现所有方法
- 接口:使用implements实现,并且需要实现所有方法
- 速度
- 抽象方法比接口快
2.反射
反射的操作都是在编译之后,即运行时
动态的获取类的信息和调用其方法
Class类的使用
类是对象,是java.lang.Class类的实例对象
任何一个类都是Class的实例对象
三种类实例对象(类类型)的表示方式
1
2
3
4
5
6
7// 1.任何一个类都有一个隐含的静态成员变量
Class c1 = Person.class;
// 2.类的对象
Class c2 = person.getClass();
// 3.通过类的全路径
Class c3 = Class.forName("com.mango.Person");
c1 == c2 == c3通过类的类类型创建实例对象
1
Person person = c1.newInstance();
- 动态加载类
Class.forName()代表动态加载类
编译时加载类是静态加载类,运行时是动态加载类
- 获取方法信息
基本的数据类型、void都存在类类型
Class
Method
1
2
3
4
5
6
7
8
9
10
11
12Class c1 = int.class;Class c2 = String.class;
// 1.类的名称:全称、简称c1.getName();
c2.getSimpleName();
// 2.Method类,方法对象:一个类方法就是一个Method对象
Method[] methods = c1.getMethods();
// 2.1方法的返回值类型的类类型
Class returnType = methods[0].getReturnType();
// 2.2方法的名称
String methodName = methods[0].getName();
// 2.3方法的参数列表类型的类类型
Class[] paramTypes = methods[0].getParameterTypes();
String paramTypeName = paramTypes[0].getName();
- 获取属性构造方法信息
属性也是对象
Field
Constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// cl.getFields() 获取的public的属性
// 1.获取所有该类属性
Field[] fields = c1.getDeclaredFields();
// 1.1获取属性类型的类类型
Class fieldType = fields[0].getType();
// 1.2获取属性名称
String fieldName = fields[0].getName();
// cl.getConstructors()获取的public的构造方法
// 2.获取类的构造方法
Constructor[] constructors = c1.getDeclaredConstructors();
String constructorName = constructors[0].getName();
// 2.1获取构造方法参数列表的类类型
Class[] paramTypes = constructors[0].getParameterTypes();
String paramTypeName = paramTypes[0].getName();
- 方法反射的操作
方法的名称和方法的参数列表才能唯一决定某个方法
反射的操作:method.invoke(对象,参数列表)
获取方法:c.getMethod(方法名,参数列表);
方法调用:method.invoke(对象,参数列表)
1
2
3
4
5
6// 1.获取类类型
Class c = person.getClass();
// 2.获取具体方法
Method method = c.getMethod("getAge",new Class[]{int.class});
// 3.调用
Object returnValue = method.invoke(person,23);
- 通过反射了解集合泛型的本质
编译之后集合的泛型是去泛型化
集合的泛型,是防止错误输入的,只在编译阶段有效
反射的操作都是在编译之后
1
2
3
4
5
6// 1.创建两个集合
ArrayList list = new ArrayList();ArrayList list2 = new ArrayList<String>();
// 2.反射操作
Class c1 = list.getClass();
Class c2 = list2.getClass();
c1 == c2
二、集合框架
单列集合根接口:Collection
双列集合:Map
1.List
- 特性
- 元素是有序的(存入和取出的顺序)、可重复的
1)ArrayList
- 数据结构:数组
- 线程不同步
2)LinkedList
- 数据结构:链表(双向)
- 线程不同步
3)Vector-效率低,废弃
- 数据结构:数组
- 线程同步
2.Set
- 特性
- 元素是无序的、不可重复的
1)HashSet
- 数据结构:哈希表
- 线程不同步
2)TreeSet
- 数据结构:二叉树
- 线程不同步
3)LinkedHashSet
- HashSet的子类
- 元素是有序的(插入和输出顺序一致)
3.Map集合
- 特性
- 存储形式是键值对,键是唯一的
- 优缺点
- 优点
- 灵活性强,易扩展,耦合度低
- 代码简单
- 缺点
- 存储的类型和内容不可知
- 优点
1)Hashtable-废弃
- 数据结构:哈希表
- 线程同步
2)HashMap
- 数据结构:哈希表
- 线程不同步
3)TreeMap
- 数据结构:二叉树
- 线程不同步
声明集合线程安全,可以使用Collections工具类的方法Collections.synchronizedList、Collections.synchronizedSet、Collections.synchronizedMap
三、线程安全
1.可见性
概述
- 可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到
- 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
- 不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后没有在工作内存与主内存同时刷新
Java内存模型:描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节
- 所有变量都存储在主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存的拷贝)
- 规定
- 线程对共享内存的操作只能在自己的工作内存中
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
实现原理
一个线程修改数据后,让其他线程能够看到
- 把该线程中的变量刷新(同步)到主内存中
- 将主内存中的最新变量值更新到其他线程的工作内存中
- 实现方式:synchronized、volatile、final
synchronized实现原理
- 规定
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中共享变量的值,从而使用需要从主内存中冲洗读取最新的值
线程执行互斥代码的过程
- 获得互斥锁
- 清空工作内存
- 从主内存拷贝变量的最新副本到工作内存
- 执行代码
- 将更改后的共享变量刷新到主内存
- 释放互斥锁
重排序:代码书写顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
- 编译器优化的重排序(编译器优化)
- 指令级并行重排序(处理器优化)
- 内存系统的重排序(处理器优化)
as-if-serial:无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
作用
- 原子性==>线程不会交叉执行
- 原子性==>重排序是在单个线程内
- 可见性==>规定加锁解锁数据的同步机制
- 规定
volatile实现原理
作用
- 保证volatile变量的可见性
- 不能保证volatile变量复合操作的原子性
原理:加入内存屏障和禁止重排序优化
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
- 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
过程
- 写操作
- 改变线程工作内存中volatile变量副本的值
- 变量值刷新到主内存
- 读操作
- 从主内存中读取volatile变量值
- 变量值同步到工作内存中
- 写操作
解决非原子性问题
使用synchronized
使用ReentrantLock
1
2
3
4
5
6
7private Lock lock = new ReentrantLock();
// 加锁开始
lock.lock()
// 操作(try)
// 加锁结束(只能在finally)
lock.unlock();使用AtomicInteger
场景(同时满足)
- 对变量的写入操作不依赖当前值
- 不满足:number++;count*=5;
- 满足:Boolean变量
- 该变量没有包含在具有其他变量的不变式中
- 不满足:不变式 low<up
- 对变量的写入操作不依赖当前值
synchronized和volatile比较
- volatile不需要加锁,更轻量级,非阻塞
- 从内存可见性角度,volatile读相当于加锁,写相当于解锁
- synchronized可以保证可见性和原子性,volatile只能保证可见性
- volatile不需要加锁,更轻量级,非阻塞
其他
- 对64位变量(long、double)的读写可能不是原子操作(java内存模型允许jvm将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来进行)
2.ThreadLocal
基础信息
- 定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值
- 特点:简单(开箱即用)、快速(无额外开销)、安全(线程安全)
- 场景:多线程场景(资源持有、线程一致性、并发计算、线程安全等)
- 实现原理:java 使用哈希表
- 应用范围:几乎所有提供多线程特征的语言
API
- 构造:ThreadLocal<T>()
- 初始化:initialVal()
- 获取/设值:get/set
- 回收:remove
四、java网络编程之NIO
简介:Non-Blockiing I/O 或New I/O
起始于JDK1.4,主要用于高并发网络服务
1.编程模型
模型:对事物共性的抽象
编程模型:对编程共性的抽象
阻塞I/O:读取时线程阻塞
BIO网络模型
- 流程
1)服务器一直监听建立连接请求
2)当客户端发起建立连接请求
3)服务端启动新线程
4)该线程与于客户端建立连接,响应客户端
5)该线程一直等待客户端的请求,阻塞等待
弊端
- 阻塞式I/O模型
- 弹性伸缩能力差
- 多线程耗资源
- 高并发时,服务器可能崩溃
基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**服务端 **/
// 1.监听端口
ServerSocket ss = new ServerSocket(8000);
while(true){
// 2.接收请求,建立连接
Socket socket = ss.accept();
// 3.数据交换
new Thread(new BIOServerHandler(socket)).start();
}
ss.close();
/**客户端 **/
// 1.建立连接
Socket s = new Socket("127.0.0.1",8000);
// 2.获取输入输出流
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
NIO网络模型
- 流程
- 服务端注册建立连接事件
- 服务端有Selector组件,用于循环检测注册事件就绪情况
- 当客户端发起建立连接请求
- 服务端会启动建立连接事件处理器
- Acceptor Handler处理器
- 处理器会创建与客户端连接
- 处理器响应客户端建立连接请求
- 处理器向Selector注册连续可读事件
- 可多个
- 客户端发送请求到Selector
- Selector启动连接读写处理器
- Read&Write Handler读写处理器
- 读写处理器处理客户端读写业务
- 读写处理器响应客户端请求
- 读写处理器向Selector注册连续可读事件
- 服务端注册建立连接事件
- 流程
- 改进(相比BIO)
- 非阻塞I/O模型
- 弹性伸缩能力强
- 单线程节省资源
- 弊端
- 麻烦:NIO类库和API繁杂
- 心累:可靠性能力补齐,工作量和难度大
- 有坑:Selector空轮询,导致CPU 100%
2.NIO核心
Channel:通道
简介:双向性、非阻塞性、操作唯一性
实现
- 文件类:FileChannel
- UDP类:DatagramChannel
- TCP类:ServerSocketChannel/SocketChannel
使用
1
2
3
4
5
6
7
8// 服务器通过服务端socket创建channel
ServerSocketChannel ssc = ServerSocketChannel.open();
// 服务器绑定端口
ssc.bind(new InetSocketAddress(8000));
// 服务器监听客户端连接,建立socketChannel连接
SocketChannel sc = ssc.accept();
// 客户端连接远程主机及端口
SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
Buffer:缓存区
- 简介
- 作用:读写Channel中数据
- 本质:一块内存区域
- 属性
- Capacity:容量
- Position:位置
- Limit:上限
- Mark:标记
- 简介
Selector:选择器/多路复用器
简介
- 作用:I/O就绪选择
- 地位:基础
使用
1
2
3
4
5
6
7
8// 创建Selector
Selector selector = Selector.open();
// 将channel注册到selector上,监听读就绪事件
SectionKey sk = channel.register(selector,SectionKey.OP_READ);
// 阻塞等待channel有就绪事件发生
int selectNum = selector.select();
// 获取发生就绪事件的channel集合
Set<SelectionKey> selectionKeySet = selector.selectedKeys();SelectionKey简介
- 四种就绪状态常量
- 有价值的属性
3.NIO实现步骤
- 创建Selector
- 创建ServerSocketChannel,并绑定监听端口
- 将Channel设置为非阻塞模式
- 将Channel注册到Selector上,监听连接事件
- 循环调用Selector的select,检测就绪情况
- 调用selectedKeys方法获取就绪channel集合
- 判断就绪状态种类,调用业务处理方法
- 根据业务需要决定是否再次注册监听事件,重复执行第三步操作