`Java`分类下的文章

Java

动态规划

从典例开始了解动态规划的前世今生

问: 有4种面值为1,5,10,25的货币,可以重复使用,需要凑成1000元,问共有多少种货币组成方案?
分析问题:

  • 当使用0张面值为5元的货币,剩下由{1,10,25}组成1000元的方案总数计为 res1
  • 当使用1张面值为5元的货币,剩下由{1,10,25}组成的995元的方案总数计为 res2
  • 当使用199张面值为5元的货币,剩下由{1,10,25}组成的5元的方案总数计为 res200
  • 当使用200张面值为5元的货币,剩下由{1,10,25}组成的0元的方案总数计为 res201

上述求方案总数的方法都会用到一个递归函数。
那么这里总的方案数为上述结果的累加,即: res1 + res2 + … + res201

这里就可以定义这个递归函数: int(int[] arr, index , aim);
这个递归函数表示的含义是如果用arr[index … N-1] 的面值货币组成的 aim 目标钱数的方案总数
于是就可以得到以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
private int violence(int[] source, int index, int aim) {
int res = 0; // 方案总数
if(index == source.length){
res = aim == 0 ? 1 : 0; // aim 为 0 表示这种方案可行,所以res = 1; 结果累加即可
}else{
for(int i = 0; source[index] * i <= aim ; i++){
// 固定一种货币情况下的递归函数求计算总数
res += violence(source,index ++ , aim - source[index] * i);
}
}
return res;
}

这种方法又叫暴力搜索算法。但是有个重复计算的问题,比如说在已经使用0张5元和1张10元的情况下,会求volience(source,2,990)的值。当使用2张5元和0张10元的情况下,也会求一次violence(source,2,990)的值。这就造成了重复计算的问题,这种问题在这个题目中出现的次数非常之多,所以我们需要将这些计算过结果的情况保存下来,所以出现了下面将要介绍的记忆搜索算法。

这里的记忆搜索算法的记忆是怎么实现的呢?需要怎样记忆?记忆什么内容?这些问题将是我们需要进一步探讨的。
从上面的分析可知,因为source参数是不变的,只有index和aim值是变化的。所以我们需要记忆的就是violence(index, aim)的值。有两个可变参数,所以我们用一个二维数组的map来记忆这个值。记为 int[][] map = new int[i][j];
i 为index 的大小 +1 ,j 为 aim的大小 + 1。 map[i][j]的值就表示当index = i, aim = j的情况时的方案总数。代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int memory(int[] source, int index, int aim,int[][] map) {
int res = 0;
if(index == source.length){
res = aim == 0 ? 1 : 0;
}else{
int mapValue = 0;
for(int i = 0; source[index] * i <= aim ; i++){
mapValue = map[index + 1][aim-source[index]*i];
if(mapValue != 0){
res += mapValue == -1 ? 0 : mapValue;
}else{
res += violence(source,index ++ , aim - source[index] * i);
}
}
}
map[index][aim] = res == 0 ? -1 : res;
return res;
}

上面的这个记忆搜索算法就相当于动态规划的一种形态了。下面讲讲两者的区别和联系:

  • 记忆搜索不关心达到某一个递归过程的路径,只是单纯的对计算过的递归过程进行记录,避免重复递归的过程。
  • 动态规划严格规定了到达目标的路径,只能依次计算得到结果。后面的结果严格依赖前面的计算结果。
  • 两者都是用空间换时间的做法,也都有枚举的过程,一个有顺序,一个没顺序。

就多了一个有顺序的枚举过程,为什么一定要去学动态规划呢?那正是因为有顺序的枚举,才有了可以优化算法的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int dpm(int[ ] source, int aim, int[][] dp){
dp[0][0] = 1;
for(int i=0;i<dp.length;i++){
dp[i][0] = 1;
for(int j=1;j<dp[0].length;j++){
if( i == 0 ){
dp[i][j] = (j % source[i] == 0) ? 1 : 0;
}else{
// 未优化
// for(int k = 0; k <= aim / source[i] && (j - source[i] * k) >= 0; k++){
// dp[i][j] += dp[i-1][j - source[i]*k ];
// }
// 优化后,少了枚举过程
dp[i][j] = dp[i-1][j];
dp[i][j] += (j - source[i] >= 0) ? dp[i][j - source[i]] : 0 ;
}
}
}
return dp[source.length-1][aim];
}

具体的动态规划过程参考动态规划(1)

Read More

默认配图
Java

java并发编程

1. 线程状态

一张图详解java多线程的多种状态以及状态之间的关系

2. 多线程的实现方式

在java应用中,共有8中实现多线程的方式,如果是spring应用,还可以有spring的基于注解的多线程开启方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
/**
* @Author: steve
* @Date: Created in 13:33 2018/1/22
* @Description: 线程的9种创建方式
* @Modified By:
*/
@Configuration
@ComponentScan()
@EnableAsync
public class Main {
/**
* 继承Thread类重些run方法来实现新线程
*/
static class Demo1 extends Thread{
@Override
public void run() {
System.out.println("Thread change run implements running ....");
}
}
/**
* 实现Runnable接口来实现新线程的创建
*/
static class Demo2 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Runnable implements method ... ");
}
}
static class Demo3 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("返回值的方式创建线程");
Thread.sleep(3000);
System.out.println("返回值的方式创建线程");
return 1;
}
}
@Service
class Demo4{
@Async
public void printA() throws InterruptedException {
while(true) {
Thread.sleep(300);
System.out.println("a");
}
}
@Async
public void printB() throws InterruptedException {
while(true) {
Thread.sleep(300);
System.out.println("b");
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1
Demo1 demo1 = new Demo1();
demo1.run();
// 2
Thread thread = new Thread(new Demo2());
thread.start();
// 3
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Demo3());
Thread thread1 = new Thread(futureTask);
thread1.start();
System.out.println("结束了吗?");
System.out.println("计算的结果: " + futureTask.get());
// 4 匿名内部类
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类的方式创建线程!!!");
}
});
thread2.start();
// 5
Thread thread3 = new Thread(){
@Override
public void run() {
System.out.println("重写run方法");
}
};
thread3.start();
// 6 Spring实现多线程技术
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
Demo4 threadService = applicationContext.getBean(Demo4.class);
threadService.printA();
threadService.printB();
// 7 lambda 方式实现多线程技术
List<Integer> lists = new ArrayList<Integer>(){
{
add(1);
add(2);
add(3);
add(4);
}
};
lists.parallelStream().forEach(System.out :: println);
// 8 线程池的实现方法
ExecutorService executorService = Executors.newFixedThreadPool(10); // 这里可以有多种不同的实现选择
for(int i=0; i<100; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
// 9 定时器实现多线程
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器的多线程");
}
},1000,3000);
}
}

3.线程安全性问题

先来了解一下java多线程引发安全问题的3个前提

  1. 多线程条件下
  2. 有共享资源
  3. 非原子性操作

看个例子

1
2
3
4
5
6
7
8
9
10
11
public class SingletonHungry {
private SingletonHungry(){}
private static SingletonHungry singletonHungry = new SingletonHungry();
public static SingletonHungry getSingletonHungry() {
return singletonHungry;
}
}

