1.Java中多线程相关
1.1 线程的建立方式
【方法1】html

【方法2】android中实现多线程,通常使用此方法为主;java

【调用】【说明】认识:start方法调用以后并不是直接能够开启多线程的执行,具体线程开始执行的时间由操做系统决定;mysql

【共同点】都须要使用到Thread类产生线程;而后start开启线程;android
【不一样点】程序员
【1】继承Thread类是单继承,所以使用runnable类弥补缺陷,更灵活;面试
【2】继承Thread类就必须产生多个相应的线程;spring
而使用runnable接口,则须要实现runnable接口的实例,而后用此实例开启多个线程就实现了多线程共享资源了;sql
【runnable的优点】数据库
【1】避免Thread类单继承带来的局限性;编程
【2】加强程序的健壮性,代码能够被多个线程共享,同时代码和数据是独立的;
【3】适合多个线程对同一个资源的共享;
1.2 Start和run方法的区别
【start方法】使用start方法以后是真正的开启了多线程,没必要等待run方法中的方法体中的内容的执行完毕就能够直接调用start方法下面的方法;
【run方法】只是一个普通的方法,其中是方法的执行体,方法的执行仍是顺序执行,没有开启多线程,没有并发;
【多线程原理】:至关于玩游戏机,只有一个游戏机(cpu),但是有不少人要玩,因而,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。
调用start()后,线程会被放到等待队列,等待CPU调度,并不必定要立刻开始执行,只是将这个线程置于可动行状态。而后经过JVM,线程Thread会调用run()方法,执行本线程的线程体。
先调用start后调用run,这么麻烦,为了避免直接调用run?就是为了实现多线程的优势,没这个start不行。
1.start()方法来启动线程,真正实现了多线程运行。
这时无需等待run方法体代码执行完毕,能够直接继续执行下面的代码;
经过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并无运行。
而后经过此Thread类调用方法run()来完成其运行操做的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。
而后CPU再调度其它线程。
2.run()方法看成普通方法的方式调用。
程序仍是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;
程序中只有主线程——这一个线程, 其程序执行路径仍是只有一条, 这样就没有达到写线程的目的。
记住:多线程就是分时利用CPU,宏观上让全部线程一块儿执行 ,也叫并发
1 public class Main {
2
3 public static void main(String[] args) {
4 Thread t1 = new Thread(new T1());
5 Thread t2 = new Thread(new T2());
6 t1.start();
7 t2.start();
8 }
9
10 }
11
12 class T1 implements Runnable {
13 public void run() {
14 try {
15 for(int i=0;i<10;i++){
16 System.out.println(i);
17 Thread.sleep(100); //模拟耗时任务
18 }
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 }
23 }
24
25 class T2 implements Runnable {
26 public void run() {
27 try {
28 for(int i=0;i>-10;i--){
29 System.out.println(i);
30 Thread.sleep(100); //模拟耗时任务
31 }
32 } catch (InterruptedException e) {
33 e.printStackTrace();
34 }
35 }
36 }

1 public class Main {
2
3 public static void main(String[] args) {
4 Thread t1 = new Thread(new T1());
5 Thread t2 = new Thread(new T2());
6 t1.run();
7 t2.run();
8 }
9
10 }

可见两线程实际是顺序执行的;
2.线程间的通讯
2.1 synchronized对象锁

2.2 synchronized实现进程间通讯

2.3 synchronized/volatile关键字

首先咱们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操做会先在寄存器或者是CPU缓存上进行,最后才写入内存.
而在这个过程当中,变量的新值对其余线程是不可见的.
当对volatile标记的变量进行修改时,会将其余缓存中存储的修改前的变量清除,而后从新读取。这里从哪读取我并不明确,
通常来讲应该是先在进行修改的缓存A中修改成新值,而后通知其余缓存清除掉此变量,
当其余缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,
将新值穿给B。最后将新值写入内存。当变量须要更新时都是此步骤,volatile的做用是被其修饰的变量,每次更新时,都会刷新上述步骤。
【原文地址】值得一看:https://my.oschina.net/tantexian/blog/808032
volatile保证了可见性,可是并不保证原子性!!!
1.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:
1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。
2)禁止进行指令重排序。
volatile的可见性,即任什么时候刻只要有任何线程修改了volatile变量的值,其余线程总能获取到该最新值。
什么是指令重排序?有两个层面:
- 在虚拟机层面,为了尽量减小内存操做速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照本身的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽量充分地利用CPU。
拿上面的例子来讲:假如不是a=1的操做,而是a=new byte[1024*1024](分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,仍是先执行下面那句flag=true呢?显然,先执行flag=true能够提早使用CPU,加快总体效率,固然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种状况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。无论谁先开始,总以后面的代码在一些状况下存在先结束的可能。
- 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,一样是基于CPU速度比缓存速度快的缘由,和上一点的目的相似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机能够在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》
2.4 volatile与synchronized 区别
一、volatile本质是在告诉jvm当前变量在寄存器中的值是不肯定的,须要从主存中读取,
synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住.
volatile的第一条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其余线程能看到变量X的变更,更详细地说是要符合如下两个规则:
线程对变量进行修改以后,要马上回写到主内存。
线程对变量读取的时候,要从主内存中读,而不是缓存。
二、volatile仅能使用在变量级别;
synchronized则能够使用在变量、方法、和类级别的
三、volatile仅能实现变量的修改可见性,而synchronized则能够保证变量的修改可见性和原子性.
四、volatile不会形成线程的阻塞,而synchronized可能会形成线程的阻塞.
五、volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化
六、使用volatile而不是synchronized的惟一安全的状况是:类中只有一个可变的域。
七、当一个域的值依赖于它以前的值时,volatile就没法工做了,如n=n+1,n++等。
若是某个域的值受到其余域的值的限制,那么volatile也没法工做,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
2.5 synchronized和lock的区别
【lock方法】只要指明起始位置和终止位置;通常就重入锁;
须要lcok和unlock对锁的开启和关闭;
通常会在finally中对锁进行unlock,防止死锁;
|
类别 |
synchronized |
Lock |
1 |
存在层次 |
Java的关键字,在jvm层面上,性能低下 |
是一个类,代码本身定义的 |
2 |
锁的释放 |
一、以获取锁的线程执行完同步代码,释放锁 二、线程执行发生异常,jvm会让线程释放锁 |
在finally中必须释放锁,否则容易形成线程死锁 |
3 |
锁的获取 |
假设A线程得到锁,B线程等待。若是A线程阻塞,B线程会一直等待 |
分状况而定,Lock有多个锁获取的方式,具体下面会说道,大体就是能够尝试得到锁,线程能够不用一直等待 |
4 |
锁状态 |
没法判断 |
能够判断 |
5 |
锁类型 |
可重入 不可中断 非公平 |
可重入 可判断 可公平(二者皆可) |
6 |
性能 |
少许同步 |
大量同步 |
7 |
cpu锁的性质 |
悲观锁 (阻塞等待) |
乐观锁 |
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,能够使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,
像数据库若是提供相似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另外一种,像乐观锁适用于写比较少的状况下,即冲突真的不多发生的时候,这样能够省去了锁的开销,加大了系统的整个吞吐量。但若是常常产生冲突,上层应用会不断的进行retry,这样反却是下降了性能,因此这种状况下用悲观锁就比较合适。
技术点:
一、线程与进程:
在开始以前先把进程与线程进行区分一下,一个程序最少须要一个进程,而一个进程最少须要一个线程。
关系是线程–>进程–>程序的大体组成结构。
因此
【线程】是程序执行流的最小单位,
【进程】是系统进行资源分配和调度的一个独立单位。
如下咱们全部讨论的都是创建在线程基础之上。
二、Thread的几个重要方法:
咱们先了解一下Thread的几个重要方法。
a、start()方法,调用该方法开始执行该线程;
b、stop()方法,调用该方法强制结束该线程执行;
d、sleep()方法,调用该方法该线程进入等待。
e、run()方法,调用该方法直接执行线程的run()方法,可是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!
看到这里,可能有些人就会问啦,那wait()和notify()呢?要注意,其实wait()与notify()方法是Object的方法,不是Thread的方法!!
同时,wait()与notify()会配合使用,分别表示线程挂起和线程恢复。
补充两个重要的方法:yield()和join()
f.yield方法 暂停当前正在执行的线程对象。 yield()方法是中止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。若是没有的话,那么yield()方法将不会起做用,而且由可执行状态后立刻又被执行。 g.join方法是用于在某一个线程的执行过程当中调用另外一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,致使结果不可预测。
这里还有一个很常见的问题,顺带提一下:
【wait()与sleep()的区别】简单来讲wait()会释放对象锁而sleep()不会释放对象锁。
【1】对于sleep()方法,咱们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
【2】sleep()方法致使了程序暂停执行指定的时间,让出cpu该其余线程,可是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
在调用sleep()方法的过程当中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态。
什么意思呢?
举个列子说明:
5
6 /**
7 * java中的sleep()和wait()的区别
10 */
11 public class TestD {
12
13 public static void main(String[] args) {
14 new Thread(new Thread1()).start();
15 try {
16 Thread.sleep(5000);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20 new Thread(new Thread2()).start();
21 }
22
23 private static class Thread1 implements Runnable{
24 @Override
25 public void run(){
26 synchronized (TestD.class) {
27 System.out.println("enter thread1...");
28 System.out.println("thread1 is waiting...");
29 try {
30 //调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
31 TestD.class.wait();
32 } catch (Exception e) {
33 e.printStackTrace();
34 }
35 System.out.println("thread1 is going on ....");
36 System.out.println("thread1 is over!!!");
37 }
38 }
39 }
40
41 private static class Thread2 implements Runnable{
42 @Override
43 public void run(){
44 synchronized (TestD.class) {
45 System.out.println("enter thread2....");
46 System.out.println("thread2 is sleep....");
47 //只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
48 TestD.class.notify();
49 //==================
50 //区别
51 //若是咱们把代码:TestD.class.notify();给注释掉,即TestD.class调用了wait()方法,可是没有调用notify()
52 //方法,则线程永远处于挂起状态。
53 try {
54 //sleep()方法致使了程序暂停执行指定的时间,让出cpu该其余线程,
55 //可是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
56 //在调用sleep()方法的过程当中,线程不会释放对象锁。
57 Thread.sleep(5000);
58 } catch (Exception e) {
59 e.printStackTrace();
60 }
61 System.out.println("thread2 is going on....");
62 System.out.println("thread2 is over!!!");
63 }
64 }
65 }
66 }
运效果
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
thread1 is going on ....
thread1 is over!!!
若是注释掉代码:
运行效果:
enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
且程序一直处于挂起状态。
===============================================================================================
Java中wait和sleep方法的区别
1、二者的区别
【1】这两个方法来自不一样的类分别是Thread和Object
【2】最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其余线程能够使用同步控制块或者方法(锁代码块和方法锁)。
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep能够在任何地方使用(使用范围)
【3】sleep必须捕获异常,而wait,notify和notifyAll不须要捕获异常
sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待必定的时间以后,自动醒来进入到可运行状态,不会立刻进入运行状态,由于线程调度机制恢复线程的运行也须要时间,一个线程对象调用了sleep方法以后,
并不会释放他所持有的全部对象锁,因此也就不会影响其余进程对象的运行。但在sleep的过程当中过程当中有可能被其余对象调用它的interrupt(),产生InterruptedException异常,
若是你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,若是你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及之后的代码。
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,经过t.sleep()让t对象进入sleep,这样的作法是错误的,它只会是使当前线程被sleep 而不是t线程
wait属于Object的成员方法,一旦一个对象调用了wait方法,必需要采用notify()和notifyAll()方法唤醒该进程;若是线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的全部同步资源,
而不限于这个被调用了wait()方法的对象。wait()方法也一样会在wait的过程当中有可能被其余对象调用interrupt()方法而产生
若是线程A但愿当即结束线程B,则能够对线程B对应的Thread实例调用interrupt方法。若是此刻线程B正在wait/sleep/join,则线程B会马上抛出InterruptedException,在catch() {} 中直接return便可安全地结束线程。
须要注意的是,InterruptedException是线程本身从内部抛出的,并非interrupt()方法抛出的。对某一线程调用interrupt()时,若是该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。可是,一旦该线程进入到wait()/sleep()/join()后,就会马上抛出InterruptedException。
waite()和notify()由于会对对象的“锁标志”进行操做,因此它们必须在synchronized函数或synchronized block中进行调用。若是在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译经过,但在运行时会发生illegalMonitorStateException的异常。
说一下为何使用wait()方法时,通常是须要while循环而不是if?
复制代码
while(!执行条件) {
wait();
}
....
if(!执行条件) {
wait();
}
....
复制代码
while会一直执行循环,直到条件知足,执行条件才会继续往下执行。if只会执行一次判断条件,不知足就会等待。这样就会出现问题。
咱们知道用notify() 和notifyAll()能够唤醒线程,通常咱们经常使用的是notifyAll(),由于notify(),只会随机唤醒一个睡眠线程,并不必定是咱们想要唤醒的线程。
若是使用的是notifyAll(),唤醒全部的线程,那你怎么知道他想唤醒的是某个正在等待的wait()线程呢,若是用while()方法,就会再次判断条件是否是成立,知足执行条件了,就会接着执行,
而if会直接唤醒wait()方法,继续往下执行,根本无论这个notifyAll()是否是想唤醒的是本身仍是别人,可能此时if的条件根本没成立。
举个例子:
while去水果店买苹果,没有了,而后while就和水果店老板说,有水果的时候通知我,我先回去了。if也去水果店买苹果,没有了,而后if就和水果店老板说,有水果的时候通知我,我先回去了。
过一段时间,水果店老板发短信告诉while和if,有水果了,while去一看,水果店只是进了香蕉,并非苹果,因此不是想要的水果,因而回去继续等水果店老板通知,
而if根本就不看是否是本身想要的苹果,直接就叫老板送10斤水果过来,这样会致使你获得错误的结果。
三、线程状态:

线程总共有5大状态,经过上面第二个知识点的介绍,理解起来就简单了。
-
新建状态:新建线程对象,并无调用start()方法以前
-
就绪状态:调用start()方法以后线程就进入就绪状态,可是并非说只要调用start()方法线程就立刻变为当前线程,在变为当前线程以前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态。
-
运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
-
阻塞状态:线程被暂停,好比说调用sleep()方法后线程就进入阻塞状态
-
死亡状态:线程执行结束
四、锁类型
2.6 Lock详细介绍与Demo
【原文地址】https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1
如下是Lock接口的源码,笔者修剪以后的结果:
1 public interface Lock {
2
3 /**
4 * Acquires the lock.
5 */
6 void lock();
7
8 /**
9 * Acquires the lock unless the current thread is
10 * {@linkplain Thread#interrupt interrupted}.
11 */
12 void lockInterruptibly() throws InterruptedException;
13
14 /**
15 * Acquires the lock only if it is free at the time of invocation.
16 */
17 boolean tryLock();
18
19 /**
20 * Acquires the lock if it is free within the given waiting time and the
21 * current thread has not been {@linkplain Thread#interrupt interrupted}.
22 */
23 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
24
25 /**
26 * Releases the lock.
27 */
28 void unlock();
29
30 }
31 从Lock接口中咱们能够看到主要有个方法,这些方法的功能从注释中能够看出:
32 lock():获取锁,若是锁被暂用则一直等待
33
34 unlock():释放锁
35
36 tryLock(): 注意返回类型是boolean,若是获取锁的时候锁被占用就返回false,不然返回true
37
38 tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
39
40 lockInterruptibly():用该锁的得到方式,若是线程在获取锁的阶段进入了等待,那么能够中断此线程,先去作别的事
41
42 经过 以上的解释,大体能够解释在上个部分中“锁类型(lockInterruptibly())”,“锁状态(tryLock())”等问题,还有就是前面子所获取的过程我所写的“大体就是能够尝试得到锁,线程能够不会一直等待”用了“能够”的缘由。
43
44 下面是Lock通常使用的例子,注意ReentrantLock是Lock接口的实现。
45 【lock()】
48 package com.brickworkers;
49
50 import java.util.concurrent.locks.Lock;
51 import java.util.concurrent.locks.ReentrantLock;
52
53 public class LockTest {
54 private Lock lock = new ReentrantLock();
55
56 //须要参与同步的方法
57 private void method(Thread thread){
58 lock.lock();
59 try {
60 System.out.println("线程名"+thread.getName() + "得到了锁");
61 }catch(Exception e){
62 e.printStackTrace();
63 } finally {
64 System.out.println("线程名"+thread.getName() + "释放了锁");
65 lock.unlock();
66 }
67 }
68
69 public static void main(String[] args) {
70 LockTest lockTest = new LockTest();
71
72 //线程1
73 Thread t1 = new Thread(new Runnable() {
74
75 @Override
76 public void run() {
77 lockTest.method(Thread.currentThread());
78 }
79 }, "t1");
80
81 Thread t2 = new Thread(new Runnable() {
82
83 @Override
84 public void run() {
85 lockTest.method(Thread.currentThread());
86 }
87 }, "t2");
88
89 t1.start();
90 t2.start();
91 }
92 }
93 //执行状况:线程名t1得到了锁
94 // 线程名t1释放了锁
95 // 线程名t2得到了锁
96 // 线程名t2释放了锁
97 【tryLock()】:
99 package com.brickworkers;
100
101 import java.util.concurrent.locks.Lock;
102 import java.util.concurrent.locks.ReentrantLock;
103
104 public class LockTest {
105 private Lock lock = new ReentrantLock();
106
107 //须要参与同步的方法
108 private void method(Thread thread){
109 /* lock.lock();
110 try {
111 System.out.println("线程名"+thread.getName() + "得到了锁");
112 }catch(Exception e){
113 e.printStackTrace();
114 } finally {
115 System.out.println("线程名"+thread.getName() + "释放了锁");
116 lock.unlock();
117 }*/
118
119
120 if(lock.tryLock()){
121 try {
122 System.out.println("线程名"+thread.getName() + "得到了锁");
123 }catch(Exception e){
124 e.printStackTrace();
125 } finally {
126 System.out.println("线程名"+thread.getName() + "释放了锁");
127 lock.unlock();
128 }
129 }else{
130 System.out.println("我是"+Thread.currentThread().getName()+"有人占着锁,我就不要啦");
131 }
132 }
133
134 public static void main(String[] args) {
135 LockTest lockTest = new LockTest();
136
137 //线程1
138 Thread t1 = new Thread(new Runnable() {
139
140 @Override
141 public void run() {
142 lockTest.method(Thread.currentThread());
143 }
144 }, "t1");
145
146 Thread t2 = new Thread(new Runnable() {
147
148 @Override
149 public void run() {
150 lockTest.method(Thread.currentThread());
151 }
152 }, "t2");
153
154 t1.start();
155 t2.start();
156 }
157 }
158
159 //执行结果: 线程名t2得到了锁
160 // 我是t1有人占着锁,我就不要啦
161 // 线程名t2释放了锁
162 看到这里相信你们也都会使用如何使用Lock了吧,关于tryLock(long time, TimeUnit unit)和lockInterruptibly()再也不赘述。
前者主要存在一个等待时间,在测试代码中写入一个等待时间,后者主要是等待中断,会抛出一个中断异常,经常使用度不高,喜欢探究能够本身深刻研究。
163
164 前面比较重提到“公平锁”,在这里能够提一下ReentrantLock对于平衡锁的定义,在源码中有这么两段:
165
166 /**
167 * Sync object for non-fair locks
168 */
169 static final class NonfairSync extends Sync {
170 private static final long serialVersionUID = 7316153563782823691L;
171
172 /**
173 * Performs lock. Try immediate barge, backing up to normal
174 * acquire on failure.
175 */
176 final void lock() {
177 if (compareAndSetState(0, 1))
178 setExclusiveOwnerThread(Thread.currentThread());
179 else
180 acquire(1);
181 }
182
183 protected final boolean tryAcquire(int acquires) {
184 return nonfairTryAcquire(acquires);
185 }
186 }
187
188 /**
189 * Sync object for fair locks
190 */
191 static final class FairSync extends Sync {
192 private static final long serialVersionUID = -3000897897090466540L;
193
194 final void lock() {
195 acquire(1);
196 }
197
198 /**
199 * Fair version of tryAcquire. Don't grant access unless
200 * recursive call or no waiters or is first.
201 */
202 protected final boolean tryAcquire(int acquires) {
203 final Thread current = Thread.currentThread();
204 int c = getState();
205 if (c == 0) {
206 if (!hasQueuedPredecessors() &&
207 compareAndSetState(0, acquires)) {
208 setExclusiveOwnerThread(current);
209 return true;
210 }
211 }
212 else if (current == getExclusiveOwnerThread()) {
213 int nextc = c + acquires;
214 if (nextc < 0)
215 throw new Error("Maximum lock count exceeded");
216 setState(nextc);
217 return true;
218 }
219 return false;
220 }
221 }
222 从以上源码能够看出在Lock中能够本身控制锁是否公平,并且,默认的是非公平锁,如下是ReentrantLock的构造函数:
223
224 public ReentrantLock() {
225 sync = new NonfairSync();//默认非公平锁
226 }
尾记录:
延伸学习:对于LOCK底层的实现,你们能够参考:
点击Lock底层介绍博客
两种同步方式性能测试,你们能够参考:
点击查看两种同步方式性能测试博客
博主18年3月新增:
回来看本身博客。发现东西阐述的不够完整。这里在作补充,由于这篇博客访问较大,因此为了避免误导你们,尽可能介绍给你们正确的表述:
一、两种锁的底层实现方式:
synchronized:咱们知道java是用字节码指令来控制程序(这里不包括热点代码编译成机器码)。
在字节指令中,存在有synchronized所包含的代码块,那么会造成2段流程的执行。

咱们点击查看SyncDemo.java的源码SyncDemo.class,能够看到以下:

如上就是这段代码段字节码指令,没你想的那么难吧。言归正传,咱们能够清晰段看到,其实synchronized映射成字节码指令就是增长来两个指令:monitorenter和monitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试得到锁,若是得到锁那么锁计数+1(为何会加一呢,由于它是一个可重入锁,因此须要用这个锁计数判断锁的状况),若是没有得到锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。
那么有的朋友看到这里就疑惑了,那图上有2个monitorexit呀?立刻回答这个问题:上面我之前写的文章也有表述过,synchronized锁释放有两种机制,一种就是执行完释放;另一种就是发送异常,虚拟机释放。图中第二个monitorexit就是发生异常时执行的流程,这就是我开头说的“会有2个流程存在“。并且,从图中咱们也能够看到在第13行,有一个goto指令,也就是说若是正常运行结束会跳转到19行执行。
这下,你对synchronized是否是了解的很清晰了呢。接下来咱们再聊一聊Lock。
Lock:Lock实现和synchronized不同,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,因此它每次吃东西前都把本身关起来。而Lock呢底层实际上是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它从新去拿吃的就好啦,因此它很乐观。具体底层怎么实现,博主不在细述,有机会的话,我会对concurrent包下面的机制好好和你们说说,若是面试问起,你就说底层主要靠volatile和CAS操做实现的。
如今,才是我真正想在这篇博文后面加的,我要说的是:尽量去使用synchronized而不要去使用LOCK
什么概念呢?我和你们打个比方:你叫jdk,你生了一个孩子叫synchronized,后来呢,你领养了一个孩子叫LOCK。起初,LOCK刚来到新家的时候,它很乖,很懂事,各个方面都表现的比synchronized好。你很开心,可是你心里深处又有一点淡淡的忧伤,你不但愿你本身亲生的孩子居然还不如一个领养的孩子乖巧。这个时候,你对亲生的孩子教育更加深入了,你想证实,你的亲生孩子synchronized并不会比领养的孩子LOCK差。(博主只是打个比方)
那如何教育呢?
在jdk1.6~jdk1.7的时候,也就是synchronized1六、7岁的时候,你做为爸爸,你给他优化了,具体优化在哪里呢:
一、线程自旋和适应性自旋
咱们知道,java’线程实际上是映射在内核之上的,线程的挂起和恢复会极大的影响开销。而且jdk官方人员发现,不少线程在等待锁的时候,在很短的一段时间就得到了锁,因此它们在线程等待的时候,并不须要把线程挂起,而是让他无目的的循环,通常设置10次。这样就避免了线程切换的开销,极大的提高了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他能够根据它前面线程的自旋状况,从而调整它的自旋,甚至是不通过自旋而直接挂起。
二、锁消除
什么叫锁消除呢?就是把没必要要的同步在编译阶段进行移除。
那么有的小伙伴又迷糊了,我本身写的代码我会不知道这里要不要加锁?我加了锁就是表示这边会有同步呀?
并非这样,这里所说的锁消除并不必定指代是你写的代码的锁消除,我打一个比方:
在jdk1.5之前,咱们的String字符串拼接操做其实底层是StringBuffer来实现的(这个你们能够用我前面介绍的方法,写一个简单的demo,而后查看class文件中的字节码指令就清楚了),而在jdk1.5以后,那么是用StringBuilder来拼接的。咱们考虑前面的状况,好比以下代码:
String str1="qwe"; String str2="asd"; String str3=str1+str2;
底层实现会变成这样:
StringBuffer sb = new StringBuffer(); sb.append("qwe"); sb.append("asd");
咱们知道,StringBuffer是一个线程安全的类,也就是说两个append方法都会同步,经过指针逃逸分析(就是变量不会外泄),咱们发如今这段代码并不存在线程安全问题,这个时候就会把这个同步锁消除。
三、锁粗化
在用synchronized的时候,咱们都讲究为了不大开销,尽可能同步代码块要小。那么为何还要加粗呢?
咱们继续以上面的字符串拼接为例,咱们知道在这一段代码中,每个append都须要同步一次,那么我能够把锁粗化到第一个append和最后一个append(这里不要去纠结前面的锁消除,我只是打个比方)
四、轻量级锁
五、偏向锁
2.8 wait/notify/notifyall
先说两个概念:锁池和等待池
【锁池】:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),
因为这些线程在进入对象的synchronized方法以前必须先得到该对象的锁的拥有权,可是该对象的锁目前正被线程A拥有,因此这些线程就进入了该对象的锁池中。
【等待池】假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,线程A进入到了该对象的等待池中
Reference:
java中的锁池和等待池
而后再来讲notify和notifyAll的区别
也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的全部线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的几率大,倘若某线程没有竞争到该对象锁,它还会留在锁池中,惟有线程再次调用 wait()方法,它才会从新回到等待池中。
而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
Reference:
线程间协做:wait、notify、notifyAll
综上,【所谓唤醒线程】另外一种解释能够说是将线程由等待池移动到锁池,notifyAll调用后,会将所有线程由等待池移到锁池,而后参与锁的竞争,竞争成功则继续执行,若是不成功则留在锁池等待锁被释放后再次参与竞争。
而notify只会唤醒一个线程。
有了这些理论基础,后面的notify可能会致使死锁,而notifyAll则不会的例子也就好解释了
3.线程池
3.1 线程池的优势
合理利用线程池可以带来三个好处。
第一:下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
第二:提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。
第三:提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。
可是要作到合理的利用线程池,必须对其原理了如指掌
3.2 线程池的使用

线程池的建立
咱们能够经过ThreadPoolExecutor来建立一个线程池。6个参数。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
建立一个线程池须要输入几个参数:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会建立一个线程来执行任务,即便其余空闲的基本线程可以执行新任务也会建立线程,等到须要执行的任务数大于线程池基本大小时就再也不建立。
若是调用了线程池的prestartAllCoreThreads方法,线程池会提早建立并启动全部基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 能够选择如下几个阻塞队列。
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量一般要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具备优先级的无限阻塞队列。
- maximumPoolSize(线程池最大大小):线程池容许建立的最大线程数。若是队列满了,而且已建立的线程数小于最大线程数,则线程池会再建立新的线程执行任务。值得注意的是若是使用了无界的任务队列这个参数就没什么效果。
- ThreadFactory:用于设置建立线程的工厂,能够经过线程工厂给每一个建立出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采起一种策略处理提交的新任务。这个策略默认状况下是AbortPolicy,表示没法处理新任务时抛出异常。如下是JDK1.5提供的四种策略。
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 固然也能够根据应用场景须要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
- keepAliveTime(线程活动保持时间):线程池的工做线程空闲后,保持存活的时间。因此若是任务不少,而且每一个任务执行的时间比较短,能够调大这个时间,提升线程的利用率。
- TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
向线程池提交任务
咱们能够使用execute提交的任务,可是execute方法没有返回值,因此没法判断任务是否被线程池执行成功。经过如下代码可知execute方法输入的任务是一个Runnable类的实例。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
咱们也能够使用submit 方法来提交任务,它会返回一个future,那么咱们能够经过这个future来判断任务是否执行成功,经过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后当即返回,这时有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理没法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}
线程池的关闭
咱们能够经过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工做线程,而后逐个调用线程的interrupt方法来中断线程,因此没法响应中断的任务可能永远没法终止。
可是它们存在必定的区别,shutdownNow首先将线程池的状态设置成STOP,而后尝试中止全部的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,而后中断全部没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当全部的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于咱们应该调用哪种方法来关闭线程池,应该由提交到线程池的任务特性决定,一般调用shutdown来关闭线程池,若是任务不必定要执行完,则能够调用shutdownNow。
3.3. 线程池的分析
流程分析:线程池的主要工做流程以下:


