本文已参与「新人创作礼」活动,一起开启掘金创作之路。
java并发
常见并发控制实例剖析
//volatile适用于单一写并发读场景,可见性指的是基本类型数值可见,引用类型引用地址(即指针地址)可见,本例就是利用引用地址的可见性
public static volatile Map<String, EventDocument> eventDocuments = null;
//原子性,利用cas和volatile可实现原子性,线程安全
public static AtomicBoolean update = new AtomicBoolean(false);
//利用应用地址的可见性,同时在操作层面加锁(双重校验锁)实现线程安全
public static volatile Set<String> eventFilters = new HashSet<>();
//ConcurrentHashMap写时:利用其的在HashEntry(首节点)加写锁;读时:利用volatile可见性,实现线程安全,Node + CAS + Synchronized
public static ConcurrentHashMap<String, EventDocumentFilter> edFilters = new ConcurrentHashMap<>();
//线程安全的先进先出队列,应用在并发写,单一读的场景
public static LinkedBlockingQueue<CallBackData> callBackDatas = newLinkedBlockingQueue<>(1000);
注意:利用volatile引用地址的可见性时可能会引发潜在的CAS的ABA问题。
线程池
参考: Java线程池的使用
ThreadPoolExecutor构造方法 Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime,// 超过corePoolSize的线程的idle时长,
TimeUnit unit, // 超过这个时间,多余的线程会被回收。
BlockingQueue<Runnable> workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
有7个参数,这些参数中,比较容易引起问题的有corePoolSize, maximumPoolSize, workQueue以及handler:
- corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;
- workQueue设置不当容易导致OOM;
- handler设置不当会导致提交任务时抛出异常。
如何正确使用线程池
避免使用无界队列 不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的行为
任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
| 拒绝策略 | 拒绝行为 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException |
| DiscardPolicy | 什么也不做,直接忽略 |
| DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
| CallerRunsPolicy | 直接由提交任务者执行这个任务 |
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512),
new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
实例
ThreadPoolExecutor executor = new ThreadPoolExecutor(numConsumers + 2,
numConsumers + 2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10),
Executors.defaultThreadFactory(),
new ETLPolicy());
private static class ETLPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
logger.error("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
//===============调度====================
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new ETLPolicy());
scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
SensorsFilter.eventFilters = new HashSet<>();
SensorsFilter.edFilters = new HashMap<>();
}
}, 30 * 60 * 1000, 30 * 60 * 1000, TimeUnit.MILLISECONDS);
Runnable和Callable
可以向线程池提交的任务有两种:Runnable和Callable,二者的区别如下:
- 方法签名不同,void Runnable.run(), V Callable.call() throws Exception
- 是否允许有返回值,Callable允许有返回值
- 是否允许抛出异常,Callable允许抛出异常。
Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。
CountDownLatch(闭锁)
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
参考:
CountDownLatch使用及应用场景例子
AQS(抽象的队列式同步器)
AQS是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
==注意:== AQS是自旋锁,在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
AQS 定义了两种资源共享方式:
- Exclusive:独占,只有一个线程能执行,如ReentrantLock
- Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
不同的自定义的同步器争用共享资源的方式也不同。
-
ReentrantLock为例,(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。 注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。
-
以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
闭锁用法(CountDownLatch)
public static AtomicInteger count = new AtomicInteger(0);
// public static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(100);
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
inc();
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("运行结果:"+count);
}
public static void inc(){
try{
Thread.sleep(1); //延迟1毫秒
}catch (InterruptedException e){ //catch住中断异常,防止程序中断
e.printStackTrace();
}
count.getAndIncrement();//count值自加1
// count++;
}
ReentrantLock
lockInterruptibly用法
lockInterruptibly:调用后如果没有获取到锁会一直阻塞,阻塞过程中会接受中断信号。 lockInterruptibly有点难以理解,假设A线程想去获取锁,但是锁被B线程持有,那么A就会发生堵塞。 A堵塞的时候,可以有以下两种方法发生状态改变:
1. A获取锁资源
2. A被其他线程中断
- 这里只得被其他线程中断的意思是,C线程调用A线程的interrupt()。那么此时A线程就会被唤醒,处理中断信号。
- lockInterruptibly是被中断,就由阻塞状态被唤醒去处理中断信号。
综合复杂示例
融合了队列、线程池、有返回值的线程、无返回值的线程、闭锁
public class QueueConcurrent {
private static CountDownLatch latch = new CountDownLatch(10);
private static AtomicInteger COUNT = new AtomicInteger();
public static LinkedBlockingQueue<Integer> integers = new LinkedBlockingQueue<>();
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10000; i++) {
integers.offer(i);
}
Thread.sleep(1000);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
10,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10),
Executors.defaultThreadFactory(),
new ETLPolicy());
int sum = 0;
/*for (int i = 0; i < 10; i++) {
*//**
* 1.启动线程时会返回一个Future对象。
2.可以通过future对象获取现成的返回值。
3.在执行future.get()时,主线程会堵塞,直至当前future线程返回结果。
会顺序执行,不会并发执行,此法不可用
*//*
//非线程池用法
FutureTask<Integer> task = new FutureTask<Integer>(new T());
new Thread(task).start();
sum = task.get() + sum;
//线程池用法
Future<Integer> future = executor.submit(new T());
sum = future.get() + sum;
}*/
for (int i = 0; i < 10; i++) {
executor.execute(new TT());
}
latch.await();
System.out.println("sum=" + COUNT);
executor.shutdown();
}
private static void show() {
}
private static class T implements Callable<Integer> {
public void run() {
int c = 0;
while (integers.size() > 0) {
Integer i = integers.poll();
if (i == null) {
break;
}
System.out.println(Thread.currentThread().getName() + " : " + i);
c++;
}
System.out.println(Thread.currentThread().getName() + "==" + c);
}
@Override
public Integer call() throws Exception {
int c = 0;
while (integers.size() > 0) {
Integer i = integers.poll();
if (i == null) {
break;
}
System.out.println(Thread.currentThread().getName() + " : " + i);
c++;
}
System.out.println(Thread.currentThread().getName() + "==" + c);
return c;
}
}
private static class TT extends Thread {
@Override
public void run() {
int c = 0;
while (integers.size() > 0) {
Integer i = integers.poll();
if (i == null) {
break;
}
// System.out.println(Thread.currentThread().getName() + " : " + i);
c++;
COUNT.getAndIncrement();
}
System.out.println(Thread.currentThread().getName() + "==" + c);
latch.countDown();
}
}
private static class ETLPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
System.out.println("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
}
java正则实用
【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。 说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则);
public static void main(String[] args) {
Pattern pattern = Pattern.compile("GraphType\\.*");
String s = "GraphType1s23ss";
boolean b = s.matches("GraphType\\.*"); // 不提倡,无预编译
boolean b1 =pattern.matcher(s).find();// 局部匹配
boolean b2 =pattern.matcher(s).matches(); //全匹配相当于"^regex$"
System.out.println(b);
}
注意
- String.matches(regex)方法本质调用了Pattern.matches(regex, str),而该方法调Pattern.compile(regex).matcher(input).matches()方法
java日志
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
原因:
SLF4J不同于其他日志类库,与其它日志类库有很大的不同。SLF4J(Simple logging Facade for Java)不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志类库。如果是在编写供内外部都可以使用的API或者通用类库,那么你真不会希望使用你类库的客户端必须使用你选择的日志类库。
如果一个项目已经使用了log4j,而你加载了一个类库,比方说 Apache Active MQ——它依赖于于另外一个日志类库logback,那么你就需要把它也加载进去。但如果Apache ActiveMQ使用了SLF4J,你可以继续使用你的日志类库而无需忍受加载和维护一个新的日志框架的痛苦。
总的来说,SLF4J使你的代码独立于任意一个特定的日志API,这是对于API开发者的很好的思想。虽然抽象日志类库的思想已经不是新鲜的事物,而且Apache commons logging也已经在使用这种思想了,但SLF4J正迅速成为Java世界的日志标准。让我们再看几个使用SLF4J而不是log4j、logback或者java.util.logging的理由。
ArrayList和LinkedList的区别
- ArrayList是实现了基于动态数组的数据结构,LinkedList是基于双向链表结构。
- 对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。
- 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
参考: ArrayList和LinkedList区别及使用场景
HashMap 1.7 1.8区别,1.8的优化,如何优化的
最大不同:
1.8 基本结构改为数组+链表+红黑树; 1.7的基本机构为数组+链表
java7 HashMap 查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)
Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)
ConcurrentHashMap 1.7和1.8区别
(1) 从1.7到1.8版本,由于HashEntry从链表 变成了红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n))
(2)HashEntry最小的容量为2
(3)Segment的初始化容量是16;
(4)HashEntry在1.8中称为Node,链表转红黑树的值是8 ,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的==ReentrantLock+Segment+HashEntry==,到JDK1.8版本中==synchronized+CAS+HashEntry+红黑树==,相对而言,总结如下思考
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含==多个HashEntry==,而JDK1.8锁的粒度就是==HashEntry(首节点)==
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档 JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock
ConcurrentHashMap 读写锁机制
参考:为什么ConcurrentHashMap的读操作不需要加锁?
异常分类以及处理机制
- Throwable是 Java 语言中所有错误或异常的超类。下一层分为Error和Exception
- Error类是指java运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
- Exception又有两个分支,一个是运行时异常RuntimeException,如:NullPointerException、ClassCastException;一个是检查异常CheckedException,如I/O错误导致的IOException、SQLException。
- RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
派生RuntimeException的异常一般包含几个方面:
1、错误的类型转换
2、数组访问越界
3、访问空指针
- 如果出现RuntimeException,那么一定是程序员的错误 RuntimeException不会有提示往往我们的代码中不会有throws try catch等异常处理,比如空指针异常
- 检查异常CheckedException一般是外部错误,这种异常都发生在编译阶段,Java编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行try catch该类
异常一般包括几个方面:
1、试图在文件尾部读取数据
2、试图打开一个错误格式的URL
3、试图根据给定的字符串查找class对象,而这个字符串表示的类并不存在
CheckedException编辑异常,即写代码时编译器就强制让我们加异常处理代码try catch等.
处理方式
- 遇到问题不进行具体处理,而是继续抛给调用者抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常。throw,throws 简单自动抛异常 没有任何处理和提示 比如除以0的错误这样的
- 针对性处理方式:捕获异常try catch finally
HashSet,TreeSet和LinkedHashSet的区别
Java中HashMap、LinkedHashMap和TreeMap区别使用场景
- LinkHashMap是基于HashMap和双向链表来实现的。
- HashMap无序,LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾。
- TreeMap 的底层就是一颗红黑树,它的 containsKey , get , put and remove 方法的时间复杂度是 log(n) ,并且它是按照 key 的自然顺序(或者指定排序)排列
==从存储角度而言==,这比HashMap与LinkedHashMap的o(1)时间复杂度要差些; ==从读取角度而言==,TreeMap同时可以保证log(N)的时间开销,这又比HashMap和LinkedHashMap的o(N)时间复杂度好不少。
LinkedHashMap默认的构造参数是默认 插入顺序的,就是说你插入的是什么顺序,读出来的就是什么顺序,但是也有访问顺序,就是说你访问了一个key,这个key就跑到了最后面
这里accessOrder设置为false,表示不是访问顺序而是插入顺序存储的,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的
hashmap 详解
hashmap 是怎么解决hash冲突的
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
整个过程本质上就是三步:
拿到 key 的 hashCode 值
将 hashCode 的高位参与运算,重新计算 hash 值
将计算出来的 hash 值与 (table.length - 1) 进行 & 运算
方法解读:
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此 JDK 团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率。
在 JDK1.8 的实现中,还优化了高位运算的算法,将 hashCode 的高 16 位与 hashCode 进行异或运算,主要是为了在 table 的 length 较小的时候,让高位也参与运算,并且不会有太大的开销。
一个简单的例子:
当 table 长度为 16 时,table.length - 1 = 15 ,用二进制来看,此时低 4 位全是 1,高 28 位全是 0,与 0 进行 & 运算必然为 0,因此此时 hashCode 与 “table.length - 1” 的 & 运算结果只取决于 hashCode 的低 4 位,在这种情况下,hashCode 的高 28 位就没有任何作用,并且由于 hash 结果只取决于 hashCode 的低 4 位,hash 冲突的概率也会增加。因此,==在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率==。
java NIO与BIO
Selector(选择器)的使用方法介绍
- Selector的创建 通过调用Selector.open()方法创建一个Selector对象,如下:
Selector selector = Selector.open();
这里需要说明一下 2. 注册Channel到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
==Channel必须是非阻塞的==。 所以==FileChannel不适用Selector==,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。
SelectableChannel抽象类 有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。
abstract SelectableChannel configureBlocking(boolean block)
queue 队列
通用方法
queue共有的方法:
offer,add 区别:
一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。
这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false。
poll,remove 区别:
remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似, 但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。
peek,element区别:
element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。
BlockingQueue特有的方法:
put(E):
put 添加一个元素 如果队列满,则阻塞
take():
take 移除并返回队列头部的元素如果队列为空,则阻塞
PriorityQueue
一个支持线程优先级排序的无界队列,默认自然序进行排,也可以自定义实现compareTo()方法来指定元素排序规则,循环遍历则不能保证同优先级元素的顺序
ConcurrentLinkedQueue
基于CAS的无锁技术,不需要在每个操作时使用锁,所以扩展性表现要更加优异,在常见的多线程访问场景,一般可以提供较高吞吐量。
BlockingQueue
线程安全,读写锁相同。
PriorityBlockingQueue
与PriorityQueue 相比,此实现类是线程安全的,其他相同。
ArrayBlockingQueue
是最典型的的有界队列,其内部以final的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建ArrayBlockingQueue时,都要指定容量。支持公平锁和非公平锁
LinkedBlockingQueue
读写分离,读写锁不同,提高高并发环境下的吞吐量。
容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为Integer.MAX_VALUE,成为了无界队列。
高并发多写一度场景实例:
private static void LinkedBlockingQueue() {
LinkedBlockingQueue queue2 = new LinkedBlockingQueue(1);
queue2.offer(45);
new Thread(()->{
try {
int c=1;
while (true) {
// queue2.put(c++);
queue2.offer(c++);
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
int c=1;
while (true) {
System.out.println(queue2.take());
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
DelayQueue
一个实现BlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。
DelayQueue扩展了Comparable接口,比较的基准为==延迟的时间值==,DelayQueue内部实现为:
DelayQueue=BlockingQueue + PriorityQueue + Delayed,可以这么说DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,==优先队列比较的基准值是是时间==
==注意:==先通过compareTo方法判断队列的先后顺序,再通过getDelay方法判断延迟时间是否到(即是否<=0)。如果排序在后,及时延迟时间到了也不会输出,需要等到排序在前面的队列输出,后面的队列才会输出。因此需要==compareTo 方法提供与 getDelay 方法一致的排序==,才可实现同时加入队列的元素,延迟时间短的先输出。但这个思路还可做他用
使用场景
1. 淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
2. 饿了吗订餐通知:下单成功后60s之后给用户发送短信通知。
原理
DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。==compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序==。
消费者线程查看队列头部的元素,注意是查看不是取出。然后调用元素的getDelay方法,如果此方法返回的值小0或者等于0,则消费者线程会从队列中取出此元素,并进行处理。如果getDelay方法返回的值大于0,则消费者线程wait返回的时间值后,再从队列头部取出元素,此时元素应该已经到期。
DelayQueue是Leader-Followr模式的变种,消费者线程处于等待状态时,总是等待最先到期的元素,而不是长时间的等待。消费者线程尽量把时间花在处理任务上,最小化空等的时间,以提高线程的利用效率。
实例:
public class DelayQueueExample {
public static void main(String[] args) {
DelayQueue<DelayTask> queue = new DelayQueue<>();
queue.add(new DelayTask("1", 1000L, TimeUnit.MILLISECONDS));
queue.add(new DelayTask("2", 5000L, TimeUnit.MILLISECONDS));
queue.add(new DelayTask("3", 10000L, TimeUnit.MILLISECONDS));
System.out.println("queue put done");
while(!queue.isEmpty()) {
try {
DelayTask task = queue.take();
System.out.println(task.name + ":" + System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class DelayTask implements Delayed {
public String name;
public Long delayTime;
public TimeUnit delayTimeUnit;
public Long executeTime;//ms
DelayTask(String name, long delayTime, TimeUnit delayTimeUnit) {
this.name = name;
this.delayTime = delayTime;
this.delayTimeUnit = delayTimeUnit;
this.executeTime = System.currentTimeMillis() + delayTimeUnit.toMillis(delayTime);
}
@Override
public int compareTo(Delayed o) {
if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
}else if(this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
}
return 0;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}
SynchronousQueue
这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是1吗?其实不是的,其内部容量是0 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收 实例:
private static void synchronousQueue(){
SynchronousQueue queue = new SynchronousQueue();
new Thread(()->{
try {
int c=1;
while (true) {
queue.put(c++);
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
int c=1;
while (true) {
System.out.println(queue.take());
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
队列的使用场景
无界队列
- PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响。
- DelayedQueue和LinkedTransferQueue同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是put操作永远也不会发生其他BlockingQueue的那种等待情况。
有界队列
-
以LinkedBlockingQueue、ArrayBlockingQueue和SynchronousQueue为例,根据需求可以从很多方面考量:
-
考虑应用场景中对队列边界的要求。ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue则干脆不能缓存任何元素。
-
从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
-
通用场景中,LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
-
ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。
-
如果需要实现的是两个线程之间接力性(handoff)的场景,可能会选择CountDownLatch,但是SynchronousQueue也是完美符合这种场景的,而且线程间协调和数据传输统一起来,代码更加规范。
-
可能令人意外的是,很多时候SynchronousQueue的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。
LinkedBlockingQueue和ConcurrentLinkedQueue区别
首先二者都是线程安全的得队列,都可以用于生产与消费模型的场景。==LinkedBlockingQueue 多用于任务队列==;
==ConcurrentLinkedQueue 多用于消息队列==
LinkedBlockingQueue是阻塞队列,其好处是:多线程操作共同的队列时不需要额外的同步,由于具有插入与移除的双重阻塞功能,对插入与移除进行阻塞,队列会自动平衡负载,从而减少生产与消费的处理速度差距。
由于LinkedBlockingQueue有阻塞功能,其阻塞是基于锁机制实现的,==当有多个线程消费时候,队列为空时线程都被阻塞,若不设置超时机制,线程在不停的空转,耗费系统资源==。从此方面来讲,LinkedBlockingQueue更适用于多线程插入,单线程取出,即多个生产者与单个消费者。
ConcurrentLinkedQueue非阻塞队列,采用 CAS操作,解决多线程之间的竞争,来保证元素的一致性,效率更高。当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue 是一个恰当的选择。从此方面来讲,ConcurrentLinkedQueue更适用于单线程插入,多线程取出,即单个生产者与多个消费者。
- 单生产者,单消费者 用 LinkedBlockingqueue
- 多生产者,单消费者 用 LinkedBlockingqueue
- 单生产者 ,多消费者 用 ConcurrentLinkedQueue
- 多生产者 ,多消费者 用 ConcurrentLinkedQueue
实战示例:
示例1:
/**
* Created by lijianjun 2020/3/10
* 线程安全的 多生产者 ,多消费者 情况,先进先出队列。
* 也可以根据实际情况把ConcurrentLinkedQueue换成 LinkedBlockingQueue 或者 ArrayBlockingQueue
*/
public class FiFoQueue<E> extends ConcurrentLinkedQueue<E> {
private static final long serialVersionUID = 1L;
private int limit;
public FiFoQueue(int limit) {
this.limit = limit;
}
@Override
public boolean add(E o) {
super.offer(o);
while (size() > limit) {
super.remove();
}
return true;
}
}
示例2:
/**
* Created by lijianjun 2020/3/10
* 线程安全的 多生产者 ,多消费者 情况,先进先出队列。
* 同时可以方便的取出Key对应的value
* 本场景中没有重复的K
*/
public class FiFoQueueAndGetValue<K, V> {
private static final long serialVersionUID = 1L;
private int limit;
private Queue<K> queue;
private Map<K, V> map;
public FiFoQueueAndGetValue(int limit) {
this.limit = limit;
queue = new ConcurrentLinkedQueue<>();
map = new ConcurrentHashMap<>((int) (limit / 0.75 + 1));
}
public boolean add(K k, V v) {
/*if (!queue.contains(k)){ //处理重复的K
queue.offer(k);
}*/
queue.offer(k);
map.put(k, v);
while (queue.size() > limit) {
map.remove(queue.peek());
queue.remove();
}
return true;
}
public V get(K k) {
return map.get(k);
}
public boolean contains(K k) {
return queue.contains(k);
}
@Override
public String toString() {
return queue.toString();
}
}
equals,hashcode和==的区别
java中的数据类型,可分为两类:
1.基本数据类型,也称原始数据类型
byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比较的是他们的值。
基本数据类型与其他内存==比较,其他类型都会拆箱转换成基本数据类型
2.引用类型(类、接口、数组)
当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。
对象是放在堆中的,栈中存放的是对象的引用(地址)。由此可见'=='是对栈中的值进行比较的。
==注意:== Byte,Short,Integer,Long 的缓存池范围默认都是: -128 到 127。可以看出,Byte的所有值都在缓存区中,用它生成的相同值对象都是相等的。
为什么覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。
因为如果不覆盖equals方法的话,相等的对象可能返回的不相同的hash code。
比如上面这种情况就会出现在hashmap中,一个对象修改equals,判断里面的字段是否一致即相等,但是没有修改hasCode
hashmap中的key根据hashCode分配到不同的元素,但是用equals尝试,key却是相等的,那就坏了,会出现很多异常bug
public class EqualDemo implements Comparable{
private String name;
public EqualDemo() {
super();
}
public EqualDemo(String name) {
this.name = name;
}
@Override
public int compareTo(Object o) {
return 0;
}
@Override
public int hashCode() {
return this.name.hashCode();
// return super.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this==obj){
return true;
}
if (obj instanceof EqualDemo) {
EqualDemo equalDemo = (EqualDemo)obj;
return this.name.equals(equalDemo.name);
} else {
return false;
}
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return super.toString();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
}
public static void main(String[] args) {
EqualDemo e1 = new EqualDemo("aa");
EqualDemo e2 = new EqualDemo("aa");
System.out.println(e1==e2);
System.out.println(e1.hashCode());
System.out.println(e2.hashCode());
System.out.println(e1.equals(e2));
Map<EqualDemo,String> map = new HashMap<>();
map.put(e1,"value");
System.out.println(map.get(e2));
}
}
java接口、抽象类的区别
abstract class 表示的是is a关系,interface表示的是like a关系。
抽象类强调的是从属关系,接口强调的是功能。
作用
抽象类
- 类型进行隐藏,我们可以构造出一个固定的一组行为的抽象描述,一个行为可以有任意个可能的具体实现方式。这个抽象的描述就是抽象类。(参考多态)
- 用于拓展对象的行为功能 这一组任意个可能的具体实现表现为所有可能的子类,模块可以操作一个抽象类,由于模块依赖于一个固定的抽象类,那么他是不允许修改的。同时通过这个抽象类进行派生,拓展此模块的行为功能。(参考开放闭合原则)
- ==为了把相同的东西提取出来,即重用==
接口
- Java单继承的原因所以需要曲线救国 作为继承关系的一个补充。
- 把程序模块进行固化的契约,==降低偶合==。把若干功能拆分出来,按照契约来进行实现和依赖。(依赖倒置原则)
- 定义接口有利于代码的规范。(接口分离原则)
举出一个例子,在这种情况你会更倾向于使用抽象类,而不是接口?
这是很常用但又是很难回答的设计面试问题。接口和抽象类都遵循”面向接口而不是实现编码”设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:
- 在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。
- ==如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码==,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。
java的反射
反射的优势和劣势
个人理解,反射机制实际上就是上帝模式,如果说方法的调用是 Java 正确的打开方式,那反射机制就是上帝偷偷开的后门,只要存在对应的class,一切都能够被调用。
那上帝为什么要打开这个后门呢?这涉及到了静态和动态的概念
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
两者的区别在于,动态编译可以最大程度地支持多态,而多态最大的意义在于降低类的耦合性,因此反射的优点就很明显了:解耦以及提高代码的灵活性。
因此,反射的优势和劣势分别在于:
- 优势:运行期类型的判断,动态类加载:提高代码灵活度
- 劣势:性能瓶颈,反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多
基于反射实现动态代理
动态代理的作用其实就是在==不修改原代码==的前提下,==对已有的方法进行增强==。
Java动态代理的优势是实现无侵入式的代码扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法;在方法的前后你可以做你任何想做的事情(甚至不去执行这个方法就可以)。
1.动态代理是设计模式中的代理模式:定义:为其它对象提供一种代理以控制对这个对象的访问控制;在某些情况下,客户不想或者不能直接引用另一个对象,这时候代理对象可以在客户端和目标对象之间起到中介的作用。
2.静态代理静态代理类:由程序员创建或者由第三方工具生成,再进行编译;在程序运行之前,代理类的.class文件已经存在了。静态代理类通常只代理一个类。静态代理事先知道要代理的是什么。
3.动态代理动态代理类:在程序运行时,通过反射机制动态生成。动态代理类通常代理接口下的所有类。动态代理事先不知道要代理的是什么,只有在运行的时候才能确定。动态代理的调用处理程序必须事先InvocationHandler接口,及使用Proxy类中的newProxyInstance方法动态的创建代理类。
Java动态代理只能代理接口,要代理类需要使用第三方的CLIGB等类库。
JDK动态代理使用步骤
参考:Java中的动态代理
动态代理小结:
- 实现动态代理的关键技术是反射;
- 代理对象是对目标对象的增强,以便对消息进行预处理和后处理;
- InvocationHandler中的invoke()方法是代理类完整逻辑的集中体现,包括要切入的增强逻辑和进行反射执行的真实业务逻辑;
- 使用JDK动态代理机制为某一真实业务对象生成代理,只需要指定目标接口、目标接口的类加载器以及具体的InvocationHandler即可。
- JDK动态代理的典型应用包括但不仅限于AOP、RPC、Struts2、Spring等重要经典框架。
JDK动态代理的一般步骤如下:
1、创建被代理的接口和类;
2、实现InvocationHandler接口,对目标接口中声明的所有方法进行统一处理;
3、调用Proxy的静态方法,创建代理类并生成相应的代理对象;
4、使用代理。
生活案例
饭前便后要洗手
一、分析出主要业务和次要业务
【主要业务】:吃饭,上厕所
【业务业务】:洗手
二、JDK代理模式实现
1、接口角色:
定义所有需要被监听行为
2、接口实现类:
中国人,印度人
3、通知类:
1)次要业务进行具体实现
2)通知JVM,当前被拦截的主要业务方法与次要业务方法应该如何绑定执行
4.监控对象(代理对象)
1) 被监控实例对象
2) 需要被监控的行为
3)具体通知类实例对象
spring IOC & AOP
IOC和DI的两个概念,其实这两者是层面的不同。
具体的区别的区别:IOC是DI的原理。依赖注入是向某个类或方法注入一个值,其中所用到的原理就是控制反转。
所以说到操作层面的时候用DI,原理层的是说IOC
Spring依赖注入的实现技术是:动态代理
IOC(Inversion Of Control),控制反转是将创建对象的控制权从程序员手中转向Spring框架。Spring框架在创建对象时使用了DI技术
IOC 和AOP都是依赖jdk动态代理实现的,而动态代理底层实现技术是反射。
java 设计模式
设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案,可以说代表了最佳的实践。
分为三大类
创建型模式(五种):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式(七种):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为型模式(十一种):策策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
工厂模式
==注意事项:== 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是==复杂对象适合使用工厂模式==,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度
java 堆外内存与堆内内存详解
参考:堆外内存与堆内内存详解
Java堆外内存管理
JAVA代码中获取JVM信息
MMF使用的就是offheap技术 jvm参数设置:
-XX:+PrintGCDetails -Xmx128m -Xms128m -XX:MaxDirectMemorySize=128m
- -Xms and -Xmx (or: -XX:InitialHeapSize and -XX:MaxHeapSize)
- System.gc我们可以禁掉,使用-XX:+DisableExplicitGC。
- 堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险
- 堆外内存-XX:MaxDirectMemorySize,默认与-Xmx(-XX:MaxHeapSize)参数值相同, 设置JVM参数-XX:MaxDirectMemorySize=100M
- ManagementFactory是一个在运行时管理和监控Java VM的工厂类,它能提供很多管理VM的静态接口,比如RuntimeMXBean; RuntimeMXBean是Java虚拟机的运行时管理接口。 参考:Java获取当前进程ID以及所有Java进程的进程ID
Direct Memory的回收机制
Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。
使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。
MappedByteBuffer的使用
三种方式:
FileChannel提供了map方法来把文件影射为内存映像文件: MappedByteBuffer map(int mode,long position,long size); 可以把文件的从position开始的size大小的区域映射为内存映像文件,mode指出了 可访问该内存映像文件的方式:READ_ONLY,READ_WRITE,PRIVATE.
a. READ_ONLY,(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)
b. READ_WRITE(读/写): 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 (MapMode.READ_WRITE)
c. PRIVATE(专用): 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)
三个方法:
a. fore();缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件
b. load()将缓冲区的内容载入内存,并返回该缓冲区的引用
c. isLoaded()如果缓冲区的内容在物理内存中,则返回真,否则返回假
java删除集合容器元素,使用迭代器
/**
*删除有下标的集合时,必须使用迭代器,
* 否则会报 java.util.ConcurrentModificationException
*/
private static void deleteCollection(){
List<String> list = new ArrayList<String>();
list.add("liudehua");
list.add("madehua");
list.add("liushishi");
list.add("tangwei");
// 抛异常
/*for (String s : list) {
if (s.equals("madehua")) {
list.remove(s);
}
}*/
Iterator iterator = list.iterator();
while (iterator.hasNext()){
if (iterator.next().equals("madehua")){
iterator.remove();
}
}
}
java 取模运算的高效写法
x % 2^n = x & (2^n - 1)
x mod 2^n = x & (2^n - 1)
相应的实现 hashmap 的取模运算
java 集合与数组的排序
demo
Collections.sort、Arrays.sort、TreeSet、TreeMap、PriorityQueue 都是string为字典排序,数字类型为自然排序
package com.arve.collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by lijianjun 2020/3/31
* Collections.sort 底层调用 Arrays.sort ,string为字典排序,数字类型为自然排序
*/
public class ARR_LIST {
public static void main(String[] args) {
arrToList();
}
private static void arrToList() {
String[] arr = new String[]{"12", "1", "4", "6", "9"};
// Arrays.asList(arr);
Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println("==============");
List list = Arrays.asList(arr);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + "\t");
}
}
private static void listToArr() {
List list = new ArrayList();
//结果为:1 12 4 5 8
list.add("12");
list.add("8");
list.add("4");
list.add("1");
list.add("5");
//结果为:1 4 5 8 12
/*list.add(12);
list.add(8);
list.add(4);
list.add(1);
list.add(5);*/
// Collections.sort 底层调用 Arrays.sort
Collections.sort(list/*, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String)o2);
}
}*/);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + "\t");
}
System.out.println("==============");
Object[] arr = list.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}
Java深拷贝与浅拷贝
1.为什么有拷贝?
因为new一个对象太占用资源,当要复制大量对象的时候用拷贝实现的方式很有优势,很快。
2.浅拷贝与深拷贝
浅拷贝是指对一个类进行拷贝是,会对基本数据类型进行值传递(string也是基本类型),而对于类属性中对象类型变量(包括数组)会让他们直接指向同一个内存地址,所以修改其中一个的值会影响到拷贝对象中的值,所以具有局限性
深拷贝基本数据类型拷贝同浅拷贝一样,引用类型变量会直接拷贝,而不是让他们指向同一地址。
具体实现方式:1.通过Object中的clone方法; 2.自己通过构造函数方式实现;3.序列化方法实现