【TOC】
Java容器
ArrayList和LinkedList的大致区别:
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向链表的数据结构。
- 对于随机访问get和set,ArrayList觉得优于LinkedList,因为ArrayList通过索引直接访问,LinkedList要逐步移动指针。
- 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。ArrayList还需要扩容。
HashMap和Hashtable及HashSet的区别
- 实现接口 HashMap和HashTable实现Map接口,HashSet实现Set接口
- HashMap是非同步的,非线程安全。HashTable是同步的。HashSet底层由HashMap实现,也是非线程安全的。
- HashMap底层是HashMap.Entry的数组,Entry本身又是单链表结构。HashMap是通过单链表法解决Hash冲突的。
- HashMap默认capacity容量是16,load factor装载因子为0.75.
- 如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同
LinkedHashMap和TreeMap
- HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序 2.TreeMap可以保持按照key的大小顺序来排序 3.TreeMap使用红黑树的好处是能够使得树具有不错的平衡性,这样操作的速度就可以达到log(n)的水平
- TreeMap支持迭代器,其实就是树的中序排列
Iterator
int cursor = 0;//
int lastRet = -1;//
int expectedModCount = modCount;//
Itr类依靠3个int变量(还有一个隐含的AbstractList的引用)来实现遍历,cursor是下一次next()调用时元素的位置,第一次调用next()将返回索引为0的元素。lastRet记录上一次游标所在位置,因此它总是比cursor少1 expectedModCount表示期待的modCount值,用来判断在遍历过程中集合是否被修改过。AbstractList包含一个modCount变量,它的初始值是0,当集合每被修改一次时(调用add,remove等方法),modCount加1。因此,modCount如果不变,表示集合内容未被修改。
- 使用场景:在单线程中遍历集合;在循环遍历中修改集合结构;
集合算法
Collections.sort()
sort()方法先将collection对象转换成对象数组,调用Arrays.sort()进行排序,在转换回collection。 Arrays.sort()默认使用TimSort算法排序
TimSort 算法是一种结合了归并排序和插入排序的混合算法。将输入数组按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿两个连续的run块出来按规则(插入法)进行合并。每次合并会将两个 run合并成一个 run。不断的划分run区块和合并run区块,直到消耗掉所有的 run。这时这个仅剩的 run 便是排好序的结果。
Collections.binarySearch()
binarySearch()为二分搜索法,故查询前需要用sort()方法将数组排序,如果数组没有排序,则结果是不确定的。
Collections.shuffle()
从最后一个开始往前遍历,通过Rondom算法选取一个索引比自己小的数,进行交换。 一般用于洗牌,抽奖等生成随机数组
Collections.reverse()
用于反转集合数据的排列顺序
多线程并发
多线程是实现多任务的一种方式。线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。 线程总是属于某个进程,进程中的多个线程共享进程的内存。 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的 阻塞状态: 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
当多个线程访问某个类时,不管运行时环境采用何种调度方法,并且在主调代码中不需要以任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就被称为线程安全的。 原子性 可见性 顺序性
- 锁和同步,将多线程访问变成单线程访问
- CAS操作,使用CAS操作保证原子性
- volatile,使用volatile修饰变量,保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值,保证可见性。
- 通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性 总之,保证了原子性、可见性、顺序性就能保证线程安全。
线程安全容器
同步容器
同步容器都是线程安全的。这些类实现线程安全的方式是把他们的状态封装起来,并对每个公共方法都进行同步,使得每次只有一个线程能够访问容器的状态。 同步容器包括Vector、Hashtable和Collections.synchronizedXxx()方法创建的容器。
ConcurrentHashMap
ConcurrentHashMap是一种弱线程安全的Map。 ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size(), containsValue())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求。 ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性,代码如下:
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
并发度实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认的并发度为16,但用户也可以在构造函数中设置并发度。 同时ConcurrentMap接口中声明一些常用的组合操作为院子操作,如putIfAbsent()。
CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 由于每次修改容器时都会复制底层数据,这需要一定的开销,特别是当容器规模较大时。 所以,仅当迭代或查询操作远大于修改操作时,才应该使用CopyOnWrite容器。比如说事件通知系统中,监听列表就可以使用CopyOnWrite容器。
BlockingQueue
阻塞队列(BlockingQueue)提供了可阻塞的put和take方法,以及支持超时的offer和poll方法。在队列为空时,获取元素的线程会阻塞,直到队列变为非空。当队列满时,存储元素的线程会阻塞,直到队列可用。阻塞队列可以是无界的,也可以是有界的。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列还支持多个生产者和多个消费者。 |方法\处理方式| 抛出异常 |返回特殊值 |一直阻塞| 超时退出| | :-------- | :-------- | :-------- | :--: | |插入方法| add(e)| offer(e)| put(e)| offer(e,time,unit)| |移除方法| remove()| poll() |take()| poll(time,unit)| |检查方法| element() |peek() |不可用 |不可用|
JDK7提供了7个阻塞队列。分别是
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
双端队列Deque和BlockingDeque
Queue除了前面介绍的实现外,还有一种双向的Queue实现Deque。这种队列允许在队列头和尾部进行入队出队操作,因此在功能上比Queue显然要更复杂。 Deque不仅具有FIFO的Queue实现,也有FILO的实现,也就是不仅可以实现队列,也可以实现一个堆栈。 BlockingDeque适合自产自销场景。自己本身是消费者,在消费任务的同时,也会产生任务。例如爬虫在处理页面时,会发现更多需要处理的页面;当一个消费者找到一个新的任务是,会将其放入队列的末尾。
与阻塞队列适用于生产者-消费者模式类似,双端队列适用于工作密取模式。在这种模式中,每个消费者都有自己的双端队列,如果一个消费者完成了自己双端队列的全部工作,那么它可以从其他消费者的双端队列尾部秘密的获取工作。工作密取 模式比传统的生产者消费者模式具有更高的可伸缩性,因为消费者线程不会在单个共享队列上产生竞争。
线程池
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务列队、任务接口等部分。其中线程池管理器的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务是进行等待;任务列队的作用是提供一种缓冲机制,将没有处理的任务放在任务列队中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。 当一个服务器接受到大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。
- newCachedThreadPool该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
- newFixedThreadPool 该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。但使用ScheduledThreadPoolExecutor时,必须注意两点:
- 一定要使用try{}catch(Throwable t){}捕获所有可能的异常,因为ScheduledThreadPoolExecutor会在任务执行遇到异常时取消后续执行。
- 注意scheduleAtFixedRate与scheduleWithFixedDelay的区别
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
从类图上可以看到,接口ExecutorService继承自Executor。不像Executor中只定义了一个方法来执行任务,在ExecutorService中,正如其名字暗示的一样,定义了一个服务,定义了完整的线程池的行为,可以接受提交任务、执行任务、关闭服务。抽象类AbstractExecutorService类实现了ExecutorService接口,也实现了接口定义的默认行为。
ThreadPoolExecutor
ThreadPoolExecutor继承于AbstractExecutorService,是Executors类的底层实现。 ThreadPoolExecutor的完整构造方法的签名是:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所保存的线程数,包括空闲线程。 maximumPoolSize-池中允许的最大线程数。 keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 unit - keepAliveTime 参数的时间单位。 workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。 threadFactory - 执行程序创建新线程时使用的工厂。 handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
在JDK帮助文档中,有如此一段话: “强烈建议程序员使用较为方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)Executors.newSingleThreadExecutor()(单个后台线程)
ScheduledThreadPoolExecutor
Java 5提供了ScheduledThreadPoolExecutor实现定时任务,每个任务由线程池中一个线程去执行,任务并发执行,且相互之间不会受到干扰。此外,ScheduledExecutorService是基于时间延迟,不会由于系统时间的改变发生执行变化。但ScheduledThreadPoolExecutor要在某个时间点开始执行任务没有Timer方便,需要先计算出和执行时间点的时间差,然后设置第一次启动的延时。
综上,对于复杂的调度,最好是使用开源软件,如Quartz。java培训机构排名对于普通的周期性任务,使用ScheduledThreadPoolExecutor就可以满足要求,但使用ScheduledThreadPoolExecutor时,必须注意两点:
- 一定要使用try{}catch(Throwable t){}捕获所有可能的异常,因为ScheduledThreadPoolExecutor会在任务执行遇到异常时取消后续执行。
- 注意scheduleAtFixedRate与scheduleWithFixedDelay的区别,scheduleAtFixedRate是上一个任务开始执行之后延迟设定时间再执行,是从上一个任务开始时计时,但对于运行时长超过延迟时长的任务,会等上一个任务执行完之后,下一个任务才开始执行,此时,延时没有任何意义。而scheduleWithFixedDelay是在上一个任务结束执行之后延迟设定时间再执行,是从上一个任务结束时开始计算。
Atomic
Lock是一种悲观锁,它确保了无论哪个线程拥有守护变量的锁,都只能采取独占的方式来访问这个变量。同时也还有一种乐观锁,可以进行更加乐观的原子修改操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改。
原子变量和非阻塞机制
近年来,并发领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令来代替锁来确保数据在并发访问中的一致性。与锁的方案相比,非阻塞算法在设计和实现上都复杂的多,在他们在可伸缩性和活跃性上却拥有巨大的优势。非阻塞算法可以使多线程在竞争相同数据时不会发生阻塞,因此他能在更细粒度上进行协调。 与锁相比,volatile是一种更轻量级的同步机制因为在使用时不会发生上下文切换和线程调度等操作。然而,volatile变量也存在一种局限,他只提供可见性保证,并不能保证符合操作的原子性,尤其是当一个变量依赖另一个变量时。
Atomic
基本类:AtomicInteger、AtomicLong、AtomicBoolean; 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference; 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater 这些类都提供了一种原子操作的方法,如compareAndSet(),incrementAndGet(),getAndIncrement(),getAndAdd()等。 原子变量是一种更好的volatile。
ThreadLocal
实际底层逻辑:Thread.ThreadLocalMap.get(ThreadLocal) 上层接口:ThreadLocal.get()
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。 从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。 对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。 每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。 所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
线程同步和互斥
线程同步
- synchronized同步方法
- synchronized(object)步代码块
- 特殊域变量(volatile)实现线程同步
- 使用ReenreantLock重入锁
- 使用ThreadLocal局部变量
- 阻塞队列实现
- 使用原子变量实现
- CountDownLatch闭锁
- Semaohore信号量
- CyclicBarrier循环栅栏
设计模式
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
工厂方法模式(Factory Method)
就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
public class SendFactory {
public Sender produce(String type) {
if ("mail".equals(type)) {
return new MailSender();
} else if ("sms".equals(type)) {
return new SmsSender();
} else {
System.out.println("请输入正确的类型!");
return null;
}
}
}
单例模式(Singleton)
单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
- 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
public class Singleton {
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
原型模式(Prototype)
原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在Java中,复制对象是通过clone()实现的。
浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。
public class Prototype implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private String string;
private SerializableObject obj;
/* 浅复制 */
public Object clone() throws CloneNotSupportedException {
Prototype proto = (Prototype) super.clone();
return proto;
}
/* 深复制 */
public Object deepClone() throws IOException, ClassNotFoundException {
/* 写入当前对象的二进制流 */
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
/* 读出二进制流产生的新对象 */
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
public String getString() {
return string;
}
public void setString(String string) {
this.string = string;
}
public SerializableObject getObj() {
return obj;
}
public void setObj(SerializableObject obj) {
this.obj = obj;
}
}
class SerializableObject implements Serializable {
private static final long serialVersionUID = 1L;
}
适配器模式(Adapter)
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
- 类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
- 对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。
- 接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。
public class Source {
public void method1() {
System.out.println("this is original method!");
}
}
public interface Targetable {
/* 与原类中的方法相同 */
public void method1();
/* 新类的方法 */
public void method2();
}
public class Adapter extends Source implements Targetable {
@Override
public void method2() {
System.out.println("this is the targetable method!");
}
}
public class AdapterTest {
public static void main(String[] args) {
Targetable target = new Adapter();
target.method1();
target.method2();
}
}
代理模式(Proxy)
代理模式就是多一个代理类出来,替原对象进行一些操作。比如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。 如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法: 1、修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。 2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。 使用代理模式,可以将功能划分的更加清晰,有助于后期维护!
public interface Sourceable {
public void method();
}
public class Source implements Sourceable {
@Override
public void method() {
System.out.println("the original method!");
}
}
public class Proxy implements Sourceable {
private Source source;
public Proxy(){
super();
this.source = new Source();
}
@Override
public void method() {
before();
source.method();
atfer();
}
private void atfer() {
System.out.println("after proxy!");
}
private void before() {
System.out.println("before proxy!");
}
}
动态代理
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。
静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。 动态:在程序运行时运用反射机制动态创建而成。
静态代理
public class UserManagerImplProxy implements UserManager {
// 目标对象
private UserManager userManager;
// 通过构造方法传入目标对象
public UserManagerImplProxy(UserManager userManager){
this.userManager=userManager;
}
@Override
public void addUser(String userId, String userName) {
try{
//添加打印日志的功能
//开始添加用户
System.out.println("start-->addUser()");
userManager.addUser(userId, userName);
//添加用户成功
System.out.println("success-->addUser()");
}catch(Exception e){
//添加用户失败
System.out.println("error-->addUser()");
}
}
@Override
public void delUser(String userId) {
userManager.delUser(userId);
}
@Override
public String findUser(String userId) {
userManager.findUser(userId);
return "张三";
}
@Override
public void modifyUser(String userId, String userName) {
userManager.modifyUser(userId,userName);
}
}
动态代理
根据如上的介绍,你会发现每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类 所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理
public class UserManagerImpl implements UserManager {
@Override
public void addUser(String userId, String userName) {
System.out.println("UserManagerImpl.addUser");
}
@Override
public void delUser(String userId) {
System.out.println("UserManagerImpl.delUser");
}
@Override
public String findUser(String userId) {
System.out.println("UserManagerImpl.findUser");
return "张三";
}
@Override
public void modifyUser(String userId, String userName) {
System.out.println("UserManagerImpl.modifyUser");
}
}
//动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类
public class LogHandler implements InvocationHandler {
// 目标对象
private Object targetObject;
//绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。
public Object newProxyInstance(Object targetObject){
this.targetObject=targetObject;
//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
//第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
//第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口
//第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法
//根据传入的目标返回一个代理对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(),this);
}
@Override
//关联的这个实现类的方法被调用时将被执行
/*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("start-->>");
for(int i=0;i<args.length;i++){
System.out.println(args[i]);
}
Object ret=null;
try{
/*原对象方法调用前处理日志信息*/
System.out.println("satrt-->>");
//调用目标方法
ret=method.invoke(targetObject, args);
/*原对象方法调用后处理日志信息*/
System.out.println("success-->>");
}catch(Exception e){
e.printStackTrace();
System.out.println("error-->>");
throw e;
}
return ret;
}
}
Cglib动态代理
本质上,它是通过动态的生成一个子类去覆盖所要代理类的不是final的方法,并设置好callback,则原有类的每个方法调用就会转变成调用用户定义的拦截方法(interceptors),这比JDK动态代理方法快多了。可见,Cglib的原理是对指定的目标类动态生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类和final方法进行代理
Spring Aop
Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。 纵观 AOP 编程,其中需要程序员参与的只有 3 个部分:
- 定义普通业务组件。
- 定义切入点,一个切入点可能横切多个业务组件。
- 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。
那么进行 AOP 编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP 框架将会自动生成 AOP 代理,而 AOP 代理的方法大致有如下公式: 代理对象的方法 = 切入点+增强处理 + 被代理对象的方法
package com.fruitking.proxy.springaop;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
/**
* 使用spring的AOP机制的事前通知接口实现
* @author fruitking
* @since 2010-02-23
*/
public class CarServiceBeforeAdvice implements MethodBeforeAdvice{
public void before(Method method, Object[] args, Object target)throws Throwable {
System.out.println("before excute target object...");
String methodName = method.getName(); //得到方法名
String targetClassName = target.getClass().getName();//得到调用类名
System.out.println(targetClassName+"."+methodName+"()");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean id="carServiceTarget" class="com.fruitking.proxy.CarServiceImpl"/>
<bean id="carServiceBeforeAdvice" class="com.fruitking.proxy.springaop.CarServiceBeforeAdvice"/>
<bean id="carService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.fruitking.proxy.CarService"/>
<property name="target" ref="carServiceTarget"/>
<property name="interceptorNames">
<list>
<value>carServiceBeforeAdvice</value>
</list>
</property>
</bean>
</beans>
package com.fruitking.proxy.springaop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.fruitking.proxy.CarService;
public class TestSpringAOP {
/**
* 利用spring的AOP机制实现“代理”的横向抽取机制方式
* @param args
*/
public static void main(String[] args) {
ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
CarService carService = (CarService) ctx.getBean("carService");
carService.start();
carService.getLoadAmount();
String driver = carService.setDriver("fruitking");
System.out.println(driver);
System.out.println("------------------------------");
carService.loadGoods("Miss Mary");
System.out.println("------------------------------");
try{
carService.loadGoods(null);
}catch(NullPointerException e){
e.printStackTrace();
}
System.out.println("------------------------------");
try{
carService.loadGoods("tiger");
}catch(IllegalArgumentException e){
e.printStackTrace();
}
}
}
配置文件解释
<!--定义切面 指定拦截方法时 做什么(增强实现)-->
<bean id="xmlAopDemoUserLog" class="com.ganji.demo.service.aspect.XmlAopDemoUserLog"></bean>
<aop:config>
<aop:aspect ref="xmlAopDemoUserLog"> <!--指定切面-->
<!--定义切点-->
<aop:pointcut id="logpoint" expression="execution(* com.ganji.demo.service.user.UserService.GetDemoUser(..))"></aop:pointcut>
<!--定义连接点-->
<aop:before pointcut-ref="logpoint" method="beforeLog"></aop:before>
<aop:after pointcut-ref="logpoint" method="afterLog"></aop:after>
<aop:after-returning pointcut-ref="logpoint" method="afterReturningLog"></aop:after-returning>
<aop:after-throwing pointcut-ref="logpoint" method="afterThrowingLog"></aop:after-throwing>
</aop:aspect>
</aop:config>
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如何强制使用CGLIB实现AOP?
- 添加CGLIB库,SPRING_HOME/cglib/*.jar
- 在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
JDBC
JDBC用法
使用JDBC批量插入
public class BatchInsert {
public static void generalInsert(){
long start = System.currentTimeMillis();
PreparedStatement cmd = null ;
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/kxh", "root", "root");
connection.setAutoCommit(false);
cmd = connection.prepareStatement("insert into test values(?,?)");
final int batchSize = 1000;
for (int i = 0; i < 1000000; i++) {
cmd.setInt(1, i);
cmd.setString(2, "test");
cmd.executeUpdate();
if(++i % batchSize == 0) {
cmd.executeBatch();
}
}
connection.commit();
} catch (Exception e) {
e.printStackTrace();
}
finally{
try {
cmd.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
}
}
- 连接池最基本的也是最重要的优化策略,总能大幅提高性能。
- 批处理在效率上总是比逐条处理有优势,要处理的数据的记录条数越大,批处理的优势越明显,批处理还有一个好处就是减少了对数据库的链接次数,从而减轻数据库的压力。
- 批处理执行SQL的时候,批处理的分批的大小与数据库的吞吐量以及硬件配置有很大关系,需要通过测试找到最佳的分批大小,一般在50-1000之间。
- 预处理SQL在没事务的表上效率较高,在有实物的情况下比静态SQL稍有不及。但预定义SQL还有个好处就是消耗的内存较少,静态SQL串会占用大量的内存资源,容易导致内存溢出的问题。因此批量执行时候可以优先选择预定义SQL。
- 在批处理执行的时候,每批执行完成后,最好显式的调用pstmt.close()或stmt.close()方法,以便尽快释放执行过的SQL语句,提高内存利用率。
数据库连接池
所谓数据库连接池,可以看作 :在用户和数据库之间创建一个”池”,这个池中有若干个连接对象,当用户想要连接数据库,就要先从连接池中获取连接对象,然后操作数据库。一旦连接池中的连接对象被拿光了,下一个想要操作数据库的用户必须等待。使用池的方式避免了频繁建立和释放连接
初始大小:10个 最小空闲连接数:3个 增量:一次创建的最小单位(5个) 最大空闲连接数:12个 最大连接数:20个 最大的等待时间:1000毫秒
DBCP连接池:DBCP连接池是开源组织Apache软件基金组织开发的连接池实现。事实上,tomcat服务器默认就会使用这个连接池道具。 C3P0连接池:是一个开源组织的产品,开源框架的内部的连接池一般都使用C3P0来实现,例如:Hibernate。 weblogic的连接池:此连接池的持续运行的稳定性很强,在大并发量的压力下性能也相当优秀,另外在一些异常情况下连接池里的连接也能够及时释放。连接池监控一目了然,及时到位。
Druid连接池:来自阿里,擅长连接池的监控和日志等功能
数据库优化
- 慢查询,根据慢查询,定位分析性能问题。
- 优化设计,减少关联查询(部分数据可冗余)。
- 合并批量操作,减少数据库执行次数。
- 使用事务,任何时候都必须默认打开事务
- 分析查询计划,合理建立索引。
- 固化view和执行计划
- 利用表分区
JAVA GC
Java 虚拟机实现了Java语言最重要的特征:即平台无关性。原理:编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现java平台无关性。
classloader
双亲委派模型(Parent Delegation Model)
类的加载过程采用双亲委托机制,这种机制能更好的保证 Java 平台的安全。 该模型要求除了顶层的Bootstrap class loader启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。 双亲委派模型的工作过程为:
1.当前 ClassLoader 首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。 2.当前 classLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 bootstrap ClassLoader.当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
执行引擎
JVM内存模型
-
PC程序计数器:线程私有的一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器, NAMELY存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。Java 的多线程机制离不开程序计数器,每个线程都有一个自己的PC,以便完成不同线程上下文环境的切换。
-
java虚拟机栈:也是线程私有的。每一个 JVM 线程都有自己的 java 虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-
本地方法栈:也是线程私有的,与虚拟机栈的作用相似,虚拟机栈为虚拟机执行执行java方法服务,而本地方法栈则为虚拟机使用到的本地方法服务。
-
方法区 是线程共享的内存区域,它用于存储每一个类的结构信息,例如运行时常量池,成员变量和方法数据,构造函数和普通函数的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。当开发人员在程序中通过Class对象中的getName、isInstance等方法获取信息时,这些数据都来自方法区。 方法区也是全局共享的,在虚拟机启动时候创建。在一定条件下它也会被GC。这块区域对应Permanent Generation 持久代。 XX:PermSize指定大小。
-
运行时常量池 也是线程共享的,其空间从方法区中分配,存放的为类中固定的常量信息、方法和域的引用信息。
-
Java堆:被所有线程共享的一块存储区域,在虚拟机启动时创建,它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配。 Java堆在JVM启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的 “Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。JVM将Heap分为两块:新生代New Generation和旧生代Old Generation。 堆在JVM是所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也是new开销比较大的原因。 鉴于上面的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效
垃圾回收算法
垃圾回收机制是由垃圾收集器Garbage Collection GC来实现的,GC是后台的守护进程。
- 标记-清除算法 标记清除收集器停止所有的工作,从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片
- 复制算法 复制收集器将内存分为两块一样大小空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的空间满的时候,GC就会将活跃的对象复制到未使用的空间中去,原来不活跃的空间就变为了活跃的空间。这种算法当空间存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。
- 分代回收算法 垃圾分代回收算法(GenerationalCollecting)基于对对象生命周期分析后得出的垃圾回收算法。 因为我们前面有介绍,内存主要被分为三块,新生代、旧生代、持久代。三代的特点不同,造就了他们所用的GC算法不同,新生代适合那些生命周期较短,频繁创建及销毁的对象,旧生代适合生命周期相对较长的对象,持久代在Sun HotSpot中就是指方法区。 新生代使用复制算法和标记-清除垃圾收集算法,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from(Survivor 0)和Survivor to(Survivor1)三部分,其占新生代内存容量默认比例分别为8:1:1。-Xmn参数可以指定新生代内存大小。 Tenured(年老代、旧生代):JVMspecification中的 Heap的一部份年老代存放从年轻代存活的对象。年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。 Perm(持久代、永久代): JVM specification中的 Method area 用于存放静态文件,如今Java类、方法等。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。永久代也使用标记-整理算法进行垃圾回收,java虚拟机参数-XX:PermSize和-XX:MaxPermSize可以设置永久代的初始大小和最大容量。
JVM中的回收器
- 串行回收器(Serial Collector):单线程执行回收操作,回收期间暂停所有应用线程的执行,client模式下的默认回收器
- 并行回收器(Parallel Collector) 使用多个线程同时进行垃圾回收,多核环境里面可以充分的利用CPU资源,减少回收时间,增加JVM生产率,Server模式下的默认回收器。与串行回收器相同,回收期间暂停所有应用线程的执行。
- 并行合并收集器(Parallel Compacting Collection) 年轻代和年老代的回收都是用多线程处理。与并行回收器相比,年老代的回收时间更短,从而减少了暂停时间间隔(Pause time)
- 并发标记清除回收器(Concurrent Mark-Sweep Collector,CMS) 又名低延时收集器(Low-latencyCollector),通过各种手段使得应用程序被挂起的时间最短。基本与应用程序并发地执行回收操作,没有合并和复制操作。 年轻代的回收算法(Minor Collection):与并行回收器(ParallelCollector)相同 年老代的回收算法(Full Collection) :分为四个步骤,初始标记(Initial Mark)、并发标记(ConcurrentMark)、再次标记(Remark)、以及并发清理(Concurrent Sweep)。特别注意,没有合并操作,所以会有碎片。
OutOfMemoryError
原因:常见的有以下几种:
- 内存中加载的数据量于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
异常分类
- java.lang.OutOfMemoryError: PermGen space这个异常比较常见,是说JVM数据区域里的Perm内存区(方法区)溢出。尤其使用到像Spring等框架的时候,由于需要使用到动态生成类,而这些类不能被GC自动释放,所以导致OutOfMemoryError: PermGen space异常。解决方法很简单,增大JVM的 -XX:MaxPermSize 启动参数
- java.lang.OutOfMemoryError:heap space这个异常是因为JVM堆内 存分少了,不够用导致溢出。使用大对象或者内存泄露导致。
- java.lang.StackOverflowError:一个是Stacks里的线程数超过允许的时候,另一个是当线程申请更大的内 存,而超过native method允许的内 存的时候。
解决; 1.应用服务器提示错误的解决:把启动参数内存值设置足够大。 2.Java代码导致错误的解决:重点排查以下几点:
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 memory leak会最终会导致out of memory!
pinfanGC
正常的JVM内存监控图如下
频繁GC监控图 左边红框是优化前效果,右边是优化后,优化后fgc基本为0。
- 分析GC操作耗时并决定是否需要GC调优。如果分析结果显示GC耗时在0.1-0.3秒以内的话,一般不需要做GC调优。如果GC耗时达到1-3秒甚至10秒以上,就需要立即对系统进行GC调优
- 设置合理的GC类型和内存大小
- 查看gc日志,old区fgc前后大小是否有明显变化
- GC日志中是否有“Full GC (System)”,若有,则说明代码中有调用System.gc();
- 查看dump内存包含对象,用memory analyzer分析
dump分析工具
jstat:用于监视虚拟机各种运行状态 jmap:生成jvm内存映像dump文件 jstack:生成虚拟机线程快照 jconsole:java自带的GUI可视化监控 VisualVM:功能强大的运行监控和故障处理工具
MAT(Memory Analyzer Tool) 一些排查方法:
- 通过top consumers查找大对象,可以按照class、classloader和package进行group by;
- 通过immediate dominator找到责任对象,对于快速定位一组对象的持有者非常有用,这个操作直接解决了“谁让这些对象alive”的问题,而不是“谁有这些对象的引用”的问题,更直接高效;
- 运行classloader分析,这个重要性体现在亮点:第一,应用使用不同的classloader加载类,第二,不同 classloader加载的类存储在不同的永久代,这理论上也是可以被回收的。当有一个类被不同的classloader加载时,这时要根据各自 loader下的instance数量判断哪个loader更重要,从而要把另一个回收掉;
- 分析线程,本身heap dump里包含了thread信息,可以通过MAT来查看threads 的overview和detail,detail中有线程的堆内存信息,也有线程栈,同时还包含了操作系统本地栈。假设不做heap dump,我们检查到系统有问题,如何通过线程的角度来排查呢?首先top -H -p <pid>以线程的模式查看java应用的运行情况,找到占用cpu或者内存大的线程,记录线程id,然后printf %x <tid>转为16进制,再jstack -l <pid> > thread.log把java进程的thread dump出来,从里面找到tid,分析是哪个线程占用了系统资源。
- 分析java容器类,因为java的容器类是最常用来存储对象的,所以理论上发生内存泄露的风险也最高。可以从几个角度来 看:1)array填充率查询(填充率fill ratio是数组中非空元素的比例),打印非原生类型数组的填充率频率分布,从而排查系统中array的利用率;2)数组按照size分组查询,打印一个 按size分组的直方图;3)collection的填充率查询,ArrayList/HashMap/Hashtable/Properties /Vector/WeakHashMap/ConcurrentHashMap$Segment;4)collection按照size分组直方图;5) 查看一个list里的所有对象;6)查看hashmap里的所有对象;7)查看hashset里的对象;8)检查map的碰撞率;9)检查所有只有一个常 量的array。
- 分析Finalizer,1)查询finalizer正在处理的对象;2)查询finalizer准备处理的对象;3)直接查看finalizer线程;4)查看finalizer线程的thread local对象。
通信
概念
同步/异步主要针对C端: 阻塞/非阻塞主要针对S端:
同步
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
Netty
Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
Netty模型
其实Netty的线程模型是Reactor模型的变种,那就是去掉线程池的实现,这也是Netty NIO的默认模式。Netty中Reactor模式的参与者主要有下面一些组件:
- Selector
- EventLoopGroup/EventLoop
- ChannelPipeline Selector作为多路复用器,EventLoop作为转发器,Pipeline作为事件处理器。Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换。用户实现的ChannelHandler可以根据需要实现其中一个或多个接口,将其放入Pipeline中的链表队列中,ChannelPipeline会根据不同的IO事件类型来找到相应的Handler来处理。
粘包断包问题
对于粘包问题的解决
粘包问题主要是由于数据包界限不清,所以这个问题比较好解决,最好的解决办法就是在发送数据包前事先发送一个int型数据,该数据代表将要发送的数据包的大小,这样接收端可以每次触发OP_READ的时候先接受一个int大小的数据段到缓存池中,然后,紧接着读出后续完整的大小的包,这样就会处理掉粘包问题。因为channel.read()方法不能给读取数据的大小的参数,所以无法手动指定读取数据段的大小。但每次调用channel.read()返回的是他实际读取的大小,这样,思路就有了:首先调整缓存池的大小固定为要读出数据段的大小,这样保证不会过量读出。由于OP_READ和OP_WRITE不是一一对应的,所以一次OP_READ可以While循环调用channel.read()不停读取channel中的数据到缓存池,并捕获其返回值,当返回值累计达到要读取数据段大小时break掉循环,这样保证数据读取充足。所以这样就完美解决粘包问题。
对于断包问题的解决
断包问题主要是由于数据包过量读入时,缓存池结尾处只有半个数据包,channel里还有半个数据包,这样造成了这个包无法处理的问题。这个问题的解决思路是保证每次不过量读入,这样也就不存在断包了。还是因为channel.read()的读取不可控的原因,所以无法从read函数中控制读取大小,还是从缓存池入手。方法是调整缓存池的大小为要读数据的大小,这样就不会断包。