源码分析。上面的流程分析让咱们很直观的了解了线程池的工做原理,让咱们再经过源代码来看看是如何实现的。线程池执行任务的方法以下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//若是线程数小于基本线程数,则建立线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如线程数大于等于基本线程数或线程建立失败,则将当前任务放到工做队列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//若是线程池不处于运行中或任务没法放入队列,而且当前线程数量小于最大容许的线程数量,
则建立一个线程执行任务。
else if (!addIfUnderMaximumPoolSize(command))
//抛出RejectedExecutionException异常
reject(command); // is shutdown or saturated
}
}
工做线程。线程池建立线程时,会将线程封装成工做线程Worker,Worker在执行完任务后,还会无限循环获取工做队列里的任务来执行。咱们能够从Worker的run方法里看到这点:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//若是线程数小于基本线程数,则建立线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如线程数大于等于基本线程数或线程建立失败,则将当前任务放到工做队列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//若是线程池不处于运行中或任务没法放入队列,而且当前线程数量小于最大容许的线程数量,
则建立一个线程执行任务。
else if (!addIfUnderMaximumPoolSize(command))
//抛出RejectedExecutionException异常
reject(command); // is shutdown or saturated
}
}
3.5. 合理的配置线程池
要想合理的配置线程池,就必须首先分析任务特性,能够从如下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
- 任务的优先级:高 中 低
- 任务的执行时间:长 中 短。
- 任务的依赖性:是否依赖其余系统资源,如数据库链接。
任务性质不一样的任务能够用不一样规模的线程池分开处理。CPU密集型任务配置尽量小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则因为线程并非一直在执行任务,则配置尽量多的线程,如2*Ncpu。混合型的任务,若是能够拆分,则将其拆分红一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,若是这两个任务执行时间相差太大,则不必进行分解。咱们能够经过Runtime.getRuntime().availableProcessors()方法得到当前设备的CPU个数。
优先级不一样的任务能够使用优先级队列PriorityBlockingQueue来处理。它可让优先级高的任务先获得执行,须要注意的是若是一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不一样的任务能够交给不一样规模的线程池来处理,或者也能够使用优先级队列,让执行时间短的任务先执行。
依赖数据库链接池的任务,由于线程提交SQL后须要等待数据库返回结果,若是等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
建议使用有界队列,有界队列能增长系统的稳定性和预警能力,能够根据须要设大一点,好比几千。有一次咱们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,经过排查发现是数据库出现了问题,致使执行SQL变得很是缓慢,由于后台任务线程池里的任务全是须要向数据库查询和插入数据的,因此致使线程池里的工做线程所有阻塞住,任务积压在线程池里。若是当时咱们设置成无界队列,线程池的队列就会愈来愈多,有可能会撑满内存,致使整个系统不可用,而不仅是后台任务出现问题。固然咱们的系统全部的任务是用的单独的服务器部署的,而咱们使用不一样规模的线程池跑不一样类型的任务,可是出现这样问题时也会影响到其余任务。
3.6 线程池的监控
经过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候能够使用
- taskCount:线程池须要执行的任务数量。
- completedTaskCount:线程池在运行过程当中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经建立过的最大线程数量。经过这个数据能够知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize:线程池的线程数量。若是线程池不销毁的话,池里的线程不会自动销毁,因此这个大小只增不+ getActiveCount:获取活动的线程数。
经过扩展线程池进行监控。经过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,咱们能够在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }
【博客参考】线程池
在Java中能够经过线程池来达到这样的效果。今天咱们就来详细讲解一下Java的线程池,首先咱们从最核心的ThreadPoolExecutor类中的方法讲起,而后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小。
如下是本文的目录大纲:
一.Java中的ThreadPoolExecutor类
二.深刻剖析线程池实现原理
三.使用示例
四.如何合理配置线程池的大小
一.Java中的ThreadPoolExecutor类
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,所以若是要透彻地了解Java中的线程池,必须先了解这个类。下面咱们来看一下ThreadPoolExecutor类的具体实现源码。
在ThreadPoolExecutor类中提供了四个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
从上面的代码能够得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,经过观察每一个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工做。
下面解释下一下构造器中各个参数的含义:
【corePoolSize】:核心池的大小,这个参数跟后面讲述的线程池的实现原理有很是大的关系。在建立了线程池后,默认状况下,线程池中并无任何线程,而是等待有任务到来才建立线程去执行任务,
除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就能够看出,是预建立线程的意思,即在没有任务到来以前就建立corePoolSize个线程或者一个线程。
默认状况下,在建立了线程池后,线程池中的线程数为0,当有任务来以后,就会建立一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
【maximumPoolSize】:线程池最大线程数,这个参数也是一个很是重要的参数,它表示在线程池中最多能建立多少个线程;
【keepAliveTime】:表示线程没有任务执行时最多保持多久时间会终止。默认状况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起做用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,若是一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。可是若是调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起做用,直到线程池中的线程数为0;
【unit】:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,通常来讲,这里的阻塞队列有如下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,通常使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
threadFactory:线程工厂,主要用来建立线程;
handler:表示当拒绝处理任务时的策略,有如下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,可是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
具体参数的配置与线程池的关系将在下一节讲述。
从上面给出的ThreadPoolExecutor类的代码能够知道,ThreadPoolExecutor继承了AbstractExecutorService,咱们来看一下AbstractExecutorService的实现:
public abstract class AbstractExecutorService implements ExecutorService {
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
public Future<?> submit(Runnable task) {};
public <T> Future<T> submit(Runnable task, T result) { };
public <T> Future<T> submit(Callable<T> task) { };
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
};
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
};
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
};
}
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。
咱们接着看ExecutorService接口的实现:
public interface ExecutorService extends Executor {
void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
而ExecutorService又是继承了Executor接口,咱们看一下Executor接口的实现:
public interface Executor {
void execute(Runnable command);
}
到这里,你们应该明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor几个之间的关系了。
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思能够理解,就是用来执行传进去的任务的;
而后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的全部方法;
而后ThreadPoolExecutor继承了类AbstractExecutorService。
在ThreadPoolExecutor类中有几个很是重要的方法:
execute()
submit()
shutdown()
shutdownNow()
【execute()】方法其实是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,经过这个方法能够向线程池提交一个任务,交由线程池去执行。
【submit()】方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并无对其进行重写,这个方法也是用来向线程池提交任务的,可是它和execute()方法不一样,它可以返回任务执行的结果,去看submit()方法的实现,会发现它实际上仍是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。
【shutdown()和shutdownNow()】是用来关闭线程池的。
还有不少其余的方法:
好比:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友能够自行查阅API。
二.深刻剖析线程池实现原理
在上一节咱们从宏观上介绍了ThreadPoolExecutor,下面咱们来深刻解析一下线程池的具体实现原理,将从下面几个方面讲解:
1.线程池状态
2.任务的执行
3.线程池中的线程初始化
4.任务缓存队列及排队策略
5.任务拒绝策略
6.线程池的关闭
7.线程池容量的动态调整
1.线程池状态
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
下面的几个static final变量表示runState可能的几个取值。
当建立线程池后,初始时,线程池处于RUNNING状态;
若是调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不可以接受新的任务,它会等待全部任务执行完毕;
若是调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,而且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,而且全部工做线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
2.任务的执行
在了解将任务提交给线程池到任务执行完毕整个过程以前,咱们先来看一下ThreadPoolExecutor类中其余的一些比较重要成员变量:
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(好比线程池大小
//、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工做集
private volatile long keepAliveTime; //线程存货时间
private volatile boolean allowCoreThreadTimeOut; //是否容许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来建立线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
每一个变量的做用都已经标明出来了,这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。
corePoolSize在不少地方被翻译成核心池大小,其实个人理解这个就是线程池的大小。举个简单的例子:
假若有一个工厂,工厂里面有10个工人,每一个工人同时只能作一件任务。
所以只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人作;
当10个工人都有任务在作时,若是还来了任务,就把任务进行排队等待;
若是说新任务数目增加的速度远远大于工人作任务的速度,那么此时工厂主管可能会想补救措施,好比从新招4个临时工人进来;
而后就将任务也分配给这4个临时工人作;
若是说着14个工人作任务的速度仍是不够,此时工厂主管可能就要考虑再也不接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增加的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量忽然过大时的一种补救措施。
不过为了方便理解,在本文后面仍是将corePoolSize翻译成核心池大小。
largestPoolSize只是一个用来起记录做用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
下面咱们进入正题,看一下任务从提交到最终执行完毕经历了哪些过程。
在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然经过submit也能够提交任务,可是实际上submit方法里面最终调用的仍是execute()方法,因此咱们只须要研究execute()方法的实现原理便可:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
上面的代码可能看起来不是那么容易理解,下面咱们一句一句解释:
首先,判断提交的任务command是否为null,如果null,则抛出空指针异常;
接着是这句,这句要好好理解一下:
1
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))
因为是或条件运算符,因此先计算前半部分的值,若是线程池中当前线程数不小于核心池大小,那么就会直接进入下面的if语句块了。
若是线程池中当前线程数小于核心池大小,则接着执行后半部分,也就是执行
1
addIfUnderCorePoolSize(command)
若是执行完addIfUnderCorePoolSize这个方法返回false,则继续执行下面的if语句块,不然整个方法就直接执行完毕了。
若是执行完addIfUnderCorePoolSize这个方法返回false,而后接着判断:
1
if (runState == RUNNING && workQueue.offer(command))
若是当前线程池处于RUNNING状态,则将任务放入任务缓存队列;若是当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行:
1
addIfUnderMaximumPoolSize(command)
若是执行addIfUnderMaximumPoolSize方法失败,则执行reject()方法进行任务拒绝处理。
回到前面:
1
if (runState == RUNNING && workQueue.offer(command))
这句的执行,若是说当前线程池处于RUNNING状态且将任务放入任务缓存队列成功,则继续进行判断:
1
if (runState != RUNNING || poolSize == 0)
这句判断是为了防止在将此任务添加进任务缓存队列的同时其余线程忽然调用shutdown或者shutdownNow方法关闭了线程池的一种应急措施。若是是这样就执行:
1
ensureQueuedTaskHandled(command)
进行应急处理,从名字能够看出是保证 添加到任务缓存队列中的任务获得处理。
咱们接着看2个关键方法的实现:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask); //建立线程去执行firstTask任务
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
这个是addIfUnderCorePoolSize方法的具体实现,从名字能够看出它的意图就是当低于核心吃大小时执行的方法。下面看其具体实现,首先获取到锁,由于这地方涉及到线程池状态的变化,先经过if语句判断当前线程池中的线程数目是否小于核心池大小,有朋友也许会有疑问:前面在execute()方法中不是已经判断过了吗,只有线程池当前线程数目小于核心池大小才会执行addIfUnderCorePoolSize方法的,为什么这地方还要继续判断?缘由很简单,前面的判断过程当中并无加锁,所以可能在execute方法判断的时候poolSize小于corePoolSize,而判断完以后,在其余线程中又向线程池提交了任务,就可能致使poolSize不小于corePoolSize了,因此须要在这个地方继续判断。而后接着判断线程池的状态是否为RUNNING,缘由也很简单,由于有可能在其余线程中调用了shutdown或者shutdownNow方法。而后就是执行
1
t = addThread(firstTask);
这个方法也很是关键,传进去的参数为提交的任务,返回值为Thread类型。而后接着在下面判断t是否为空,为空则代表建立线程失败(即poolSize>=corePoolSize或者runState不等于RUNNING),不然调用t.start()方法启动线程。
咱们来看一下addThread方法的实现:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w); //建立一个线程,执行任务
if (t != null) {
w.thread = t; //将建立的线程的引用赋值为w的成员变量
workers.add(w);
int nt = ++poolSize; //当前线程数加1
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
在addThread方法中,首先用提交的任务建立了一个Worker对象,而后调用线程工厂threadFactory建立了一个新的线程t,而后将线程t的引用赋值给了Worker对象的成员变量thread,接着经过workers.add(w)将Worker对象添加到工做集当中。
下面咱们看一下Worker类的实现:
private final class Worker implements Runnable {
private final ReentrantLock runLock = new ReentrantLock();
private Runnable firstTask;
volatile long completedTasks;
Thread thread;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
boolean isActive() {
return runLock.isLocked();
}
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) {
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
void interruptNow() {
thread.interrupt();
}
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
if (runState < STOP &&
Thread.interrupted() &&
runState >= STOP)
boolean ran = false;
beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户能够根据
//本身须要重载这个方法和后面的afterExecute方法来进行一些统计信息,好比某个任务的执行时间等
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this); //当任务队列中没有任务时,进行清理工做
}
}
}
它实际上实现了Runnable接口,所以上面的Thread t = threadFactory.newThread(w);效果跟下面这句的效果基本同样:
1
Thread t = new Thread(w);
至关于传进去了一个Runnable任务,在线程t中执行这个Runnable。
既然Worker实现了Runnable接口,那么天然最核心的方法即是run()方法了:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
从run方法的实现能够看出,它首先执行的是经过构造器传进来的任务firstTask,在调用runTask()执行完firstTask以后,在while循环里面不断经过getTask()去取新的任务来执行,那么去哪里取呢?天然是从任务缓存队列里面去取,getTask是ThreadPoolExecutor类中的方法,并非Worker类中的方法,下面是getTask方法的实现:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //若是线程数大于核心池大小或者容许为核心池线程设置空闲时间,
//则经过poll取任务,若等待必定的时间取不到任务,则返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) { //若是没取到任务,即r为null,则判断当前的worker是否能够退出
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers(); //中断处于空闲状态的worker
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
在getTask中,先判断当前线程池状态,若是runState大于SHUTDOWN(即为STOP或者TERMINATED),则直接返回null。
若是runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。
若是当前线程池的线程数大于核心池大小corePoolSize或者容许为核心池中的线程设置空闲存活时间,则调用poll(time,timeUnit)来取任务,这个方法会等待必定的时间,若是取不到任务就返回null。
而后判断取到的任务r是否为null,为null则经过调用workerCanExit()方法来判断当前worker是否能够退出,咱们看一下workerCanExit()的实现:
private boolean workerCanExit() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
boolean canExit;
//若是runState大于等于STOP,或者任务缓存队列为空了
//或者 容许为核心池线程设置空闲存活时间而且线程池中的线程数目大于1
try {
canExit = runState >= STOP ||
workQueue.isEmpty() ||
(allowCoreThreadTimeOut &&
poolSize > Math.max(1, corePoolSize));
} finally {
mainLock.unlock();
}
return canExit;
}
也就是说若是线程池处于STOP状态、或者任务队列已为空或者容许为核心池线程设置空闲存活时间而且线程数大于1时,容许worker退出。若是容许worker退出,则调用interruptIdleWorkers()中断处于空闲状态的worker,咱们看一下interruptIdleWorkers()的实现:
void interruptIdleWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) //实际上调用的是worker的interruptIfIdle()方法
w.interruptIfIdle();
} finally {
mainLock.unlock();
}
}
从实现能够看出,它实际上调用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) { //注意这里,是调用tryLock()来获取锁的,由于若是当前worker正在执行任务,锁已经被获取了,是没法获取到锁的
//若是成功获取了锁,说明当前worker处于空闲状态
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
这里有一个很是巧妙的设计方式,假如咱们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。可是在这里,并无采用这样的方式,
由于这样会要额外地对任务分派线程进行管理,无形地会增长难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。
咱们再看addIfUnderMaximumPoolSize方法的实现,这个方法的实现思想和addIfUnderCorePoolSize方法的实现思想很是类似,惟一的区别在于addIfUnderMaximumPoolSize方法是在线程池中的线程数达到了核心池大小而且往任务队列中添加任务失败的状况下执行的:
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < maximumPoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
看到没有,其实它和addIfUnderCorePoolSize方法的实现基本如出一辙,只是if语句判断条件中的poolSize < maximumPoolSize不一样而已。
到这里,大部分朋友应该对任务提交给线程池以后到被执行的整个过程有了一个基本的了解,下面总结一下:
1)首先,要清楚corePoolSize和maximumPoolSize的含义;
2)其次,要知道Worker是用来起到什么做用的;
3)要知道任务提交给线程池以后的处理策略,这里总结一下主要有4点:
若是当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会建立一个线程去执行这个任务;
若是当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(通常来讲是任务缓存队列已满),则会尝试建立新的线程去执行这个任务;
若是当前线程池中的线程数目达到maximumPoolSize,则会采起任务拒绝策略进行处理;
若是线程池中的线程数量大于corePoolSize时,若是某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;若是容许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
3.线程池中的线程初始化
默认状况下,建立线程池以后,线程池中是没有线程的,须要提交任务以后才会建立线程。
在实际中若是须要线程池建立以后当即建立线程,能够经过如下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化全部核心线程
下面是这2个方法的实现:
public boolean prestartCoreThread() {
return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
public int prestartAllCoreThreads() {
int n = 0;
while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
++n;
return n;
}
注意上面传进去的参数是null,根据第2小节的分析可知若是传进去的参数为null,则最后执行线程会阻塞在getTask方法中的
1
r = workQueue.take();
即等待任务队列中有任务。
4.任务缓存队列及排队策略
在前面咱们屡次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue<Runnable>,一般能够取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列建立时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,若是建立时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
5.任务拒绝策略
当线程池的任务缓存队列已满而且线程池中的线程数目达到maximumPoolSize,若是还有任务到来就会采起任务拒绝策略,一般有如下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,可是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
6.线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会当即终止线程池,而是要等全部任务缓存队列中的任务都执行完后才终止,但不再会接受新的任务
shutdownNow():当即终止线程池,并尝试打断正在执行的任务,而且清空任务缓存队列,返回还没有执行的任务
7.线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能建立的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能当即建立新的线程来执行任务。
三.使用示例
前面咱们讨论了关于线程池的实现原理,这一节咱们来看一下它的具体使用:
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
执行结果:
复制代码
正在执行task 0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 14
task 3执行完毕
task 0执行完毕
task 2执行完毕
task 1执行完毕
正在执行task 8
正在执行task 7
正在执行task 6
正在执行task 5
task 4执行完毕
task 10执行完毕
task 11执行完毕
task 13执行完毕
task 12执行完毕
正在执行task 9
task 14执行完毕
task 8执行完毕
task 5执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕
复制代码
从执行结果能够看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了以后,便建立新的线程。若是上面程序中,将for循环中改为执行20个任务,就会抛出任务拒绝异常了。
不过在java doc中,并不提倡咱们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来建立线程池:
Executors.newCachedThreadPool(); //建立一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //建立容量为1的缓冲池
Executors.newFixedThreadPool(int); //建立固定容量大小的缓冲池
下面是这三个静态方法的具体实现;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从它们的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。
newFixedThreadPool建立的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;
newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就建立线程运行,当线程空闲超过60秒,就销毁线程。
实际中,若是Executors提供的三个静态方法能知足要求,就尽可能使用它提供的三个方法,由于本身去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。
另外,若是ThreadPoolExecutor达不到要求,能够本身继承ThreadPoolExecutor类进行重写。
四.如何合理配置线程池的大小
本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。
通常须要根据任务的类型来配置线程池大小:
若是是CPU密集型任务,就须要尽可能压榨CPU,参考值能够设为 NCPU+1
若是是IO密集型任务,参考值能够设置为2*NCPU
固然,这只是一个参考值,具体的设置还须要根据实际状况进行调整,好比能够先将线程池大小设置为参考值,再观察任务运行状况和系统负载、资源利用率来进行适当调整。
参考资料:
http://ifeve.com/java-threadpool/
http://blog.163.com/among_1985/blog/static/275005232012618849266/
http://developer.51cto.com/art/201203/321885.htm
http://blog.csdn.net/java2000_wl/article/details/22097059
http://blog.csdn.net/cutesource/article/details/6061229
http://blog.csdn.net/xieyuooo/article/details/8718741
《JDK API 1.6》
4.java的堆与栈
4.1 java的内存分配策略
【静态存储区(方法区)】:主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,而且在程序整个运行期间都存在。
【栈区】:当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上建立,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。由于栈内存分配运算内置于处理器的指令集中,效率很高,可是分配的内存容量有限。
【堆区】: 又称动态内存分配,一般就是指在程序运行时直接 new 出来的对象和数组,也就是对象的实例。这部份内存在不使用时将会由 Java 垃圾回收器来负责回收。
4.2 栈和堆的区别
1、概述
在Java中,内存分为两种,一种是栈内存,另外一种就是堆内存。
2、堆内存
1.什么是堆内存?
堆内存是是Java内存中的一种,它的做用是用于存储Java中的对象和数组,当咱们new一个对象或者建立一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。
2.堆内存的特色是什么?
第一点:堆其实能够相似的看作是管道,或者说是平时去排队买票的的状况差很少,因此堆内存的特色就是:先进先出,后进后出,也就是你先排队,好,你先买票。
第二点:堆能够动态地分配内存大小,生存期也没必要事先告诉编译器,由于它是在运行时动态分配内存的,但缺点是,因为要在运行时动态分配内存,存取速度较慢。
3.new对象在堆中如何分配?
由Java虚拟机的自动垃圾回收器来管理
3、栈内存
1.什么是栈内存
栈内存是Java的另外一种内存,主要是用来执行程序用的,好比:基本类型的变量和对象的引用变量
2.栈内存的特色
第一点:栈内存就好像一个矿泉水瓶,像里面放入东西,那么先放入的沉入底部,因此它的特色是:先进后出,后进先出
第二点:存取速度比堆要快,仅次于寄存器,栈数据能够共享,但缺点是,存在栈中的数据大小与生存期必须是肯定的,缺少灵活性
3.栈内存分配机制
栈内存能够称为一级缓存,由垃圾回收器自动回收
4.数据共享
例子:
int a = 3;
int b = 3;
第一步处理:
1.编译器先处理int a = 3;
2.建立变量a的引用
3.在栈中查找是否有3这个值
4.没有找到,将3存放,a指向3
第二步处理:
1.处理b=3
2.建立变量b的引用
3.找到,直接赋值
第三步改变:
接下来
a = 4;
同上方法
a的值改变,a指向4,b的值是不会发生改变的
PS:若是是两个对象的话,那就不同了,对象指向的是同一个引用,一个发生改变,另外一个也会发生改变
4、栈和堆的区别
JVM是基于堆栈的虚拟机.JVM为每一个新建立的线程都分配一个堆栈.也就是说,对于一个Java程序来讲,它的运行就是经过对堆栈的操做来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操做:以帧为单位的压栈和出栈操做。
差别
1.堆内存用来存放由new建立的对象和数组
2.栈内存用来存放方法或者局部变量等
3.堆是先进先出,后进后出
4.栈是后进先出,先进后出
相同
1.都是属于Java内存的一种
2.系统都会自动去回收它,可是对于堆内存通常开发人员会自动回收它
4.3 java的内存管理
Java的内存管理就是对象的分配和释放问题。
在 Java 中,程序员须要经过关键字 new 为每一个对象申请内存空间 (基本类型除外),全部的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。
在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工做。
但同时,它也加剧了JVM的工做。这也是 Java 程序运行速度较慢的缘由之一。
由于,GC 为了可以正确释放对象,GC 必须监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都须要进行监控。
监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象再也不被引用。
为了更好理解 GC 的工做原理,咱们能够将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。
另外,每一个线程对象能够做为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。
在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。若是某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),
那么咱们认为这个(这些)对象再也不被引用,能够被 GC 回收。如下,咱们举一个例子说明如何用有向图表示内存管理。
对于程序的每个时刻,咱们都有一个有向图表示JVM的内存分配状况。如下右图,就是左边程序运行到第6行的示意图。