这是典型的饿汉式单例模式,不存在线程安全性问题,因为getSingletonHungry()方法是原子性的。再看一个懒汉式的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonLazy {
private SingletonLazy(){}
private static SingletonLazy singletonLazy;
public static SingletonLazy getSingletonLazy() throws InterruptedException {
if(singletonLazy == null){
synchronized (SingletonLazy.class){
if(singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
}

表面看这个代码没问题,也加了锁,但是这里还有一个涉及到指令重排序的问题,就是说jvm执行上述代码的时候并不一定是按照正常顺序来执行的。比如,正常来讲,jvm实例化对象是首先申请一块内存,然后实例化对象,最后再指向对象。然而经过指令重排序的优化过程,有可能会先实例化对象,再去执行其他步骤,这样就可能造成第二个if(singletonLazy == null)的判断不准确。所以这里需要给singletonLazy加个 volatile 关键字,意思是不给这个对象进行指令重排序等jvm执行优化操作。
在这里我们研究研究 synchronized 这个关键字,在现在的这个版本(jdk8),这个关键字已经被进行了多重优化,在某些情况下可以摆脱重型锁的束缚了。在这里我们讲讲它的应用步骤:

  • 偏向锁:jvm执行引擎会一直获取锁信息,只有当发生竞争的时候,它才会放弃锁,然后再重新获取对象锁信息(锁信息保存在对象头中)。相比较以前每次进入方法前都需要获取锁信息操作性能提升了不少
  • 轻量级锁: 在多线程竞争的环境下,线程尝试获取被加锁的对象时,会先将对象头(里面包含对象的锁信息)复制一份,然后尝试获取对象锁,如果获取锁失败,再重复一次复制操作。这叫做自旋锁。直到获取成功。
  • 重量级锁: 多次自旋操作失败后就会升级为重量级锁。

参考:Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

3.1 synchorized

synchroized从1.6开始变得不再像以前那么的重量级了,它有三个转变阶段,分别为偏向锁,轻量级锁,重量级锁。它的主要作用是锁住对象,保证只有一个线程能获取到该对象的实例,从而保证线程安全。它可以使用在方法体中和方法声明中,在方法声明中代表的是锁住这个类的对象,方法体中看实现可以随意锁住某个对象。日常使用建议锁的细粒度越细越好。以前jdk中的Vector类就是通过锁方法而保证线程安全的,现在已经不再使用它了,而Collections中以synchronized 开头的方法锁的是一个mutex对象,而不是类对象。
SynchronizedList的get方法

1
2
3
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}

Vector的部分方法

1
2
3
4
5
6
7
8
9
10
11
public synchronized int capacity() {
return elementData.length;
}
/**
* Returns the number of components in this vector.
*
* @return the number of components in this vector
*/
public synchronized int size() {
return elementCount;
}

3.2 volatile 关键字

有时候只需要保证共享变量在各个线程中的数据的一致性,就不需要使用synchorized这个锁了,只需要在变量声明时加上volatile关键词就行了,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作,它的汇编语义就是一个Lock指令,这个指令会将CPU里的缓存写入系统内存。所以每个线程读取到的数据都是一致的。然而这样也有一个弊端,随着CPU的三级缓存越来越重要了,这样做无非会降低系统的性能。而且volatile的使用也有一个条件,变量真正独立于其他变量和自己以前的值。比如什么 x++ 操作就不行,依赖于以前的x值。 具体使用请参考正确使用 Volatile 变量

3.3 Lock 锁

lock更强调的是锁住代码块,与对象无关。而且他的实现更有针对性。由于是java实现的,所以它拥有许多synchorized没有的特性,比如说公平与非公平锁(内部维护一个FIFO队列)。

  • ReentrantLock 这是Lock的一个常用实现,身为一个可重入锁,底层是基于AQS实现的,使用也很简单
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ReentrantLockDemo {
    private int value;
    private Lock lock = new ReentrantLock(); // 需要保证多个线程拿到的是同一个lock对象。
    public int getNext(){
    lock.lock();
    try {
    return value++;
    }finally {
    lock.unlock();
    }
    }

3.4 ReadWriteLock 锁

在某些情况下,读和写操作并不一定一样多,所以就诞生了这个读写锁。

  • ReentrantReadWriteLock 时ReadWriteLock的一个实现,通过这个实例,可以获取到对应的读锁和写锁,从而根据实际需求使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    /**
    * @Author: steve
    * @Date: Created in 11:20 2018/1/23
    * @Description: 读写锁使用实例
    * 读锁为共享锁,写锁为独占锁。
    *
    * @Modified By:
    */
    public class ReadAndWriteDemo {
    private Map<String, String> map = new HashMap<>();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();
    /**
    * 读锁 方法
    * @param key
    * @return
    */
    public String get(String key){
    readLock.lock();
    System.out.println(Thread.currentThread().getName() + " 读取锁开始 。。。。。。 ");
    try {
    Thread.sleep(300);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try{
    return map.get(key);
    } finally {
    readLock.unlock();
    System.out.println(Thread.currentThread().getName() + " 读取锁结束 。。。。。。");
    }
    }
    /**
    * 写锁 方法
    * @param key
    * @param value
    */
    public void put(String key, String value){
    writeLock.lock();
    System.out.println(Thread.currentThread().getName() + " 写入锁开始 。。。。。。 ");
    try{
    Thread.sleep(300);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try{
    map.put(key,value);
    }finally {
    writeLock.unlock();
    System.out.println(Thread.currentThread().getName() + " 写入锁结束 。。。。。。 ");
    }
    }
    private boolean isUpdate = true;
    /**
    * 降级锁 方法
    * 由写锁降级为读锁
    */
    public String readWrite(String key,String value){
    readLock.lock(); // 加读锁,保证变量在线程间的一致性
    if(isUpdate){
    readLock.unlock();
    writeLock.lock();
    map.put(key,value);
    readLock.lock();
    writeLock.unlock(); // 在写锁释放前给读锁加锁
    }
    try{
    return map.get(key);
    }finally {
    readLock.unlock();
    }
    }
    public static void main(String[] args) {
    ReadAndWriteDemo demo = new ReadAndWriteDemo();
    readWriteDemo(demo);
    }
    public static void readWriteDemo(ReadAndWriteDemo demo){
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve1","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve2","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    }
    public static void readDemo(ReadAndWriteDemo demo){
    demo.put("steve1","liu");
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.get("steve1");
    }
    }).start();
    }
    /**
    * 写锁测试用例
    * @param demo
    */
    public static void writeDemo(ReadAndWriteDemo demo){
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.put("steve","liu");
    }
    }).start();
    }
    }

3.5 StampedLock 锁

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

3.6 重入锁

当被锁住的方法里面调用了其他也加了锁的方法时,如果可以调用成功,就说这个锁是可重入锁,这个调用操作成为锁重入。话不多说,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ThreadReentrant {
/**
* synchronized 标志的方法锁住的是当前的类实例
*/
public synchronized void a(){
System.out.println("a ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
b();
}
public synchronized void b() {
System.out.println("b ... ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadReentrant threadReentrant1 = new ThreadReentrant();
new Thread(() -> threadReentrant1.b()).start();
new Thread(() -> threadReentrant1.a()).start();
}
}

3.7 自旋锁

就是等待其他线程结束的一个操作,相当于while-true。话不多说,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程开始 。。。 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程结束 。。。 ");
}).start();
while(Thread.activeCount() != 2){
}
System.out.println("over 。。。");
}

3.8 死锁

就是两个线程,两个资源之间的一种竞争关系,A线程持有资源A,接下来想要取到资源B,但是B线程同时持有资源B,想要获取资源A。然后两个线程一直处于等待状态,导致死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DeadLockDemo {
private static final String A = "A";
private static final String B = "B";
public static void main(String... args){
new DeadLockDemo().deadLock();
}
private void deadLock(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(A) {
try {
Thread.sleep(2); //sleep 方法可是演示并发案例的好工具
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(B){
System.out.print("1");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(B){
synchronized(A){
System.out.println("2");
}
}
}
});
thread1.start();
thread2.start();
}
}

3.9 AQS的原理

AQS是jdk8并发包里的抽象类AbstractQueuedSynchronizer的简称。先来看看他的文档,看看他的设计:

为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。

这一段表明了它的作用是用于实现相关同步器,还讲了部分原理,他的内部实现是依靠一个FIFO的等待队列,来实现对象的等待唤醒机制。内部有一个int类型的变量保存对象的同步状态。

应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。

这一段表明了它的使用方法。

此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

这一段表明AQS支持两种锁模式,一个是独占模式,典型的应用是ReentrantLock,另一种是共享模式,这个例子就比较多了,如CountDownLatch和CyclicBarrier。然后介绍了一些常见的使用情景。

为了将此类用作同步器的基础,需要适当地重新定义以下方法,这是通过使用 getState()、setState(int) 和/或 compareAndSetState(int, int) 方法来检查和/或修改同步状态来实现的:

  • tryAcquire(int)
  • tryRelease(int)
  • tryAcquireShared(int)
  • tryReleaseShared(int)
  • isHeldExclusively()

默认情况下,每个方法都抛出 UnsupportedOperationException。这些方法的实现在内部必须是线程安全的,通常应该很短并且不被阻塞。定义这些方法是使用此类的 唯一 受支持的方式。其他所有方法都被声明为 final,因为它们无法是各不相同的。

这一段就是告诉你要实现自定义的同步器需要适当的重写这几个方法.下面我们就 ReentrantLock 这个独占锁类来看看AQS给我们提供的功能吧。
lock方法

1
2
3
public void lock() {
sync.lock();
}

看源码可以知道sync是ReentrantLock内部的一个AQS实现的抽象类,它还有两个实现类NonfairSync和FairSync,就是公平锁和非公平锁的实现。它的lock方法是个抽象方法,所以我们需要找到它的默认实现,看源码可知默认实现是非公平锁,所以我们就看非公平锁的lock方法实现

1
2
3
4
5
6
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

这里首先用CAS方法尝试将State的值变为1(state就是维持同步器的状态的那个int值,CAS内部实现可以理解为一个while无限循环,当当前的值等于expect时,会将当前的值设置为update的值),设置成功后就调用setExclusiveOwnerThread方法,并将当前线程传入进去,这个方法就是将传入的线程赋值给了exclusiveOwnerThread 变量,这个变量就是保存的当前的线程。设置失败后就会调用AQS的acquire方法,来看这个方法的实现

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

arg就是1,这个是state的值。表示同步器的状态计数器。如果if里面的条件不成立,这个对象(ReentranLock实例)就不会上锁(state没增加就表明当前lock失败)。我们继续来看它的tryAcquire(arg)的实现,这个方法内部只有一行,调用了nonfairTryAcquire方法。来看源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

首先令current为当前的变量,将当前的state赋值给c,当state的值等于0时,会进行一次CAS,将state修改为acquires的值,也就是1。返回true。当state的值不为0的时候并且当前线程等于内存中存的值(getExclusiveOwnerThread 方法返回的就是exclusiveOwnerThread的值)时(这一步判断是为了防止这不是重入进来的),将c 和 acquires 的值相加并赋值给nextc变量,当nextc的值出现小于0的情况时,表明内存发生了溢出。抛出异常。如果没发生内存溢出就将state的值设为(c + acquires),返回true; 还有其它的情况返回false。 当tryAcquire(arg)方法返回true时,那么表明acquire方法获取成功了。当tryAcquire返回false,我们再来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法,首先来看addWaiter的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