4.4 java的内存泄露
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特色,
首先,这些对象是可达的,即在有向图中,存在通路能够与其相连;
其次,这些对象是无用的,即程序之后不会再使用这些对象。
若是对象知足这两个条件,这些对象就能够断定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

【java内存泄露的根本缘由】 长生命周期的对象持有短生命周期的对象的引用,短生命周期的引用已经无用了,可是仍然被持有而没法释放;
5.IO相关-Socket
5.1 网络的基本认识
【IP地址】一个ip地址每每对应了一个服务器,或者说一台主机。
【端口号】区分计算机上的软件 至关于房门两个字节0~65535 共65536个(2 的16次方)
逻辑端口是指逻辑意义上用于区分服务的端口,如TCP/IP协议中的服务端口,端口号的范围从0到65535,好比用于浏览网页服务的80端口,用于FTP服务的21端口等。
端口有什么用呢?
咱们知道,一台拥有IP地址的
主机能够提供许多服务,好比Web服务、FTP服务、SMTP服务等,这些服务彻底能够经过1个IP地址来实现。那么,
主机是怎样区分不一样的网络服务呢?
显然不能只靠IP地址,由于IP 地址与网络服务的关系是一对多的关系。其实是经过“IP地址+端口号”来区 分不一样的服务的。
服务器通常都是经过知名端口号来识别的。例如,对于每一个TCP/IP实现来讲,
FTP服务器的TCP端口号都是21,每一个Telnet服务器的TCP端口号都是23,每一个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
任何TCP/IP实现所提供的服务都用知名的1~1023之间的端口号。这些知名端口号由Internet号分配机构(InternetAssignedNumbersAuthority,IANA)来管理。
到1992年为止,知名端口号介于1~255之间。256~1023之间的端口号一般都是由Unix系统占用,以提供一些特定的Unix服务—也就是说,提供一些只有Unix系统才有的、而其余
操做系统可能不提供的服务,IANA管理1~1023之间全部的端口号。
Internet扩展服务与Unix特定服务之间的一个差异就是Telnet和Rlogin。它们两者都容许经过计算机网络登陆到其余
主机上。Telnet是采用端口号为23的TCP/IP标准且几乎能够在全部
操做系统上进行实现。Rlogin只是为Unix系统设计的(尽管许多非Unix系统也提供该服务),它的有名端口号为513。
客户端一般对它所使用的端口号并不关心,只需保证该端口号在本机上是惟一的就能够了。
客户端口号又称做临时端口号(即存在时间很短暂)。这是由于它一般只是在用户运行该客户程序时才存在,而服务器则只要
主机开着的,其服务就运行。
大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。大于5000的端口号是为其余服务器预留的(Internet上并不经常使用的服务)。咱们能够在后面看见许多这样的给临时端口分配端口号的例子。
Solaris2.2是一个颇有名的例外。一般TCP和UDP的缺省临时端口号从32768开始。
逻辑端口是逻辑上用于区分服务的端口。TCP/IP协议中的端口就是逻辑端口,经过不一样的逻辑端口来区分不一样的服务。一个IP地址的端口经过16bit进行编号,最多能够有65536个端口。端口是经过端口号来标记的,端口号只有整数,范围是从0 到65535。
TCP与UDP段结构中端口地址都是16比特,能够有在0---65535范围内的端口号。对于这65536个端口号有如下的使用规定:
(1)端口号小于256的定义为经常使用端口,服务器通常都是经过经常使用端口号来识别的。任何TCP/IP实现所提供的服务都用1---1023之间的端口号,是由ICANN来管理的;
(2)客户端只需保证该端口号在本机上是唯一的就能够了。客户端口号因存在时间很短暂又称临时端口号;
(3)大多数TCP/IP实现给临时端口号分配1024---5000之间的端口号。大于5000的端口号是为其余服务器预留的。
【域名】域名是方便记忆的网址,
在访问这些网址的时候会经过两种方法转换成IP地址才能链接到相应主机访问相应的文件
【方法1】是经过,本机所特有的hosts文件,这个文件中定义了一些网址域名所对应的IP地址,当hosts文件中不存在这样的对应关系时,就会采起
【方法2】另外一种万能的方法,就是向不属于本机的DNS服务器发送请求,DNS服务器进行解析变成IP地址返回,再访问IP地址,从而达到目的。
【主机】主机一台电脑,或者说是一台有本身独立IP地址的电脑,注意这里电脑能够是笔记本,台式,甚至巨型计算机。
【tcp/udp协议】
UDP通信协议的特色:
1. 将数据极封装为数据包,面向无链接。
2. 每一个数据包大小限制在64K中
3.由于无链接,因此不可靠
4. 由于不须要创建链接,因此速度快
5.udp 通信是不分服务端与客户端的,只分发送端与接收端。
还有TCP的特色以下:
TCP通信协议特色:
1. tcp是基于IO流进行数据 的传输的,面向链接。
2. tcp进行数据传输的时候是没有大小限制的。
3. tcp是面向链接,经过三次握手的机制保证数据的完整性。 可靠协议。
4. tcp是面向链接的,因此速度慢。
5. tcp是区分客户端与服务端 的。
顺便附上咱们在利用JAVA写两个协议的过程:
UDP:
发送端的使用步骤:
1. 创建udp的服务。
2. 准备数据,把数据封装到数据包中发送。 发送端的数据包要带上ip地址与端口号。
3. 调用udp的服务,发送数据。
4. 关闭资源。
接收端的使用步骤:
1. 创建udp的服务
2. 准备空 的数据 包接收数据。
3. 调用udp的服务接收数据。
4. 关闭资源
TCP:
tcp的客户端使用步骤:
1. 创建tcp的客户端服务。
2. 获取到对应的流对象。
3.写出或读取数据
4. 关闭资源。
ServerSocket的使用 步骤:
1. 创建tcp服务端 的服务。
2. 接受客户端的链接产生一个Socket.
3. 获取对应的流对象读取或者写出数据。
4. 关闭资源。
【url】统一资源定位符是对能够从互联网上获得的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。
互联网上的每一个文件都有一个惟一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
【InternetAddress】
InetAddress(IP类)
经常使用方法:
getLocalHost();//获取本机的IP地址对象
getHostAddress(); //返回一个IP地址的字符串表示形式
getHostName();//获取主机名
getByName("IP或者主机名");//根据IP地址的字符串形式或者主机名生成一个IP地址对象----很经常使用
UDP:
//发送端代码
public class Sender{
public static void main(String[] args)
{
//创建udp服务
DatagramSocket datagramSocket = new DatagramSocket();
String data = @"upd的数据";
//把数据封装到数据包中 DatagramPacket(buf, length, address) buf:发送的数据内容 length:发送数据内容的大小 address:发送目的IP地址对象 port:端口号
DatagramPacket packet = new DatagramPacket(data.getBytes(),data.getBytes().length,InetAddress.getLocalHost(),9091);
//发送数据包
datagramSocket.send(packet);
//关闭资源
datagramSocket.close();
}
}
----------
//接收端代码
public class Receive {
public static void main(String[] args)
{
//创建udp服务,而且监听一个端口
DatagramSocket socket = new DatagramSocket(9091);
//准备空的数据包用于存放数据
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,buf.length);
//接收数据 数据实际上存储在buf数组中 receive是一个阻塞型方法,没有接收到数据就一直阻塞
socket.receive(packet);
//取出数据
String data = new String(buf,0,packet.getLength());//getLength() 获取数据包存储了几个字节
//关闭资源
socket.close();
}
}
TCP:
1 //客户端代码
2 public class Client{
3
4 public static void main(String[] args)
5 {
6 //创建链接
7 Socket socket = new Socket(InetAddress.getLocalHost(),9090);
8 //获取输出流
9 OutputStream os = socket.getOutPutStream();
10 //向服务端发送数据
11 os.write("这是客户端的数据".getBytes());
12 //关闭资源
13 socket.close();//由于os是从socket中获取的,因此socket关闭的时候os也关闭了
14 }
15 }
16
17
18 ----------
19
20
21 //服务端代码
22 public class Server{
23
24 public static void main(String[] args)
25 {
26 //创建tcp的服务端,而且监听一个端口
27 ServerSocket serverSocket = new ServerSocket(9090);
28 //接受客户端的链接
29 Socket socket = serverSocket.accept();//accept() 这是一个阻塞型的方法,没有客户端与其链接时,会一直等待下去
30 //获取输入流对象,读取客户端发送的内容
31 InputStream is = socket.getInputStream();
32 byte[] buf = new byte[1024];
33 int length = is.read(buf);
34 System.out.println("接收到的数据:"+new String(buf,0,length));
35 serverSocket.close();//由于socket是从serverSocket中获取的,因此serverSocket关闭的时候socket也关闭了
36 }
37 }
5.2 建立socket的实例

主机 A 的应用程序要能和主机 B 的应用程序通讯,必须经过 Socket 创建链接,而创建 Socket 链接必须须要底层 TCP/IP 协议来创建 TCP 链接。
创建 TCP 链接须要底层 IP 协议来寻址网络中的主机。
咱们知道网络层使用的 IP 协议能够帮助咱们根据 IP 地址来找到目标主机,可是一台主机上可能运行着多个应用程序,
如何才能与指定的应用程序通讯就要经过 TCP 或 UPD 的地址也就是端口号来指定。
这样就能够经过一个 Socket 实例惟一表明一个主机上的一个应用程序的通讯链路了。


创建通讯链路
当客户端要与服务端通讯,客户端首先要建立一个 Socket 实例,操做系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并建立一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个链接关闭。在建立 Socket 实例的构造函数正确返回以前,将要进行 TCP 的三次握手协议,TCP 握手协议完成后,Socket 实例对象将建立完成,不然将抛出 IOException 错误。、


与之对应的服务端将建立一个 ServerSocket 实例,ServerSocket 建立比较简单只要指定的端口号没有被占用,通常实例建立都会成功,同时操做系统也会为 ServerSocket 实例建立一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,一般状况下都是“*”即监听全部地址。以后当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个链接建立一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新建立的数据结构将会关联到 ServerSocket 实例的一个未完成的链接数据结构列表中,注意这时服务端与之对应的 Socket 实例并无完成建立,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。因此 ServerSocket 所关联的列表中每一个数据结构,都表明与一个客户端的创建的 TCP 链接。
数据传输
传输数据是咱们创建链接的主要目的,如何经过 Socket 传输数据,下面将详细介绍。
当链接已经创建成功,服务端和客户端都会拥有一个 Socket 实例,每一个 Socket 实例都有一个 InputStream 和 OutputStream,正是经过这两个对象来交换数据。同时咱们也知道网络 I/O 都是以字节流传输的。当 Socket 对象建立时,操做系统将会为 InputStream 和 OutputStream 分别分配必定大小的缓冲区,数据的写入和读取都是经过这个缓存区完成的。写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另外一端 InputStream 的 RecvQ 队列中,若是这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。值得特别注意的是,这个缓存区的大小以及写入端的速度和读取端的速度很是影响这个链接的数据传输效率,因为可能会发生阻塞,因此网络 I/O 与磁盘 I/O 在数据的写入和读取还要有一个协调的过程,若是两边同时传送数据时可能会产生死锁,在后面 NIO 部分将介绍避免这种状况。