这里面的node表示的是FIFO里面的一个个对象,这个FIFO是通过双向链表实现的。传入的mode是Node.EXCLUSIVE,看源码可知它是null的,表示的是独占模式下的一个等待节点(Node源码自行查看,这里就不提了)。然后令pred = tail节点,如果tail节点不为空,则会尝试将node节点设置为尾节点(tail),设置失败就只是一个单向链表指向,设置成功就变成双向链表了并返回node。如果tail节点为空(表明刚初始化)。执行enq(node)方法,继续来看它的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

有了前面的基础,再看这段代码会觉得很简单,就是一个无限循环,强制保证node这个传入的节点设置为tail节点。回到上一个方法addWaiter, 往下执行就是返回这个node了。在回溯到acquire方法,这时候是到了acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法了,来看它的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这里的两个参数我们可知node为链表的尾节点(FIFO的尾部),arg为1.看这个无限for循环,首先令p为node的前一个节点(predecessor方法就是返回前一个节点),判断p是否为head节点并且试图设置容器的状态值,两个条件都成立就执行setHead方法将node节点设置为head节点,并将p节点的下一个节点设置为null,就是解除了node节点和node前一个节点的关联关系。来看看setHead方法

1
2
3
4
5
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}

这里将node节点的部分属性都设置为null了。返回上一个调用方法acquireQueued,这里将failed变量设置为false,并返回了一个false结果(根据变量名可以猜到这是一个表示当前锁没被中断的结果),结束了无限循环。如果前面的条件不成立,进入到shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt() 两个方法的判断,这里主要是判断锁状态是否被中断。看看他们的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

这一段首先令ws变量为节点pred的waitStatus(等待状态值),它共有以下几种情形

  • CANCELLED(1) 表示线程退出了
  • SIGNAL(-1) 表示线程状态为等待唤醒(unparking)
  • CONDITION(-2) 表示线程在等待指定的条件(condition)成立
  • PROPAGATE(-3) 表示一个共享的释放锁应该传播给其它的节点
  • 0 默认值,无意义
    首先判断ws是否为SIGNAL,是的话返回true,表示这个节点处于等待唤醒状态,可以释放(ralease)它了,如果ws的值大于0的话,执行一个do-while循环,循环代码表示意义是当pred的等待状态值(waitStatus)大于0的时候,令pred指向pred的前一个节点,node的前一个节点指向pred节点,这样一步一步往FIFO队列的前方挤去。当条件不成立退出循环时令pred的next节点指向node节点,继续使这两个节点为双向链表指定。当ws值小于0的时候,执行CAS方法将pred节点的waitStatus变为SIGNAL,就是进入等待唤醒状态。然后返回false. 返回到上一个方法中看第二个判断条件parkAndCheckInterrupt方法。
    1
    2
    3
    4
    private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
    }

这段代码简单点理解就是返回当前线程是否被中断。我们可以继续看看park方法的实现

1
2
3
4
5
6
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}

这里就是将blocker对象设置为阻塞状态,这段代码有点难以理解,以后再看看。
到这里为止,我们就能大概的理解开头的AQS文档了,剩下的代码也不多了,看看也能懂个大概。就不再细述了,其实AQS的底层原理还是依赖了unsafe类的CAS和park方法,CAS设置状态值,park设置线程状态。设计模式使用的是模板方法设计模式,只要子类实现它的5个方法就可以直接用了。

参考资料:
CAS详解:JAVA CAS原理深度分析
Unsafe: JAVA并发编程学习笔记之Unsafe类

4.线程间通信

这里就举个生产者与消费者的例子来演示线程间的通信。

4.1 wait/notify 方法

wait是一个等待方法,和本文开头的流程图说的一样,当线程执行到wait时,进入到等待状态,并同时释放对象的锁,然后只能等待对象的notify或者notifyAll方法唤醒。如果这时候有关闭线程的操作发出,则会抛出 InterruptedException 异常。被唤醒后会继续执行后面的代码。

4.2 await/signal 方法