6.java的io接口

6.1 阻塞IO
BIO 带来的挑战

咱们知道阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;
一样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端链接才会返回,
每一个客户端链接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通讯模型示意图以下:
若是你细细分析,必定会发现阻塞I/O存在一些缺点。根据阻塞I/O通讯模型,总结了它的两点缺点:
1. 当客户端多时,会
建立大量的处理线程。且
每一个线程都要占用栈空间和一些CPU时间
2.
阻塞可能带来频繁的上下文切换,且大部分上下文切换多是无心义的。
【什么是上下文?】
对于代码中某个值来讲,上下文是指这个值所在的局部(全局)做用域对象。
相对于进程而言,上下文就是进程执行时的环境,具体来讲就是各个变量和数据,包括全部的寄存器变量、进程打开的文件、内存(堆栈)信息等。
阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;
一样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端链接才会返回,每一个客户端链接过来后,服务端都会启动一个线程去处理该客户端的请求

BIO即阻塞I/O,无论是磁盘I/O仍是网络I/O,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,一旦有阻塞,线程将会失去CPU的使用权,这在当前的大规模访问量和有性能要求的状况下是不能被接受的。虽然当前的网络I/O有一些解决办法,如一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其余线程工做,还有为了减小系统线程的开销,采用线程池的办法来减小线程建立和回收的成本,可是有一些使用场景下仍然是没法解决的。如当前一些须要大量HTTP长链接的状况,像淘宝如今使用的Web旺旺,服务端须要同时保持几百万的HTTP链接,但并非每时每刻这些链接都在传输数据,这种状况下不可能同时建立这么多线程来保持链接。即便线程的数量不是问题,仍然有一些问题是没法避免的,好比咱们想给某些客户端更高的服务优先级,很难经过设计线程的优先级来完成。另一种状况是,每一个客户端的请求在服务端可能须要访问一些竞争资源,这些客户端在不一样线程中,所以须要同步,要实现这种同步操做远比用单线程复杂得多。以上这些状况都说明,咱们须要另一种新的I/O操做方式
6.2 NIO
工做原理:
1. 由一个专门的线程来处理全部的 IO 事件,并负责分发。
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3. 线程通信:线程之间经过 wait,notify 等方式通信。保证每次上下文切换都是有意义的。减小无谓的线程切换。
Java NIO的服务端只需启动一个专门的线程来处理全部的 IO 事件,这种通讯模型是怎么实现的呢?

java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上能够注册咱们感兴趣的事件。一共有如下四种事件:
事件名 |
对应值 |
服务端接收客户端链接事件 |
SelectionKey.OP_ACCEPT(16) |
客户端链接服务端事件 |
SelectionKey.OP_CONNECT(8) |
读事件 |
SelectionKey.OP_READ(1) |
写事件 |
SelectionKey.OP_WRITE(4) |
服务端和客户端各自维护一个管理通道的对象,咱们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。咱们以服务端为例,若是服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法 阻塞地读取数据,
而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,若是访问selector时发 现有感兴趣的事件到达,则处理这些事件,若是没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
1 package com.oztaking.www.myapplication;
2
3
4 import java.io.IOException;
5 import java.net.InetSocketAddress;
6 import java.nio.ByteBuffer;
7 import java.nio.channels.SelectionKey;
8 import java.nio.channels.Selector;
9 import java.nio.channels.ServerSocketChannel;
10 import java.nio.channels.SocketChannel;
11 import java.util.Iterator;
12
13 /**
14 * NIO服务端 17 */
18 public class NIOServer {
19 //通道管理器
20 private Selector selector;
21
22 /**
23 * 得到一个ServerSocket通道,并对该通道作一些初始化的工做
24 *
25 * @param port 绑定的端口号
26 * @throws IOException
27 */
28 public void initServer(int port) throws IOException {
29 // 得到一个ServerSocket通道
30 ServerSocketChannel serverChannel = ServerSocketChannel.open();
31 // 设置通道为非阻塞
32 serverChannel.configureBlocking(false);
33 // 将该通道对应的ServerSocket绑定到port端口
34 serverChannel.socket().bind(new InetSocketAddress(port));
35 // 得到一个通道管理器
36 this.selector = Selector.open();
37 //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
38 //当该事件到达时,selector.select()会返回,若是该事件没到达selector.select()会一直阻塞。
39 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
40 }
41
42 /**
43 * 采用轮询的方式监听selector上是否有须要处理的事件,若是有,则进行处理
44 *
45 * @throws IOException
46 */
47 @SuppressWarnings("unchecked")
48 public void listen() throws IOException {
49 System.out.println("服务端启动成功!");
50 // 轮询访问selector
51 while (true) {
52 //当注册的事件到达时,方法返回;不然,该方法会一直阻塞
53 selector.select();
54 // 得到selector中选中的项的迭代器,选中的项为注册的事件
55 Iterator ite = this.selector.selectedKeys().iterator();
56 while (ite.hasNext()) {
57 SelectionKey key = (SelectionKey) ite.next();
58 // 删除已选的key,以防重复处理
59 ite.remove();
60 // 客户端请求链接事件
61 if (key.isAcceptable()) {
62 ServerSocketChannel server = (ServerSocketChannel) key
63 .channel();
64 // 得到和客户端链接的通道
65 SocketChannel channel = server.accept();
66 // 设置成非阻塞
67 channel.configureBlocking(false);
68
69 //在这里能够给客户端发送信息哦
70 channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
71 //在和客户端链接成功以后,为了能够接收到客户端的信息,须要给通道设置读的权限。
72 channel.register(this.selector, SelectionKey.OP_READ);
73
74 // 得到了可读的事件
75 } else if (key.isReadable()) {
76 read(key);
77 }
78
79 }
81 }
82 }
83
84 /**
85 * 处理读取客户端发来的信息 的事件
86 *
87 * @param key
88 * @throws IOException
89 */
90 public void read(SelectionKey key) throws IOException {
91 // 服务器可读取消息:获得事件发生的Socket通道
92 SocketChannel channel = (SocketChannel) key.channel();
93 // 建立读取的缓冲区
94 ByteBuffer buffer = ByteBuffer.allocate(10);
95 channel.read(buffer);
96 byte[] data = buffer.array();
97 String msg = new String(data).trim();
98 System.out.println("服务端收到信息:" + msg);
99 ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
100 channel.write(outBuffer);// 将消息回送给客户端
101 }
102
103 /**
104 * 启动服务端测试
105 *
106 * @throws IOException
107 */
108 public static void main(String[] args) throws IOException {
109 NIOServer server = new NIOServer();
110 server.initServer(8000);
111 server.listen();
112 }
113
114 }
115
118
119 客户端:
120
121 package cn.nio;
122
123 import java.io.IOException;
124 import java.net.InetSocketAddress;
125 import java.nio.ByteBuffer;
126 import java.nio.channels.SelectionKey;
127 import java.nio.channels.Selector;
128 import java.nio.channels.SocketChannel;
129 import java.util.Iterator;
130
131 /**
132 * NIO客户端
133 *135 */
136 public class NIOClient {
137 //通道管理器
138 private Selector selector;
139
140 /**
141 * 得到一个Socket通道,并对该通道作一些初始化的工做
142 *
143 * @param ip 链接的服务器的ip
144 * @param port 链接的服务器的端口号
145 * @throws IOException
146 */
147 public void initClient(String ip, int port) throws IOException {
148 // 得到一个Socket通道
149 SocketChannel channel = SocketChannel.open();
150 // 设置通道为非阻塞
151 channel.configureBlocking(false);
152 // 得到一个通道管理器
153 this.selector = Selector.open();
154
155 // 客户端链接服务器,其实方法执行并无实现链接,须要在listen()方法中调
156 //用channel.finishConnect();才能完成链接
157 channel.connect(new InetSocketAddress(ip, port));
158 //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
159 channel.register(selector, SelectionKey.OP_CONNECT);
160 }
161
162 /**
163 * 采用轮询的方式监听selector上是否有须要处理的事件,若是有,则进行处理
164 *
165 * @throws IOException
166 */
167 @SuppressWarnings("unchecked")
168 public void listen() throws IOException {
169 // 轮询访问selector
170 while (true) {
171 selector.select();
172 // 得到selector中选中的项的迭代器
173 Iterator ite = this.selector.selectedKeys().iterator();
174 while (ite.hasNext()) {
175 SelectionKey key = (SelectionKey) ite.next();
176 // 删除已选的key,以防重复处理
177 ite.remove();
178 // 链接事件发生
179 if (key.isConnectable()) {
180 SocketChannel channel = (SocketChannel) key
181 .channel();
182 // 若是正在链接,则完成链接
183 if (channel.isConnectionPending()) {
184 channel.finishConnect();
185
186 }
187 // 设置成非阻塞
188 channel.configureBlocking(false);
189
190 //在这里能够给服务端发送信息哦
191 channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
192 //在和服务端链接成功以后,为了能够接收到服务端的信息,须要给通道设置读的权限。
193 channel.register(this.selector, SelectionKey.OP_READ);
194
195 // 得到了可读的事件
196 } else if (key.isReadable()) {
197 read(key);
198 }
199
200 }
201
202 }
203 }
204
205 /**
206 * 处理读取服务端发来的信息 的事件
207 *
208 * @param key
209 * @throws IOException
210 */
211 public void read(SelectionKey key) throws IOException {
212 //和服务端的read方法同样
213 }
214
215
216 /**
217 * 启动客户端测试
218 *
219 * @throws IOException
220 */
221 public static void main(String[] args) throws IOException {
222 NIOClient client = new NIOClient();
223 client.initClient("localhost", 8000);
224 client.listen();
225 }
226
227 }
7.异常相关
java 异常是程序运行过程当中出现的错误。Java把异常看成对象来处理,并定义一个基类java.lang.Throwable做为全部异常的超类。
在Java API中定义了许多异常类,分为两大类,错误Error和异常Exception。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常(非runtimeException),
也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

7.一、Error与Exception
Error是程序没法处理的错误,好比OutOfMemoryError、ThreadDeath等。
这些异常发生时,Java虚拟机(JVM)通常会选择线程终止。
Exception是程序自己能够处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽量去处理这些异常。
7.二、运行时异常和非运行时异常
运行时异常: 都是RuntimeException类及其子类异常:
IndexOutOfBoundsException索引越界异常
ArithmeticException:数学计算异常
NullPointerException:空指针异常
ArrayOutOfBoundsException:数组索引越界异常
ClassNotFoundException:类文件未找到异常
ClassCastException 类型转换异常
这些异常是不检查异常(Unchecked Exception),程序中能够选择捕获处理,也能够不处理。这些异常通常是由程序逻辑错误引发的。
非运行时异常:是RuntimeException之外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,若是不处理,程序就不能编译经过。如:
IOException、文件读写异常
FileNotFoundException:文件未找到异常
EOFException:读写文件尾异常
MalformedURLException:URL格式错误异常
SocketException:Socket异常
SQLException:SQL数据库异常
7.三、异常的捕获和处理
一、try···catch语句
在try代码块中抛出异常以后,当即转到catch代码块执行或者退栈到上一层方法处寻找catch代码块。
二、finally语句:任何状况下都必须执行的代码
因为异常会强制中断正常流程,这会使得某些无论在任何状况下都必须执行的步骤被忽略,从而影响程序的健壮性。
使用finally语句,无论try代码块中是否出现了异常,都会执行finally代码块。
在某些状况下,把finally的操做放在try···catch语句的后面,这也能保证这个操做被执行。
这种状况尽管在某些状况下是可行的,但不值得推荐,觉得它有两个缺点:
@把与try代码块相关的操做孤立开来,使程序结构松散,可读性差。
@影响程序的健壮性。假如catch代码块继续抛出异常,就不会执行catch代码块以后的操做。
三、throws子句:声明可能会出现的异常
若是一个方法可能会抛出异常,但没有能力来处理这种异常,能够在方法声明处用throws子句来声明抛出异常。
一个方法可能会出现多种异常,throws子句容许声明抛出多个异常,中间用“,”隔开。
异常声明是接口(概念上的接口)的一部分,在JavaDoc文档中应描述方法可能抛出某种异常的条件。根据异常声明,方法调用者了解到被调用方法可能抛出的异常,从而采起相应的措施:捕获异常,或者声明继续抛出异常。
四、throw语句:抛出异常
throw语句用于抛出异常。
值得注意的是,有throw语句抛出的对象必须是java.lang.Throwable类或者其余子类的实例。
7.4 异常调用处理的流程
Java虚拟机用方法调用栈(method invocation stack)来跟踪每一个线程中一系列的方法调用过程。
该堆栈保存了每一个调用方法的本地信息(好比方法的局部变量)。
每一个线程都有一个独立的方法调用栈。
对于Java应用程序的主线程,堆栈底部是程序的入口方法main()。
当一个新方法被调用时,Java虚拟机把描述该方法的栈结构置入栈顶,位于栈顶的方法为正在执行的方法。
当一个方法正常执行完毕,Java虚拟机会从调用栈中弹出该方法的栈结构,而后继续处理前一个方法。
若是在执行方法的过程当中抛出异常,则Java虚拟机必须找到能捕获该异常的catch代码块。
它首先查看当前方法是否存在这样的catch代码块,若是存在,那么就执行该catch代码块;
不然,Java虚拟机会从调用栈中弹出该方法的栈结构,继续到前一个方法中查找合适的catch代码块。
在回溯过程当中,若是Java虚拟机在某个方法中找到了处理该异常的代码块,则该方法的栈结构将成为栈顶元素,程序流程将转到该方法的异常处理代码部分继续执行。
当Java虚拟机追溯到调用栈的底部的方法时,若是仍然没有找处处理该异常的代码块,按如下步骤处理。
(1)调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息。
(2)若是该线程不是主线程,那么终止这个线程,其余线程继续正常运行。若是该线程是主线程(即方法调用栈的底部为main()方法),那么整个应用程序被终止。
7.5 异常的运行流程
异常流程有try···catch···finally语句来控制。若是程序中还包含了return和System.exit()语句,就会使流程变得更加复杂。
(1)finally语句不被执行的惟一状况就是先执行了用于终止程序的System.exit()方法。
java.lang.System类的静态方法exit()用于终止当前的Java虚拟机进程,Java虚拟机所执行的Java程序也随之终止。
另外,当catch语句中也抛出异常的状况下,在程序退栈寻找上一个方法的catch代码块以前,会先执行finally代码块。

(2)return语句用于退出本方法。在执行try或catch代码块中的return语句时,假若有finally代码块,会先执行finally代码块。

(3)finally代码块虽然在return语句以前就被执行(这里是指在return返回以前执行,若是return a+b;那么是先执行了a+b,再执行finally代码块,再返回),
但finally代码块不能经过从新给变量赋值的方式改变return语句的返回值。
在这里涉及到return语句的机制,若是return a;
a是基本类型的变量,这里能够理解的是传值,a的值赋给了一个不知名的变量,return将这个不知名的变量内容返回,因此finally语句只能更改a的内容,不能更改那个和a的值相同的不知名变量的值,因此return的结果不能够被finally中的代码改变;
可是若是a是引用类型的变量,这里就不是传值了而是传的引用,这样不知名的变量和a都指向了同一个对象,咱们能够经过引用a来改变这个对象,使得这个不知名变量所引用的对象发生改变,一样也不能改变这个不知名变量的内容,
它仍然指向这个对象,咱们不可让它指向其余对象或者变成null,由于咱们不知道这个不知名变量的名字。

(4)建议不要在finally代码块中使用return语句,觉得它会致使如下两种潜在的错误。
第一种错误是覆盖try或catch代码块的return语句。能够这样理解,在try或者catch中的return在把返回的结果赋给一个不知名的临时变量后,执行finally,若是没有finally里的return语句,接着回来将这个不知名变量的内容返回,若是在finally中出现了return语句,那么这个return语句没有被打断,给另外一个不知名变量赋值以后,直接返回了,方法退栈,try或catch里的返回没有别执行,这样的结果就是finally中的return覆盖了try和catch中的return语句。
第二种错误是丢失异常。若是catch代码块中有throw语句抛出异常,因为先执行了finally代码块,又由于finally代码块中有return语句,因此方法退栈,catch代码块中的throw语句就没有被执行。

7.6 常见的异常的面试题
【1】java中的检查型异常和非检查型异常有什么区别?(常考)
Java里面异常分为两大类:checkedexception(检查异常)和unchecked exception(未检查异常),
对于未检查异常也叫RuntimeException(运行时异常),对于运行时异常,java编译器不要求必定要把它捕获或者必定要继续抛出,
可是对checkedexception(检查异常)要求必需要在方法里面或者捕获或者继续抛出。
非检测异常抛出时,可不声明不使用try-catch语句;对于检测异常抛出时须声明且使用try-catch语句
Java的可检测异常和非检测异常泾渭分明。
可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则。
非检测异常不遵循处理或声明规则。
在产生此类异常时,不必定非要采起任何适当操做,编译器不会检查是否已解决了这样一个异常。有两个主要类定义非检测异常:RuntimeException和Error。
为何Error子类属于非检测异常?这是由于没法预知它们的产生时间。若Java应用程序内存不足,则随时可能出现OutOfMemoryError;原由通常不是应用程序中的特殊调用,而是JVM自身的问题。另外,Error类通常表示应用程序没法解决的严重问题,故将这些类视为非检测异常。
RuntimeException类也属于非检测异常,一个缘由是普通JVM操做引起的运行时异常随时可能发生。与Error不一样,此类异常通常由特定操做引起。但这些操做在Java应用程序中会频繁出现。例如,若每次使用对象时,都必须编写异常处理代码来检查null引用,
则整个应用程序很快将变成一个庞大的try-catch块。所以,运行时异常不受编译器检查与处理或声明规则的限制。
将RuntimeException类做为未检测异常还有一个缘由:它们表示的问题不必定做为异常处理。能够在try-catch结构中处理NullPointerException,但若在使用引用前测试空值,则更简单,更经济。一样,能够在除法运算时检查0值,而不使用ArithmeticException。
【2】throw和throws的区别
一、throws出如今方法头;而throw出如今方法体。
二、throws表示出现异常的一种可能性,并不必定会发生这些异常;throw则是抛出了异常,执行throw则必定抛出了某种异常对象。
三、二者都是消极处理异常的方式(这里的消极并非说这种方式很差),只是抛出或者可能抛出异常,可是不会由方法去处理异常,真正的处理异常由方法的上层调用处理。
好的编程习惯:
1.在写程序时,对可能会出现异常的部分一般要用try{...}catch{...}去捕捉它并对它进行处理;
2.用try{...}catch{...}捕捉了异常以后必定要对在catch{...}中对其进行处理,那怕是最简单的一句输出语句,或栈输入e.printStackTrace();
3.若是是捕捉IO输入输出流中的异常,必定要在try{...}catch{...}后加finally{...}把输入输出流关闭
4.若是方法体内用throw抛出了某种异常,最好要在方法名中加throws抛异常声明,而后交给调用它的上层函数进行处理。
【3】若是执行finally代码块以前方法返回告终果/JVM退出,finally代码块会执行吗?

【答】在执行finally代码块以前方法返回结果是会执行finally代码块的;
可是JVM退出则不会执行finally代码块;
finally代码块不被执行的三种状况:
【状况1】在try..代码块以外异常或者返回,不会执行finally代码块;
【状况2】在执行try代码块的时候退出了jvm虚拟机则不会执行finally代码块
【状况3】在子线程中执行try代码块忽然关闭了子线程;
【4】java中final finally finalize 关键字的区别?
一、final
Final能够用于成员变量(包括方法参数),方法、类。
Final成员
变量一旦被初始化便不可改变(对于基本类型,指的是值不变;对于对象类型,指的是引用不变),初始化只可能在两个地方:定义处和构造函数。
对于基本类型,定义成final参数没有什么意义,由于基本类型就是传值,不会影响调用语句中的变量;
对于对象类型,在方法中若是参数确认不须要改变时,定义成final参数能够防止方法中无心的修改而影响到调用方法。
Final方法
- 不可覆写
- 编译器将对此方法的调用转化成行内(inline)调用,即直接把方法主体插入到调用处(方法主体内容过多的时候反而会影响效率)
Final类
二、finally
异常处理关键字,finally中的主体总会执行,无论异常发生是否。
2.一、当try中有return时执行顺序
return语句并非函数的最终出口,若是有finally语句,这在return以后还会执行finally(return的值会暂存在栈里面,等待finally执行后再返回)
2.二、return和异常获取语句的位置
- 状况一(try中有return,finally中没有return)


“return num += 80”被拆分红了“num = num+80”和“return num”两个语句,线执行try中的“num =num+80”语句,将其保存起来,在try中的”return num“执行前,先将finally中的语句执行完,然后再将90返回。
- 状况二(try和finally中均有return)


try中的return被”覆盖“掉了,再也不执行。


虽然在finally中改变了返回值num,但由于finally中没有return该num的值,所以在执行完finally中的语句后,test()函数会获得try中返回的num的值,而try中的num的值依然是程序进入finally代码块前保留下来的值,所以获得的返回值为10。而且函数最后面的return语句不会执行。


6.2.三、简单地总结以下:
try语句在返回前,将其余全部的操做执行完,保留好要返回的值,然后转入执行finally中的语句,然后分为如下三种状况:
状况一:若是finally中有return语句,则会将try中的return语句”覆盖“掉,直接执行finally中的return语句,获得返回值,这样便没法获得try以前保留好的返回值。
状况二:若是finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回以前保留的值。
状况三:若是finally中没有return语句,可是改变了要返回的值,这里有点相似与引用传递和值传递的区别,分如下两种状况,:
- 若是return的数据是基本数据类型,则在finally中对该基本数据的改变不起做用,try中的return语句依然会返回进入finally块以前保留的值。
- 若是return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起做用,try中的return语句返回的就是在finally中改变后的该属性的值。
6.三、finalize
类的Finalize方法,能够告诉垃圾回收器应该执行的操做,该方法从Object类继承而来。
在从堆中永久删除对象以前,垃圾回收器调用该对象的Finalize方法。
finalize()是Object的protected方法,子类能够覆盖该方法以实现资源清理工做,GC在回收对象以前调用该方法
注意,没法确切地保证垃圾回收器什么时候调用该方法,也没法保证调用不一样对象的方法的顺序。
即便一个对象包含另外一个对象的引用,或者在释放一个对象好久之前就释放了另外一个对象,也可能会以任意的顺序调用这两个对象的Finalize方法。
若是必须保证采用特定的顺序,则必须提供本身的特有清理方法。
7.注解相关
7.1.什么是注解