这两个比上面的两个方法细粒度更小一点,可以指定对象等待和唤醒。可控度更高一点,具体看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class ConditionDemo {
private int signal = 0; // 控制等待状态。
Lock lock = new ReentrantLock();
Condition a = lock.newCondition();
Condition b = lock.newCondition();
Condition c = lock.newCondition();
private void a(){
try {
lock.lock();
while (signal != 0){
try {
a.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("a方法执行");
signal ++ ;
b.signal();
}finally {
lock.unlock();
}
}
private void b(){
try {
lock.lock();
while (signal != 1){
try {
b.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("b方法执行");
signal ++ ;
c.signal();
}finally {
lock.unlock();
}
}
private void c(){
try {
lock.lock();
while (signal != 2){
try {
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("c方法执行");
signal = 0 ;
a.signal();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.c();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.b();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
while(true) {
demo.a();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
service.shutdown();
}
}

5.并发工具类(jdk8)

5.1 CountDownLatch

countDownLatch 简单来说就是一个线程等待其它N个线程执行到某个状态的一个计数器,也就是一等多。其它线程完成指定的任务后(countDown方法)还可以继续做它自己的事,而等待的线程需要等所有的线程完成指定的任务时才能继续它接下来的事。看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
CountDownLatchDemo demo = new CountDownLatchDemo();
long start = System.currentTimeMillis();
for(int i=0; i< 3; i++){
new Thread(new Runnable() {
@Override
public void run() {
demo.execute(countDownLatch);
}
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("所有线程准备完毕,准备时间: " + (end-start) + " 开始执行下一段任务。" );
}
public void execute(CountDownLatch countDownLatch){
System.out.println(Thread.currentThread().getName() + "执行2秒任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + " 线程 执行完毕,返回主线程执行");
}
}

5.2 CyclicBarrier

CyclicBarrier 相当于一个栏杆,当线程都到达了栏杆位置处(await方法)才可以继续往下执行,当一个没到达(执行出异常)就不放行。简单来说就是多等多。N个线程互相等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CyclicBarrierDemo {
private void eatting(CyclicBarrier cyclicBarrier, Random random) {
System.out.println(Thread.currentThread().getName() + "ready to eat");
try {
Thread.sleep(random.nextInt(2000));
cyclicBarrier.await();
}catch (Exception e){
}
System.out.println(Thread.currentThread().getName() + " finally, I just can eat now.");
}
public static void main(String[] args) {
CyclicBarrierDemo demo = new CyclicBarrierDemo();
Random random = new Random();
CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("all member are rady to eat, Then just go to eat");
}
});
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0;i < 4; i++){
executor.execute(new Runnable() {
@Override
public void run() {
demo.eatting(cyclicBarrier,random);
}
});
}
executor.shutdown();
}
}

5.3 Semaphore

Semaphore 相当于一个停车库,当车太多的时候(并发访问),每次只能放行指定数量的车(构造函数指定)。当一辆车走后(release方法),才会放另一辆车进来(acquire方法)。相当于是一个限制资源访问量的工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SemaphoreDemo {
/**
* 这里每次只有5个线程能同时访问
* @param semaphore
*/
private void demo(Semaphore semaphore){
try {
semaphore.acquire(); // 获取资源
System.out.println(Thread.currentThread().getName() + " 开始执行1s的任务");
Thread.sleep(1000);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(10);
SemaphoreDemo demo = new SemaphoreDemo();
ExecutorService exec = Executors.newCachedThreadPool();
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){
exec.execute(new Runnable() {
@Override
public void run() {
demo.demo(semaphore);
}
});
}
exec.shutdown();
while(Thread.activeCount() != 2){} // 等待所有线程执行完毕
long end = System.currentTimeMillis();
System.out.printf("任务执行完毕,共耗时: " + (end-start)); // 理论上是要多于10秒,而且线程切换的时间并不高,平均每3个线程间切换需要5ms。所以不会大于11秒。
}
}

5.4 Exchanger

Exchanger的使用场景有两个线程执行的结果进行比较(exchange方法),还有就是线程间交换对象。相当于一个栅栏,当两个线程到这里之后就进行数据之间的交换。看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class ExchangerDemo {
/**
* 使用Exchanger进行数据比较的两个方法
* @param exch
*/
private void a(Exchanger<Integer> exch){
System.out.println(Thread.currentThread().getName() + " 的a方法正在执行抓取任务。。。");
try {
Thread.sleep(2500);
Integer result = 12345;
System.out.println(Thread.currentThread().getName() + " 的a方法抓取任务结束,结果为: " + result);
// 这一方法会阻塞直到另一线程比较完毕。
Integer res = exch.exchange(result);
System.out.println("a方法两个线程的结果是否相等: " + res.equals(result));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void b(Exchanger<Integer> exch){
System.out.println(Thread.currentThread().getName() + " 的b方法正在执行抓取任务。。。");
try {
Thread.sleep(2500);
Integer result = 12344;
System.out.println(Thread.currentThread().getName() + " 的b方法抓取任务结束,结果为: " + result);
// 这一方法会阻塞直到另一线程比较完毕。
Integer res = exch.exchange(result);
System.out.println("b方法两个线程的结果是否相等: " + res.equals(result));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 使用Exchanger进行数据交换的例子
* @param exchanger
*/
private void c(Exchanger<List<String>> exchanger){
List<String> list = new CopyOnWriteArrayList<String>(){
{
add("steve");
add("steve1");
add("steve2");
add("steve3");
}
};
System.out.println("c方法负责生产对象");
try {
Thread.sleep(1000);
System.out.println("a 方法生产完毕");
// 这个方法开始执行
exchanger.exchange(list);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void d(Exchanger<List<String>> exchanger){
List<String> list = null;
System.out.println("d方法负责消费对象");
try {
Thread.sleep(2000);
list = exchanger.exchange(list);
System.out.println("d方法对象交换完毕。。。");
list.stream().forEach(e -> System.out.print("d " + e + " "));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(2);
ExchangerDemo demo = new ExchangerDemo();
Exchanger<Integer> exch = new Exchanger<>();
Exchanger<List<String>> exchanger = new Exchanger<>();
exec.execute(new Runnable() {
@Override
public void run() {
demo.c(exchanger);
}
});
exec.execute(new Runnable() {
@Override
public void run() {
demo.d(exchanger);
}
});
// exec.execute(new Runnable() {
// @Override
// public void run() {
// demo.a(exch);
// }
// });
// exec.execute(new Runnable() {
// @Override
// public void run() {
// demo.b(exch);
// }
// });
exec.shutdown();
}
}

参考资料:
Java并发新构件之Exchanger
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

5.5 CountDownLatch,CyclicBarrier,Semaphore 源码分析

前面说过,在jdk8里面几乎所有的并发容器都是基于AQS(AbstractQueuedSynchronizer)实现的。这里首先来看CountDownLatch中对AQS的应用(共享锁实现)。
CountDownLatch的CountDown方法

1
2
3
public void countDown() {
sync.releaseShared(1);
}

来看sync类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}

可以知道sync是AQS抽象类的一个实现,countDown方法调用了AQS的一个releaseShared方法,并传入了一个1,接下来看这个方法

1
2
3
4
5
6
7
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

这里调用了tryReleaseShared 方法判断,如果结果为false,方法返回false。这个判断方法在sync里面被重写了,下面来分析这段代码

1
2
3
4
5
6
7
8
9
10
11
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

首先getState方法获取同步器的状态值(在这里是初始化CountDownLatch时传入的值),当状态值为0的时候,返回false,退出循环,当状态值不为0的时候,令nextc = c - 1,调用compareAndSetState(CAS)方法尝试将内存中的state值设置为nextc,如果成功赋值,判断nextc是否为0.并返回结果。这里的compareAndState就是我们常说的CAS。这里不管结果,只需关注state的值是否减少了。当c == 1 的时候,进入到doReleaseShared方法,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

看过上面分析的AQS源码看这段代码就很简单了,这里主要做的事是当条件成立时将FIFO队列里面的头节点的下一个节点设置为等待状态。看unparkSuccessor的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //这里就是释放线程的代码的实现
}

接下来继续看它的await方法

1
2
3
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 可被中断的增加容器计数器的方法
}

看AQS的实现

1
2
3
4
5
6
7
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

当没发生中断的时候,看sync实现的tryAcquireShared方法

1
2
3
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

这里很简单,就是判断当前同步器的状态值是否为0,若为0.表示其他线程都到达了指定的位置,然后调用AQS的doAcquireSharedInterruptibly方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

具体分析和上面AQS差不多,主要作用是把FIFO队列的node前一节点设置为等待唤醒状态。
再来看看CyclicBarrier的await方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

这个它的await里面调用的doAwait方法。首先对这段执行的代码进行lock操作,然后根据Generation的boolean属性broken判断是否被中断(初始化为false)。下一步判断线程是否被中断,如果被中断,将broken值设置true,并唤醒所有在等待中的线程,令等待中线程的数量等于初始化CyclicBarrier传入的值。如果没被中断,往下执行,令index等于count-1,index表示的是等待中线程的数量。如果index等于0,表示没有线程处在等待状态了,那么就执行这个线程命令。接下来看这个无限循环代码。timed默认为false(await方法传入),这个awaitNanos方法表示当前线程等待指定的nanos时间被唤醒。后面还有一系列操作,也挺简单的。就不细说了。

Read More

默认配图
Java

jvm学习(java se8)

1.各种jvm特点介绍(包含以前的):

  1. Sun Classic VM (jdk1.1 版本的虚拟机)

    特点: 全世界第一个商用的虚拟机,只能使用纯解释器的方式来执行java代码

  2. Exact VM (jdk1.2 solari平台)

    特点: 开始具备高性能vm的特点,有准确性内存管理,解释器和编译器混合使用以及二级即时编译特性,但是只在jdk1.2的solari平台使用过

  3. HotSpot VM (jdk1.3发布)

    特点: 热点代码追踪技术等等

  4. KVM (j2ME 时代sun公司的产物)

    特点:简单,轻便,就是比较慢

  5. JRockit VM (sun公司的产品,2008年呗Oracle收购之后就呗HotSpot整合了)

    特点: 快,有垃圾收集器,MissionControl控制套件(用来控制生产环境内存泄漏的)

  6. J9 VM (IBM 为自家java程序开发的一个VM)

    特点: 高度模块化,任意平台通用(SE,EE,ME),快

  7. DalVik (android 平台的VM)

    特点: 性能好,快.只限android平台使用

扩展: 目前主流的 Java 虚拟机有哪些?

2.JVM 的内存管理

jvm的内存管理分为两大部分,线程共享区和线程独占区,具体划分看下图:

接下来重点讲解几个重要的概念:

2.1动态常量池

存在于方法区中,常量都是不变的,为什么叫动态常量池呢,就是因为这个池可以动态的增加(String的intern()方法),看以下代码:

String a = "abc";
String b = "abc";

logger.info(a == b);  // 1

String c = new String("abc");

logger.info(b == c);  // 2

logger.info(b == c.intern()); // 3

在这里变量a和b就是一个常量,值存放在java的线程共享区的方法区中,引用存放在线程独享区中的虚拟机栈中,在方法区中存在一个StringTable相当于一个HashSet,只会存在一个“abc”实例,所以1对应的输出为true。
变量c的引用存放在线程独享区中的虚拟机栈中,值因为是一个变量,存放在线程共享区中的java堆中,所以和b变量的地址不同,2的对应输出为false。
当调用c.intern()方法时,jvm会将对应的值从java堆移动到方法区中,即动态常量池中。所以3对应的输出为true,对应的流程图如下:

2.2直接内存

java的NIO使用,javaNIO是一种面向缓冲的技术实现框架,其中缓冲的内存使用的不是java堆内存,使用的是直接内存,当然同样受限于物理机器的内存。

2.3对象在内存中的布局

对象的创建步骤:

变量包括数据头(header)和变量值(instanceData)和填充(padding),数据头包含了变量的各种运行时数据(哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳…)和类型指针。变量值就是变量的值,填充是由于数据的大小只能为8的倍数,当数据大小不为8的倍数时,就会进行填充位数。

3. 垃圾回收

3.1 在哪里进行垃圾回收?

大部分是线程共享区的java堆,小部分是线程共享区的方法区(可设置关闭这个区的 GC)

3.2什么时候开始回收?

如今的JVM在java堆里面分了两个代,新生代和老年代。新生代里面有三个区,Eden区和两个survivor区。当Eden区满的时候,会触发一次 Minor GC,会将Eden区中剩余的对象拷贝至一个survivor区中,对象的存活年龄加1,清空Eden区,再次存放对象,等Eden区再次满的时候,触发 Minor GC,将Eden区中剩余的对象和survivor区剩余的对象移动至另一个survivor区中,存活年龄再+1。等年龄增长到了默认值的时候,剩余的对象就会从新生代复制到老年代。当老年代快要满的时候(虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小),如果大于,直接触发 Minor GC. 如果不停的 Minor GC 下去,老年代还是要满了,就会根据配置的参数来决定是要同时执行FULL GC 和 Minor GC 还是只执行 Minor GC 。

3.3 怎么回收?

回收算法有两类:

  • 引用计数收集算法

    判断对象的实例是否不再被引用了。当引用为0的时候,就把他标记清除。这样有个问题就是当别的对象实例引用它而引用指针为null的时候,这个就不会被收集。导致垃圾存留。这个算法已经不被使用了。

  • 可达性分析法

    可达性分析法都是需要从根GC 扫描判断对象是否被引用,被引用了就标记起来,扫描完了后再扫描一遍内存,查看哪些对象没被标记,没被标记则为待回收对象。针对所有的对象衍生出三种收集方法

    1. 复制法:新开辟一片内存区域,每标记一个对象,就将标记对象复制一份到新内存上。虽然需要消耗一定的内存,但在对象数量特别少的时候这个算法特别高效(新生代的Eden区使用的就是它)。
    2. 标记-清除算法: 清除所有待回收对象,然后不对内存进行整理,导致内存空间不连续,生成内存碎片。
    3. 标记-整理算法: 在清除完所有待回收对象之后,对内存进行整理排序,虽然成本增加了,但效率确实提高了不少。
      根GC 对象的选择: 虚拟机栈中所引用的对象,方法区中类的引用的对象,方法区中常量的引用的对象,本地方法区中引用的对象。四种选择方案

3.4 垃圾收集器

  • Serial 垃圾收集器
    Serial垃圾收集器是最初的垃圾收集器,在GC的时候需要停止用户线程,如果待回收对象太大会造成卡顿现象,适合于client端的java应用。

  • ParNew 垃圾收集器
    在Serial垃圾收集器的基础上增加了并发收集的实现。可和Cms垃圾收集器一起工作(ParNew负责新生代的收集工作),

  • Parallel 垃圾收集器
    也是并行的多线程收集器,只不过关注点在于控制垃圾收集的吞吐量(其他的关注点在于减少垃圾的收集时间)。

  • Cms垃圾收集器
    这个是可以并发执行的一款垃圾收集器,执行流程如下图:

  • G1 垃圾收集器
    目前为止最牛逼的垃圾收集器,智能型垃圾收集器,虽然原理和流程和Cms垃圾收集器一样,但是里面却有几个不同的概念,比如Region,可预测时间模型等等。

参考: 深入理解JVM(3)——7种垃圾收集器
扩展: JVM内存管理及GC机制

4 内存分配

4.1 对象优先分配到Eden区上

在jvm中,除大对象以外,所有的对象几乎都是优先分配Eden区里面,看以下代码:

1
2
3
4
5
6
7
8
9
10
11
public static final int M = 1024 * 1024;
public static void main(String[] args) {
byte[] b1 = new byte[2 * M];
byte[] b2 = new byte[2 * M];
byte[] b3 = new byte[2 * M];
byte[] b4 = new byte[4 * M];
System.gc();
}

配置运行参数为: -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=8 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
[GC (Allocation Failure) [PSYoungGen: 6914K->952K(9216K)] 6914K->5056K(19456K), 0.0045301 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
[GC (System.gc()) --[PSYoungGen: 7252K->7252K(9216K)] 11356K->13412K(19456K), 0.0019800 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 7252K->4840K(9216K)] [ParOldGen: 6160K->6145K(10240K)] 13412K->10986K(19456K), [Metaspace: 3494K->3494K(1056768K)], 0.0071106 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 5087K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 62% used [0x00000000ff600000,0x00000000ffaf7c38,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6145K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200518,0x00000000ff600000)
Metaspace used 3509K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K

这里设置了Eden区大小为8M,两个Survivor区各1M,老年代10M。开始将b1,b2,b3都分配在Eden区,在分配b4的时候,会用到空间分配担保(-XX:+HandlePromotionFailure)技术(jdk8默认开启),然后会将b1,b2,b3 分配至老年代,b4分配到Eden区,占4M

4.2 大对象优先分配到老年代

在jvm的理解中,大对象一般不会变化的,所以会将大对象放入老年代。大对象的确定是动态确定的,根据你设置的Eden去大小而定,大于Eden区,就是大对象,直接将其放入老年代储存。

4.3多次存活的对象分配到老年代

jvm有一个存活阈值,对象生存年龄大于这个阈值就会将对象存入老年代,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,有可能存活一两次就进去了。设置阈值的参数是: -XX:MaxTenuringThreshold=15 这个参数指定的是阈值年龄的最大值,默认是15

4.4 逃逸分析和栈上分配

对象放入堆区之前会进行逃逸分析,当方法内的对象不引用别的方法区域内的对象或者全局变量,则称该变量不发生逃逸,会将该变量进行栈上分配,也就是分配到线程独占区的虚拟机栈区里面。否则分配到java堆里面。

本节扩展【JVM】12_空间分配担保

5 性能分析工具

5.1 Jps

jps 就是查看java进程id和开启进程具体命令的一个工具.类似linux的ps命令. 常用使用方法为: jps -mlv (-m: 查看运行的参数,-l: 具体到包名的运行类, -v: 查看运行的vm参数)。 结果截图:

5.2 Jstat

jstat 是用来查看java进程的各种详细信息,类似linux的top命令,它有多个运行参数。常用命令如下: jstat -gcutil [vmid] vmid 即java进程Id(PID),vmid 可以根据jps命令获取到。例如: jstat -gcutil 20412,得到的结果如下所示:

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.81   0.00  78.06  32.64  93.06  89.60    784   17.194    70   72.187   89.381

S0: survivor 0 区占用的百分比(KB)
S1: survivor 1 区占用的百分比(KB)
E: Eden 区占用的百分比(KB)
O: 老年代占用的百分比(KB)
M: Metadata 空间利用率(KB)
CCS: 类压缩空间的百分比(KB)
YGC: Minor GC总次数
YGCT:Minor GC总耗时(s)
FGC: Full GC 总次数
FGCT: Full GC总耗时(s)
GCT: 用于垃圾回收的总时间(s)

更多Jstat 选项信息参考jstat命令官方文档

5.3 Jinfo

jinfo命令是用来查看并修改java进程运行的VM参数的命令。使用方法: jinfo -flag [vmid], 例如:

+ 表示启用了这个参数(用的是Parallel GC)
更多使用方法: jinfo -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usage:
jinfo [option] <pid>
(to connect to running process)
jinfo [option] <executable <core>
(to connect to a core file)
jinfo [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
-flag <name> to print the value of the named VM flag
-flag [+|-]<name> to enable or disable the named VM flag # 这个参数就是开启或关闭某个VM功能
-flag <name>=<value> to set the named VM flag to the given value # 这个就相当于开启并设置参数值
-flags to print VM flags
-sysprops to print Java system properties # 列出系统所有属性(windows可能用不了)
<no option> to print both of the above
-h | -help to print this help message

5.4 Jmap

jmap 用来查看java进程的堆栈信息,可根据选项打印到控制台或者保存到文件里面.
帮助信息: jmap -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live" # 直接在控制台打印java进程的内存使用信息
suboption is specified, only count live objects
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system

例如: jmap -dump:format=b,file=f:\jmap.bin 13756
该命令会将 pid为13756进程id的信息保存到f盘的jmap.bin文件里,怎么打开?请看下文

5.5 Jhat

jhat命令就是用来打开jmap命令生成的二进制文件的,应为它是非常耗费内存和cpu的,所以一般会在自己本地环境运行这个命令来查看java进程内存使用信息,而不会在服务端做这种事。打开后,会在http服务器中将这些信息展示出来,默认端口为7000,可通过 -port 参数指定。
帮助信息: jhat -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Usage: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
-J<flag> Pass <flag> directly to the runtime system. For
example, -J-mx512m to use a maximum heap size of 512MB
-stack false: Turn off tracking object allocation call stack.
-refs false: Turn off tracking of references to objects
-port <port>: Set the port for the HTTP server. Defaults to 7000
-exclude <file>: Specify a file that lists data members that should
be excluded from the reachableFrom query.
-baseline <file>: Specify a baseline object dump. Objects in
both heap dumps with the same ID and same class will
be marked as not being "new".
-debug <int>: Set debug level.
0: No debug output
1: Debug hprof file parsing
2: Debug hprof file parsing, no server
-version Report version number
-h|-help Print this help and exit
<file> The file to read
For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".
All boolean options default to "true"

例如: jhat f:\jmap.bin
等待一会,看到 Started HTTP server on port 7000 字样的时候,打开 http://127.0.0.1:7000 就可以看到所有的实例信息了,这里我们主要看两个地方,一个是 http://127.0.0.1:7000/histo/ ,查看Heap Histogram,查看的是所有类型对象的实例总数和实例大小。还有一个是 http://127.0.0.1:7000/oql/, 一个oql(Object query Language)查询工具,和sql一样,可以根据条件筛选出指定的对象,例如下面这个语句就是用来查询所有字符串长度大于1000的字符串对象:

1
select s from java.lang.String s where s.value.length > 1000

更多有关oql的语法可以查看他的帮助文档 http://127.0.0.1:7000/oqlhelp/

5.6 Jstack

jstack 命令就是用来查看java进程的线程信息的一个工具。帮助: jstack -help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)
Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

例如: jstack -l 13756
会在控制台打印出许多有关的线程信息,其实也就是Thread的getAllStackTraces() 方法返回出来的信息,截取其中的一段显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"Netty Builtin Server 1" #16 prio=5 os_prio=0 tid=0x0000000018f2b000 nid=0x2100 runnable [0x000000001bf3f000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(Native Method)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:296)
at sun.nio.ch.WindowsSelectorImpl$SubSelector.access$400(WindowsSelectorImpl.java:278)
at sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:159)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
- locked <0x00000000e155f760> (a io.netty.channel.nio.SelectedSelectionKeySet)
- locked <0x00000000e155f790> (a java.util.Collections$UnmodifiableSet)
- locked <0x00000000e29d20e8> (a sun.nio.ch.WindowsSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
at io.netty.channel.nio.SelectedSelectionKeySetSelector.select(SelectedSelectionKeySetSelector.java:62)
at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:752)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:408)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None

5.7Jconsole 和 VisualVM

Jconsole是jdk自带的可视化分析工具,VisualVM是一个集众多jvm分析插件工具于一身的jvm强大实时可视化分析工具,Jconsole在VisualVM中就是一个插件而已,主页VisualVM。能有效分析内存分配和线程等等指标。VisualVM启动前需要配置jdk的路径,否则会显示找不到jdk。

6 Class文件结构

Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据。根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数相当于java中的基本数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数。表相当于抽象数据类型,由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯性地以“_info”结尾。整个 Class 文件本质上就是一张表,它由如下所示的数据项构成:

名称 类型 数量
magic U4 1
minor_version U2 1
major_version U2 1
constant_pool_count U2 1
constant_pool cp_info constant_pool_count - 1
access_flags U2 1
this_class U2 1
super_class U2 1
interfaces_count U2 1
interfaces U2 interfaces_count
fields_count U2 1
fields field_info fields_count
methods_count U2 1
methods method_info methods_count
attributes_count U2 1
attributes attribute_info attributes_count

这个表的顺序就是class文件的解析顺序,接下来看一个 java 类(Hello.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class Hello extends FileInputStream implements Runnable,ActionListener{
private String name;
public Hello(String name) throws FileNotFoundException {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void run() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

编译后的字节码为(Hello.class):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
CA FE BA BE 00 00 00 34 00 1F 0A 00 04 00 18 09
00 03 00 19 07 00 1A 07 00 1B 07 00 1C 07 00 1D
01 00 04 6E 61 6D 65 01 00 12 4C 6A 61 76 61 2F
6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 3C
69 6E 69 74 3E 01 00 15 28 4C 6A 61 76 61 2F 6C
61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04
43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65
72 54 61 62 6C 65 01 00 0A 45 78 63 65 70 74 69
6F 6E 73 07 00 1E 01 00 0F 61 63 74 69 6F 6E 50
65 72 66 6F 72 6D 65 64 01 00 1F 28 4C 6A 61 76
61 2F 61 77 74 2F 65 76 65 6E 74 2F 41 63 74 69
6F 6E 45 76 65 6E 74 3B 29 56 01 00 03 72 75 6E
01 00 03 28 29 56 01 00 07 67 65 74 4E 61 6D 65
01 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F
53 74 72 69 6E 67 3B 01 00 07 73 65 74 4E 61 6D
65 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00
0A 48 65 6C 6C 6F 2E 6A 61 76 61 0C 00 09 00 0A
0C 00 07 00 08 01 00 05 48 65 6C 6C 6F 01 00 17
6A 61 76 61 2F 69 6F 2F 46 69 6C 65 49 6E 70 75
74 53 74 72 65 61 6D 01 00 12 6A 61 76 61 2F 6C
61 6E 67 2F 52 75 6E 6E 61 62 6C 65 01 00 1D 6A
61 76 61 2F 61 77 74 2F 65 76 65 6E 74 2F 41 63
74 69 6F 6E 4C 69 73 74 65 6E 65 72 01 00 1D 6A
61 76 61 2F 69 6F 2F 46 69 6C 65 4E 6F 74 46 6F
75 6E 64 45 78 63 65 70 74 69 6F 6E 00 21 00 03
00 04 00 02 00 05 00 06 00 01 00 02 00 07 00 08
00 00 00 05 00 01 00 09 00 0A 00 02 00 0B 00 00
00 22 00 02 00 02 00 00 00 06 2A 2B B7 00 01 B1
00 00 00 01 00 0C 00 00 00 0A 00 02 00 00 00 0B
00 05 00 0C 00 0D 00 00 00 04 00 01 00 0E 00 01
00 0F 00 10 00 01 00 0B 00 00 00 19 00 00 00 02
00 00 00 01 B1 00 00 00 01 00 0C 00 00 00 06 00
01 00 00 00 11 00 01 00 11 00 12 00 01 00 0B 00
00 00 19 00 00 00 01 00 00 00 01 B1 00 00 00 01
00 0C 00 00 00 06 00 01 00 00 00 16 00 01 00 13
00 14 00 01 00 0B 00 00 00 1D 00 01 00 01 00 00
00 05 2A B4 00 02 B0 00 00 00 01 00 0C 00 00 00
06 00 01 00 00 00 19 00 01 00 15 00 0A 00 01 00
0B 00 00 00 22 00 02 00 02 00 00 00 06 2A 2B B5
00 02 B1 00 00 00 01 00 0C 00 00 00 0A 00 02 00
00 00 1D 00 05 00 1E 00 01 00 16 00 00 00 02 00
17

这里的两个数字为一个字节,且都是10进制,首先名字为magic(魔数)的占四个字节,即 CA FE BA BE, 这个是标识这个文件为class文件的,不可能随便改个后缀名就是class文件了吧,读作: 咖啡baby(是不是和java的标志很像). 第二个是 minor_version(次版本), 占两个字节,即 0000, 表明这个class文件的此版本号(class 文件作为一个规范也是有版本号的),接下来是主版本号0034,转成16进制为52,即jdk8对应的版本号。
接下来是constant_pool(常量池)相关的知识点了,常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用总结起来则包括了下面三类常量:

  • 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)
  • 字段的名称和描述符(private、static 等描述符)
  • 方法的名称和描述符(private、static 等描述符)
    虚拟机在加载 Class 文件时才会进行动态连接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

constant_pool 的类型 cp_info 的通用结构如下:

1
2
3
4
cp_info {
u1 tag;
u1 info[];
}

每个constant(常量)开头都是由1个字节的tag标识其属于哪种cp_info,下面是一个字节的info数组(其它信息,这里只是放在一个数组里面进行通用的描述)。在jdk8中,各种各样的constant_pool_tags如下表所示(共14种):

常量类型 值(10进制)
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

这里面就包括上面所说的两大类常量:

  • 字面量: Utf8,Integer,Float,Long,Double,String
  • 引用类型: Class,Fieldref,Methodref,InterfaceMethodref,NameAndType,MethodHandle,MethodType,InvokeDynamic

接下来我们解析一下常量池部分
constatn_pool_count(常量池数量),001F表明有30个常量([1,31)),接下来是constant_pool(常量池), 由cp_info组成,首先根据tag找对应的tag信息(1字节),开始是0A, 二进制的10(#1),找到对应的 CONSTANT_Methodref 类常量(表明这是个引用类型的常量),去官方文档找它的结构如下:

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

然后找class_index , 两个字节,0004,记住它引用的是4号常量(记作#4),继续,name_and_type_index, 0018, 即24,引用的是24号常量(#24)。
再看下一个常量(#2): 09,表明是一个CONSTANT_Fieldref 类型的引用类型的常量。对应的结构为

1
2
3
4
5
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

和method一样,得到他的引用变量为3号常量(#3)和25号常量(#25).继续这样寻找下去,直到找到 CONSTANT_Utf8 字面量类型的常量,,然后根据他的结构信息

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

得到它的具体值,length表明的是接下来有几个字节数。下来就是根据这写10进制数找到对应的ascii表的字母了。 其实以上信息可以用jdk自带的javap命令帮你读取出来:

执行: javap -verbose Hello.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
PS C:\Users\liu\Desktop> javap -verbose .\Hello.class
Classfile /C:/Users/liu/Desktop/Hello.class
Last modified 2017-11-11; size 657 bytes
MD5 checksum 8ed8a940ffff9d0b7b27a59e1e90abe7
Compiled from "Hello.java"
public class Hello extends java.io.FileInputStream implements java.lang.Runnable,java.awt.event.ActionListener
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#24 // java/io/FileInputStream."<init>":(Ljava/lang/String;)V
#2 = Fieldref #3.#25 // Hello.name:Ljava/lang/String;
#3 = Class #26 // Hello
#4 = Class #27 // java/io/FileInputStream
#5 = Class #28 // java/lang/Runnable
#6 = Class #29 // java/awt/event/ActionListener
#7 = Utf8 name
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 <init>
#10 = Utf8 (Ljava/lang/String;)V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 Exceptions
#14 = Class #30 // java/io/FileNotFoundException
#15 = Utf8 actionPerformed
#16 = Utf8 (Ljava/awt/event/ActionEvent;)V
#17 = Utf8 run
#18 = Utf8 ()V
#19 = Utf8 getName
#20 = Utf8 ()Ljava/lang/String;
#21 = Utf8 setName
#22 = Utf8 SourceFile
#23 = Utf8 Hello.java
#24 = NameAndType #9:#10 // "<init>":(Ljava/lang/String;)V
#25 = NameAndType #7:#8 // name:Ljava/lang/String;
#26 = Utf8 Hello
#27 = Utf8 java/io/FileInputStream
#28 = Utf8 java/lang/Runnable
#29 = Utf8 java/awt/event/ActionListener
#30 = Utf8 java/io/FileNotFoundException

下面这些#号开头的都是我们的常量信息。
常量信息结束后开始解析我们的 access_flags (访问标记),用工具可以看出来我们这里的访问标记为0021,这个是啥意思呢,还是看官方文档给我们的信息

标识名 值(16进制)
ACC_PUBLIC 0x0001
ACC_FINAL 0x0010
ACC_SUPER 0x0020
ACC_INTERFACE 0x0200
ACC_ABSTRACT 0x0400
ACC_SYNTHETIC 0x1000
ACC_ANNOTATION 0x2000
ACC_ENUM 0x4000

对几个特殊的标识解释一下:

  • ACC_SUPER : 是否有超类(简单解释)
  • ACC_SYNTHETIC: 非源码中的类
  • ACC_ABSTRACT: 声明为抽象的,不可实例化的类。

这就是我们的类的开始, 这个0021 就是 0020 加上 0001 的结果。也就是说这个类是PUBLIC的而且有父类。
接下来是this_class,占了两个字节,为0003,指向的是3号常量,可以查看上面的常量信息,看到是 Hello 这个类。接下来是 super_class,两个字节,0004,是 java/io/FileInputStream这个类, 下来是interfaces_count,这是因为java是单继承多实现机制的,才会有的这个变量.是0002,表明共有两个接口类,接下来看interfaces,共有两个,分别是0005 和 0006, 代表的是5号和6号变量,Runable 和 ActionListener,
下面继续就是变量,方法,属性的语义部分了,和类差不多,有访问标记,名字和修饰符,不同的是属性就是传参的属性和些格式的问题,可以参考官方文档或者《java虚拟机规范(java se8版)》。其实我们的javap命令还有些输出上面没贴出来,这里加上注释,就能看出class文件后面的大概结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
public Hello(java.lang.String) throws java.io.FileNotFoundException; #构造函数,从这里可以看出编译器会为我们的类生成一个默认的构造函数方法
descriptor: (Ljava/lang/String;)V # 这个就是属性了,括号里面就是属性类型,L开头就是引用类型,括号外面的V标识返回类型为void类型
flags: ACC_PUBLIC # 访问标记为Public类型
Code: # 这个Code标识方法
stack=2, locals=2, args_size=2 #stack表示方法的执行栈深度,最大为2, locals是局部变量表所需空间,用来存放方法内部的变量的,args_size为方法参数的数量,非static方法都会加上类的引用this参数,所以这里为2
0: aload_0 # 这些是具体的指令名
1: aload_1
2: invokespecial #1 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
5: return
LineNumberTable:
line 11: 0
line 12: 5
Exceptions: # Exceptions标识方法抛出的异常
throws java.io.FileNotFoundException
public void actionPerformed(java.awt.event.ActionEvent);
descriptor: (Ljava/awt/event/ActionEvent;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 17: 0
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 22: 0
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 25: 0
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 29: 0
line 30: 5
}

扩展:jvm解析系列][六]class里的常量池,访问标志,类的继承关系

7. 类加载机制

先来看类加载流程图

注意: 这里的加载和连接时并行进行的(加载一开始,连接也开始进行),和流程图画的不同。
再来细看每一步jvm做的事情:

7.1 加载

加载需做一下三件事:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

第一条中的二进制流并不只是从class文件中获取,共有以下几种获取方式:

  • class文件
  • 网络(Applet)
  • 计算生成(jdk的动态代理)
  • jar包
  • 其它文件生成(jsp等)

加载是通过类加载器来进行的,可以自定义类加载器(重写ClassLoader类的loadClass方法)来实现自定义类的加载过程。最终会将加载的二进制流储存进线程共享的方法区中,并且会在java堆中生成一个java.lang.Class对象,这样可以通过这个对象访问方法区中的数据。

7.2 验证

验证主要是验证Class文件中的字节流符合当前虚拟机的规范,主要包括一下四种

  • 文件格式验证(魔数)
  • 元数据验证(java数据类型)
  • 字节码验证(对代码进行校验,保证虚拟机安全运行)
  • 引用验证(看常量池中的各种引用符号进行匹配性校验)

7.3 准备

准备是正式的给类变量分配内存和初始值的一个阶段,所有属于类的变量(static)就分配到方法区里面,实例变量则会在对象实例化时随着对象一块分配在java堆中,同时根据不同类型的变量赋予不同的初始值。注意,这个时候并不会直接给变量赋予代码里的那个具体数值(常量除外)。还有几点:

  • 对于基本数据类型来说,类变量(static)和全局变量,如果没有给他显示的赋予值,虚拟机会为它赋予默认的初始值.如果是局部变量没有赋予初始值的话,就会报编译错误。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

7.4 解析

前面提到过符号引用这个概念,这个阶段就是将符号引用转为直接引用的过程。解析阶段的顺序不一定是在初始化之前,也有可能是在初始化之后,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。

7.5 初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器\()方法的过程。
这里简单说明下\()方法的执行规则:

  1. \()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  2. \()方法与实例构造器\()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的\()方法执行之前,父类的\()方法已经执行完毕。因此,在虚拟机中第一个被执行的\()方法的类肯定是java.lang.Object。
  3. \()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成\()方法。
  4. 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成\()方法。但是与类不同的是:执行接口的\()方法不需要先执行父接口的\()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的\()方法。
  5. 多线程情况下只会给类执行一遍\()方法.

7.6 类加载器与双亲委派模型

一张图说明类加载器的执行方式:

  • 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器: 重写ClassLoader中的loadClass方法就可自定义实现。主要优点是高度的灵活性,可实现代码的热部署,class文件的解密等等。

下麦呢看一下官方实现的类加载器loadClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1); // 检查这个类是否被加载过
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false); // 递归调用父类加载器去尝试加载( 应用程序类加载器 ---> 扩展类加载器 ----> 启动类加载器 )
} else {
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
;
}
if (var4 == null) { // 如果还是加载不成功,就自己去实现加载功能。
long var7 = System.nanoTime();
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}

扩展:【深入Java虚拟机】之四:类加载机制

Read More

默认配图
Java

无状态RESTFul web服务实现

项目架构图:

这里只写我司的认证中心设计,先从认证流程图开始:

先从第二步开始(Oauth2的认证 /oauth/login 接口):

/oauth/login 资源接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public Response<Map<String, Object>> login(String username,
String password, HttpServletRequest request,
HttpServletResponse response)
throws HttpRequestMethodNotSupportedException {
// 非空校验(前端也有部分校验)
Asserts.notEmpty(username);
Asserts.notEmpty(password);
// Oauth2的 密码授权模式
Map<String, String> map = Maps.newHashMap();
// 获取数据库配置的 clientId
String clientId = ${clientId};
map.put("username", username);
map.put("password", password);
map.put("grant_type", "password");
map.put("scope", "read write trust"); // 权限共有 read write trust 三种
// 构建客户端的Authentication
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
clientId, "", Lists.newArrayList());
// 调用Spring security Oauth2 的postAccessToken 生成 Oauth2AccessToken
ResponseEntity<OAuth2AccessToken> responseEntity = tokenEndpoint
.postAccessToken(token, map);
OAuth2AccessToken oAuth2AccessToken = responseEntity.getBody();
JwtAuthenticationToken authenticationToken = null;
if (oAuth2AccessToken != null) {
// 对生成的token包装成Jwt的实现, tokenProvider是自己写的一个接口,共有两个方法,下文会分析
authenticationToken = tokenProvider.createToken(request, response,
oAuth2AccessToken);
}
// 把一些不敏感的信息提取出来
Map<String, Object> oauthInfo = extract(authenticationToken);
// 存入redis
tokenRedisService.putOauthInfo(authenticationToken.getAccessToken(),oauthInfo);
return Response.ok(oauthInfo);
}

这里首先构建了一个UsernamePasswordAuthenticationToken类型的Authentication.然后调用TokenPoint的postAccessToken方法去创建一个Oauth2AccessToken, 接下来通过Oauth2AccessToken创建一个JwtAuthenticationToken, 在创建JwtAuthentiationToken的时候就会将access_token等信息写入客户端Cookie, 再从token里面取一些不敏感信息返回。

TokenEnpoint的PostAccessToken方法(Spring 官方实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
// 有五种类型的Granter,我司只实现了两种。
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}

接下来将第一步的实现。因为每个服务都是一个资源服务器,所以在资源服务器的配置文件里配置了对应的信息:

资源服务器的配置文件(省略部分信息):

1
2
3
4
5
6
7
8
9
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.accessDeniedHandler(new ThrowAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new ThrowEntryPoint(objectMapper))
.stateless(true)
// 重点是这个配置,通过这个来实现
.tokenServices(new UAALoadBalancerUserInfoTokenServices(loadBalancerClient,resourceServerProperties.getServiceId(), resourceServerProperties.getUserInfoUri()));
}

这里是配置获取用户信息的地址(userInfoUrl).和官方的UserInfoTokenServices不同的是这里用的是从注册中心获取认证中心的实例地址,用到了Ribbon的LoadBalancerClient来根据serviceId获取认证中心的物理地址。核心的getMap方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected Response getMap(String path, String accessToken) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken);
token.setTokenType(this.tokenType);
((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().setAccessToken(token);
}
return (Response)((OAuth2RestOperations)restTemplate).getForEntity(path, Response.class, new Object[0]).getBody();
} catch (Exception var6) {
LOGGER.warn("Could not fetch user details: " + var6.getClass() + ", " + var6.getMessage());
return null;
}
}
}

和官方的实现不同就是返回值不同。接下来再看认证中心对应的接口实现(/oauth/me):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RequestMapping(method = RequestMethod.GET, value ="/oauth/me")
public Map<String, Object> me(
@PathVariable(required = false) String service,
OAuth2Authentication auth) {
Map<String, Object> me = Collections.EMPTY_MAP;
//往用户信息里面添加权限信息
List<SimpleGrantedAuthorityImpl> authorities = new ArrayList<SimpleGrantedAuthorityImpl>();
logger.info(
"/oauth/me request... OAuth2Authentication[{}] service[{}]",
auth, service);
// 认证失败
if (auth == null) {
throw new OAuth2Exception("Authorization message is not null.");
}
OAuth2Request request = auth.getOAuth2Request();
if (request == null) {
me.clear();
throw new OAuth2Exception("OAuth2Request message is not null.");
}
Authentication authentication = auth.getUserAuthentication();
// 登录用户授权模式 password,authorization_code,refresh_token
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
UserDetails userInfo = userService
.selectByUserNameOrMobileOrEmail(usernamePasswordAuthenticationToken
.getName());
// 将密码清空
userInfo.setLoginPasswd("");
me.clear();
// 授权部分
//...
userInfo.setAuthorities(authorities);
me.put("name", userInfo.getLoginName());
// USERINFO_FIELDS_CACHE 常量是UserDetail的所有属性集合
for (Field field : USERINFO_FIELDS_CACHE) {
// 找寻所有的get方法
Method m = ReflectionUtils.findMethod(userInfo.getClass(),
SecurityUtils.getMethodName(field.getName()));
if (null != m) {
me.put(field.getName(),
ReflectionUtils.invokeMethod(m, userInfo));
}
}
return Response.ok(me);
}
// 如果不是这几种授权类型,抛出异常
me.clear();
throw new OAuth2Exception("Bad request.");
}

在认证中心创建token时对token还进行了部分操作,这里就不细说了。数据库表设计的话是Oauth2官方提供的那个Schema加上根据Rbac法则设计的业务表。这里看看流程即可。学的只是个思想。

Read More

默认配图
Java

Jenkins实现CI

目前公司初步涉及微服务架构,那么持续集成就比较重要了,采用的CI的流程如图:

流程说明:

公司的项目需要使用到windows服务器,所以初步就这样做了,从svn拉取最新的项目进行构建,构建完成之后调用脚本文件发送最新的项目war包至开发和测试的应用启动目录,再配置一个监控目录文件改动的脚本,进而实现启动最新的项目,但每次去服务器的jenkins控制台启动项目不方便,而且不是每个开发者都会jenkins,所以在控制jenkins上我又写了一个ps脚本,通过远程在公司内网调用jenkins实现项目的构建,这样一套流程下来,每个开发者提交了项目之后只需启动我给的脚本过一会就能看到上线的效果了.

具体实现流程:

  1. jenkins安装 进入: Jenkin下载页面 选择 Generic Java package(.war)
  2. jenkins配置(maven,jdk,svn路径) ,maven项目支持需安装 Maven Release Plug-in Plug-in 插件
  3. 创建maven项目,配置好就行(开始不需要填写构建后操作),立即构建会在jenkins安装目录下生成workspace和项目目录,我们需要的就是 workspace目录,
  4. 将项目构建成功后的ps脚本拷贝至workspace目录,在项目配置页里面配置调用脚本命令(cmd 调用ps脚本需要特殊配置,下文会讲解)
  5. 到这一步就完成了将最新的项目war包拷贝至开发,测试服务器的功能了,然后在启动远程调用的ps脚本,就可以通过调用jenkins api启动项目的构建了

脚本讲解:

  1. 首先是构建成功后的ps脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# ps version 2.0 scripts
# this script is used to copy project to $PROJECTS_HOME after it loaded by jenkins projects
#################### HOW TO USE IT ##########################
# first copy this script to $JENKINS_HOME (default= "C:\Users\Administrator\.jenkins")
# then in jenkins project config page ( url = "http://server:port/job/project/configure" it must be maven project) find Post Steps block
# choose " Execute Windows batch command " and write next code in the input area
### code in jenkins config page start #####
# @echo off
# cd ..
# powershell.exe -noprofile Set-ExecutionPolicy Unrestricted
# powershell.exe -file automate.ps1 -job %JOB_NAME%
### code end ###############################
# this script will auto close after execute over
param(
[string]$job=$(throw "Parameter missing: -job %JOB_NAME%")
)
$configFile="ps.json"
$targetDrive="D:"
$targetDir="project-dev"
# 运行中脚本的绝对路劲
$scriptPath=Split-Path -Parent $MyInvocation.MyCommand.Definition
if(!(Test-Path $configFile)){
throw "$configFile 配置文件不存在!"
}
# 初始化函数
function init{
#write-host "init 方法开始执行"
$targetPath=$targetDrive+"\"+$targetDir
if(!(Test-Path -path $targetPath)){
if(!(Test-Path -path $targetDrive)){
throw " $targetDrive 盘符不存在,请创建一个再重试。 "
}
New-Item -path $targetPath -type directory
}
return $targetPath
}
function main{
#write-host "main 方法开始执行"
$targetPath=init
write-debug "$targetPath"
Start-Sleep -Seconds 10
$modules=Get-Content $configFile -encoding utf8 | ConvertFrom-Json
foreach($module in $modules){
write-debug "$module.server : $module.port"
if($module.server -eq $job){
# stopserver
stopServer $module
copyFile $module $targetPath
}
}
}
function copyFile($module,$targetPath){
#Write-Debug "copyFile 方法开始执行"
if(($module -eq "") -or ($module -eq $null)){
throw "$module must not be null"
}
$date=get-date -format MMdd_HHmm
$bacName=$module.server+"_"+$date+"_back"+$module.suffix
Write-Host "当前脚本执行目录: $scriptPath"
$proFloder=$module.server
$copyFileName=$module.fullName+"-"+$module.version+$module.suffix
Write-Host "备份文件名: $bacName"
if(Test-Path -path $targetPath){
$copyFilePath=$targetPath+"\"+$proFloder
if(Test-Path -path $copyFilePath){
$fileName=$module.fullName+"-"+$module.version+$module.suffix
if(Test-Path -path $module.server){
$copy1=$copyFilePath+"\"+$fileName
$copy2=$copyFilePath+"\"+$bacName
Copy-Item $copy1 $copy2
}else{
Write-Debug "无需备份"
}
}else{
Write-Debug "目标文件夹不存在"
New-Item -type directory -Path $copyFilePath
}
cd $scriptPath
$loadProPath=""
# 公司项目结构不统一,加上这个判断获取文件路径
if($module.hasSecond){
$loadProPath=$proFloder+"\"+$module.fullName+"\target"
}else{
$loadProPath=$proFloder+"\target"
}
if(Test-Path -path $loadProPath){
cd $loadProPath
if(Test-Path -path $copyFileName){
Copy-Item $copyFileName $copyFilePath
}else{
write-host "待复制的文件不存在,请重新构建"
}
}
}else{
Write-Debug "$targetPath 未创建成功" #可能权限问题,没遇到过
}
}
# jenkins和开发服务器放在一起的做法,否则关闭功能单独放在一个脚本里实现
function stopServer($module){
#Write-Debug "stopServer 方法开始执行"
$port=$module.port
$content=(netstat -ano | findstr $port) #这里调用的是cmd语法,ps的没这么直接的功能
if(($content -eq "") -or ($content -eq $null)){
echo "当前端口 $port 没程序正在运行"
return
}
$content=$content.split("\n")[0]
$proPID=$content.split(" ")[-1]
if($proPID -match "[\d]+"){
Stop-Process -Id $proPID -Force
}else{
echo "进程id为: $proPID 的进程停止不了,请手动关闭后再试" #这种情况还没遇到过
}
}
main

main 函数为脚本的入口,write-debug 是测试的时候输出用的,write-host是正式运行时输出用的
由于直接在jenkins里面运行项目会导致关闭困难,所以我们就在外面运行了,上面实现了文件的复制(开发服务器 + 测试服务器),下面就来实现文件的监控,运行,关闭了。文件拷贝至服务器,我们需要监控文件夹的改变才能知道文件更新了,进而才能重启项目,实现更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
write-host "jar监听"
$floder="X:\project-test" # 测试服务器上的监控文件
#文件类型
$jar="*.jar"
#时间间隔
$timeout = 1 # 60s
$projects_path = "configure.txt"
$bat_path = "..\project-test\bats"
#创建监控对象
if(test-path "$floder"){
Write-Host "文件夹已存在,不需要创建!"
}else{
md $floder
}
#监控所有jar文件的改变
$jar_watch = new-object System.IO.FileSystemWatcher $floder,$jar
$jar_watch.IncludeSubDirectories = $true
Write-Host "Ctrl+C 退出文件夹的监控"
while($true){
$jar_result = $jar_watch.WaitForChanged('Changed',$timeout)
if ($jar_result.TimedOut -eq $false)
{
$lastof = $jar_result.Name.LastIndexOf("\")
#覆盖的项目名称
$jar_name = $jar_result.Name.Substring($lastof + 1)
#write-host $jar_name
#读取项目数组
$array_projects = Get-Content $projects_path
#write-host $array_projects.Contains($war_name)
foreach($pa in $array_projects) {
write-host $pa
if($jar_name.contains($pa)) {
$bat_name = "start"+$pa.Split(".")[0]+".bat"
write-host $jar_name
cd $bat_path
start $bat_name
}
}
}
}
write-host "取消监控"

这是jar类型文件的监控,只监控change事件的文件改变。然后先启动关闭脚本关闭项目,再启动启动脚本启动项目。启动和关闭就不贴了。上面有关闭的部分代码,启动的话就和普通命令一样了。接下来再看看远程api操作jenkins进行构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
##################### HOW TO USE IT ########################
## 自动构建项目脚本,通过jenkins的远程api调用来实现的
## 构建的项目是根据文件名字而变化的,所以必须保证文件名的正确性(大小写敏感)
$user = "xx"
$password = "xx"
$pair = "$($user):$($password)"
$job = $MyInvocation.MyCommand.Name.Split('.')[0]
$url = "//serverurl/job/"+ $job + "/build"
$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$basicAuthValue = "Basic $encodedCreds"
$basicHeader = @{
Authorization = $basicAuthValue
}
Invoke-WebRequest -Uri http:$url -Method Post -Headers $basicHeader

这是通过设置headers来进行basic authentication的。/job/jobname/build/ 来进行执行构建请求。jobname是根据文件名来获取的,所以只需要一个文件,改个名字,其他微服务就都能跑了。

在有些系统里powershell脚本默认是记事本打开的,需要改变默认打开方式,在打开方式这里选择

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe 为默认打开方式,首次执行powershell脚本还需要在cmd窗口执行以下命令 powershell.exe -noprofile Set-ExecutionPolicy Unrestricted 开启执行权限

Read More

默认配图
返回顶部