常见的做用有如下几种:
1.生成文档。这是最多见的,也是java 最先提供的注解。经常使用的有@see @param @return 等;
2.跟踪代码依赖性,实现替代配置文件功能。比较常见的是spring 2.5 开始的基于注解配置。做用就是减小配置。如今的框架基本都使用了这种配置来减小配置文件的数量;
3.在编译时进行格式检查。如@Override放在方法前,若是你这个方法并非覆盖了超类方法,则编译时就能检查出;
包 java.lang.annotation 中包含全部定义自定义注解所需用到的原注解和接口。如接口 java.lang.annotation.Annotation 是全部注解继承的接口,而且是自动继承,不须要定义时指定,相似于全部类都自动继承Object。
该包同时定义了四个元注解,
Documented,
Inherited,
Target(做用范围,方法,属性,构造方法等),
Retention(生命范围,源代码,class,runtime)。
下面将在实例中逐个讲解他们的做用及使用方法。
Inherited : 在您定义注解后并使用于程序代码上时,预设上父类别中的注解并不会被继承至子类别中,您能够在定义注解时加上java.lang.annotation.
Inherited 限定的Annotation,这让您定义的Annotation型别被继承下来。注意注解继承只针对class 级别注解有效(这段建议看彻底文后在来回顾)。
多说无益,下面就一步步从零开始建一个咱们本身的注解。
自定义一个注解
package com.tmser.annotation;
public @interface TestA {
//这里定义了一个空的注解,它能干什么呢。我也不知道,但他能用。
}
在下面这个程序中使用它:
package com.tmser.annotation;
import java.util.HashMap;
import java.util.Map;
@TestA //使用了类注解
public class UserAnnotation {
@TestA //使用了类成员注解
private Integer age;
@TestA //使用了构造方法注解
public UserAnnotation(){
}
@TestA //使用了类方法注解
public void a(){
@TestA //使用了局部变量注解
Map m = new HashMap(0);
}
public void b(@TestA Integer a){ //使用了方法参数注解
}
}
编译没有报错,ok,一个注解实验完成。这个注解也太简单了吧,好像什么信息也不能传递。别急下面就来一步步完善它,也该四位元注解依次开始上场了。
四个元注解分别是:@Target,@Retention,@Documented,@Inherited ,再次强调下元注解是java API提供,是专门用来定义注解的注解,其做用分别以下:
@Target 表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括:
ElemenetType.CONSTRUCTOR----------------------------构造器声明
ElemenetType.FIELD --------------------------------------域声明(包括 enum 实例)
ElemenetType.LOCAL_VARIABLE------------------------- 局部变量声明
ElemenetType.METHOD ----------------------------------方法声明
ElemenetType.PACKAGE --------------------------------- 包声明
ElemenetType.PARAMETER ------------------------------参数声明
ElemenetType.TYPE--------------------------------------- 类,接口(包括注解类型)或enum声明
@Retention 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
RetentionPolicy.SOURCE ---------------------------------注解将被编译器丢弃
RetentionPolicy.CLASS -----------------------------------注解在class文件中可用,但会被VM丢弃
RetentionPolicy.RUNTIME VM-------将在运行期也保留注释,所以能够经过反射机制读取注解的信息。
@Documented 将此注解包含在 javadoc 中 ,它表明着此注解会被javadoc工具提取成文档。在doc文档中的内容会由于此注解的信息内容不一样而不一样。至关与@see,@param 等。
@Inherited 容许子类继承父类中的注解。
学习最忌好高骛远,最重要的仍是动手实践,咱们就一个一个来实验。
第一个:@Target,动手在前面咱们编写的注解上加上元注解。
package com.tmser.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.PACKAGE) //与前面定义的注解不一样的地方,这里使用了元注解Target
public @interface TestA {
}
ctrl+ s 保存,今天电脑比较给力,咱们的测试类那边立马出现了一堆错误,除了类注解。我想到这,聪明的你马上明白了这个元注解的意义了。是否是想固然的偷起懒来了。
?难道还有意外?细心的朋友应该发现了,咱们的测试类少了一个属性没用,就是ElemenetType.PACKAGE。在咱们的注解加上这个属性的元注解后,咱们测试程序的元注解所有阵亡,不对,还有一个没加呢,好加上。package 包,想固然是加载 package 前面。即
@TestA package com.tmser.annotation;
什么也报错。这就搞不明白了,不加在这加哪去呢。我也不知道了,不过这是编译错误,咱们的eclipse 将错误给咱们指出了,就是Package annotations must be in file package-info.java ,e 文虽然很差,
但这个简单的仍是难不倒几我的的,package 注解必须定义在 package-info.java 中。package-info 又是什么东西,好了为节省大家的时间帮你百度好了(在另外一篇个人另外一篇博文里面,本身找吧)。ok,到此 target 元注解就所有完成了。
第二个元注解: @Retention 参数 RetentionPolicy。有了前面的经验这个注解理解起来就简单多了,而且幸运的是这个注解尚未特殊的属性值。 简单演示下如何使用:
package com.tmser.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestA {
}
第三和第四个元注解就再也不举例了。比较简单,也没有值,相信看过上面的解释也就清楚了。下面咱们仍是继续来深刻的探讨下注解的使用。上面的例子都很是简单,注解连属性都没有。ok,下面咱们就来定义一个有属性的注解,并在例子程序中获取都注解中定义的值。
开始以前将下定义属性的规则:
@interface用来声明一个注解,其中的每个方法其实是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。能够经过default来声明参数的默认值。
代码:
@Target({TYPE,METHOD,FIELD,CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestA {
String name();
int id() default 0;
Class gid();
}
下面改下咱们的测试类:
package com.tmser.annotation;
import java.util.HashMap;
import java.util.Map;
@TestA(name="type",gid=Long.class) //类成员注解
public class UserAnnotation {
@TestA(name="param",id=1,gid=Long.class) //类成员注解
private Integer age;
@TestA (name="construct",id=2,gid=Long.class)//构造方法注解
public UserAnnotation(){
}
@TestA(name="public method",id=3,gid=Long.class) //类方法注解
public void a(){
Map m = new HashMap(0);
}
@TestA(name="protected method",id=4,gid=Long.class) //类方法注解
protected void b(){
Map m = new HashMap(0);
}
@TestA(name="private method",id=5,gid=Long.class) //类方法注解
private void c(){
Map m = new HashMap(0);
}
public void b(Integer a){
}
}
下面到了最重要的一步了,就是如何读取咱们在类中定义的注解。只要读取出来了使用的话就简单了。
package com.tmser.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ParseAnnotation {
public static void parseTypeAnnotation() throws ClassNotFoundException {
Class clazz = Class.forName("com.tmser.annotation.UserAnnotation");
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
TestA testA = (TestA)annotation;
System.out.println("id= ""+testA.id()+""; name= ""+testA.name()+""; gid = "+testA.gid());
}
}
public static void parseMethodAnnotation(){
Method[] methods = UserAnnotation.class.getDeclaredMethods();
for (Method method : methods) {
boolean hasAnnotation = method.isAnnotationPresent(TestA.class);
if (hasAnnotation) {
TestA annotation = method.getAnnotation(TestA.class);
System.out.println("method = " + method.getName()
+ " ; id = " + annotation.id() + " ; description = "
+ annotation.name() + "; gid= "+annotation.gid());
}
}
}
public static void parseConstructAnnotation(){
Constructor[] constructors = UserAnnotation.class.getConstructors();
for (Constructor constructor : constructors) {
boolean hasAnnotation = constructor.isAnnotationPresent(TestA.class);
if (hasAnnotation) {
TestA annotation =(TestA) constructor.getAnnotation(TestA.class);
System.out.println("constructor = " + constructor.getName()
+ " ; id = " + annotation.id() + " ; description = "
+ annotation.name() + "; gid= "+annotation.gid());
}
}
}
public static void main(String[] args) throws ClassNotFoundException {
parseTypeAnnotation();
parseMethodAnnotation();
parseConstructAnnotation();
}
}
先别说话,运行:
id= “0”; name= “type”; gid = class java.lang.Long method = c ; id = 5 ; description = private method; gid= class java.lang.Long method = a ; id = 3 ; description = public method; gid= class java.lang.Long method
= b ; id = 4 ; description = protected method; gid= class java.lang.Long constructor = com.tmser.annotation.UserAnnotation ; id
= 2 ; description = construct; gid= class java.lang.Long
看到了吧,咱们定义的注解都完整的输出了,你要使用哪一个,直接拿去用就行了。
为了避免让这篇文章打开太慢,我省略了类属性注解,及参数注解的解析。其实都大同小异。
另外,我也没有举使用例子。由于我认为好的教程是讲的详细的同时,还会留有扩展。若是我所有写出来,而你只是学习的话,那基本不会本身去动脑了,而是复制粘贴运行一遍完事。
最后提醒下:
要用好注解,必须熟悉java 的反射机制,从上面的例子能够看出,注解的解析彻底依赖于反射。
不要滥用注解。日常咱们编程过程不多接触和使用注解,只有作设计,且不想让设计有过多的配置时
这个网址能够给你参考一些注解的例子:http://blog.sina.com.cn/s/blog_7540bf5f0100t3mv.html
转自:http://blog.sina.com.cn/s/blog_93dc666c0101gzn5.html

元数据是指用来描述数据的数据,更通俗一点,就是描述代码间关系,或者代码与其余资源(例如数据库表)之间内在联系的数据。元数据的功能做用有不少,
好比:你可能用过Javadoc的注释自动生成文档。这就是元数据功能的一种。
总的来讲,元数据能够用来建立文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件。
在Java中元数据以标签的形式存在于Java代码中,元数据标签的存在并不影响程序代码的编译和执行,它只是被用来生成其它的文件或针在运行时知道被运行代码的描述信息。
综上所述:
第一,元数据以标签的形式存在于Java代码中。
第二,元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的。
第三,元数据须要编译器以外的工具额外的处理用来生成其它的程序部件。
第四,元数据能够只存在于Java源代码级别,也能够存在于编译以后的Class文件内部。
7.3.注解分类
1)系统内置标准注解:

a.Override,限定重写父类方法
@Override 是一个标记注解类型,它被用做标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的做用。
若是咱们使用了这种Annotation在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。
这个annotaton经常在咱们试图覆盖父类方法而确又写错了方法名时发挥威力。
使用方法极其简单:在使用此annotation时只要在被修饰的方法前面加上@Override便可。
这里咱们定义了一个父类father 里面定义了一个方法getName,定义了一个son类,里面复写了父类的getName方法并用Override,限定重写父类方法

b.@Deprecated,标记已过期:


同 样Deprecated也是一个标记注解。
当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。
并且这种修饰具备必定的 “延续性”:若是咱们在代码中经过继承或者覆盖的方式使用了这个过期的类型或者成员,虽然继承或者覆盖后的类型或者成员并非被声明为 @Deprecated,但编译器仍然要报警。
这里咱们看下代码:定义了一个ParentDeprecated,并把他标记为过期Deprecated,而后咱们就能发现ide工具在他的类名上划了一条横线,并且当我定义他的子类childDeprecated时,发如今她的类名前也会被代表过期。
最后,值得注意,@Deprecated这个annotation类型和javadoc中的 @deprecated这个tag是有区别的:前者是java编译器识别的,然后者是被javadoc工具所识别用来生成文档(包含程序成员为何已通过 时、它应当如何被禁止或者替代的描述)。
c.SuppressWarnnings,抑制编译器警告:

@SuppressWarnings 被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。在java5.0,sun提供的javac编译器为咱们提供了-Xlint选项来使编译器对合法的程序代码提出警告,此种警告从某种程度上表明了程序错误。例如当咱们使用一个generic collection类而又没有提供它的类型时,编译器将提示出"unchecked warning"的警告。一般当这种状况发生时,咱们就须要查找引发警告的代码。若是它真的表示错误,咱们就须要纠正它。例如若是警告信息代表咱们代码中的switch语句没有覆盖全部可能的case,那么咱们就应增长一个默认的case来避免这种警告。
有时咱们没法避免这种警告,例如,咱们使用必须和非generic的旧代码交互的generic collection类时,咱们不能避免这个unchecked warning。此时@SuppressWarning就要派上用场了,在调用的方法前增长@SuppressWarnings修饰,告诉编译器中止对此方法的警告。
SuppressWarnings注解的常见参数值的简单说明:
1.deprecation:使用了不同意使用的类或方法时的警告;
2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
当咱们不使用@SuppressWarnings注释时,编译器就会有以下提示:
注意:SuppressWarningsDemo.java 使用了未经检查或不安全的操做。
注意:要了解详细信息,请使用 -Xlint:unchecked 从新编译。
SuppressWarnings 被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告
Overload是重载的意思,Override是覆盖的意思,也就是重写。
重载Overload:在同一个类中,容许存在一个以上的同名函数,只要他们的参数个数或者参数类型不一样便可。
重载的特色:与返回值类型无关,只看参数列表。
7.4 元注解
元注解:元注解的做用就是负责注解其余注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型做说明。

a.@Target:

@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
取值(ElementType)有:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
看table这个类:注解Table 能够用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。
b.@Retention:

@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出如今源代码中,而被编译器丢弃;而另外一些却被编译在class文件中;
编译在class文件中的Annotation可能会被虚拟机忽略,而另外一些在class被装载时将被读取(请注意并不影响class的执行,由于Annotation与class在使用上是被分离的)。
使用这个meta-Annotation能够对 Annotation的“生命周期”限制。
做用:表示须要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)
看Column这个类:Column注解的的RetentionPolicy的属性值是RUTIME,这样注解处理器能够经过反射,获取到该注解的属性值,从而去作一些运行时的逻辑处理
c.@Documented:

@Documented用于描述其它类型的annotation应该被做为被标注的程序成员的公共API,所以能够被例如javadoc此类的工具文档化。
Documented是一个标记注解,没有成员
咱们能够在上面那个类中添加@Documented
d.@Inherited:

@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
若是一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
看Greeting这个类代表这个类型时被继承的
7.5.Android support annotations
Android support library从19.1版本开始引入了一个新的注解库,它包含不少有用的元注解,你能用它们修饰你的代码,帮助你发现bug。
Support library本身自己也用到了这些注解,因此做为support library的用户,Android Studio已经基于这些注解校验了你的代码而且标注其中潜在的问题。
注解默认是没有包含的;它被包装成一个独立的库,若是使用了appcompat库,那么Support Annotations就会自动引入进来,由于appcompat使用了Support Annotations,若是没有则须要在build.gradle中添加配置
分类:
1)Nullness注解
@NonNull注解能够用来标识特定的参数或者返回值不能够为null

因为代码中参数String s使用@NonNull注解修饰,所以IDE将会以警告的形式提醒咱们这个地方有问题:
若是咱们给name赋值,例如String name = “Our Lord Duarte”,那么警告将消失。

使用@Nullable注解修饰的函数参数或者返回值能够为null。假设User类有一个名为name的变量,使用User.getName()访问,
由于getName函数的返回值使用@Nullable修饰,因此调用:toast的时候没有检查getName的返回值是否为空,将可能致使crash。
2)Resource Type 注解
资源在Android中做为整型值来传递。
这意味着但愿获取一个drawable做为参数的代码很容易被传递了一个string类型的资源,由于他们资源id都是整型的,编译器很难区分。
Resource Type注解在这种条件下能够提供类型检查
是否曾经传递了错误的资源整型值给函数,还可以愉快的获得原本想要的整型值吗?
资源类型注解能够帮助咱们准确实现这一点。在下面的代码中,咱们的testStringRes函数预期接受一个字符串类型的id,并使用@StringRes注解修饰:

3)Threading 注解

好比咱们在项目中处理比较耗时的操做,须要制定在工做子线程中执行,能够使用Threading 注解,若是没有在制定的线程中执行也是编译不过的
几种Threading注解
@UiThread UI线程
@MainThread 主线程
@WorkerThread 子线程
@BinderThread 绑定线程
4)Overriding Methods
注解: @CallSuper
若是你的API容许使用者重写你的方法,可是呢,你又须要你本身的方法(父方法)在重写的时候也被调用,这时候你能够使用@CallSuper标注
看代码

7.6.总结
注解是如何被处理的:
当Java源代码被编译时,编译器的一个插件annotation处理器则会处理这些annotation。
处理器能够产生报告信息,或者建立附加的Java源文件或资源。
若是annotation自己被加上了RententionPolicy的运行时类,则Java编译器则会将annotation的元数据存储到class文件中。
而后,Java虚拟机或其余的程序能够查找这些元数据并作相应的处理。
固然除了annotation处理器能够处理annotation外,咱们也能够使用反射本身来处理annotation
Annotation翻译为中文即为注解,意思就是提供除了程序自己逻辑外的额外的数据信息。
Annotation对于标注的代码没有直接的影响,它不能够直接与标注的代码产生交互,但其余组件能够使用这些信息。
Annotation信息能够被编译进class文件,也能够保留在Java 虚拟机中,从而在运行时能够获取。
甚至对于Annotation自己也能够加Annotation。
8. 类加载器相关
8.1 什么是ClassLoader?

你们都知道,当咱们写好一个Java程序以后,不是管是CS仍是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,
当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不一样的class文件当中,因此常常要从这个class文件中要调用另一个class文件中的方法,若是另一个文件不存在的,则会引起系统异常。
而程序在启动的时候,并不会一次性加载程序所要用的全部class文件,而是根据程序的须要,经过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存以后,才能被其它class所引用。
因此ClassLoader就是用来动态加载class文件到内存当中用的。
ClassLoader的具体做用就是将class文件加载到jvm虚拟机中去,程序就能够正确运行了。
可是,jvm启动的时候,并不会一次性加载全部的class文件,而是根据须要去动态加载。
想一想也是的,一次性加载那么多jar包那么多class,那内存不崩溃?
8.2 累加器的类型

- BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可经过以下程序得到该类加载器从哪些地方加载了相关的jar或class文件:
- URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
- for (int i = 0; i < urls.length; i++) {
- System.out.println(urls[i].toExternalForm());
- }
如下内容是上述程序从本机JDK环境所得到的结果:
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/classes/
其实上述结果也是经过查找sun.boot.class.path这个系统属性所得知的。
- System.out.println(System.getProperty("sun.boot.class.path"));
打印结果:C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.6.0_22\jre\classes
- Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的全部jar。
- App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下的全部jar和class文件。
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身须要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中,注意必因为虚拟机是按照文件名识别加载jar包的,如rt.jar,若是文件名不被虚拟机识别,即便把jar包丢到lib目录下也是没有做用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader
类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext
目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者能够直接使用标准扩展类加载器。
系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader
。
它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是咱们常常用到的classpath路径,开发者能够直接使用系统类加载器,通常状况下该类加载是程序中默认的类加载器,经过ClassLoader#getSystemClassLoader()
方法能够获取到该类加载器。
在Java的平常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,咱们还能够自定义类加载器,须要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当须要使用该类时才会将它的class文件加载到内存生成class对象,并且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式。
8.3 双亲委派代理模式

【双亲委派模型过程】
ClassLoader使用的是双亲委托模型来搜索类的,每一个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)自己没有父类加载器,但能够用做其它ClassLoader实例的父类加载器。
当一个ClassLoader实例须要加载某个类时,它会试图亲自搜索某个类以前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,若是没加载到,则把任务转交给Extension ClassLoader试图加载,若是也没加载到,则转交给App ClassLoader 进行加载,若是它也没有加载获得的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。
若是它们都没有加载到这个类时,则抛出ClassNotFoundException异常。不然将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
8.4【类的加载过程】
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
加载、验证、准备、初始化这四个时间是肯定的,
解析的时间是不肯定的,某些状况支持初始化以后进行,支持java的动态绑定;
使用和卸载是在全部顺序定完以后才进行的

程序绑定的概念:
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来讲,绑定分为静态绑定和动态绑定;或者叫作前期绑定和后期绑定.
静态绑定:
在程序执行前方法已经被绑定(也就是说在编译过程当中就已经知道这个方法究竟是哪一个类中的方法),此时由编译器或其它链接程序实现。例如:C。
针对java简单的能够理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定
动态绑定:
后期绑定:在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。
也就是说,编译器此时依然不知道对象的类型,但方法调用机制能本身去调查,找到正确的方法主体。
不一样的语言对后期绑定的实现方法是有所区别的。
但咱们至少能够这样认为:它们都要在对象中安插某些特殊类型的信息。
1、类加载过程
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段。
这里简要说明下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来讲,绑定分为静态绑定和动态绑定:
- 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它链接程序实现。针对java,简单的能够理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
- 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在java中,几乎全部的方法都是后期绑定的。
下面详细讲述类加载过程当中每一个阶段所作的工做
加载
加载时类加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下三件事情:
一、经过一个类的全限定名来获取其定义的二进制字节流。
二、将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
三、在Java堆中生成一个表明这个类的java.lang.Class对象,做为对方法区中这些数据的访问入口。
注意,这里第1条中的二进制字节流并不仅是单纯地从Class文件中获取,好比它还能够从Jar包中获取、从网络中获取(最典型的应用即是Applet)、由其余文件生成(JSP应用)等。
相对于类加载的其余阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动做)是可控性最强的阶段,由于开发人员既能够使用系统提供的类加载器来完成加载,也能够自定义本身的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,并且在Java堆中也建立一个java.lang.Class类的对象,这样即可以经过该对象访问方法区中的这些数据。
验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
不一样的虚拟机对类验证的实现可能会有所不一样,但大体都会完成如下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
- 文件格式的验证:验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区以内。通过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
- 字节码验证:该阶段验证的主要工做是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会作出危害虚拟机安全的行为。
- 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身之外的信息(常量池中的各类符号引用)进行匹配性的校验。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意:
一、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
二、这里所设置的初始值一般状况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:
public static int value = 3;
那么变量value在准备阶段事后的初始值为0,而不是3,由于这时候还没有开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,因此把value赋值为3的动做将在初始化阶段才会执行。
下表列出了Java中全部基本数据类型以及reference类型的默认零值:

这里还须要注意以下几点:
- 对基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过;而只被final修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 若是在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
解析
解析阶段是虚拟机将
常量池中的符号引用转化为直接引用的过程。在
Class类文件结构一文中已经比较过了符号引用和直接引用的区别和关联,这里再也不赘述。
前面说解析阶段可能开始于初始化以前,也可能在初始化以后开始,虚拟机会根据须要来判断,究竟是在类被加载器加载时就对常量池中的符号引用进行解析(初始化以前),仍是等到一个符号引用将要被使用前才去解析它(初始化以后)。
对同一个符号引用进行屡次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动做重复进行。
解析动做主要针对
类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。
一、类或接口的解析:判断所要转化成的直接引用是对数组类型,仍是普通的对象类型的引用,从而进行不一样的解析。
二、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,若是有,则查找结束;
若是没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,尚未,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程以下图所示:
三、类方法解析:对类方法的解析与对字段解析的搜索步骤差很少,只是多了判断该方法所处的是类仍是接口的步骤,并且对类方法的匹配搜索,是先
搜索父类,再搜索接口。
四、接口方法解析:与类方法解析步骤相似,只是接口不会有父类,所以,只递归向上
搜索父接口就好了。
初始化
初始化是类加载过程的最后一步,到了此阶段,才
真正开始执行类中定义的Java程序代码。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序指定的主观计划去初始化类变量和其余资源,或者能够从另外一个角度来表达:
初始化阶段是执行类构造器<clinit>()方法的过程。
这里简单说明下<clinit>()方法的执行规则:
一、<clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句中能够赋值,可是不能访问。
二、<clinit>()方法与实例构造器<init>()方法(类的构造函数)不一样,它不须要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以,在虚拟机中第一个被执行的<clinit>()方法的类确定是java.lang.Object。
三、<clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句块,也没有对类变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法。
四、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操做,所以接口与类同样会生成<clinit>()方法。可是接口鱼类不一样的是:执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。
五、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit>()方法中有耗时很长的操做,那就可能形成多个线程阻塞,在实际应用中这种阻塞每每是很隐蔽的。
下面给出一个简单的例子,以便更清晰地说明如上规则:
- class Father{
- public static int a = 1;
- static{
- a = 2;
- }
- }
-
- class Child extends Father{
- public static int b = a;
- }
-
- public class ClinitTest{
- public static void main(String[] args){
- System.out.println(Child.b);
- }
- }
执行上面的代码,会打印出2,也就是说b的值被赋为了2。
咱们来看获得该结果的步骤。首先在准备阶段为类变量分配内存并设置类变量初始值,这样A和B均被赋值为默认值0,然后再在调用<clinit>()方法时给他们赋予程序中指定的值。当咱们调用Child.b时,触发Child的<clinit>()方法,根据规则2,在此以前,要先执行完其父类Father的<clinit>()方法,又根据规则1,在执行<clinit>()方法时,须要按static语句或static变量赋值操做等在代码中出现的顺序来执行相关的static语句,所以当触发执行Father的<clinit>()方法时,会先将a赋值为1,再执行static语句块中语句,将a赋值为2,然后再执行Child类的<clinit>()方法,这样便会将b的赋值为2.
若是咱们颠倒一下Father类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。很明显是根据规则1,执行Father的<clinit>()方法时,根据顺序先执行了static语句块中的内容,后执行了“public static int a = 1;”语句。
另外,在颠倒两者的顺序以后,若是在static语句块中对a进行访问(好比将a赋给某个变量),在编译时将会报错,由于根据规则1,它只能对a进行赋值,而不能访问。
总结
整个类加载过程当中,除了在加载阶段用户应用程序能够自定义类加载器参与以外,其他
全部的动做彻底由虚拟机主导和控制。到了初始化才开始执行类中定义的Java程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于<clinit>()方法。类加载过程当中主要是将Class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操做,在加载完成后才真正开始。
8.5 自定义类加载器
Java类加载机制及自定义加载器
一:ClassLoader类加载器,主要的做用是将class文件加载到jvm虚拟机中。jvm启动的时候,并非一次性加载全部的类,而是根据须要动态去加载类,主要分为隐式加载和显示加载。
隐式加载:程序代码中不经过调用ClassLoader来加载须要的类,而是经过JVM类自动加载须要的类到内存中。例如,当咱们在类中继承或者引用某个类的时候,JVM在解析当前这个类的时,发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
显示加载:代码中经过Class.forName(),this.getClass.getClassLoader.LoadClass(),自定义类加载器中的findClass()方法等。
二:jvm自带的加载器
(1)BootStrap ClassLoader:主要加载%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。能够通System.getProperty("sun.boot.class.path")
查看加载的路径,以下:
复制代码
package test;
public class TestGC {
public static void main(String []args){
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
复制代码
显示结果以下:
D:\Program Files\Java\jdk1.7.0_45\jre\lib\resources.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\rt.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jsse.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jce.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\charsets.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\lib\jfr.jar;
D:\Program Files\Java\jdk1.7.0_45\jre\classes
(2)Extention ClassLoader:主要加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。也能够经过System.out.println(System.getProperty("java.ext.dirs"))查看加载类文件的路径。
(3)AppClassLoader:主要加载当前应用下的classpath路径下的类。以前咱们在环境变量中配置的classpath就是指定AppClassLoader的类加载路径。
三:类加载器的继承关系
先看一下这三个类加载器之间的继承关系,以下图:

ExtClassLoader,AppClassLoder继承URLClassLoader,而URLClassLoader继承ClassLoader,BoopStrap ClassLoder不在上图中,由于它是由C/C++编写的,它自己是虚拟机的一部分,并非一个java类。
jvm加载的顺序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder,下面看一段源码:
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
从源码中咱们看到:
(1)Launcher初始化的时候建立了ExtClassLoader以及AppClassLoader,并将ExtClassLoader实例传入到AppClassLoader中。
(2)虽然上一段源码中没见到建立BoopStrap ClassLoader,可是程序一开始就执行了System.getProperty("sun.boot.class.path")。
四:类加载器之间的父子关系
AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrap ClassLoader为顶级加载器。
下面一个小例子就能够证实,以下:新建一个Test类,能够经过getParent()方法获取上一层父机载器,执行以下代码:
复制代码
package test;
public class TestGC {
public static void main(String []args){
System.out.println(Test.class.getClassLoader().toString());
System.out.println(Test.class.getClassLoader().getParent().toString());
System.out.println(Test.class.getClassLoader().getParent().getParent().toString());
}
}
复制代码
输出结果以下:

五:类加载机制-双亲委托机制
例如:当jvm要加载Test.class的时候,
(1)首先会到自定义加载器中查找,看是否已经加载过,若是已经加载过,则返回字节码。
(2)若是自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。
(3)若是没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。
(4)若是没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
(5)若是BoopStrap ClassLoader依然没有加载过,则到本身指定类加载路径下("sun.boot.class.path")查看是否有Test.class字节码,有则返回,没有通
知下一层加载器ExtClassLoader到本身指定的类加载路径下(java.ext.dirs)查看。
(6)依次类推,最后到自定义类加载器指定的路径尚未找到Test.class字节码,则抛出异常ClassNotFoundException。以下图:

六:类加载过程的几个方法
(1)loadClass
(2)findLoadedClass
(3)findClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空,调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则,调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
七:自定义类加载器步骤
(1)继承ClassLoader
(2)重写findClass()方法
(3)调用defineClass()方法
下面写一个自定义类加载器:指定类加载路径在D盘下的lib文件夹下。
(1)新建一个Test.class类,代码以下:
package com.test;
public class Test {
public void say(){
System.out.println("Hello MyClassLoader");
}
}
(2)cmd控制台执行javac Test.java,将生成的Test.class文件放到D盘lib文件夹->com文件夹->test文件夹下。
(3)自定义类加载器,代码以下:
package test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
private String classpath;
public MyClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte [] classDate=getDate(name);
if(classDate==null){}
else{
//defineClass方法将字节码转化为类
return defineClass(name,classDate,0,classDate.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//返回类的字节码
private byte[] getDate(String className) throws IOException{
InputStream in = null;
ByteArrayOutputStream out = null;
String path=classpath + File.separatorChar +
className.replace('.',File.separatorChar)+".class";
try {
in=new FileInputStream(path);
out=new ByteArrayOutputStream();
byte[] buffer=new byte[2048];
int len=0;
while((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
return out.toByteArray();
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
finally{
in.close();
out.close();
}
return null;
}
测试代码以下:
package test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestMyClassLoader {
public static void main(String []args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException{
//自定义类加载器的加载路径
MyClassLoader myClassLoader=new MyClassLoader("D:\\lib");
//包名+类名
Class c=myClassLoader.loadClass("com.test.Test");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("say", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
复制代码
输出结果以下:

自定义类加载器的做用:jvm自带的三个加载器只能加载指定路径下的类字节码。
若是某个状况下,咱们须要加载应用程序以外的类文件呢?好比本地D盘下的,或者去加载网络上的某个类文件,这种状况就能够使用自定义加载器了。
参考网址:http://blog.csdn.net/briblue/article/details/54973413
9.反射
9.1 编译时运行时的概念
【说明】二者最大的不一样是:是否涉及到内存的调用;
【编译时】只涉及纠正内存的语法正确与否,不涉及内存运行;
【运行时】java虚拟机执行.class文件的过程,涉及到内存的调用;
编译期,就是将Java代码编译成.class文件的过程,该过程只涉及到语法句法的正确与否,不涉及内存方面及执行方面的检查。
所谓的运行期,就是Java虚拟机执行.class文件的过程。该过程会涉及到内存调用。实际类型检查等方面。



关于动态绑定,在调用该引用实例的方法的时候,会优先去调用该实例引用的运行时方法,也就是实际类型的方法。
而在调用该引用实例的成员变量的时候,会优先去调用该实例应用的编译时的成员变量,也就是声明的类型的成员变量。
对于,调用引用实例的方法,在编译时,是调用声明类型的成员方法(多态的实现原理),也就是所谓的编译时类型的方法,而到了运行时,调用的是实际的类型的成员方法,也就是所谓的运行时类型的方法。
而对于调用引用实例的成员变量,在编译时,便是调用声明类型的成员变量,在运行时更是调用声明类型的成员变量,
也就是说,对于调用引用实例的成员变量,不管是编译时仍是运行时,均是调用编译时类型的成员变量。
9.2 什么是反射

JAVA语言的反射机制:
JAVA反射机制是在运行状态中,对于任意一个类,都可以知道这个类的全部属性和方法;
对于任意一个对象,都可以调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
在运行状态中,对于任意一个类,都可以知道这个类的全部属性和方法;
对于任意一个对象,都可以调用它的任意一个方法;
这 种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
主要功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具备的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。
9.3.class文件
jvm虚拟机会加载.class文件,.class文件是由编译器将java源代码编译成.class文件的。
每个.class文件都有一个class对象,这里的class和.class文件是不同的。
编译好一个类class是生成在.class文件中的;
生一个.class文件就会生成一个class对象,是记录class对象的信息的;
9.4 反射的应用






9.5 android 中反射的应用
【说明】若是xml布局文件、r文件的使用等等;


