哲学家就餐

问题描述:指定有五个哲学家(当然可以是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
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
/**
* 哲学家就餐问题
* 问题描述:指定有五个哲学家(当然可以是n个哲学家),每个哲学家有两件事必须做,第一件事就是思考,第二件事就是吃饭,
* 当他们思考的时候不需要任何的共享资源,我们可以看作是是一个独立的事件,当他们就餐的时候,就需要餐具来进行就餐。
* 他们围坐在一个圆桌前面,每两个哲学家之间就有一个筷子,我们知道筷子只有两个的时候,才可以方便使用,当一个哲学家要吃饭的时候,
* 那就需要拿起他两边的筷子,如果筷子被别人拿起了,则该哲学家就需要等待。其他人释放这个筷子才可以吃饭。
*
* @author pengchengliu
*
*/

public class Philosopher {
public static void main(String[] args) throws Exception {
int ponder = 5 ;
if (args.length > 0) {
ponder = Integer.parseInt(args[0]);
}
int size = 5 ;
if (args.length > 1) {
size = Integer.parseInt(args[1]);
}
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for (int i = 0; i < size; i++) {
sticks[i] = new Chopstick();
newCachedThreadPool.execute(new PhilosopherTask(sticks[i], sticks[(i + 1) % size], i, ponder));
}
if (args.length == 3 && args[2].equals("timeout")) {
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println("press 'Entry' to quit");
System.in.read();
}
newCachedThreadPool.shutdownNow();
}
}
/**
* 哲学家任务,每一个哲学家左边还有右边各是一个筷子
* 还有一个思考工厂
* @author pengchengliu
*
*/
class PhilosopherTask implements Runnable {
private Chopstick left ;
private Chopstick right ;
private final int id ;
private final int phonderFactory ;
private Random rand = new Random(47) ;

public PhilosopherTask(Chopstick l, Chopstick r, int id, int phonder) {
this.left = l ;
this.right = r ;
this.id = id ;
this.phonderFactory = phonder ;
}

/**
* 思考相关的方法,通过一个传入的思考因子来决定。
* @throws InterruptedException
*/
public void pause () throws InterruptedException {
if (this.phonderFactory == 0) {
return ;
}
TimeUnit.MILLISECONDS.sleep(rand.nextInt(phonderFactory + 250));
}

@Override
public void run() {
try {
while (Thread.interrupted()) {
System.out.println(this + " thinking");
pause();
System.out.println(this + " take right");
this.right.take();
System.out.println(this + " take left");
this.left.take();
System.out.println(this + "eating");
pause();
this.left.drop();
this.right.drop();
}
} catch (InterruptedException e) {
System.out.println(this + " exiting via interrupt");
}
}

@Override
public String toString() {
return "PhilosopherTask " + this.id;
}
}
/**
* 筷子类,共享资源,(一共有5个)通过一个标示位,
* 来控制当前筷子的状态是可以用还是不可用。
* 由于该筷子又可能存在竞争条件,所以加锁是必须的。
* @author pengchengliu
*
*/
class Chopstick {
private boolean taken ;
public synchronized void take () throws InterruptedException {
while (taken) {
wait();
taken = true ;
}
}

public synchronized void drop () {
taken = false ;
}
}

//out
press 'Entry' to quit

这个版本就是典型的会出现死锁的情况,我们可以把思考的时间因子调整到0,便会100%的出现死锁的情况,那么如何避免上述所出现的死锁的现象呢,我们接着分析。

这里有是死锁必然会出现的前置条件,一共有一下这4点,当满足这四点的时候出现死锁,而如果这四点里面只要一点没有满足,则死锁情况造成不了,

  • 1、互斥条件,任务使用的资源至少有一个是不能共享的
  • 2、至少有一个任务获取了一个资源,并且等待下一个被别的线程所持有的资源。
  • 3、资源不能被抢占,必须按照锁机制来运行。
  • 4、存在循环等待的情况。

要存在死锁发生就必须达到上述的四个条件,缺一不可,那么要解决死锁问题,那么只需要破坏上述所述的这4个条件即可。这里最容易破坏掉的便是最后一条,因为最后一条是循环等待,如果我们让最后一个哲学家拿起左边的筷子,那么第四个情况也就算是被破坏了,这样遍可以解决哲学家就餐问题。(这只是最为简单的一种方式)当然还有其他比较优雅的方式。本节暂不讨论。

通过上述的思路,我们来实现我们的代码。来验证上述猜想是否正确。

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
public class Philosopher {
public static void main(String[] args) throws Exception {
int ponder = 5 ;
if (args.length > 0) {
ponder = Integer.parseInt(args[0]);
}
int size = 5 ;
if (args.length > 1) {
size = Integer.parseInt(args[1]);
}
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for (int i = 0; i < size; i++) {
sticks[i] = new Chopstick();
if (i < size - 1) {
newCachedThreadPool.execute(new PhilosopherTask(sticks[i], sticks[i + 1], i, ponder));
} else {
newCachedThreadPool.execute(new PhilosopherTask(sticks[0], sticks[i], i, ponder));
}
}
if (args.length == 3 && args[2].equals("timeout")) {
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println("press 'Entry' to quit");
System.in.read();
}
newCachedThreadPool.shutdownNow();
}
}

修改了相应的初始化顺序,这样遍不会造成死锁了。

线程协作

1、线程之间的协作

可以通过多个任务去解决某一个问题,这便是线程之间的协作。这里重在强调的便是线程和线程之间的协调,并不是彼此之间的冲突。有点类似于一个项目工程,有前置条件和后置条件,比如只有在前置条件满足的情况下,后置条件才可以触发,比如一个厨师做菜,如果简化一下,可以分出几个小的任务,比如有

  • 买菜
  • 摘菜
  • 洗菜
  • 热锅
  • 炒菜
  • 洗米
  • 蒸米
  • 盛米
  • 盛菜

可以把做饭分为以上就个小的任务,以上就有前置条件只说,比如炒菜必须是在热锅和洗菜结束之后才可以进行的,但是洗菜和热锅却是可以同步进行的,这便是前置条件,想要几个小的任务协作的有条理,必须我们开发人员巧妙的设计才可以实现,上述的这9件小任务,我们开理一下,不涉及代码,只是设计一个流程。具体流程如下所示 :

买菜
 |
摘菜
 |
洗菜    热锅    洗米
 |              |
炒菜           蒸米
 |              |
盛菜             |
                |
               盛米

大体就是这样的,那么我们应该如何控制线程的等待,开始呢?通过线程提供的wait()方法和notifAll()方法。

2、wait()与notifyAll()

wait方法的含义便是等待的意思,这里的意思便是由当前线程进行等待,也就是让当前线程挂起的意思,比如一开始的时候我们便要让洗菜任务挂起,因为此时买菜任务和摘菜任务都没执行完毕,所以洗菜任务不能开始。如果没有wait方法,我们是不是就要设置一个boolean,在摘菜任务结束的时候将boolean设置为true,然后让洗菜任务在一个死循环当中不断的进行boolean判断的轮循呢?好像也只能是这个样子了,这种情况被称为“忙等待”,而忙等待是一种不良的CPU周期使用方式。不提倡使用。这里需要注意的一点的是,wait使当前的线程挂起,同时也会释放线程的锁。如果我们调用wait方法的时候,其实就是在告诉线程调度器,告诉线程调度器“我已经刚刚做完了能做的所有事情,因此我需要等待一会,但是我希望其他的synchorized操作在条件适当的情况下可以执行”

有两种调用wait的方法,一个是通过指定相应的毫秒数值,有点类似于sleep,一个是没有参数,则无限制等待下去。直到唤醒。

notifAll和notify方法的含义便是叫醒正处于等待途中的线程,比如上述例子当中的洗菜任务一开始由于条件不满足,使得洗菜任务挂起,随着时间的推移,最后摘菜任务已经结束,这个时候需要洗菜任务进行执行,但是现在洗菜任务处于挂起状态,这个时候遍可以使用notify来叫醒洗菜任务,让其开始执行。

最后需要注意一点的就是,调用wait、notifAll、notifity都必须是在同步块当中,如果不是同步块当中会出现相应的错误。下面我们来一个简单的例子,来学习一下,我们应该如何使用上述所说的三种方法。

2.1 、线程协作

给汽车进行美容,我们需要给车进行打蜡,然后清洗,清洗任务必须是在打蜡任务以后才可以进行,否则运行不了。

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
/**
* 线程当中的协作,汽车打蜡-清洗Demo
* @author pengchengliu
*
*/
public class WaxOMatic {
public static void main(String[] args) throws InterruptedException {
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOn(car));
exec.execute(new Buff(car));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
class Car {
private boolean isWaxFinish = false ;

public synchronized void waxOn () {
isWaxFinish = true ;
}

public synchronized void buffed () {
isWaxFinish = false ;
notifyAll();
}

public synchronized void waitForWaxing () throws InterruptedException {
while (!isWaxFinish) {
wait();
}
}

public synchronized void waitForBuffing () throws InterruptedException {
while (isWaxFinish) {
wait();
}
}
}
class WaxOn implements Runnable {
private Car car ;

public WaxOn (Car car) {
this.car = car ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()){
System.out.println("Wax on!");
TimeUnit.MILLISECONDS.sleep(200);
car.waxOn();
car.waitForBuffing();
}
} catch (InterruptedException e) {
System.out.println("exit via interrupt");
}
System.out.println("ending wax On task");
}
}
class Buff implements Runnable{
private Car car ;

public Buff (Car car) {
this.car = car ;
}

@Override
public void run() {
try{
while (!Thread.interrupted()){
car.waitForWaxing();
System.out.println("Wax Off");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
}
} catch (InterruptedException e){
System.out.println("exit via interrupt");
}
System.out.println("ending Wax off");
}

}
Wax on!
Wax Off
Wax on!
Wax Off
Wax on!
Wax Off
Wax on!
Wax Off
Wax on!
Wax Off
Wax on!

2.2、错失的信号量

上述例子当中就是典型的线程之间的协作问题,关于线程之间的协作问题。但是在线程协作的问题当中存在一个很严重的问题,那便是信号错失,如何造成信号的错失呢,我们知道线程的执行时没有任何规律可寻的,如果notyif运行在wait之前呢? 很明显这便是死锁,很严重的问题,下面我们来看一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
T1
synchorized(lockBoject){
someCondition = false ;
lockBoject.notify();
}


T2
someCondition = true
while(someCondition){
//setup01
synchorized(lockBoject){
wait();
}
}

上述例子当中,我们可以看出,应该是由T1去唤醒T2的操作,正常的流程便是,T2执行完毕,处于等待状态,然后T1开始执行,然后唤醒T2,但是线程的执行顺序是不可控的,现在我们来假设一种情况便是,T2开始执行,但是执行到setup01的时候,线程调度器开始调度到T1,然后T1执行完毕,完毕以后回来执行T1,我们发现T2将会无限制的挂起,最后导致死锁。

如何解决上述的问题呢,其实很简单,就是控制someCondition变量上面的竞争条件。从而达到不让信号错失的情况产生,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
T1
synchorized(lockBoject){
someCondition = false ;
lockBoject.notify();
}


T2
someCondition = true ;
//setup01
synchorized(lockBoject){
while(someCondition){
wait();
}
}

我们可以看到使用同步块将while包裹住,这样来避免错失的信号量,现在我们再次模拟一遍上述的特殊情况,到setup的时候切换线程,然后再回来,我们发现while的循环压根进不去,这样也就避免的错失信号量了。

2.3、notify和notifyAll的区别。

这两个方法都是把一个线程从等待状态唤醒,而notify是notifyAll的一种优化,正如它们的名字一样,一个是单纯的唤醒一个,而另外的一个是唤醒全部,notify是在众多的等待同一个锁的任务当中只有一个被唤醒,因此如果你希望使用notify,就必须保证被唤醒的是恰当的任务。另外如果使用了notify,就必须当前所有的等待任务都是在等待同一个条件,如果是不同的任务,那么我们就不知道唤醒的是否是恰当的任务。如果使用notify,当条件变化的时候,必须只有一个任务从当中收益。最后,这些限制对所有可能存在的子类都必须总是起作用的。如果这些规则当中有任何一条不满足。我们就必须使用All而不是notifty。

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

notify和notifyAll的区别

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了

注意:notifyAll因某一个特定的锁被调用的时候,只有等待这个锁的任务才会被唤醒。

下面来看一个Demo来验证这一点

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
package com.suansuan.cooperation;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 对比Notify和NotifyAll
* notify用得不好容易导致死锁.
* @author pengchengliu
*
*/
public class NotifyVsNotifyAll {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++){
executorService.execute(new Task01());
}
executorService.execute(new Task02());

Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask(){
boolean prod = true ;
public void run() {
if (prod) {
System.out.println("notify()");
Task01.blocker.prod();
prod = false ;
} else {
System.out.println("notifyAll()");
Task01.blocker.prodAll();
prod = true ;
}
}
}, 400, 400);


TimeUnit.SECONDS.sleep(2);
timer.cancel();
System.out.println("timer is cancel");

TimeUnit.MILLISECONDS.sleep(500);
System.out.println("Task02 blocker prodAll()");
Task02.blocker.prodAll();

TimeUnit.MILLISECONDS.sleep(500);
System.out.println("shutting down");
executorService.shutdownNow();
}
}
class Blocker {
synchronized void waitingCall () {
try{
while (!Thread.interrupted()) {
wait() ;
System.out.println(Thread.currentThread() + "");
}
} catch (InterruptedException e) {}
}

synchronized void prod () {
notify();
}

synchronized void prodAll () {
notifyAll();
}
}
class Task01 implements Runnable {
static Blocker blocker = new Blocker();
@Override
public void run() {
blocker.waitingCall();
}
}
class Task02 implements Runnable {
static Blocker blocker = new Blocker();
public void run() {
blocker.waitingCall();
}
}

notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-5,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-2,5,main]
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-2,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-5,5,main]
timer is cancel
Task02 blocker prodAll()
Thread[pool-1-thread-6,5,main]
shutting down

上述例子展示了什么这两个方法的区别。现在举一个例子为什么说notify使用不恰当会引起死锁:比如,你是你家挣钱的,儿子和女儿是花钱的。儿子给家里要100,女儿要30。可是家里没钱,他们只能等。后来你出去打工,赚钱了,赚了50,这时你要在儿子和女儿之间选择一个人叫醒。如果不凑巧,你把儿子叫醒了,儿子发现钱还是不够,又去等。因为你只能叫一次,女儿就错过了使用这50块钱的机会。所以,你决定把所有的人都叫醒,虽然费劲一点。这样一来,儿子发现不够,接着等,女儿发现够了,就用了。

3、生产者和消费者

本例子从最基础的生产者和消费者开始讲起,不会使用什么共享队列,这个后面会有说。我们先来看看生产者和消费者分别代表着什么,

有这样一家饭店,有一个厨师,有一个服务员,当厨师做好饭菜以后,由服务员来将饭菜送到客人面前,没有准备好时,服务员等待,准备好时通知服务员,服务员端菜,送完以后,周而复始。

分析:厨师代表生产者,服务员代表消费者,两个任务必须在饭菜准备好时进行握手,而系统必须有序的进行关闭。下面为演示Demo。

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

package com.suansuan.cooperation;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 最原始的生产者-消费者模型,不具有相应的共享队列
* @author pengchengliu
*
*/
public class Restaurant {
public Meal meal ;
public Consumer consumer = new Consumer(this);
public Chef chef = new Chef(this);
public ExecutorService exec = Executors.newCachedThreadPool();

public Restaurant(){
exec.execute(chef);
exec.execute(consumer);
}

public static void main(String[] args) {
new Restaurant();
}
}
/**
* 饭菜,也就是我们需要在两个协同任务当中传递的东西
* @author pengchengliu
*
*/
class Meal {
private final int orderNum ; //订购的数量
public Meal (int orderNumber) {
this.orderNum = orderNumber ;
}
public String toString () {
return "Meal " + this.orderNum ;
}
}
/**
* 厨师,生产者,
* 注意:生产者的唤醒服务员的时机, 还有生产等待的时机
* @author pengchengliu
*
*/
class Chef implements Runnable {
private Restaurant restaurant ;
private int count ;
public Chef (Restaurant restaurant) {
this.restaurant = restaurant ;
}
//run方法当中,要确保正确的关闭方式。
public void run () {
try{
while (!Thread.interrupted()) {

// 锁住本身,服务员遍可以通过这个锁来唤醒这个厨师。
synchronized (this) {
while (restaurant.meal != null){
wait();
}
}

if(++count == 10) {
System.out.println("Out of food close");
restaurant.exec.shutdownNow();
}

// 因为服务员就是在这个锁上面等待的,所以我们获取服务员的锁,来唤醒所有等待服务员锁的对象
synchronized (restaurant.consumer) {
restaurant.meal = new Meal(count);
System.out.println("Chef output " + restaurant.meal);
restaurant.consumer.notifyAll();
}

TimeUnit.MILLISECONDS.sleep(100);
}
} catch (Exception e) {
System.out.println("Chef interrupt");
}
}
}
/**
* 服务员-消费者
* 注意:等待的条件,唤醒条件,以及唤醒的时候拿到的锁。
* @author pengchengliu
*
*/
class Consumer implements Runnable {
private Restaurant restaurant ;
public Consumer(Restaurant restaurant) {
this.restaurant = restaurant ;
}
public void run (){
try{
while (!Thread.interrupted()){
synchronized (this) {
while (restaurant.meal == null) {
wait();
}
}

System.out.println("Consumer input " + restaurant.meal);

synchronized (restaurant.chef) {
restaurant.meal = null ;
restaurant.chef.notifyAll();
}
}
} catch (Exception e) {
System.out.println("consumer interrupt");
}
}
}
Chef output Meal 1
Consumer input Meal 1
Chef output Meal 2
Consumer input Meal 2
Chef output Meal 3
Consumer input Meal 3
Chef output Meal 4
Consumer input Meal 4
Chef output Meal 5
Consumer input Meal 5
Chef output Meal 6
Consumer input Meal 6
Chef output Meal 7
Consumer input Meal 7
Chef output Meal 8
Consumer input Meal 8
Chef output Meal 9
Consumer input Meal 9
Out of food close
consumer interrupt
Chef output Meal 10
Chef interrupt

4、显示的Lock与Condition对象

我们可以通过显示的使用Lock与Condition对象来控制线程之间的协作,Condition这个类的作用主要就是使用锁可以使当前的任务挂起,也可以唤醒挂起的任务,或者唤醒所有的任务,使用await来挂起一个任务,使用signal来唤醒任务,还有通过signalAll来唤醒所有的任务,

注意:signalAll要比notifyAll更加的安全。下面我们通过本章节刚刚开始的汽车美容的例子,来使用一下这个例子:

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

/**
* 线程当中的协作,汽车打蜡-清洗Demo 使用显示的Lock和Condition来控制
* @author pengchengliu
*
*/
public class WaxOMaticLockCondition {
public static void main(String[] args) throws InterruptedException {
Cars car = new Cars();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Waxon2(car));
exec.execute(new Buffed(car));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
class Buffed implements Runnable {
private Cars car ;
public Buffed (Cars car) {
this.car = car ;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println("Buffed on");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
car.waitForWaxing();
}
} catch (InterruptedException e) {
System.out.println("exit via interrupt");
}
System.out.println("ending buffed on task");
}
}
class Waxon2 implements Runnable {
private Cars car ;
public Waxon2 (Cars car) {
this.car = car ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println("Wax on");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffed();
}
} catch (InterruptedException e) {
System.out.println("exit via interrupt");
}
System.out.println("ending wax on task");
}
}
/**
* 共享资源类
* @author pengchengliu
*
*/
class Cars {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean isWaxFinish = false ;

/**
* 使用try的原因就是怕在显示的锁 不能被释放,所以这么写,确保锁可以被正确的释放掉。
*/
public void waxed () {
lock.lock();
try {
isWaxFinish = true ;
condition.signalAll();
} finally {
lock.unlock();
}
}

public void buffed () {
lock.lock();
try{
isWaxFinish = false ;
condition.signalAll();
} finally {
lock.unlock();
}
}

public void waitForWaxing () throws InterruptedException {
lock.lock();
try{
while (!isWaxFinish) {
condition.await();
}
} finally {
lock.unlock();
}
}

public void waitForBuffed () throws InterruptedException {
lock.lock();
try{
while (isWaxFinish) {
condition.await();
}
} finally {
lock.unlock();
}
}
}
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
Wax on
Buffed on
exit via interrupt
ending wax on task
exit via interrupt
ending buffed on task

5、共享队列

上述生产者与消费者以一种非常低级的方式解决了任务互相操作的问题,及每次交互时都握手,我们可以使用更加高级的方式去处理这种问题,及使用同步队列来解决任务协作的问题,同步队列的特点就是任何时刻只允许一个任务进行插入或者移除元素。

JDK为我们提供了各种各样的同步队列,它们这些同步队列统一实现了BlockingQueue接口。比较常用的有两个队列,分别是

  • LinkedBlockingQueue:使用链表实现,没有固定大小,是一个无界队列
  • ArrayNBlockingQueue:使用顺序表实现,有固定的大小。

消费者试图在队列当中获取元素,如果获取不到元素,即队列为空的时候,挂起消费者任务的驱动线程,并且当队列当中存在元素的时候,及可以被消费者获取的时候,唤醒消费者任务的驱动线程。这个被称之为阻塞队列。组素队列可以解决大量的问题,而其方式与wait()与notiftyAll相比,阻塞队列简单的多。

5.1、共享队列使用

下面我们来看一个例子程序,有一个阻塞队列,将多个LiftOff对象串行执行,消费者为LiftOffRunner,消费者的任务便是将每一个LiftOff对象从阻塞队列中取出并且运行,

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
174
175
176
177
178
179
/**
* 探究共享队列:
* 有一个阻塞队列,将多个LiftOff对象串行执行,消费者为LiftOffRunner,消费者的任务便是将每一个LiftOff对象从阻塞队列中取出并且运行
* @author pengchengliu
*
*/
public class BlockingQueues {
static void getKey () {
try {
new BufferedReader(new InputStreamReader(System.in)).readLine();
} catch (Exception e) {
throw new RuntimeException(e) ;
}
}

static void getKey (String message) {
System.out.println(message);
getKey();
}

static void test (String message, BlockingQueue<LiftOff> queue) {
System.out.println(message);
LiftOffRunner liftOffRunner = new LiftOffRunner(queue);
Thread thread = new Thread(liftOffRunner);
thread.start();

for (int i = 0; i < 5; i++) {
liftOffRunner.add(new LiftOff(5));
}

getKey("press 'Entry' (" + message + ")" );
thread.interrupt();
System.out.println("Finished " + message + " test");
}

public static void main(String[] args) {
test("LinkedBlockingQueue", new LinkedBlockingQueue<LiftOff>());
test("ArrayBlockingQueue", new ArrayBlockingQueue<LiftOff>(3));
test("SynchronousQueue", new SynchronousQueue<LiftOff>());
}
}
class LiftOffRunner implements Runnable {

private BlockingQueue<LiftOff> rockets ;
public LiftOffRunner(BlockingQueue<LiftOff> rocket) {
this.rockets = rocket ;
}
@Override
public void run() {
try {
while (! Thread.interrupted()) {
LiftOff take = rockets.take();
take.run();
}
} catch (InterruptedException e) {
System.out.println("waking form take");
}
System.out.println("exiting LiftOffRunner");
}

public void add (LiftOff lo) {
try {
rockets.put(lo);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
class LiftOff implements Runnable{

protected int countDown = 10 ;
private static int taskCount = 0;
private final int id = taskCount ++ ;

public LiftOff(){}
public LiftOff(int countDown){
this.countDown = countDown ;
}

public String status(){
return "#" + this.id + "(" + (countDown > 0 ? countDown : "Liftoff!" ) + ").";
}
@Override
public void run() {
while(countDown-- > 0){
System.out.println(status());
Thread.yield();
}
}

}
LinkedBlockingQueue
press 'Entry' (LinkedBlockingQueue)
#0(4).
#0(3).
#0(2).
#0(1).
#0(Liftoff!).
#1(4).
#1(3).
#1(2).
#1(1).
#1(Liftoff!).
#2(4).
#2(3).
#2(2).
#2(1).
#2(Liftoff!).
#3(4).
#3(3).
#3(2).
#3(1).
#3(Liftoff!).
#4(4).
#4(3).
#4(2).
#4(1).
#4(Liftoff!).
Finished LinkedBlockingQueue test
waking form take
exiting LiftOffRunner
ArrayBlockingQueue
#5(4).
#5(3).
#5(2).
#5(1).
#5(Liftoff!).
#6(4).
press 'Entry' (ArrayBlockingQueue)
#6(3).
#6(2).
#6(1).
#6(Liftoff!).
#7(4).
#7(3).
#7(2).
#7(1).
#7(Liftoff!).
#8(4).
#8(3).
#8(2).
#8(1).
#8(Liftoff!).
#9(4).
#9(3).
#9(2).
#9(1).
#9(Liftoff!).
Finished ArrayBlockingQueue test
waking form take
exiting LiftOffRunner
SynchronousQueue
#10(4).
#10(3).
#10(2).
#10(1).
#10(Liftoff!).
#11(4).
#11(3).
#11(2).
#11(1).
#11(Liftoff!).
#12(4).
#12(3).
#12(2).
#12(1).
#12(Liftoff!).
#13(4).
#13(3).
#13(2).
#13(1).
#13(Liftoff!).
#14(4).
press 'Entry' (SynchronousQueue)
#14(3).
#14(2).
#14(1).
#14(Liftoff!).

各个任务由main放置到了BlockingQueue当中,并且由LiftOffRunner从BlockingQueue当中取出来执行(当取出来执行的时候使用的run方法不是重新开线程),注意:LiftOffRunner可以忽略同步问题,因为他们已经由BlockingQueue解决了。

5.2、吐司BlockingQueue

谈及共享队列,那么就不可能不谈及吐司例子,这个是共享队列当中比较有名的例子,下面来描述一下这个例子。

有一台机器一共具有三个任务,分别是制作吐司,一个是给吐司上黄油、另一个便是给上了黄油的吐司上果酱。我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作的程序。

下面使用程序来模拟上述制作吐司的过程。具体实现如下所示 :

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* ToastBlockingQueue,
* 有一台机器一共具有三个任务,分别是
* 1、制作吐司,
* 2、给吐司上黄油、
* 3、给上了黄油的吐司上果酱。
*
* 我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作的程序。
* @author pengchengliu
*
*/
public class ToastOMatic {

/**
* 开启所有的制作Toast的相关任务,然后运行5秒钟,结束所有的任务
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
ToastQueue dryQueue = new ToastQueue(),
butterQueue = new ToastQueue(),
finishQueue = new ToastQueue();

ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Toaster(dryQueue));
exec.execute(new Butter(dryQueue, butterQueue));
exec.execute(new Jammer(butterQueue, finishQueue));
exec.execute(new Eater(finishQueue));

TimeUnit.SECONDS.sleep(5);

exec.shutdownNow();
}
}
/**
* Toast本身,也就是我们的资源共享类,
* 思考:为什么关于资源类的状态改变没有加锁,这个类被三个任务所操作,不加锁安全么?
* @author pengchengliu
*
*/
class Toast {
public enum Status {
DRY,BUTTERED,JAMMED
}

private Status status = Status.DRY ;
private final int id ;

public Toast (int id) {
this.id = id ;
}

public void butter () {
this.status = Status.BUTTERED;
}

public void jam () {
this.status = Status.JAMMED ;
}

public Status getStatus () {
return this.status ;
}

public int getId () {
return this.id ;
}

public String toString () {
return "Toast " + id + " : " + status ;
}

public void recyleToast () {
this.status = null ;
}
}
/**
* 使用定制的相关队列,如果有特殊的需求,通过继承LinkedBlockingQueue来做自定义
* 遵守单一职责的原理,应该定制单一的属于队列的相关需求,
* 这里只是演示Demo,所以没有做任何定制服务。
* @author pengchengliu
*
*/
class ToastQueue extends LinkedBlockingQueue<Toast> {}
/**
* 第一步,制作Toast,将制作出来的Toast至于共享队列当中。
* 注意:只有一个共享队列
* @author pengchengliu
*
*/
class Toaster implements Runnable {

private ToastQueue toastQueue ;
private int count ;
private Random rand = new Random(47);

public Toaster (ToastQueue toastQueue) {
this.toastQueue = toastQueue ;
}

@Override
public void run() {
try {
while (! Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(100 + rand.nextInt(500));
Toast toast = new Toast(count ++) ;
System.out.println(toast);
toastQueue.add(toast);
}
} catch (InterruptedException e) {
System.out.println("Toaster interrupt");
}
System.out.println("Toaster off");
}
}
/**
* 第二步,上黄油,
* 注意:需要两个队列,一个是输入队列,一个是输出队列
* @author pengchengliu
*
*/
class Butter implements Runnable {

private ToastQueue dryQueue, butterQueue ;

public Butter (ToastQueue dryQueue, ToastQueue butterQueue) {
this.dryQueue = dryQueue ;
this.butterQueue = butterQueue ;
}

@Override
public void run() {
try {
while (! Thread.interrupted()) {
Toast take = dryQueue.take();
take.butter();
System.out.println(take);
butterQueue.add(take);
}
} catch (InterruptedException e) {
System.out.println("Butter interrupt");
}
System.out.println("Butter off");
}
}
/**
* 第三步:上果酱,基本操作与步骤二相似
* @author pengchengliu
*
*/
class Jammer implements Runnable {

private ToastQueue butterQueue, finishedQueue ;

public Jammer (ToastQueue butterQueue, ToastQueue finishedQueue) {
this.butterQueue = butterQueue ;
this.finishedQueue = finishedQueue ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
Toast take = butterQueue.take();
take.jam();
System.out.println(take);
finishedQueue.add(take);
}
} catch (InterruptedException e) {
System.out.println("Jammer interrupt");
}
System.out.println("Jammer off");
}
}
/**
* 最后的消费者
* @author pengchengliu
*
*/
class Eater implements Runnable {

private ToastQueue finishedQueue ;
private int counter = 0 ;

public Eater (ToastQueue finishedQueue) {
this.finishedQueue = finishedQueue ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
Toast take = finishedQueue.take();
if (take.getId() != counter ++ || take.getStatus() != Toast.Status.JAMMED) {
System.out.println(">>> Error: " + take);
System.exit(1);
} else {
System.out.println("chomp! " + take);
take.recyleToast();
take = null;
}
}
} catch (InterruptedException e) {
System.out.println("Eater interrupt");
}
System.out.println("Eater off");
}
}
Toast 0 : DRY
Toast 0 : BUTTERED
Toast 0 : JAMMED
chomp! Toast 0 : JAMMED
Toast 1 : DRY
Toast 1 : BUTTERED
Toast 1 : JAMMED
chomp! Toast 1 : JAMMED
Toast 2 : DRY
Toast 2 : BUTTERED
Toast 2 : JAMMED
chomp! Toast 2 : JAMMED
Toast 3 : DRY
Toast 3 : BUTTERED
Toast 3 : JAMMED
chomp! Toast 3 : JAMMED
Toast 4 : DRY
Toast 4 : BUTTERED
Toast 4 : JAMMED
chomp! Toast 4 : JAMMED
Toast 5 : DRY
Toast 5 : BUTTERED
Toast 5 : JAMMED
chomp! Toast 5 : JAMMED
Toast 6 : DRY
Toast 6 : BUTTERED
Toast 6 : JAMMED
chomp! Toast 6 : JAMMED
Toast 7 : DRY
Toast 7 : BUTTERED
Toast 7 : JAMMED
chomp! Toast 7 : JAMMED
Toast 8 : DRY
Toast 8 : BUTTERED
Toast 8 : JAMMED
chomp! Toast 8 : JAMMED
Toast 9 : DRY
Toast 9 : BUTTERED
Toast 9 : JAMMED
chomp! Toast 9 : JAMMED
Toast 10 : DRY
Toast 10 : BUTTERED
Toast 10 : JAMMED
chomp! Toast 10 : JAMMED
Toast 11 : DRY
Toast 11 : BUTTERED
Toast 11 : JAMMED
chomp! Toast 11 : JAMMED
Toast 12 : DRY
Toast 12 : BUTTERED
Toast 12 : JAMMED
chomp! Toast 12 : JAMMED
Toast 13 : DRY
Toast 13 : BUTTERED
Toast 13 : JAMMED
chomp! Toast 13 : JAMMED
Toast 14 : DRY
Toast 14 : BUTTERED
Toast 14 : JAMMED
chomp! Toast 14 : JAMMED
Toaster interrupt
Toaster off
Butter interrupt
Butter off
Eater interrupt
Eater off
Jammer interrupt
Jammer off

通过上述的例子,我们得出两点我们需要学习的地方,

  • 通过一个Enum类型来控制共享资源当中的状态变化
  • 通过BlockingQueue来实现出来无锁的同步代码

上述我们说过一个共享队列任何时刻都只允许一个任务插入或者移除元素,通过这点,我们根本不需要给Toast这个共享资源类进行相关的加锁逻辑。

6、管道信号

如果我们把相应的队列换成一个管道,通过输入/输出在线程间进行通信。其实这种想法是在Sun公司没有引入BlockingQueue队列之前的一种方式。同样可以解决大部分的线程协作问题。

关于管道这里主要有两种管道,分别是PipedWriter(允许任务向相关管道当中写),PiepedReader(允许不同的任务从同一个管道当中读取)。管道基本上是一个阻塞队列,所以我们同样也不需要考虑相关加锁问题。下面我们通过一个Demo来学习一下这个吧。

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
/**
* 探究Piped 管道 控制任务间的协作
* @author pengchengliu
*
*/
public class PipedIO {
public static void main(String[] args) throws Exception {
Sender sender = new Sender();
Receiver receiver = new Receiver(sender);

ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(sender);
exec.execute(receiver);

TimeUnit.SECONDS.sleep(4);
exec.shutdownNow() ;
}
}
/**
* 发送任务
* @author pengchengliu
*
*/
class Sender implements Runnable {

private Random rand = new Random(47);
private PipedWriter out = new PipedWriter();

public PipedWriter getPepedWriter() {
return this.out ;
}

@Override
public void run() {
try {
while (true) {
for (char c = 'A'; c < 'Z'; c++) {
out.write(c);
TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
}
}
} catch (IOException e) {
System.out.println(e + " Sender writer execption");
} catch (InterruptedException e) {
System.out.println("Sender interrput");
}
}
}
/**
* 接收任务
* @author pengchengliu
*
*/
class Receiver implements Runnable {

private PipedReader reader ;

public Receiver(Sender sender) throws IOException {
reader = new PipedReader(sender.getPepedWriter());
}

@Override
public void run() {
try {
while (true) {
System.out.println("Read : " + (char) reader.read() + ".");
}
} catch (IOException e) {
System.out.println(e + "Receiver read exception");
}
}

}
Read : A.
Read : B.
Read : C.
Read : D.
Read : E.
Read : F.
Read : G.
Read : H.
Read : I.
Read : J.
Read : K.
Read : L.
Read : M.
Sender interrput
java.io.InterruptedIOExceptionReceiver read exception

享元模式-对象池

  • 1、享元模式的作用
  • 2、享元模式的使用场景
  • 3、享元模式的UML类图
  • 4、享元模式Demo
    • request的缓存池
    • result的缓存池
    • JDK当中的常量池

1、享元模式的作用

享元模式实际上就是对象的缓存池,英文为Flyweight,目的是尽可能地减少内存的使用,来缓存大量重复对象的场景,来缓存可共享的对象,避免创建过多的对象的场景,来提升性能,避免内存溢出的场景。

2、享元模式的使用场景

使用享元模式的使用场景比较多,又有着显著的特点,有以下三个特点的可以使用享元模式

  • 系统中存在大量的相似的对象,
  • 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,对象没有特定的身份
  • 需要缓存池的场景

上述说到相应的外部状态,内部状态,这两个状态,我们建立自己的缓存池的时候,一般使用外部状态,也就是这个对象会随着变换的那个状态,做为一个key存储在一个map当中,或者使用list当中,使用list的时候,相应的inedx便是该对象为一标示的外部状态,当然了,有时候需要我们自己来维护,相应的index到外部状态的一种映射。注意;在使用享元模式的时候,一定要明白什么是外部状态,什么是内部状态。这个搞清楚了,这种模式也就相应的搞定了

3、享元模式的UML类图

为了让这一种设计模式看起来更加的易于理解,所以这里使用UML来描述该设计模式。

享元模式,UML类图结构

4、享元模式的Demo

4.1、请求缓存池

例子:客户端的程序设计一般都是含有相应的网络请求模块的,本地请求服务器拿到相应的数据,然后显示这些数据。现在有这样一种情况,当客户端请求服务器,如果相同的请求,一下就请求了100次,或者上千次,或者相同的请求只是参数不同,然后请求了很多次。

如果一个请求在我们的程序当中就是一个对象的话,当我们请求的时候,我们就需要new出来一个请求的对象,然后在我们请求完毕以后,由GC销毁。很明显上述的这种情况不是一种合理的解决方式,其实如果我们代入相应的享元模式的话,就会发现其实很简单。具体的相应实现如下所示:

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
//Request.java

/**
* 网络框架 请求缓存池 - Request对象
* @author pengchengliu
*
*/
public class Request {
private String url;
private int offset ;
private int total ;

public Request (String url, int offset, int total) {
this.url = url ;
this.offset = offset ;
this.total = total ;
}

public void setParams (int offset, int total) {
this.offset = offset ;
this.total = total ;
}
@Override
public String toString() {
return "Request [url=" + url + ", offset=" + offset + ", total=" + total + "] = " + Integer.toHexString(hashCode());
}

}

//RequestPool.java

/**
* 网络框架 请求缓存池 - RequestFactory对象
* @author pengchengliuå
*
*/
public class RequestFactory {

/**
* 使用static的意义在于,该缓存map不必单独被对象所持有,可以使用static来所谓共享变量来使用。
*/
private static final Map<String, Request> sMap = new HashMap<String, Request>();
/**
* 将一个对象缓存在了一个map当中,使用url来充当这个Map的key,如果可以从map当中取到这个数据,则返回,否则重新创建
* @param offset :当前地址所携带的参数信息
* @param total :当前地址所携带的参数信息
* @return : Request对象
*/
public static Request getRequest (String url, int offset, int total) {
Request request ;
if (sMap.containsKey(url)) {
request = sMap.get(url);
request.setParams(offset, total);
return request ;
}
request = new Request(url, offset, total);
sMap.put(url, request);
return request ;
}
}

//Test.java

/**
* 网络框架 请求缓存池 - 主函数
* @author pengchengliu
*
*/
public class Main {
public static void main(String[] args) {
int offset = 0 , total = 0;
Request request0 = RequestFactory.getRequest("http://test.test.test", offset, total);
System.out.println("request0 = " + request0);

Request request1 = RequestFactory.getRequest("http://test.test.test", offset + 100, total + 10);
System.out.println("request1 = " + request1);

Request request2 = RequestFactory.getRequest("http://test.test.test", offset + 200, total + 20);
System.out.println("request2 = " + request2);

System.out.println(request0 == request1);
System.out.println(request1 == request2);
}
}

request0 = Request [url=http://test.test.test, offset=0, total=0] = dcf3e99
request1 = Request [url=http://test.test.test, offset=100, total=10] = dcf3e99
request2 = Request [url=http://test.test.test, offset=200, total=20] = dcf3e99
true
true

通过上述的Demo,我们确实发现了,使用的都是一个对象,当然我们只是一个简单的例子,比如,如果前一个对象还没有使用完毕,我们在创建第二个对象的时候改变了上一个对象的状态,导致第一次查询的时候拿到是offset=100 total=10的情况,所以后续还需要我们优化。

4.2、result缓存池。

我们去数据库当中查询数据的时候,有时候会遇到多次查询相同的数据,这样的话我们把查询结果result至于缓存池当中,就不必多次去数据库当中查找了。

下面就以查询火车票为例子,假设从A到B只有一趟列车,而列车的席位有硬卧上铺、硬卧下铺、坐票三种。下面我们来看这个Demo是如何运作的。

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
//Ticket.java
package flyweight.queryTicket;
/**
* 查询火车票-票抽象对象
* @author pengchengliu
*
*/
public interface Ticket {
public void showTicketInfo(String bunk);
}

//TrainTicket.java
/**
* 查询火车票-火车票对象
* @author pengchengliu
*
*/
public class TrainTicket implements Ticket{

public String from ; // 出发地
public String to ; // 目的地
public String bunk ; // 席位

public int price ; // 价格
public TrainTicket(String from, String to) {
this.from = from ;
this.to = to ;
}

@Override
public void showTicketInfo(String bunk) {
this.bunk = bunk ;
price = new Random(47).nextInt(300);

System.out.println("该买从 " + this.from + " 到 "
+ this.to + "的 " + this.bunk + " 火车票" + ", 价格为 " + this.price);
}
}

//TicketFactory.java
/**
* 查询火车票-火车票工厂对象
* @author pengchengliu
*
*/
public class TicketFactory {

private static Map<String, Ticket> sTicketMap = new HashMap<String, Ticket>() ;

public static Ticket getTicket (String from, String to) {
String key = from + "-" + to ;
if (sTicketMap.containsKey(key)) {
System.out.println("使用缓存 -- > " + key);
return sTicketMap.get(key);
} else {
System.out.println("创建对象 -- > " + key);
Ticket ticket = new TrainTicket(from, to);
sTicketMap.put(key, ticket);
return ticket ;
}
}
}


//Main.java
public class Main {
public static void main(String[] args) {
Ticket ticket01 = TicketFactory.getTicket("北京", "西安");
ticket01.showTicketInfo("上铺");
Ticket ticket02 = TicketFactory.getTicket("北京", "西安");
ticket02.showTicketInfo("下铺");
Ticket ticket03 = TicketFactory.getTicket("北京", "西安");
ticket03.showTicketInfo("坐票");
}
}

创建对象 – > 北京-西安
该买从 北京 到 西安的 上铺 火车票, 价格为 158
使用缓存 – > 北京-西安
该买从 北京 到 西安的 下铺 火车票, 价格为 158
使用缓存 – > 北京-西安
该买从 北京 到 西安的 坐票 火车票, 价格为 158

4.3、JDK当中的常量池

我们知道一个String对象是被存储在常量池当中的,也就是说一个String被定义以后,就被缓存在了常量池当中,当其他地方要使用字符串的时候,则直接使用的是缓存,不会重复创建对象,我们通过一个简单的Demo来看一下这个机制。

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
/**
* 探究JDK当中的常量池的工作原理
* @author pengchengliu
*
*/
public class Main {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
String str3 = new String("abc");
String str4 = "ab" + "c";

//判定内容是否相等
System.out.println(str1.equals(str2)); //true
System.out.println(str1.equals(str3)); //true
System.out.println(str1.equals(str4)); //true
System.out.println(str2.equals(str3)); //true
System.out.println(str2.equals(str4)); //true
System.out.println(str3.equals(str4)); //true

System.out.println("========");

//判定内存地址是否相等
System.out.println(str1 == str2); //false
System.out.println(str1 == str3); //false
System.out.println(str1 == str4); //false
System.out.println(str2 == str3); //false
System.out.println(str2 == str4); //true
System.out.println(str3 == str4); //false
}
}

其实我们大部分都知道它的原理是什么,唯独str2 == str4 这个优点模糊,为什么会是true,其实只要代入本设计模式之前说过,常量池的概念,应该可以很容易的理解,就是str4使用的是str2的缓存对象 。

线程死亡

  • 1、线程的状态
  • 2、使用标示符号结束线程
  • 3、关闭显示阻塞状态的线程
  • 4、关闭被IO所阻塞的线程
  • 5、关闭被等待锁而阻塞的线程
  • 6、检查中断状态

关于线程正确关闭的说法,一直都存在,最早最原始的使用stop方法,但是这种方式已经被禁止使用了,至于为什么禁止使用,原因大概是sun公司感觉使用stop方法来关闭线程太过于暴力,就是如果使用stop方法关闭线程,那么当前线程的状态不管处于什么状态都会进行关闭,并且也不会释放当前线程所获得的锁,这样的操作会导致整个线程的逻辑混乱无比。所以使用stop方法关闭线程不是一种正确关闭线程的方式,本篇讨论的就是如何正确的线程。

1、线程的状态

线程从创建出来到最后的消亡,一共会经历以下四个方面,分别是:

  • 1.1、new 当线程被创建出来的时候会短暂处于这一种状态,此时已经得到了相应的必需资源,并且执行了相应的初始化操作。
  • 1.2、runnable 这种状态下,只要调度器将时间片分配给当前的线程,线程就可以正常运行,这种状态有资格去竞争时间片。
  • 1.3、blocked 线程能够运行,但是由于某一种条件没满足,调度器不会考虑分配资源,调度它,直到条件满足以后回到runnable状态。
  • 1.4、dead 处于死亡或者即将死亡的线程,并且再也不会得到cpu时间片的分配,任务已经结束,但是需要注意的一点便是此时线程还可以被中断的。

2、使用标示符号结束线程

具体的意思就是,通过一个所有线程都可以看到的地方,比如共享资源类当中的一个boolean值来进行控制,让一个线程合理的终结掉。具体的实现如下所示:

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
public class EventCheck implements Runnable{

private IntGenerator generator ;
private final int id ;

public EventCheck (IntGenerator generator, int ident) {
this.generator = generator ;
this.id = ident ;
}

@Override
public void run() {
while (!generator.isCancel()) {
int val = generator.next() ;
if(val % 2 != 0) {
System.out.println("var = " + val + ", id = " + id + ", not event");
//如果是奇数,则退出循环。
generator.cancel();
}
}
}

public static void test (IntGenerator generator, int count) {
System.out.println("press Control-C to exit");
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
threadPool.execute(new EventCheck(generator, i));
}
threadPool.shutdown();
}

public static void test (IntGenerator generator) {
test(generator, 10);
}
}

使用这一种方式可以关闭大多数的线程,但是如果我们的一个线程处于阻塞状态,也就代表着,这个线程无法运行到循环开始的地方,因为当前线程阻塞,这种情况下的线程还会无法进行关闭的,所以以下讨论如何关闭阻塞的线程

3、关闭显示阻塞状态的线程

一般情况下一个任务进入阻塞状态有如下几种可能

  • sleep使当前的线程进入休眠的状态
  • wait使当前的线程挂起
  • 任务等待IO操作完成
  • 任务等待锁的到来。

以上的四种情况总结一下就是线程处于阻塞状态分类大体两种情况,第一种情况就是显示的调用让线程挂起,第二种情况就是被动的等待可以运行的条件。如果我们要显示的关闭这类阻塞的线程的话,那么我们就想着,如何让这个线程从run方法当中跳出来,我们必须强制这个任务跳出阻塞的状态,

如果一个线程是阻塞状态,那么我们通过什么办法打断这个线程呢?答案就是我们主动的打断这个线程,而我们只要在run方法当中使用try到相应的打断异常,然后通过catch语句做相应的清理工作即可,Thread类有一个interrupt方法,这个方法将设置这个线程的中断状态,如果一个线程是阻塞状态,或者正在进行阻塞操作,调用这个线程的interrupt方法会使这个线程抛出InterruptedException。当抛出异常或者该任务调用Thread.interrupted时,中断状态将被复位,也就是当我们一个线程处于阻塞状态,在其他的任务当中调用interrupted,如果这个线程当中定义抛出异常的语句,或则调用Thread.interrupted时,线程将被复位。

通过异常来控制程序的流程属于不恰当的用法,这里还是推荐使用Thread.interrupted。

调用interrupt方法,我们必须持有相应的Thread,但是在JDK新的API当中,提倡的是使用Executors来执行所有的操作。如果我们调用shutdownNow,那么这个Executors将会给所有的线程调用interrupt方法。这里是所有的,如果我们只想关闭单独的线程,那么我们应该怎么做呢?那么我们只能通过submit一个线程,我们可以拿到一个Future<?>对象,如果想要结束submit的线程,我们只需要调用cancel方法即可,下面展示一个Demo,展示Interrupt的用法

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
/**
* 基本的interrupt的使用方法,
* 主要用户结束线程。
* @author pengchengliu
*
*/
public class Interrupting {

private static ExecutorService exec = Executors.newCachedThreadPool() ;

static void test (Runnable task) throws InterruptedException {
Future<?> future = exec.submit(task);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Interrupting " + task.getClass().getName());
future.cancel(true) ;
System.out.println("Interrupt send to " + task.getClass().getName());
}
public static void main(String[] args) throws Exception {
test(new SleepBlocked());
test(new IoBlocked(System.in));
test(new SynchronizedBlocked());
TimeUnit.SECONDS.sleep(3);
System.out.println("Aboring wilth System.exit(0)");
System.exit(-1);
}
}
/**
* 因为主动进入sleep,而让当前线程进入阻塞状态
* @author pengchengliu
*
*/
class SleepBlocked implements Runnable {
@Override
public void run() {
try{
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("SleepBlocked, InterruptedException");
}
System.out.println("SleepBlocked Exiting run");
}
}
/**
* 因为IO,让当前的线程进入阻塞状态
* @author pengchengliu
*
*/
class IoBlocked implements Runnable {
private InputStream input ;
public IoBlocked(InputStream in) {
this.input = in ;
}
@Override
public void run() {
try {
System.out.println("Waiting for read");
input.read() ;
} catch (IOException e) {
if(Thread.currentThread().isInterrupted()){
System.out.println("Interrupted from blocked I/O");
} else {
throw new RuntimeException(e);
}
}
System.out.println("IoBlocked Exiting run");
}
}
/**
* 因为得不到锁,让线程进入阻塞状态
* @author pengchengliu
*
*/
class SynchronizedBlocked implements Runnable {

public SynchronizedBlocked () {
new Thread(){
@Override
public void run() {
f() ;
}
}.start();
}

public synchronized void f () {
while(true){
Thread.yield();
}
}
@Override
public void run() {
System.out.println("try to call f()");
f();
System.out.println("SynchronizedBlocked Exiting run");
}

}
Interrupting com.suansuan.interrupt.SleepBlocked
Interrupt send to com.suansuan.interrupt.SleepBlocked
SleepBlocked, InterruptedException
SleepBlocked Exiting run
Waiting for read
Interrupting com.suansuan.interrupt.IoBlocked
Interrupt send to com.suansuan.interrupt.IoBlocked
try to call f()
Interrupting com.suansuan.interrupt.SynchronizedBlocked
Interrupt send to com.suansuan.interrupt.SynchronizedBlocked
Aboring wilth System.exit(0)

上述例子当中展示了三种常见的阻塞场景,分别是通过sleep进行阻塞,第二种方式就是通过IO上面的等待进行阻塞,第三种就是通过获取锁,而得不到锁进行阻塞,这三种方式,我们统一进行结束,我们发现只有sleep这种被打断,并且退出了,而其余的两种方式并没有结束。使用什么样的方式让其与的两种方式可以结束呢?

4、关闭被IO所阻塞的线程

通过上述的例子,我们看出了IO操作的线程,如果调用interrupt是不好使的,达不到我们的预期效果,那么如何才能有效的关闭被IO所阻塞的线程呢?这里有一种比较笨拙但实际很有效的方法,那就是关闭任务在其上发生阻塞的底层资源,如果一个任务因为inputStream所阻塞,那么我们只要关闭了底层资源,(inputStream.close)那么当前的intputStream也会被关闭。

下面我们通过一个例子来验证我们上述的说法是否正确。

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
/**
* 关闭被IO所阻塞的线程,
* 关闭任务所依赖的底册资源。
* @author pengchengliu
*
*/
public class CloseResource {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();

ServerSocket serverSocket = new ServerSocket(8080);
InputStream inputStream = new Socket("localhost", 8080).getInputStream();

exec.execute(new IoBlocked(System.in));
exec.execute(new IoBlocked(inputStream));

TimeUnit.MILLISECONDS.sleep(100);
System.out.println("shutting down all threads");
exec.shutdownNow();
TimeUnit.SECONDS.sleep(1);
System.out.println("closeing " + inputStream.getClass().getName());
inputStream.close();
TimeUnit.SECONDS.sleep(1);
System.out.println("closeing " + System.in.getClass().getName());
System.in.close();
}
}
Waiting for read
Waiting for read
shutting down all threads
closeing java.net.SocketInputStream
Interrupted from blocked I/O
IoBlocked Exiting run
closeing java.io.BufferedInputStream
IoBlocked Exiting run

注意:某一些版本的JDK还提供了InterruptedIOExecption的支持,但是这些都只是部分的实现,而且只在某一些平台上面可用。所以不能太依赖只有一部分的东西。

上述程序的预期结果和我们的之前猜想的差不多,关闭底层资源确实可以有效的关闭当前被IO所阻塞的线程。还有一种情况便是Nio为我们提供了更加人性化的IO中断操作,被阻塞的Nio通道会自动的响应中断。下面我们通过一个Demo来了解一下

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
/**
* 通过使用NIO,来让被阻塞的nio通道来自动的响应中断
* @author pengchengliu
*
*/
public class NIOBlocked {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();

ServerSocket serverSocket = new ServerSocket(8080);
InetSocketAddress address = new InetSocketAddress("localhost", 8080);
SocketChannel channel1 = SocketChannel.open(address);
SocketChannel channel2 = SocketChannel.open(address);

Future<?> future = exec.submit(new NIOBlockedTask(channel1));
exec.execute(new NIOBlockedTask(channel2));
exec.shutdown();

TimeUnit.SECONDS.sleep(1);
future.cancel(true);

TimeUnit.SECONDS.sleep(1);
channel2.close();

}
}
class NIOBlockedTask implements Runnable {
private final SocketChannel sc ;

public NIOBlockedTask(SocketChannel sc) {
this.sc = sc ;
}

@Override
public void run() {
try{
System.out.println("Waiting for read in " + this) ;
sc.read(ByteBuffer.allocate(1));
} catch (ClosedByInterruptException e){
System.out.println("ClosedByInterruptException");
} catch (AsynchronousCloseException e){
System.out.println("AsynchronousCloseException");
} catch (IOException e) {
throw new RuntimeException(e) ;
}
System.out.println("Exiting NIOBlockedTask.run() " + this);
}
}
Waiting for read in com.suansuan.interrupt.NIOBlockedTask@3c03e5f5
Waiting for read in com.suansuan.interrupt.NIOBlockedTask@352ebfc0
ClosedByInterruptException
Exiting NIOBlockedTask.run() com.suansuan.interrupt.NIOBlockedTask@3c03e5f5
AsynchronousCloseException
Exiting NIOBlockedTask.run() com.suansuan.interrupt.NIOBlockedTask@352ebfc0

通过NIO可很容易的规避之前所出现的问题,我们可以选择使用NIO当中提供的ClosedByInterruptException来关闭我们线程,还可以通过AsynchronousCloseException来关闭线程,一个是通过调用interrupt,一个直接关闭底层的资源,而对于调用interrupt来说,必须使用Nio才OK,而直接关闭底层资源来关闭则适用于所有的IO。

5、关闭被等待锁而阻塞的线程

如果一个线程进入同步块的时候需要获取该对象上面的锁,方能进入,如果这个锁被别的线程所获取,那就必须等待,直到获取锁的线程释放掉锁,放可以进入。这样就造成了互斥情况,方可发生线程的阻塞,下面演示同一个互斥可以如何被同一个任务多次获得。

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
/**
* 同一个互斥可以如何能被同一个对象多次获得
* @author pengchengliu
*
*/
public class MultiLock {
public synchronized void f1 (int count) {
if(count-- > 0) {
System.out.println("f1() calling f2() with count " + count);
f2(count);
}
}
public synchronized void f2(int count) {
if(count-- > 0) {
System.out.println("f2() calling f1() with count " + count);
f1(count);
}
}

public static void main(String[] args) {
final MultiLock multiLock = new MultiLock();
new Thread(){
public void run() {
multiLock.f1(10);
};
}.start();
}
}
f1() calling f2() with count 9
f2() calling f1() with count 8
f1() calling f2() with count 7
f2() calling f1() with count 6
f1() calling f2() with count 5
f2() calling f1() with count 4
f1() calling f2() with count 3
f2() calling f1() with count 2
f1() calling f2() with count 1
f2() calling f1() with count 0

Synchronized内部维护着一个计数器,而一个任务获得锁后,该计数器加1,而后又获得这个对象的锁以后,计数器再加1,而后释放一次锁,则减1,最后直到0为止,代表着当亲锁对象被彻底释放,其他的任务便可以获取这个锁了。

这种线程阻塞的情况,我们调用线程的interrupt来中断当前的阻塞状态不是OK的,例子上述也有,那么我们如何通过合理的方式,让当前的线程结束掉呢?使用JDK5以后的并发类库当中提供的ReentrantLock锁去同步我们的任务,这个锁可以使在其上阻塞的任务具备可以被中断的能力。下面通过一个示例来学习一下。

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
/**
* 关闭被等待锁而阻塞的线程
* @author pengchengliu
*
*/
public class SyncBlocked {
public static void main(String[] args) throws InterruptedException {
//在这里初始化对象,并且初始化BlockedSyncTask的成员变量,当前线程还未开启
Thread thread = new Thread(new BlockedSyncTask());
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Issuing t.interrupt");
thread.interrupt();
}
}
class BlockedSyncTask implements Runnable {
//这个是在主线程当中调用的,初始化BlockedResource,并且在BlockedResource的构造器当中锁住的是main线程,
BlockedResource blockedResource = new BlockedResource();
@Override
public void run() {
System.out.println("waiting for f() in BlockedResource");
//main线程已经锁住了相应的blockedResource对象,当该线程调用的时候发生阻塞
blockedResource.f();
System.out.println("broken out of blocked call");
}
}
class BlockedResource{
private Lock lock = new ReentrantLock();
public BlockedResource () {
//创建自身的时候,锁住自身
lock.lock();
}

public void f() {
try{
/*
* 1)如果当前线程未被中断,则获取锁。
* 2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
* 3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
* 4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:
* 1)锁由当前线程获得;或者
* 2)其他某个线程中断当前线程。
* 5)如果当前线程获得该锁,则将锁保持计数设置为 1。
*
* 如果当前线程:
* 1)在进入此方法时已经设置了该线程的中断状态;或者
* 2)在等待获取锁的同时被中断。
* 则抛出 InterruptedException,并且清除当前线程的已中断状态。
* 6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或重入获取
*/
lock.lockInterruptibly();
System.out.println("lock acquired in f()");
} catch (InterruptedException e){
System.out.println("interrupted from lock acquisition in f()");
}
}
}
waiting for f() in BlockedResource
Issuing t.interrupt
interrupted from lock acquisition in f()
broken out of blocked call

上述的例子当中比较抽象,抽象的点和lock.lockInterruptibly相应的说明,已经在上述例子当中通过注释所说明。

6、检查中断状态

当我们调用一个thread.interrupt方法的时候,thread也许此时是在阻塞状态,同时也有可能是在运行状态,如果是阻塞状态的话,会抛出InterruptedExecption异常,我们只要把握好catch字句就OK,那么如果是运行状态呢? 所以我们需要检查当前线程的中断状态,好判断是否继续运行,一般在run方法的开头这么做,例如下面的例子当中的那样。

在线程当中维护这一个中断状态,其状态可以通过调用线程的interrupt来进行设置。我们可以通过调用interrupted来检查中断状态,这不仅仅可以知道interrupt是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断而通知你两次,你可以经由单一的InterruptedExecption或者单一的成功的Thread.interrupted来测试得到这种通知,如果想要再次检查以了解是否被中断,则可以通过调用Thread.interrupted时将结果保存起来。下面通过一个例子了解一下,检查线程的中断状态。

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
/**
* 检查线程的中断状态
* @author pengchengliu
*
*/
public class Interrupted {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new BlockedTask());
thread.start();

TimeUnit.MILLISECONDS.sleep(1000);

System.out.println("close interrupts");
thread.interrupt();
}
}
/**
* 清理资源
* @author pengchengliu
*
*/
class NeedsCleanup {
private final int id ;
public NeedsCleanup (int ident) {
this.id = ident ;
System.out.println("NeedsCleanup " + this.id);
}
public void cleanup () {
System.out.println("Cleaning up" + this.id);
}
}
/**
*
* @author pengchengliu
*
*/
class BlockedTask implements Runnable {
private volatile double d = 0.0 ;
@Override
public void run() {
try{
while(! Thread.interrupted()) {
NeedsCleanup needsCleanup1 = new NeedsCleanup(1);
try{
System.out.println("Sleeping ");
TimeUnit.SECONDS.sleep(1);

NeedsCleanup needsCleanup2 = new NeedsCleanup(2);
try{
System.out.println("Calculating");
for(int i = 1; i < 250000; i++){
d = d + (Math.PI + Math.E) / d ;
}
} finally {
needsCleanup2.cleanup();
}
} finally {
needsCleanup1.cleanup();
}
}
System.out.println("Exiting via while() test");
} catch (InterruptedException e) {
System.out.println("Exeting via InterruptedException");
}
}
}
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
close interrupts
Cleaning up2
Cleaning up1
Exiting via while() test

线程资源共享

  • 1、不正确的访问资源
  • 2、解决共享资源竞争
  • 3、原子性和易变性
  • 4、原子类
  • 5、临界区
  • 6、在其他对象之上同步
  • 7、线程本地存储

1、不正确的访问资源

关于线程的不正确访问资源所导致的问题,这里就不过多的进行赘述了,有很多的问题,比如:银行存取款之类的。下面主要看一个例子,来看一下现象。

1.1、我们定一个抽象类,这个类的任务就是产出一个int的偶数,并且在里面维护者一个boolean值,这个boolean具体是做什么,等会大家就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 一个数字生成器的抽象类
* @author pengchengliu
*
*/
public abstract class IntGenerator {
private volatile boolean canceled = false ;
public void cancel () {
canceled = true ;
}
public boolean isCancel () {
return canceled ;
}
public abstract int next () ;
}

1.2、定义一个检查一个数字是否为偶数的任务,如果这个数字不是偶数,当前的任务取消,关于任务的取消,我们依赖于刚刚定义的抽象类里面的boolean值,为什么要这么做呢,因为IntGnerator在这例子里面扮演着共公资源,共享公共资源的任务可以观察该资源的终止信号。这样做可以消除所谓的竞争条件,竞争条件 :即两个或者更多的任务竞争响应某一个条件,

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 EventCheck implements Runnable{
private IntGenerator generator ;
private final int id ;
public EventCheck (IntGenerator generator, int ident) {
this.generator = generator ;
this.id = ident ;
}
@Override
public void run() {
while (!generator.isCancel()) {
int val = generator.next() ;
if(val % 2 != 0) {
System.out.println("var = " + val + ", id = " + id + ", not event");
//如果是奇数,则退出循环。
generator.cancel();
}
}
}
public static void test (IntGenerator generator, int count) {
System.out.println("press Control-C to exit");
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
threadPool.execute(new EventCheck(generator, i));
}
threadPool.shutdown();
}
public static void test (IntGenerator generator) {
test(generator, 10);
}
}

即当终止信号一亮,所有的线程都必须退出当前的资源的访问,这样也方便所有的线程终止。

1.3、最后通过实现IntGenerator抽象类来让程序运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*
* @author pengchengliu
*
*/
public class IntGeneratorImpl extends IntGenerator{
private int currentEventValue = 0 ;
@Override
public int next() {
++currentEventValue;
//——————flags——————//
++currentEventValue;
return currentEventValue;
}
public static void main(String[] args) {
EventCheck.test(new IntGeneratorImpl());
}
}

这里使用了两个++,为什么不是+2呢,因为这里要模拟一种场景,所以必须要保证+2这个操作不是原子性的,这种场景就是,一个线程调用到了flags标示的地方,然后另外一个线程已经到了next 方法返回的地方,这样就会看出相应的当前共享资源状态不一致的情况。好让读者看出线程的不确定访问资源的特性。

2、解决共享资源竞争

2.1、为什么会存在资源竞争

进程是操作系统分配资源的最小单位,而线程是操作系统进行调度的最小单位,一个进程当中往往包含着许多的线程,这里需要注意的一点就是:对于在同一个进程的线程来说,这些属于进程的资源是共享的。但是关于并发编程存在这一个基本问题,就是我们永远都不知道一个线程何时在运行,据一个例子,我们坐在餐桌前面准备吃饭,饭刚刚好,我们要吃的时候,突然时间暂停了,等我们回过神的时候,发现饭不见了。这就是一个典型的并发访问资源的例子。这个章节也主要就是处理上述这种问题。

2.2、资源竞争的解决方式

上述这种情况的解决方式便是当资源被一个任务使用的时候,在上面加锁,第一个访问某项资源的任务必须获取该资源的这个锁,才可以对这个资源进行操作,当我们操作完毕完毕以后释放这个锁,在一个任务获取锁之后,其他的任何任务都无法获取锁,并且无法访问这个资源,直到拿锁的那个任务释放锁以后,其他任务获取到这个锁,以此类推。

并发编程当中,所有由线程冲突引起的问题,一般都是通过“序列化访问共享资源”这种方案,大体的意思就是在给定的一段时间内只允许一个任务访问共享资源。实现的时候,我们通常在要操作共享资源的线程代码中加入一条锁语句,这代表的便是在一段时间内只有一个任务可以运行这段代码。因为我们设置的锁语句导致的,通过锁语句实现这种机制,我们一般叫做互斥量

考虑一个例子,多个人上厕所,有一个人是这个厕所的老大(线程调度机制),为了能够上厕所,一个人先去问厕所老大里面人有人么,看看能否使用,如果没人,就进去上厕所并且锁上厕所门,这个时候其他人要使用厕所的时候,就会被阻挡到厕所外面,当第一个人出来的时候释放锁,并告诉厕所的老大,它是用完了,并且打开厕所门(释放锁),这个时候厕所的老大开始衡量下一个进入厕所的人是大便还是小便,选取最合适的人进入厕所。(厕所老大选择人的时候,我们可以给厕所老大通过yield和setPriority提供建议,注意:仅仅只是建议,具体情况需要通过具体平台和JVM的实现)。例子有点污,凑活看

2.3、Java实现

java对防止线程冲突提供了内置支持,也就是我们通过Synchronized关键字就可以做到,由Synchronized包裹的代码段,在执行的时候,jvm检查锁,执行,jvm释放锁。

共享资源一般都是以对象的形式存在,但是也可以是文件,输入输出端口,或者是打印机,如果要控制对共享资源的访问,我们首先要把它包装成为一个类,然后把这个类当中所有访问共享资源的方法加锁。如果某一个任务处于锁块当中,那么在这个任务释放锁之前,其他要调用包装有资源共享类中任何加锁方法的线程都会被阻塞。

在上述生成偶数的例子里面已经看出,我们应该将这个包装类的成员变量声明为private,并且只能通过方法来访问这个成员。如果访问资源只有通过包装类的方法,那么把方法设置成为Synchronized,保证在访问资源的时候只有一个任务在访问它。

2.4、Java内置锁Synchronized的运行原理

a、多个任务调用同一个类的多个Synchronized方法

每一个对象都含有单一的锁,当调用这个对象上面的Synchronized的方法的时候,就会对这个对象加锁,这时这个对象上面的其他的Synchronized方法只有等到上一个方法调用完毕释放锁以后才可以被调用,这里举例子,一个名为A的类里面有两个Synchronized方法,分别是Synchronized b()、Synchronized c()。当一个任务调用了b方法,对于a这个对象而言,其他任务就只有等到b方法调用完毕以后,才可以调用a这个对象的b、c方法。所以对于a这个对象而言,里面所有的Synchronized方法都是在共享一把锁,这样就可以防止多个任务同时访问a这个对象的内存。

b、一个任务在一个方法当中调用了同一个对象的多个Synchronized方法

一个任务可以多次获取同一个对象的锁,这种情况下,jvm内部是通过一个计数器来实现的,当一个任务调用了一次Synchronized方法,内部计数器就会是1,调用了第二次的时候,内部计数器就是2,以此类推,当第一个方法调用完毕的时候,Synchronized就会释放锁-1,直到为0时,该任务就完成了调用,此时别的任务就可以进入锁了。

c、static ,类层级的加锁

针对每一个类,同样也有一把锁(作为类的Class对象的一部分),所以Synchronized static 方法可以防止在类的范围对static数据的并发访问。

注意;我们应该什么时候进行同步控制呢,如果我们正在写一个变量,它可能接下来将被另一个线程所读取,或者正在读取一个上一次被另外一个线程所写过的变量时,我们就必须加锁。如果我们在类当中超过一个方法在处理临界数据,那么我们必须同步所有的相关方法,如果只同步一个方法,那么其他方法将会随意的忽略这个对象的锁。每一个访问临界数据的方法必须被同步,否则加锁的方法将毫无意义。还记得上述的例子么? 我们通过加锁来避免1小节的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 加锁避免线程的不确定访问
* @author pengchengliu
*
*/
public class IntGeneratorImpl1 extends IntGenerator{
private int currentEventValue = 0 ;
@Override
public synchronized int next() {
currentEventValue++ ;
//更有效的切换线程,让出错的可能增加。
Thread.yield();
currentEventValue++;
return currentEventValue;
}
public static void main(String[] args) {
EventCheck.test(new IntGeneratorImpl1());
}
}
press Control-C to exit, 程序将一直运行,不会结束。

上述例子里面,我们使用了Thread.yield方法让出错的可能大大增加,

2.5、显示对象锁Lock对象

在JDK5.0之后,concurrent包还定义第二种加锁方式,那就是Lock对象,这种加锁对象和Synchronized加锁方式相比较,优点就是,我们必须显示的创建、加锁、释放锁。可以让整个加锁流程更加的灵活,更加的多变,缺点就是:使用起来增加了代码量,以及实现加锁没有Synchronized来的简洁明了。关于这种方式的使用,还是套用上述的例子来展示一个Demo来让大家多多了解。

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
/**
* 使用Lock来防止线程的不确定访问。
* @author pengchengliu
*
*/
public class LockIntGenerator extends IntGenerator{
private int currentEventValue = 0 ;
private Lock lock = new ReentrantLock();
@Override
public int next() {
lock.lock();
try{
currentEventValue++ ;
Thread.yield();
currentEventValue++;
return currentEventValue ;
} finally {
//为了防止出现死锁,所以在这里使用finally块当中释放lock,注意后台线程哦。
lock.unlock();
}
}
public static void main(String[] args) {
EventCheck.test(new LockIntGenerator());
}
}

上述例子当中,我们看到了如何使用Lock,但是这里有两点需要强调的地方。

  • 我们一定要使用try-finally字句,来确保我们的锁一定被释放掉了,因为我们使用显示锁的时候,如果我们不显示的释放掉锁,可能我们造成死锁。
  • return语句必须放在在try块当中,这样做的目的就是我们一定要确保在return之后释放锁,确保不是过早的释放锁。

Synchronized内置对象和Lock显示对象到底如何选择呢,这里有一篇博客推荐大家读一下,

3、原子性和易变性

原子操作:就是一组操作不能被线程调度器所中断,一旦操作开始,那么在它执行期间就不会发生上下文切换(线程调度器)。这样也就避免了很多的线程冲突问题。

仔细思考一个问题,如果我们的一组代码都是原子操作,那么我们是不是就没有必要去加锁,反正在其运行期间,线程调度器也不可能切换上下文。这个问题理论上是Ok的,那么我们如何确定一组操作是原子操作呢? 答案是专家及程序员也许可以做到,但是我们呢,很难,很难,首先我们不知道什么样的操作是原子操作,这个地方坑很多,所以我们要坚决抵抗完全依赖自己的能力去写出无锁的同步代码。我们可以看一个例子,来了解原子操作。

在Java当中,内置对象的读操作和写操作都是原子性操作,但是除了long类型的和double类型的,为什么会出现这一种情况,因为long、double 是64位的操作,它们的读写JVM是将其分离位32位进行操作的,这个时候,如果线程调度器在32位结束下一个32位开始之前切换了线程,那么我们的原子性将彻底失败,这种情况就是典型的Java字撕裂。如何避免字撕裂呢,我们可通过volatitle这个关键字来避免。

可视性问题:

在多处理器系统上,在一个应用程序当中不同的任务对应用程序的状态有着不同的视图,例如:一个任务做出了修改,即使在不中断的原子操作下,对其他任务也可能是不可视的(修改只是暂时的存在了本地处理器的缓存当中,并没有刷新到主村当中),那么如何让其他的任务对该操作可视呢,这里有两种解决方式

  • 使用java关键字volatile
  • 使用同步块,也就是加锁

3.1、使用同步块解决可视性问题。

其实当我们对一组操作加锁的时候(这里没有内置锁和显示锁的区别),JVM会将这一组操作所操作的域,强制的刷新到主存当中,这样一个任务做出的修改,一定在应用程序当中对于其他的任务而言是可视的。

3.2、使用Java关键字volatile解决可视性问题

当我们将一个域申明位volatile域的时候,那么只要对这个域产生了写操作,那么所有的读操作都会看到这个修改。即使使用了本地缓存,volatile域会立即的刷新到主存当中。而所有的读操作都是发生在主存当中。

注意:

  • 非volatile域上面的原子操作不必强制刷新到主存,因此其他的读取操作的任务是不会看到这个修改以后的值的。
  • 如果多个任务在同时访问这个域,那么这个域就必须是volatile的,否则,就应该让这个域加锁,让其他任务同步来访问,上述描述过这个,同步也会导致向主存当中进行刷新,
  • 一个域完全由Synchronized方法,或者同步块所包含,那就不必将其设置为volatile的。

两种方式的比较

使用volatile而不是Synchronized的唯一安全情况就是只有一个可变的域,例如:一个域的值依赖于另外一个域,(a++ )某个域的值收到其他域的值的限制,类似于(a < b)小于限制。

再次提醒,我们不是专家,不用写出无锁的代码,所以我们的第一选择是使用同步块,

我们如何查看一个操作是否是原子操作呢,使用java的元命令。 javap -c ClassName 即可。例如下面的操作,注意:必须先编译成为.class文件以后才可以使用javap命令

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
public class Commands{
int i ;
void f(){
i++ ;
}
void b(){
i+=3 ;
}
}
public class Commands {
int i;

public Commands();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

void f1();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return

void f2();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_3
6: iadd
7: putfield #2 // Field i:I
10: return
}

通过上述指令可以看到,在方法内部操作成员的时候,都是先获取然后再次赋值的,在这中间还有好几部操作,而就是因为这几步操作,导致了线程的上下文切换,所以上述的这两个方法都不是相应的原子操作。

下面看一个例子,来看看如果要通过原子操作来写出无锁的同步代码是有多么的难。

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
/**
*
*
* @author pengchengliu
*
*/
public class AtomicityTest implements Runnable{
private int i = 0 ;
public int getValue(){
return i ;
}
private synchronized void eventIncrement(){
i ++ ;
i ++ ;
}
@Override
public void run() {
while(true)
eventIncrement();
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
AtomicityTest test = new AtomicityTest();
threadPool.execute(test);
while(true){
int value = test.getValue();
if(value % 2 != 0){
System.out.println(value);
System.exit(-1);
}
}
}
}

上述的例子最终以失败告终,我们明明加了锁呀,怎么还会失败呢。尽管return i是原子操作,我们没必要加锁,但是缺少同步使得其数值可以在处于不稳定的情况下被读取。除此之外,还存在可视性的问题。

如果从上述加锁的规则来说,我们应该给所有访问共享资源的方法加上锁,这样来达到当前资源的所有状态统一。
解决方式便是给getValue加上锁

4、原子类

JDK5以后引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,他们提供了一些更新操作。

这些类被调整为可以使用在某些现代处理器上的可获得,并且是机器级别上面的原子性,因此在使用它们的时候,通常不用担心。下面来观察一个使用原子类的例子,

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
/**
* 使用原子类,可以提供一些原子类操作
* @author pengchengliu
*
*/
public class AtomicIntegerTest implements Runnable{
private AtomicInteger i = new AtomicInteger(0);
@Override
public void run() {
while(true){
eventIncrement();
}
}
public int getVaule () {
return i.get();
}
private void eventIncrement(){
i.addAndGet(2);
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Aborting");
System.exit(-1);
}
}, 5000);
ExecutorService threadPool = Executors.newCachedThreadPool();
AtomicIntegerTest atomicInteger = new AtomicIntegerTest();
threadPool.execute(atomicInteger);

while(true){
int vaule = atomicInteger.getVaule();
if(vaule % 2 != 0){
System.out.println(vaule);
System.exit(-1);
}
}
}
}

上述代码,我们没有加锁,但是却没有出现关于共享资源的状态不统一的状态,这里我们分为两个方向,来研究一下,分别是操作的原子性,和共享资源的可视性。

  • 操作的原子性,这一点上面,我们使用了Java提供的原子类,那么其操作不用说,肯定就是所谓的原子性了。
  • 资源的可视性,这一点上面,我们没有使用相应的volatile关键字,这些任务对于这个资源是可视的么?答案是一定的 在使用原子类的时候,会默认的向主存当中刷新最新数据,而不会向缓存当中去写。

下面,我们再来看一个Demo,还记得我们之前的那个IntGenerator么?如果我们使用原子类,是否可以避免不用加锁的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 使用原子类来避免加锁
* @author pengchengliu
*
*/
public class AtomicEventGeneratorImpl extends IntGenerator{

private AtomicInteger currentEventValue = new AtomicInteger(0);
@Override
public int next() {
return currentEventValue.addAndGet(2);
}

public static void main(String[] args) {
EventCheck.test(new AtomicEventGeneratorImpl());
}

}

通过上述的代码,我们看到同样成功啦,其实如果我们要实现同步代码,并且不加锁的话,或者考虑到当前的资源操作必须是一些原子操作的时候,我们可以使用JDK5以后提供的这些类,但是这里给大家的建议就是尽量不要依靠自己的能力去写无锁代码,最好是使用锁来进行控制

5、临界区

如果我们要加锁的话,我们会让一个方法进行加锁,这种方式成为同步方法,但是有时候我们的需求是一个方法太过庞大,一下锁这么多代码。感觉执行效率会下降,所以我们解决方式就是使用加锁块,也称为同步块,在进入同步块之前我们需要得到同步块的锁对象,这个对象由我们自己来声明出来。

使用同步块的好处在于不是使用整个方法进行同步控制,可以使多个任务访问对象的时间性能显著提高,下面我们来看一个Demo

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
/**
* 类似于一个JavaBean,当中含有一个内部类,这个内部类是一个异常,
* 并且当中含有一个比较条件,如果条件不成立,则抛出异常。
* @author pengchengliu
*
*/
class Pair {
private int x, y ;
public Pair (int x, int y) {
this.x = x ;
this.y = y ;
}
public Pair () {
this(0, 0) ;
}
public int getX () {
return this.x ;
}

public int getY (){
return this.y ;
}

public void incrrementX () {
x ++ ;
}

public void incrrementY () {
y ++ ;
}

@Override
public String toString() {
return "x : " + x + ", y : " + y;
}

/**
* 一个异常类,如果当前Pair对象的x!= y的时候抛出异常
* @author pengchengliu
*
*/
public class PairVaulesNotEqualException extends RuntimeException{
public PairVaulesNotEqualException () {
super("Pair values not equal :" + Pair.this);
}
}

public void checkState() {
if (x != y) {
throw new PairVaulesNotEqualException();
}
}
}

abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
return new Pair(p.getX(), p.getY());
}
protected void store (Pair p) {
storage.add(p);
try{
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {}
}
abstract public void increment () ;
}
class PairManager1 extends PairManager {
@Override
public synchronized void increment() {
p.incrrementX();
p.incrrementY();
store(p);
}
}
class PairManager2 extends PairManager {
@Override
public void increment() {
Pair tempPair ;
synchronized (this) {
p.incrrementX();
p.incrrementY();
tempPair = getPair();
}
store(tempPair);
}
}
class PairManipulator implements Runnable {

private PairManager manager ;

public PairManipulator(PairManager manager) {
this.manager = manager ;
}
@Override
public void run() {
while(true) {
manager.increment();
}
}

@Override
public String toString() {
return "Pair : " + manager.getPair() + "checkCounter = " + manager.checkCounter.get();
}

}
class PairChecker implements Runnable {
private PairManager manager ;

public PairChecker(PairManager manager) {
this.manager = manager ;
}

@Override
public void run() {
while (true) {
manager.checkCounter.incrementAndGet() ;
manager.getPair().checkState();
}
}
}
/**
* 同步块的展示
* @author pengchengliu
*
*/
public class CriticalSection {
static void testApproaches (PairManager manager1, PairManager manager2) {
ExecutorService threadPool = Executors.newCachedThreadPool();

PairManipulator manipulator1 = new PairManipulator(manager1) ;
PairManipulator manipulator2 = new PairManipulator(manager2);

PairChecker checker1 = new PairChecker(manager1);
PairChecker checker2 = new PairChecker(manager2);

threadPool.execute(manipulator1);
threadPool.execute(manipulator2);

threadPool.execute(checker1);
threadPool.execute(checker2);

try{
TimeUnit.MILLISECONDS.sleep(500);
} catch (Exception e) {
System.out.println("sleep is interrupted");
}

System.out.println("pm1 : " + manipulator1 + "\npm2 : " + manipulator2);
System.exit(0);
}

public static void main(String[] args) {
PairManager pm1 = new PairManager1() ;
PairManager pm2 = new PairManager2() ;
testApproaches(pm1, pm2);
}
}
pm1 : Pair : x : 61, y : 61checkCounter = 3
pm2 : Pair : x : 61, y : 61checkCounter = 123286104

在上述的例子当中,我们通过一个Pair来模拟我们的共享资源,它本来是不安全的,然后我们通过一个PairManager来统一将Pair的访问统一做了相应的加锁处理,而对于我们所要进行加锁的处理方式有两种,

  • 1、就是通过同步方法进行处理,
  • 2、通过同步块来进行处理,
    然后就是模拟我们的任务,我们的任务主要有两种,

  • 1、是控制增量

  • 2、检查增量是否正确,通过结果我们可以看出来,在500毫秒的时间当中,同步方法的执行效率远远的没有同步块的高,所以适当的选择同步块来增加我们代码的效率吧。

6、在其他对象之上同步

如果我们使用同步块的话,我们就必须要使用一个被加锁的对象,默认的写法是锁this,这个效果和同步方法差不多,但是有时候,我们开发的过程当中不得不在另外一个对象上面进行同步。下面我们来看一个Demo,给大家展示一下,如果我们没有在一个对象上面进行同步那么会出现什么情况。

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
/**
* 没有在同一个对象上面进行加锁
* @author pengchengliu
*
*/
public class SyncObject {
public static void main(String[] args) {
final Dualsynch dualsynch = new Dualsynch();
new Thread(){
public void run() {
dualsynch.fun();
};
}.start();
dualsynch.g();
}
}
class Dualsynch {
private Object syncObject = new Object () ;
public synchronized void fun () {
for (int i =0 ; i < 5; i++) {
System.out.println("fun()");
Thread.yield();
}
}
public void g () {
synchronized (syncObject) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
}

由于锁的对象不是同一个对象,所以我们这里的同步方法将会失效,为什么会出现这一种情况呢,原因就是,当我们的任务进入同步方法的时候,获取了相对于this这个对象的锁,而当主线程的任务调用g()方法的时候,获取的是syncObject这个对象的锁,所以出现上述的情况,解决办法就是锁同一个对象就OK

注意:当我们在一个类当中同时需要给多个代码块加锁的话,那么我们就必须要保证相关任务都是在同一个对象上面进行同步的。

7、线程本地存储

其实我们要消除多个任务共享资源时,对资源的冲突操作,其实根本的解决方法就是消除掉这种情况,从根源上面进行消除,我们可以想象如果每一个线程可以像每一个进程一样,都有着自己特有的资源,那么也就从根本上面根除了资源的冲突。那就是线程的本地化存储。

线程本地化存储是一种自动化机制,可以为使用相同变量的不同线程创建不同的存储,比如:有5个线程都要使用变量x,那么线程本地化存储会生成5个x的不同存储块,最重要的便是它们可以将状态和线程相关联。

下面展示一个例子来展示关于线程本地化存储。

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
/**
* 线程的本地化存储
* @author pengchengliu
*
*/
public class ThreadLoaclVariableHolder {

//线程的本地化存储,在使用的时候,一般在类当中的静态变量进行使用
private static ThreadLocal<Integer> sLocalVar = new ThreadLocal<Integer>(){
private Random rand = new Random () ;
protected synchronized Integer initialValue(){
return rand.nextInt(10000);
}
};

public static void increment () {
sLocalVar.set(sLocalVar.get() + 1);
}

public static int get () {
return sLocalVar.get() ;
}

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(new Accessor(i));
}
TimeUnit.MILLISECONDS.sleep(300);
threadPool.shutdownNow() ;
}
}
class Accessor implements Runnable {
private final int id ;
public Accessor (int id) {
this.id = id ;
}

@Override
public void run() {
while (!Thread.interrupted()) {
ThreadLoaclVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
return "#" + id + ": " + ThreadLoaclVariableHolder.get() ;
}
}

装饰者花园

题目:有一所很大的花园,花园提供了多个大门供游人进出花园,花园供应商希望了解每天通过多个大门进入公园的总人数。每个大门都有一个计数器,并且任何一个计数器递增的时候,就表示公园当中的总人数也会递增,反之同理。每一个游客可能从一个大门进入以后从另外的一个大门出去。

分析:这是一个典型的多线程共享资源冲突的算法,有一个总人数,为共享资源,而那几个大门就是不同的任务,随时随地的对人数这个资源进行着读写,所以同步操作一定要注意。下面通过一个Demo来模拟上述场景。

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
/**
* 装饰者花园:
* 题目:有一所很大的花园,花园提供了多个大门供游人进出花园,花园供应商希望了解每天通过多个大门进入公园的总人数。
* 每个大门都有一个计数器,并且任何一个计数器递增的时候,就表示公园当中的总人数也会递增,反之同理。
* 每一个游客可能从一个大门进入以后从另外的一个大门出去。
*
* @author pengchengliu
*
*/
public class OrnamentalGrden {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(new Door(i));
}
TimeUnit.SECONDS.sleep(3);
Door.cancel();
threadPool.shutdown();

if (!threadPool.awaitTermination(250, TimeUnit.MILLISECONDS)) {
System.out.println("some tasks were not terminated!");
}
System.out.println("Total : " + Door.getTotalCount());
System.out.println("sum of door: " + Door.sumCount());
}
}

class Door implements Runnable {
//使用static,让这个变量成为多个Door的持有者,也就是共享域
private static Count sCount = new Count() ;
//每创建一个大门,将大门压入一个共享域的List当中,这样的好处是,将多个相互独立的大门对象托管到大门类的共享域当中
private static List<Door> sDoorList = new ArrayList<Door>();
//终结任务的标示符,为了保证每一个任务都对该变量具有可视性,所以必须保持volatile,并且被多个任务所共享,只需要一份,所以为static
private static volatile boolean sCanceled = false ;

//当前大门所经过的人数,每一个大门和每一个大门不同,所以属于对象层次,不属于类层次
private int number = 0;
// ID 是每一个门的标示,所以置为final,代表只在第一次初始化的时候赋值
private final int ID ;

public Door (int id) {
this.ID = id ;
sDoorList.add(this) ;
}
@Override
public void run() {
while (!sCanceled) {
synchronized (this) {
++ number;
}
System.out.println(this + "Totle : " + sCount.increment());
try{
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("sleep interrupted");
}
}
System.out.println("stopping " + this);
}

public synchronized int getValue () {
return number ;
}

//修改终结标示符,让所有的任务结束。
public static void cancel() {
sCanceled = true ;
}

@Override
public String toString() {
return "Door " + ID + " : " + getValue();
}

/* * * * * * * * * * * static fun * * * * * * * * * * */

public static int getTotalCount () {
return sCount.value() ;
}

public static int sumCount () {
int sum = 0 ;
for (Door door : sDoorList) {
sum += door.getValue() ;
}
return sum ;
}
}

/**
* 花园当中的总计数器
* @author pengchengliu
*
*/
class Count {
private int count = 0 ;
//布鲁斯.艾克尔也这么提到:由47做种后,产生的随机数更加体现了随机性。
//它没有什么具体的意义,只要理解随机数如果有一个种子,那么会出现比较随机的随机数,而当种子是47的时候,随机率是最大的。
private Random random = new Random(47);
public synchronized int increment () {
int temp = count ;
if (random.nextBoolean()) {
//建议调度器切换到其他线程,注意:此时没有释放相应的锁
Thread.yield();
}
return (count = ++temp);
}
public synchronized int value () {return count ;}
}

线程基础(后篇)

  • 1、线程的优先级
  • 2、线程的让步
  • 3、后台线程
  • 3、编码变体
  • 4、合并线程
  • 5、主线程(UI线程)
  • 6、线程组
  • 7、捕获异常

1、线程的优先级

每一个线程都有自己的优先级,默认不设置线程的优先级的话,就是使用默认的优先级,线程的优先级将该线程的重要性传递给线程调度器,尽管CPU处理现有线程的顺序是不确定的,但是调度器将更加倾向于让优先级最高的线程先执行。这里需要注意,并不是意味着优先级低的线程将得不到执行,(优先级不会导致死锁),意思是优先级比较低的线程执行的频率比较低而已

开发的大多数情况下,所有线程都应该使用默认的优先级运行。不要试图操作线程优先级。

优先级在线程当中是一个属性,我们可以通过get/set来操作它。下面我们来看一个关于优先级的示例:

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
/**
* 线程的优先级
* @author pengchengliu
*
*/
public class PriorityDemo implements Runnable{

private int mCountDwon = 5 ;
private volatile double mD ;
private int mPriority ;

public PriorityDemo(int priority) {
this.mPriority = priority ;
}
@Override
public void run() {
//设置线程的优先级
Thread.currentThread().setPriority(mPriority);
while(true){
for (int i = 1; i < 10000; i++){
mD += (Math.PI + Math.E) / (double) i ;
if(i % 1000 == 0){
Thread.yield();
}
}
System.out.println(this);
if(--mCountDwon == 0){
return ;
}
}
}

@Override
public String toString() {
return Thread.currentThread() + " :" + mCountDwon;
}

public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new PriorityDemo(Thread.MIN_PRIORITY));
threadPool.execute(new PriorityDemo(Thread.MAX_PRIORITY));
threadPool.shutdown();
}

}
Thread[pool-1-thread-2,10,main] :5
Thread[pool-1-thread-1,1,main] :5
Thread[pool-1-thread-2,10,main] :4
Thread[pool-1-thread-1,1,main] :4
Thread[pool-1-thread-2,10,main] :3
Thread[pool-1-thread-1,1,main] :3
Thread[pool-1-thread-2,10,main] :2
Thread[pool-1-thread-1,1,main] :2
Thread[pool-1-thread-2,10,main] :1
Thread[pool-1-thread-1,1,main] :1

明白Thread.tostring方法,会自动的打印出线程的名称,还有线程的优先级以及线程所属的线程组,具体打印会是什么样的规则,我们可以通过查看源码来熟悉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* Returns a string representation of this thread, including the
* thread's name, priority, and thread group.
*
* @return a string representation of this thread.
*/
public String toString() {
ThreadGroup group = getThreadGroup();
if (group != null) {
return "Thread[" + getName() + "," + getPriority() + "," +
group.getName() + "]";
} else {
return "Thread[" + getName() + "," + getPriority() + "," +
"" + "]";
}

}

通过查看tostring的源码,我们知道了在第二个参数会主动的打印当前线程的优先级。

我们可以在一个任务的内部拿到驱动这个任务的线程,并且如果我们要给当前的线程设置优先级的话,因该在run的开头部分进行设定,在构造器当中设定当前线程的优先级我们没有任何好处,因为此时Executor此时还没有开始执行任务。

通过上述的Demo,我们不难看出,确实是优先级为10的先执行了。在JDK当中一共描述了10个优先级,但是在不同的平台上面有着不同的映射,最后可移植且效果比好的,就只剩下三个了,分别就是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;

2、线程的让步

如果在run方法当中已经做完了最重要的工作,就可以给线程调度机制一个暗示,我的重要工作已经做完,可以让别的线程优先使用CPU,这个暗示主要是通过yield方法做出,不过这终究是一个暗示,没有任何机制保证它一定会被线程调度机制所采纳。当调用yield的时候,我们也只是在建议具有相同优先级的其他线程可以运行。

注意:对于任何重要的控制或者在调整应用的时候,我们都不能依赖yield,实际上yield经常被误用。

3、后台线程

后台线程(daemon),指的是在线程运行的时候在后台提供一种通用的服务的线程,并且这种线程并不属于程序中不可获取的部分,因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台还在运行,程序就不会终止。那么什么属于非后台线程呢,比如客户端当中控制UI的线程、Java当中的main线程,那么什么属于后台线程呢,比如控制垃圾回收机制的线程。下面Demo来演示如何创建一个后台的线程。

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
/**
* 创建一个后台线程(守护线程)
* @author pengchengliu
*
*/
public class DaemonDemo implements Runnable{
@Override
public void run() {
try {
while(true){
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + "" + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("sleep() interrupted");
}
}

public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 10; i++){
Thread thread = new Thread(new DaemonDemo());
thread.setDaemon(true); //必须在Thread的start方法之前调用有效
thread.start();
}
System.out.println("all daemons started");
TimeUnit.MILLISECONDS.sleep(120);
}

}

注意:必须在线程启动之前调用setDaemon方法,才可以把它设置成后台线程。一旦main线程结束,也就是非后台线程结束,程序也就结束了,后台线程也就结束了,

通过上述例子,我们知道只要我们start之前拿到驱动任务的线程,然后设置它的Daemon,这个线程也就变成了一个后台线程,但是如果我们通过ThreadPool去驱动任务呢?通过ThreadPool驱动任务是sun公司底层已经写好的,所以想要拿到驱动线程还是有点难度,那这个时候,我们应该如何开启后台线程呢,

通过ThreadFactory去定制我们的线程即可,通过ThreadPool驱动线程,都会去调用ThreadFactory去创建线程,我们只要实现这个接口即可,如下例子所示:

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
/**
* 设置ThreadPool当中的线程为后台线程
* @author pengchengliu
*
*/
public class DaemonThreadFactoryDemo implements Runnable{

@Override
public void run() {
try {
while(true){
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + "," +this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new ThreadFactory() {

@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
});
for(int i = 0; i < 10; i++){
newCachedThreadPool.execute(new DaemonThreadFactoryDemo());
}
System.out.println("all daemon started");
TimeUnit.MILLISECONDS.sleep(500);
}
}

注意:如果在一个后台线程当中开启的新的线程也是后台线程,这点需要注意,可以使用isDaemon方法来判断当前的线程是不是后台线程。为了验证这一点,看下面的这个例子:

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
/**
* 探究由后台线程开启的线程是否是后台线程
* @author pengchengliu
*
*/
public class Daemons {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Daemon());
thread.setDaemon(true);
thread.start();
System.out.println("thread.isDaemon = " + thread.isDaemon() + ",");
TimeUnit.SECONDS.sleep(1);
}
}
class Spawn implements Runnable{
@Override
public void run() {
while(true){
Thread.yield();
}
}
}
class Daemon implements Runnable{
private Thread[] threads = new Thread[10];
@Override
public void run() {
for(int i = 0; i < threads.length; i++){
threads[i] = new Thread(new Spawn());
threads[i].start();
System.out.println("spawn " + i + " started");
}

for(int i = 0; i < threads.length; i++ ){
System.out.println("t[" + i + "].isDaemon = " + threads[i].isDaemon() + ".");
}

while(true){
Thread.yield();
}
}
}
thread.isDaemon = true,
spawn 0 started
spawn 1 started
spawn 2 started
spawn 3 started
spawn 4 started
spawn 5 started
spawn 6 started
spawn 7 started
spawn 8 started
spawn 9 started
t[0].isDaemon = true.
t[1].isDaemon = true.
t[2].isDaemon = true.
t[3].isDaemon = true.
t[4].isDaemon = true.
t[5].isDaemon = true.
t[6].isDaemon = true.
t[7].isDaemon = true.
t[8].isDaemon = true.
t[9].isDaemon = true.

通过上述例子,我们可以明白在后台线程当中开启的线程默认也会是后台线程。

注意:后台进程在不执行finally字句的情况下就会终结其run方法。

为什么会出现上述的这种情况呢?,其实这么java这么设计也不是没有一点道理可言的,试想一下,如果我们所有的非后台线程都关闭了,程序会突然停止,所有的资源都会被系统所回收,那么后台线程所占有的资源同样会被回收,那么就没有在finally字句当中确认的必要了。

3、编码变体

到目前为止,所有的例子都是实现Runnable接口,然后通过Thread或者ThreadPool来驱动,但是在实际的开发当中会有非常灵活的用法,下面就把日常开发当中比较灵巧的用法总结一下。

3.1、直接继承Thread,在构造器当中开启自己,然后在使用的地方,只要new出自己即可

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
/**
* 日常开发Thread编码汇总01,
* 直接继承自Thread类,在构造器当中开启。
* @author pengchengliu
*
*/
public class ThreadCoding1 extends Thread{
private int countDown = 5 ;
private static int threadCount = 0 ;

public ThreadCoding1(){
super(Integer.toString(++threadCount));
this.start();
}

@Override
public String toString() {
return "# " + this.getName() + "(" + countDown + ").";
}

@Override
public void run() {
while(true){
System.out.println(this);
if(--countDown == 0)
return ;
}
}

public static void main(String[] args) {
for(int i = 0; i < 6 ; i++){
new ThreadCoding1();
}
}
}

3.2、实现Runnable接口,自管理Runnable接口,让Runnable接口当中含有Thread,然后在构造Runnable接口的时候,通过含有的Thread驱动当前线程。如下所示

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
/**
* 日常开发Thread编码汇总02,
* 实现Runnable接口,然后在runnable接口当中实现相应的内容,
* 内含有Thread对象,在构造方法的时候就去驱动这个Runnable
* @author pengchengliu
*
*/
public class ThreadCoding2 implements Runnable{

private int counDown = 6 ;
private Thread mThread = new Thread(this);

public ThreadCoding2() {
mThread.start();
}
@Override
public void run() {
while(true){
System.out.println(this);
if(--counDown == 0)
return ;
}
}

public static void main(String[] args) {
for(int i = 0; i < 6; i++)
new ThreadCoding2();
}

}

上面两种方式开启线程,在逻辑简单的情况下,没有任何的问题,但是当中逻辑复杂的时候也就会存在诟病,在构造器当中开启线程将会变的非常有问题,因为另一个任务可能会在构造器结束之前开始执行,这便意味着该任务当中存在着不稳定状态的对象。所以上述两种方式这里不是很推荐使用。

3.3、通过内部类来将线程相关的代码隐藏在类中。

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
/**
* 关于线程的变种编码。
* @author pengchengliu
*
*/
public class ThreadCoding4 {
public static void main(String[] args) {
new InnerThread1("innerThread01");
new InnerThread2("innerThread02");
}
}
/**
* 使用内部类来实现,使用内部类的好处在于:
* 1.内部类可以很好的实现隐藏:一般的非内部类,是不允许有 private 与protected权限的,但内部类可以
* 2.内部类拥有外围类的所有元素的访问权限
* 3.可是实现多重继承
* 4.可以避免修改接口而实现同一个类中两种同名方法的调用。
*
* 通过外部的一个类把内部的类包裹一层,这样就对外部的类隐藏了相应的并发的细节
* @author pengchengliu
*
*/
class InnerThread1 {
private int countDown = 5 ;
private Inner inner ;

public InnerThread1 (String name) {
this.inner = new Inner(name);
}

private class Inner extends Thread {
public Inner (String name) {
super(name);
this.start();
}

@Override
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0)
return ;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return this.getName() + " : " + countDown ;
}
}
}
/**
* 通过匿名内部类来实现,
* 一:什么时候选择使用匿名内部类
* 1·只用到类的一个实例。
* 2·类在定义后马上用到。
* 3·类非常小(SUN推荐是在4行代码以下)
* 4·给类命名并不会导致你的代码更容易被理解。
*
* 二:在使用匿名内部类时,要记住以下几个原则:
* 1·匿名内部类不能有构造方法。
* 2·匿名内部类不能定义任何静态成员、方法和类。
* 3·匿名内部类不能是public,protected,private,static。
* 4·只能创建匿名内部类的一个实例。
* 5·一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
* 6·因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效
*
* @author pengchengliu
*
*/
class InnerThread2 {
private int countDown = 5 ;
private Thread mThread ;
public InnerThread2 (String name) {
mThread = new Thread(name) {
@Override
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0)
return ;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString () {
return this.getName() + " : " + countDown ;
}
};
}
}

关于上述实现方式可以根据自己的需求,以及场景灵活改变,毕竟我们要的就是如何实现一个漂亮并且流畅的程序。

4、合并线程

一个线程可以在其他线程上面调用join方法,效果就是等待一段时间直到第二个线程结束才可以执行。如果某个线程在另外一个线程thread1上调用join,那么此线程将被挂起,直到目标线程thread1结束才恢复(即thread1.isAlive返回为假)。下面来看一个例子

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
/**
* 探究Thread.join方法的意义
* @author pengchengliu
*
*/
public class JoinDemo {
public static void main(String[] args) {
Sleeper sleeper01 = new Sleeper("sleeper01", 4000);
Sleeper sleeper02 = new Sleeper("sleeper02", 3000);

Joniner joniner01 = new Joniner("join01", sleeper01);
Joniner joniner02 = new Joniner("join02", sleeper02);

joniner02.interrupt();
}
}
class Sleeper extends Thread {

private int duration ;

public Sleeper (String name, int sleeperTime) {
super(name);
this.duration = sleeperTime ;
this.start();
}

@Override
public void run() {
try {
sleep(duration);
} catch (InterruptedException e) {
System.out.println(this.getName() + " was interrupted. isInterrupted: " + this.isInterrupted() );
return ;
}
System.out.println(this.getName() + " has awakened");
}

}
class Joniner extends Thread {

private Sleeper sleeper ;

public Joniner (String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper ;
this.start();
}

@Override
public void run() {
try {
sleeper.join();
} catch (InterruptedException e) {
System.out.println("Interrupdate");
}
System.out.println(this.getName() + " join completed ");
}
}
Interrupdate
join02 join completed
sleeper02 has awakened
sleeper01 has awakened
join01 join completed

如果在等待期间,不想继续join的时候,可以通过打断挂起的线程来终结任务。就如上述join2所示的一样。

总结:谁调join方法,就是在当前的线程当中等待谁。这里的概念有点抽象。

也可以在调用join方法的时候携带参数,意思就是如果目标线程在这段时间到期时还没有结束的话,join方法失效。

5、主线程(UI线程)

接触过客户端的开发人员应该明白UI线程到底是什么东西,包括最初线程的概念也是从这里衍生出来的,因为主线程也就是UI线程要一直阻塞,用来监听用户所触发的事件,同时还证明了一点就是主线程不能做过多的耗时操作,因为如果耗时操作过多的话,对于用户而言,就比较的卡顿,所以关于许多耗时操作都是交由后台线程去做的。下面介绍一个例子,来证明这一点。

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
/**
* 探究UI线程于后台线程,
* 关于UI线程的监听,则通过从控制台当中读取数据来模拟,
* 耗时操作则通过大量复杂的计算来模拟
* @author pengchengliu
*
*/
public class ResponsiveUI extends Thread{
private static volatile double d = 1 ;

public ResponsiveUI() {
setDaemon(true);
this.start();
}
@Override
public void run() {
while(true){
d = d + (Math.E + Math.PI) /d ;
}
}

public static void main(String[] args) throws IOException {
new UnResponsiveUI();
// new ResponsiveUI();
System.in.read() ;
System.out.println(d);
}

}
/**
* 单线程执行
* @author pengchengliu
*
*/
class UnResponsiveUI {
private volatile double e = 1 ;

public UnResponsiveUI () throws IOException {
while(e > 0){
e += (( Math.PI + Math.E ) /e );
}
System.in.read() ; //导致当前的线程阻塞
}
}

如果想让程序有响应的话,我们就必须把大量的复杂计算放置到后台线程当中。

6、线程组

线程组其实就是线程的集合,它是之前JDK并发里面的一个产物,但是它是一个失败的作品,这里就是了解一下概念即可,以及它产生的原因就是去处理线程的异常。

7、捕获异常

由于线程的特殊性,导致我们不能捕获从线程中逃逸出来的异常。一旦异常逃出了任务的run方法,那么它就会直接向外传播到控制台,除非我们采取特殊的方法捕获这些异常,在JDK5之前,可以通过线程组来捕获这些异常,但是在这之后,我们使用Executor就可以解决这类问题,下面我们一步步来研究如何捕获线程的异常。

7.1、任务产生异常,如果我们不去处理,是否会抛向控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 探究线程当中的异常,如果我们不做处理,将会发生什么事
* @author pengchengliu
*
*/
public class NomalThreadException implements Runnable{

@Override
public void run() {
throw new RuntimeException() ;
}

public static void main(String[] args) {
ExecutorService threadService = Executors.newCachedThreadPool();
threadService.execute(new NomalThreadException());
}
}
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at com.suansuan.thread.ThreadException.NomalThreadException.run(NomalThreadException.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

at java.lang.Thread.run(Thread.java:748)

上述例子很简单,没有什么难度,我们也看出来了,当我们如果在run当中没有处理异常,那么这个异常将会直接抛出到控制台,而我们的程序同时也会崩溃。

7.2、如果我们在线程启动的时候,进行处理,那么我们能否捕获异常。

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
/**
* 探究线程当中的异常,如果我们在启动线程的时候,通过try-catch能否捕获相应的异常
* @author pengchengliu
*
*/
public class StartThreadCatchException implements Runnable{
@Override
public void run() {
throw new RuntimeException() ;
}

public static void main(String[] args) {
try{
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new StartThreadCatchException());
} catch (RuntimeException e){
System.out.println("Exception has been handled !");
}
}
}
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at com.suansuan.thread.ThreadException.StartThreadCatchException.run(StartThreadCatchException.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)

看到了上述结果就知道没有成功,没有捕获到相应的异常。

据官方文档描述,我们需要修改Executor产生线程的方式。Thread.UncaughtExceptionHandler,这个接口是在JDK5以后新出的接口,而这个接口产生的目的就是处理线程当中的异常未捕获的问题,其实我们看它的名字也能猜出来是一个干什么的接口。这个接口允许我们在每一个Thread对象上面浮着一个异常处理器。并且只有一个方法,而这个方法会在线程没有被捕获的异常临近死亡的时候回调。我们可以看一下官方描述

1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}

下面我们来看一个如何通过附着在Thread上面的异常处理器处理异常的例子。

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
/**
* 通过异常处理机制,来处理线程当中产生的异常
* @author pengchengliu
*
*/
public class ExectuorsException {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool(new HandlerThreadFactory());
threadPool.execute(new ExceptionTHread2());
}
}
class ExceptionTHread2 implements Runnable{
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("run() by : " + currentThread.toString());
System.out.println("eh = " + currentThread.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughException implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r) {
System.out.println("create new Thread :" + this);
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyUncaughException());
return thread;
}

}
create new Thread :com.suansuan.thread.ThreadException.HandlerThreadFactory@41a4555e
run() by : Thread[Thread-0,5,main]
eh = com.suansuan.thread.ThreadException.MyUncaughException@4800002d
create new Thread :com.suansuan.thread.ThreadException.HandlerThreadFactory@41a4555e
caught java.lang.RuntimeException

根据编程原则来说,上面是典型的单一职责的设计理念,我们上面使用了ThreadFactory、UncaughtExceptionHandler、Runnable 这三个接口,而这三个接口分别扮演着不同的角色,通过上述例子,关于线程里面的基础部分也就算是告一段落,下面我们来学习,线程之间的资源共享问题。

线程基础(前篇)

  • 1、线程当中的基本概念
  • 2、定义任务
  • 3、驱动任务
  • 4、驱动大量任务
  • 5、任务产出
  • 6、暂停任务

1、线程当中的基本概念

计算机的CPU发展到多个核心数的时候,就可以并发的跑程序,对于用户而言程序越来越快,对于开发者而言就是很大的挑战。并发编程当中,主要的概念就是任务、以及驱动任务这两个概念。

Java常用API也为我们提供了相应的类,来表述上面的两个概念,就是Runnable、Thread这两个概念,在这里我们需要明白的就是Runnable对应着我们新开辟的线程将要做什么样子的事情、而Thread就是去驱动也就是开启我们的定义好的任务。

2、定义任务

Runnable接口来定义一个任务,去实现这个类,然后重写这个类当中的run方法就可以很轻松的去定义一个任务,这里把我们要做的事情在Runnable的run方法当中表述出来即可,下面来看一个小例子,描述如何在Java当中定义一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefineTask implements Runnable{

protected int countDown = 10 ;
private static int taskCount = 0 ;
private final int id = taskCount++ ;
@Override
public void run() {
while(countDown-- > 0){
System.out.println(status());
Thread.yield();
}
}

public String status(){
return "#" + id + "(" + (countDown > 0 ? countDown : "denfine task") + ")," ;
}
}

上述代码,我们很轻松的创建了一个任务,我们通过几个变量来标示这个线程,比如当前线程的ID等,然后就是倒计时的变量。

关于Run方法的实现,在我们日常开发当中,比较常用的做法就是通过一个Looper来让一个任务一直的运行,然后通过一个标示位来控制这个Looper,当任务做完了以后我们主动的调用了yield方法,这个方法是对线程调度器的一种操作,大体的意思就是,我这边已经执行完了相应的任务,可以切换出去给别的线程使用了,需要明白的就是不是一调用这个方法立马就会把切出去,这完全取决于JDK。

3、驱动任务

上述定义了任务,但是如何开启我们的任务呢,这个时候,我们就需要使用Thread类来驱动我们的任务,上面我们定义好了,相应的任务,现在可以尝试的使用Thread来启动这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}
output:
main Thread
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(denfine task),

驱动我们的线程很简单,通过Thread的构造器,把定义好的任务传进去,然后调用start方法就开启了我们的任务,这里需要我们注意的一点就是start方法看起来像是一个长时间执行的方法,但是通过执行结果来看的话,却是立即返回了,原因就是后面的输出语句打印出来了。所以在此刻底层为我们重新创建了一个线程,去执行我们的任务,

这里有有一点需要我们注意一下,那就是关于run和start方法的区别,例如下面的这一条示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	public static void main(String[] args) {
DefineTask defineTask = new DefineTask();
defineTask.run();
System.out.println("main Thread ");
}
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(denfine task),
main Thread

主要看两次的输出结果就可以发现,到底是如何才能区分它们两个。

当我们有大量的任务需要处理的时候,应该如何去做了,也许你已经想到了,那就是在main当中通过一个Looper来创建相应的线程,如下示例所示:

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
public static void main(String[] args) {
for(int i = 0; i < 5; i++){
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}
}


public static void main(String[] args) {
for(int i = 0; i < 5; i++){
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}

}
main Thread main Thread #0(9),
#1(9), main Thread
#2(9), #0(8),
#1(8), main Thread
main Thread #3(9),
#4(9), #2(8),
#4(8), #3(8),
#1(7), #0(7),
#4(7), #2(7),
#3(7), #0(6),
#3(6), #0(5),
#2(6), #0(4),
#4(6), #1(6),
#2(5), #3(5),
#0(3), #4(5),
#0(2), #4(4),
#3(4), #2(4),
#4(3),#3(3),
#1(5), #0(1),
#2(3), #4(2),
#3(2), #1(4),
#2(2), #0(denfine task),
#4(1), #3(1),
#1(3), #2(1),
#4(denfine task), #3(denfine task)
#1(2), #2(denfine task),
#1(1), #1(denfine task),

通过上述的输出结果,可以看到,线程是混在一起执行的,也就是每一次执行的结果都不会相同,这就说明了相应的线程执行的时候,将会存在很多的时许问题。

当main创建相应的线程的时候,当前的类并没有捕获任何对这些对象的引用。在使用普通对象的时候,会获取相应的引用,如果不需要了释放引用,那么垃圾回收就认为这个是一个垃圾,但是使用线程相应的对象的时候就不一样了,在使用的时候,每一个线程都会注册自己,这样就让一个引用都持有它,而且在它的任务退出之前,垃圾回收机制都无法清理它。

需要记住,在对start的调用完毕以后,这个对象将依旧会持续存在。

4、驱动大量任务

如果现在存在大量的任务需要我们去做的时候,应该怎么办,上述方式可以解决,但是如果有1000个任务,那么就需要创建1000一个Thread对象,通过上述知道了Thread对于垃圾回收机制的特殊性,那么我们的内存可想而知,并且我们也不能管理我们的任务。

有了上述的需求,这个时候我们就不能使用单个的Thread类去驱动任务了,需要Java为我们提供的另外一个叫做线程池的概念去驱动,和它的名字是一样的,那就是线程池就像是一个水池一样,当中放着大量的单个Thread,然后通过管理大量的Thread去驱动我们的任务。线程池这个概念是介于客户端和任务之间的一个中间层,与客户端直接执行任务不同,这个中间层允许你管理异步任务的执行,而无需显示的管理线程的生命周期。

Java为我们提供了四种常用的线程池

  • newCacheThreadPool
  • newFixedThreadPool
  • newSingleThreadExecutor
  • newScheduleThreadPool

基本的用法都差不多类似,sun为我们抽出来了特定的接口,我们只要看接口怎么使用就好了,只是具体实现不同而已。先来熟悉一下具体的用法吧,还是使用上述的例子

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
/**
* 线程池的研究
* @author pengchengliu
*
*/
public class ThreadPool implements Runnable{
//当前线程的执行次数。
protected int countDown = 10 ;
private static int taskCount = 0;

//使用一个ID来唯一表示当前的一个任务是必须的!使用fainl是因为我们希望一旦被赋值就不会改变。
private final int id = taskCount ++ ;

public String status(){
return "#" + this.id + "(" + (countDown > 0 ? countDown : "Liftoff!" ) + ").";
}
@Override
public void run() {
//使用循环,使得任务一直运行下去直到不再需要。所以需要设定跳出循环的条件,
//通常run被写成无限循环的形式,这意味着,除非有某个条件是的线程终止,否则他就会永远运行下去
while(countDown-- > 0){
System.out.println(status());
//是对线程调度器的一种建议,它在声明“我已经执行完声明周期中最重要的部分,此刻正是切换给其他任务执行的大好时机”
Thread.yield();
}
}

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++){
service.execute(new ThreadPool());
}
service.shutdown();
}

}

具体的输出和上面一致,这里我们看到sun为我们提供的公共接口就是ExecutorService,我们这里使用了该接口的两个方法,一个是execute,类似于start方法,还有一个就是shutdown方法,使用这个方法防止新任务被提交到线程池,main线程将继续运行在shutdown被调用之间提交的所有任务,这个程序将在Executor中的所有任务完成之后尽快退出。

由于sun为我们抽离出来了共有的接口,所以将原有的实现类替换成为其他的实现类特别简单,下面我们试着替换一下

1
2
3
4
5
6
7
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++){
newFixedThreadPool.execute(new ThreadPool());
}
newFixedThreadPool.shutdown();
}

下面详细说一下这四种线程池的区别的使用场景吧。

4.1、newCacheThreadPool

创建一个可以缓存的线程池,如果线程池的长度超过处理的需要,可灵活回收,若无可回收,则新建线程。

特点

  • 工作线程的创建数量几乎没有限制(通过源码可以知道是Int的最大值),这样就可以灵活的向线程池中添加线程
  • 如果长时间没有往线程池当中提交任务,即如果工作线程空闲了指定的时间(默认是1分钟),则该工作线程将自动终止。终止后,如果你有提交了新的任务,则线程池重新创建一个工作线程
  • 使用该类线程池需要注意:控制任务的数量,否则由于大量线程同时运行,可能会造成系统瘫痪

4.2、newFixedThreadPool

创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始化的最大数,则将提交的任务存入到池队列当中进行等待。

特点

  • FixedThreadPool是一个典型并且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所消耗的开销的优点。
  • 但是在空闲的时候,即线程池中没有运行任务的时候,它不会释放工作线程,并且还会占用一定的系统资源。

4.3、newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的一个工作线程来执行任务,它只会使用唯一的工作线程来执行任务,保证所有任务按照指定顺序(这里给出三种常用的顺序,FIFO,LIFO,优先级)来执行。

特点:

  • 如果这个线程异常结束,遍会新建一个取代它保证顺序执行。
  • 单工作线程可以保证顺序的执行各个任务,并且在任意给定的时间不会有多个线程活动

4.4、newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期的任务执行,支持定时及周期性的任务执行

这个比较特殊,我们来看一个演示的Demo

1
2
3
4
5
6
7
8
9
10
11
12
//延时5秒以后启动任务。
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.schedule(new ThreadPool(), 5, TimeUnit.SECONDS);
}


//延时2秒启动任务,以后每3秒执行一次
public static void main(String[] args) {
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
newScheduledThreadPool.scheduleAtFixedRate(new ThreadPool(), 2, 3, TimeUnit.SECONDS);
}

5、任务产出

我们知道在线程和线程之间是独立的,但是当开发程序的时候,需要两个线程要交互,比如我们有一个大计算需要在子线程当中做,然后反馈给主线程,这个时候就需要使用带有返回值的线程了,我们把这种需求叫做任务产出。

sun为我们定义了Runnable来表示任务,但是当需要返回值的时候,Runnable就感觉力不存心了,这个时候我们使用Callable接口,是sun公司在5.0的时候引入的,它是一种具有类型参数的范型,它的类型参数表示的是从call方法中返回的值,并且必须使用ExectorService.submit()方法驱动这种特殊的任务,下面是一个Demo

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
/**
* 线程当中的带有返回值进行交互
* @author pengchengliu
*
*/
public class CallableDemo{

public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ArrayList<Future<String>> array = new ArrayList<Future<String>>();
//获取数据
for(int i = 0; i < 10; i++){
Future<String> future = newCachedThreadPool.submit(new TaskWithResult(i));
array.add(future);
}

//打印数据
for(Future<String> temp : array){
try {
System.out.println(temp.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
newCachedThreadPool.shutdown();
}
}
}
}
class TaskWithResult implements Callable<String>{
private int id ;

public TaskWithResult(int id){
this.id = id ;
}

public String call() {
return "result of TaskWithResult" + id ;
};

}

result of TaskWithResult0
result of TaskWithResult1
result of TaskWithResult2
result of TaskWithResult3
result of TaskWithResult4
result of TaskWithResult5
result of TaskWithResult6
result of TaskWithResult7
result of TaskWithResult8
result of TaskWithResult9

submit方法会产生一个Future的对象,使用get方法获取里面的值,也可以使用isDone来查看Future是否已经完成。当任务完成的时候,它具有一个结果,我们也可以直接调用get方法获取值,但是需要注意的是,如果结果还没有产生,当前线程将阻塞,知道结果产生。

6、暂停任务

影响任务行为最简单的方式就是sleep了,这将是任务中止执行给定的时间.

调用sleep方法的时候会抛出InterruptedException异常,并且这个异常直接在run当中进行捕获,没有跨越线程到达main,这是因为线程之间异常不能传递,当然上述的Callable却是可以的,这个以后再说,这里需要我们铭记于心的就是,我们必须在本地处理所有任务内部产生的异常。

这里的InterruptedException是在什么情况下触发的呢。

InterruptedException的意思是说当一个线程处于等待,睡眠,或者占用,也就是说阻塞状态,而这时线程被中断就会抛出这类错误。Java6之后结束某个线程A的方法是A.interrupt()。如果这个线程正处于非阻塞状态,比如说线程正在执行某些代码的时候,不过被interrupt,那么该线程的interrupt变量会被置为true,告诉别人说这个线程被中断了(只是一个标志位,这个变量本身并不影响线程的中断与否),而且线程会被中断,这时不会有interruptedException。但如果这时线程被阻塞了,比如说正在睡眠,那么就会抛出这个错误。请注意,这个时候变量interrupt没有被置为true,而且也没有人来中断这个线程。例如:

1
2
3
4
5
6
7
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
System.err.println("thread interrupted");
}
}

当线程执行sleep(1000)之后会被立即阻塞,如果在阻塞时外面调用interrupt来中断这个线程,那么就会执行

System.err.println(“thread interrupted”);

这个时候其实线程并未中断,执行完这条语句之后线程会继续执行while循环,开始sleep,所以说如果没有对InterruptedException进行处理,后果就是线程可能无法中断

所以,在任何时候碰到InterruptedException,都要手动把自己这个线程中断。由于这个时候已经处于非阻塞状态,所以可以正常中断,最正确的代码如下:

1
2
3
4
5
6
7
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.interrup();
}
}

在5.0以后sleep是可以带TimeUtil的,这也就方便了我们的使用。

Android数据库(3)、Android对SQLite数据库的支持

Android官方默认的数据库就是SQLite数据库,并且相应的Android API 也对SQLite数据做了一定的支持,下面主要通过四个方面来了解一下,具体在Android的程序代码当中如何使用相应的数据库。

一、创建数据库,(SQLiteOpenHelper)

Android系统当中,数据库其实就是一个文件系统的文件,不过这个文件是以DB结尾的文件。创建数据库,主要分为两种,就是使用默认的SQLiteOpenHelper来创建属于当前应用程序的沙盒数据库,什么是沙盒数据库呢,就是默认的db文件在/data/data当中,第二种创建数据库就是在SD卡当中创建数据库。下面通过两个示例来了解一下。

1、使用默认的SQLiteOpenHelper创建沙盒数据库。

使用这种方式,主要就是让数据库当中的数据只给当前的应用来使用,当然通过ContentProvider属于另当别论了,想要创建一个数据库,很简单,我们只需要创建一个类,让这个来继承相应的SQLiteOpenHelper即可,重写相应的方法,来让Android来进行回调,完成一个数据库的创建,下面就是示例代码。

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
* 创建数据库
* @author pengchengliu
*/
public class KeyValueDataHelper extends SQLiteOpenHelper{

private static final String DB_NAME = "test.db";
private static final int DB_VERSION = 1 ;

private static final String DB_INIT_TABLE = "CREATE TABLE test(id INTEGER " +
"PRIMARY KEY NOT NULL,key TEXT,value TEXT)" ;

/**
* 创建相应的数据库,
* @param context:底层需要通过Context拿到相应的沙盒文件路径,以及通过Context来打开或者创建数据库
*
* 到相应的沙盒文件路径
* final String path = mContext.getDatabasePath(mName).getPath();
*
* 通过Context来打开或者创建数据库
* db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
* Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
* mFactory, mErrorHandler);
*/
public KeyValueDataHelper(Context context) {
//CursorFactory:
//String name, 数据库文件的名字
//CursorFactory factory, Cursor工厂,查询时返回自定义的Cursor对象
//int version:当前数据库的版本,通过该标示来判断数据库的降级以及升级操作
super(context, DB_NAME, null, DB_VERSION);
}

/**
* 数据库刚刚建立的时候进行初始化工作,在我们日常开发当中常常使用这个方法来创建我们需要表结构,以及一些常用的
* 初始化工作。
* @param db :底层刚刚创建好的DataBase数据库操作对象,通过该对象我们就可以创建表结构。
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_INIT_TABLE);
}

/**
* 数据库刚刚建立的时候进行初始化工作,在我们日常开发当中常常使用这个方法来配置我们的数据库,
* 比如打开我们数据的关系完整性等,具体常用配置,如下所示。
*
* @param db:底层刚刚创建好的DataBase数据库操作对象,通过该对象我们就可以通过这个对象来配置我们的数据库
*
* pragma auto_vacuum = 0|1; 没有数据有效,有数据无效,顾应该在创建表之前
* pragma cache_size=9000;default_cache_size
* pragma page_size = bytes;
* pragma synchronous = Full/Normal/off
* pragma temp_store = Memory/File/Default
*
* 注意:这个方法会在,我们SQLiteOpenHelper#onCreate回调之前进行回调,
*
* 回调时机
* SQLiteOpenHelper#getDatabaseLocked(){
* onConfigure(db);
* final int version = db.getVersion();
* if (version != mNewVersion) {
* if (db.isReadOnly()) {
* throw new SQLiteException("Can't upgrade read-only database from version " +
* db.getVersion() + " to " + mNewVersion + ": " + mName);
* }
* db.beginTransaction();
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* onUpgrade(db, version, mNewVersion);
* }
* }
* db.setVersion(mNewVersion);
* db.setTransactionSuccessful();
* } finally {
* db.endTransaction();
* }
* onOpen(db);
* }
* 默认不会对数据库做任何配置。
*/
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
}

/**
* 数据库版本升级的时候,我们就可以使用这个方法来维护我们的数据库,让其变为更加强大的存在。
* @param db :底层创建好的DataBase数据库操作对象,通过该对象我们就可以在版本变动的时候,维护我们的数据了
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}

/**
* 数据库版本降级的时候,会进行回调,注意,这个方法不是空实现,这个方法的实现如下所示:
*
* public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
* throw new SQLiteException("Can't downgrade database from version " +
* oldVersion + " to " + newVersion);
* }
*
* 这也是为什么我们数据降级的时候,会报错的主要原因,如果要解决上述这个问题,那么我们需要重写这个方法,让其空实现即可
*
* @param db :底层创建好的DataBase数据库操作对象,通过该对象我们就可以在版本变动的时候,维护我们的数据了
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
super.onDowngrade(db, oldVersion, newVersion);
}

/**
* 获取数据库的名字。
* @return 数据库的名字,其值就是我们在SQLiteOpenHelper构造函数当中传入的Name的值
*/
@Override
public String getDatabaseName() {
return super.getDatabaseName();
}

/**
* 获取可读的数据库操作对象SQLiteDatabase,其底层是通过一个getDatabaseLocked方法来实现的
* @return 数据库操作对象SQLiteDatabase
*/
@Override
public SQLiteDatabase getReadableDatabase() {
//getDatabaseLocked
return super.getReadableDatabase();
}

/**
* 获取可写的数据库操作对象SQLiteDatabase,其底层是通过一个getDatabaseLocked方法来实现的
* @return 数据库操作对象SQLiteDatabase
*/
@Override
public SQLiteDatabase getWritableDatabase() {
//getDatabaseLocked
return super.getWritableDatabase();
}

/**
* 启用或禁用数据库的预写日志记录的使用。
* 预写日志记录不能与只读数据库一起使用,如果数据库以只读方式打开,则忽略此标志。
* @param enabled,启用预写式日志记录,true为启用,false为被禁用。
*
* 注意:其底层是通过
* Database.enableWriteAheadLogging();
* Database.disableWriteAheadLogging();来实现的。
*/
@Override
public void setWriteAheadLoggingEnabled(boolean enabled) {
super.setWriteAheadLoggingEnabled(enabled);
}
}

上述代码当中,应该注意的东西都使用注释的形式,进行了相应的标注,这里还有两个比较容易混淆的两个概念,和一个关于初始化SQLiteOpenHelper的相应问题,需要我们具体的进行讨论。

1.1、getReadableDatabase()方法和getWritableDatabase()的区别究竟是什么?
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
//SQLiteDatabaseHelper#getDatabaseLocked(boolean writable)
if (db != null) {
if (writable && db.isReadOnly()) {
db.reopenReadWrite();
}
} else if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
try {
if (DEBUG_STRICT_READONLY && !writable) {
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
} else {
db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
mFactory, mErrorHandler);
}
} catch (SQLiteException ex) {
if (writable) {
throw ex;
}
Log.e(TAG, "Couldn't open " + mName
+ " for writing (will try read-only):", ex);
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
}
}

//SQLiteDatabaseHelper#openDatabase
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
db.open();
return db;

在源码当中找到了,上述一段代码。可以大体看一下,上述代码用语言总结一下就是

  • getWritableDatabase()和getReadableDatabase()方法都可以获取一个用于操作数据库的SQLiteDatabase实例。但是getWritableDatabase()方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,getWritableDatabase()打开数据库就会出错。getReadableDatabase()方法先以读写方式打开数据库,倘若使用如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。

一般情况下两者返回情况都是相同的,唯一的区别是:在数据库仅开放只读权限或磁盘已满时,getReadableDatabase只会返回一个只读的数据库对象。

1.2、SQLiteOpenHelper的初始化,做了哪些重量级的操作,真正的初始化工作,是在什么地方开始做的。

通过研究SQLiteOpenHelper的源码,我们发现正常去和数据库建立连接不是在new SQLiteOpenHelper的时候触发的,真正触发的点就是getWritableDatabase()和getReadableDatabase(),所以在使用的时候,切记UI线程当中最好不要去调用getReadableDatabase/getWritableDatabase这两个方法,这只是为了我们程序的稳定性来说的。

2、在文件系统的SD当中创建数据库。

通过研究和仿照SQLiteOpenHelper,可以提取到精髓的地方,那就是可以使用Google的方式,在SD卡当中创建一个数据库,首先说说在什么情况下,需要在SD卡上面创建我们的数据库,也就是说在SD卡上面创建数据库的优点是什么?

2.1、当系统卸载我们的应用的时候,当我们重新安装应用的时候,数据不会丢失。
2.2、方便备份、恢复。

当然缺点也是有的,数据库不安全等。笔者这里以技术的角度来说明,如何在SD卡上面创建我们的数据,但是不代表笔者赞同这一种做法,这种做法会很严重的破坏Android的生态,对于用户来说,安装了应用,在卸载了应用以后,数据没有清干净,这就代表着,应用程序不是以一个完整沙盒的模式呈现给操作系统,操作系统也不方便管理应用程序,

笔者在这里的建议就是,最好使用Google原生提供的目录来存储哪些需要存储的数据。给出以下目录,供各位参考

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
// 获得缓存文件路径,磁盘空间不足或清除缓存时数据会被删掉,一般存放一些临时文件
// /data/data/<application package>/cache目录
File cacheDir = getCacheDir();
Log.d("TAG", "getCacheDir() : " + cacheDir.getAbsolutePath());

// 获得文件存放路径,一般存放一些需要长期保留的文件
// /data/data/<application package>/files目录
File fileDir = getFilesDir();
Log.d("TAG", "getFilesDir() : " + fileDir.getAbsolutePath());

// 这是一个可以存放你自己应用程序自定义的文件,你可以通过该方法返回的File实例来创建或者访问这个目录
// /data/data/<application package>/
File dir = getDir("fileName", MODE_PRIVATE);
Log.d("TAG", "getDir() : " + dir.getAbsolutePath());

// 获取应用程序外部存储的缓存目录路径
// SDCard/Android/data/<application package>/cache目录
File externalCacheDir = getExternalCacheDir();
Log.d("TAG", "getExternalCacheDir() : " + externalCacheDir.getAbsolutePath());

// 获取应用程序外部存储的某一类型的文件目录,
// SDCard/Android/data/<application package>/files目录
// 这里的类型有
// Environment.DIRECTORY_MUSIC音乐
// Environment.DIRECTORY_PODCASTS 音频
// Environment.DIRECTORY_RINGTONES 铃声
// Environment.DIRECTORY_ALARMS 闹铃
// Environment.DIRECTORY_NOTIFICATIONS 通知铃声
// Environment.DIRECTORY_PICTURES 图片
// Environment.DIRECTORY_MOVIES 视频
File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
Log.d("TAG", "getExternalFilesDir() : " + externalFilesDir.getAbsolutePath());

// 获取应用的外部存储的缓存目录
File[] externalCacheDirs = getExternalCacheDirs();
for (int i = 0; i < externalCacheDirs.length; i++) {
Log.d("TAG", "getExternalCacheDirs() " + i + " : " + externalCacheDirs[i].getAbsolutePath());
}

// 获取应用的外部存储的某一类型的文件目录
File[] externalFilesDirs = getExternalFilesDirs(Environment.DIRECTORY_MUSIC);
for (int i = 0; i < externalFilesDirs.length; i++) {
Log.d("TAG", "getExternalFilesDirs() " + i + " : " + externalFilesDirs[i].getAbsolutePath());
}

// 获取应用的外部媒体文件目录
File[] externalMediaDirs = getExternalMediaDirs();
for (int i = 0; i < externalMediaDirs.length; i++) {
Log.d("TAG", "getExternalMediaDirs() " + i + " : " + externalMediaDirs[i].getAbsolutePath());
}

// 获得应用程序指定数据库的绝对路径
// /data/data/<application package>/database/database.db目录
File databasePath = getDatabasePath("database.db");

SD卡创建数据库的具体思路是: 通过android的SQLiteOpenHelper类的源码,可以看到SQLiteOpenHelper类的getWritableDatabase这个接口实际上调用的是Context的openOrCreateDatabase方法,而这个方法是不支持带路径的数据库名称的,也就是说,用这个方法创建的数据库只能放在/data/data/包名称/ 目录下;要想在SD卡上创建数据库,我们可以调用SQLiteDatabase类的openOrCreateDatabase方法,这个方法是支持带路径的数据库名称的。

下面来看一则示例,

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
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;

import java.io.File;
import java.io.IOException;

/**
* SD卡创建数据库,SQLiteDatabase类的openOrCreateDatabase支持带参数,所以选择该方法
* Created by pengchengliu on 2018/3/24.
*/
public class DatabaseSDManager {

private static final String DATABASE_FILE_DIRECTORY = "/dateBase" ;
private static final String DATABASE_PATH = Environment.getExternalStorageDirectory().getPath() +
File.separator + DatabaseSDManager.class.getPackage().getName() + DATABASE_FILE_DIRECTORY;

private String mDataBaseName ;
private SQLiteDatabase.CursorFactory mFactory ;
private SQLiteDatabase mSQLiteDatabase ;

public DatabaseSDManager(String dtaBaseName){
this(dtaBaseName,null);
}

public DatabaseSDManager(String dataBaseName, SQLiteDatabase.CursorFactory factory ){
this.mDataBaseName = dataBaseName ;
this.mFactory = factory ;
}

public SQLiteDatabase getDataBase(){
if(mSQLiteDatabase == null){
mSQLiteDatabase = SQLiteDatabase.
openOrCreateDatabase(initDatabaseFilePath(), this.mFactory);
}
return mSQLiteDatabase;
}


/**
* 初始化DataBase的文件目录
*/
private File initDatabaseFilePath() {
boolean sdIsExist = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
if(!sdIsExist){
throw new IllegalArgumentException("SD is not Find, Please check Sd state");
}

File dirfile = new File(DATABASE_PATH + File.separator);
if(!dirfile.exists()){
if(!dirfile.mkdirs()){
throw new RuntimeException("create file is fail");
}
}

boolean isCreateDbFileSuccess = false ;

File dbFile = new File(dirfile, this.mDataBaseName);
if(!dbFile.exists()){
try {
isCreateDbFileSuccess = dbFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {
isCreateDbFileSuccess = true ;
}
return isCreateDbFileSuccess ? dbFile : null ;
}

public void close(){
if(mSQLiteDatabase != null){
mSQLiteDatabase.close();
mSQLiteDatabase = null ;
}
}
}

上述的Demo当中,我们没有加入Version的维护,只是一个简单的小例子,如果在开发工作当中,必定会加入版本的维护,具体代码如何实现,可以具体参考SQLiteOpenHelper的实现,

还有一种实现方式便是我们自定义Context来实现,我们通过源码知道,我们使用SQLiteOpenHelper的时候,具体的路径是写死的,我们可以自定义Context来实现我们的这个功能,具体的源码实现如下所示:

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
import android.content.Context;
import android.content.ContextWrapper;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.io.File;
import java.io.IOException;

/**
*
* Created by pengchengliu on 2018/3/25.
*/

public class DataBaseContext extends ContextWrapper {
/**
* 构造函数
*
* @param base 上下文环境
*/
public DataBaseContext(Context base) {
super(base);
}

/**
* 获得数据库路径,如果不存在,则创建对象对象
*/
@Override
public File getDatabasePath(String name) {
//判断是否存在sd卡
boolean sdExist = android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState());
if (!sdExist) {//如果不存在,
Log.e("SD卡管理:", "SD卡不存在,请加载SD卡");
return null;
} else {//如果存在
//获取sd卡路径
String dbDir = android.os.Environment.getExternalStorageDirectory().toString();
dbDir += "/scexam";//数据库所在目录
String dbPath = dbDir + "/" + name;//数据库路径
//判断目录是否存在,不存在则创建该目录
File dirFile = new File(dbDir);
if (!dirFile.exists())
dirFile.mkdirs();

//数据库文件是否创建成功
boolean isFileCreateSuccess = false;
//判断文件是否存在,不存在则创建该文件
File dbFile = new File(dbPath);
if (!dbFile.exists()) {
try {
isFileCreateSuccess = dbFile.createNewFile();//创建文件
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else
isFileCreateSuccess = true;

//返回数据库文件对象
if (isFileCreateSuccess)
return dbFile;
else
return null;
}
}

/**
* 重载这个方法,是用来打开SD卡上的数据库的,android 2.3及以下会调用这个方法。
*
* @param name
* @param mode
* @param factory
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
SQLiteDatabase.CursorFactory factory) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
return result;
}

/**
* Android 4.0会调用此方法获取数据库。
*
* @param name
* @param mode
* @param factory
* @param errorHandler
* @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, int,
* android.database.sqlite.SQLiteDatabase.CursorFactory,
* android.database.DatabaseErrorHandler)
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
return result;
}
}

然后,我们使用的时候只要按照,最初的时候就OK。

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
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
* DataBase helper class
* Created by pengchengliu on 2018/3/25.
*
*/
public class DbOpenHelper extends SQLiteOpenHelper {

private static final String DB_NAME = "test.db";
private static final int VERSION = 1;

public DbOpenHelper(Context context) {
super(context instanceof DataBaseContext ? context : new DataBaseContext(context),
DB_NAME, null, VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {//It will be called when the database was created first
db.execSQL("CREATE TABLE IF NOT EXISTS exam_type (id integer primary key autoincrement, type_name varchar(100), type_id INTEGER)");
}

@Override // It'll be called when the database was updated
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}
}

到此,关于数据库的创建的所有知识都梳理了一遍。

二、管理数据库

据Google官方所说,一个打开数据库的连接,将会占用1kb的内存,在内存受限的移动设备来说,这1kb足够引起我们的注意,现在来讨论一下,如何管理一个数据库的生命周期,这里笔者给出两种管理策略,这两种策略也是我们日常开发当中常常使用的,具体的策略如下所示:

1、让应用程序来一直持有相应的数据库连接的引用,也就是一直持有SQLitDatabase这个类的引用。

这种方式很简单,就是申明一个成员变量来保持当前数据库引用。如果我们这样做了,其实就代表着这一块的内存,我们无法回收,并且无法重用。这里需要注意的是,当我们的应用程序至于后台,并且在手机内存不足的情况下回收内存的时候,如果我们没有显示的关闭和数据库的连接,那么应用程序将会报错。所以在使用这一种的策略的时候,一定要注意关于db的关闭问题。

其次还有一个需要我们注意的是在Activity当中持有一个数据库的连接的时候,必要的是在Actiivty销毁的时候,关闭该连接,其实如果我们的应用程序当中存在大量的数据访问工作,我们需要在Application当中去持有这个连接引用,好方便在多个Actiivty,Service当中去获取这个引用,具体实现如下所示:

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
import android.app.Application;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import suansuan.com.contentprovider.sql.KeyValueDataHelper;

/**
* 管理数据库的引用
* Created by pengchengliu on 2018/3/25.
*/

public class DemoApplication extends Application{

private SQLiteOpenHelper mDbHelper ;
private Thread mUiThread ;

@Override
public void onCreate() {
super.onCreate();

mDbHelper = new KeyValueDataHelper(this);
mUiThread = Thread.currentThread() ;
}

public SQLiteDatabase getSQLiteDatabase(){
if(Thread.currentThread().equals(mUiThread)){
throw new RuntimeException("Database opened on main Thread");
}
//根据自己的具体需求,确定使用可写数据库还是可读数据库。具体区别,可以参考上述章节内容
return mDbHelper.getWritableDatabase();
}
}

2、需要连接的时候再获取。

使用这种策略,必须要清楚当前应用程序当中有哪些对象获取了数据库的连接,关于Java的垃圾回收机制,我们知道当一个对象只有不被任何对象引用的时候,才会被回收,因此,如果我们关闭数据库而没有释放所有对它的引用,将是毫无用处的,因为除非释放最后一个引用,否则该对象不能被回收。我们知道SQLiteOpenHelper、SQLiteQuery、SQLiteCursor保存着数据库的引用,所以在不用的时候,记得回收。

三、操作数据库

1、基本SQL语句的嵌入

Android,当中提供了SQLiteDatebase这个类来操作数据库,这个类提供了一个最基本的和数据库交互的方法,那就是execSQL这个方法,通过这个最基本的方法,我们就可以去操作数据库,可以使用这个方法去创建表结构等,但是使用这种方法,有很多的确定,下列就来描述一下,对开发人员来说很不要的缺点。

  • A、在SQLite数据库当中有一部分的命令不是直接解释给SQL的,而是面向SQLite的,我们称这些为元命令,使用execSQL方法的第一个缺点就是不能执行所谓的元命令。常见元命令有以下这几种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.nullvalue STRING    在 NULL 值的地方输出 STRING 字符串。
.output FILENAME 发送输出到 FILENAME 文件。
.output stdout 发送输出到屏幕。
.print STRING... 逐字地输出 STRING 字符串。
.prompt MAIN CONTINUE 替换标准提示符。
.quit 退出 SQLite 提示符。
.read FILENAME 执行 FILENAME 文件中的 SQL。
.schema ?TABLE? 显示 CREATE 语句。如果指定了 TABLE 表,则只显示匹配 LIKE 模式的 TABLE 表。
.separator STRING 改变输出模式和 .import 所使用的分隔符。
.show 显示各种设置的当前值。
.stats ON|OFF 开启或关闭统计。
.tables ?PATTERN? 列出匹配 LIKE 模式的表的名称。
.timeout MS 尝试打开锁定的表 MS 毫秒。
.width NUM NUM"column" 模式设置列宽度。
.timer ON|OFF 开启或关闭 CPU 定时器。
  • B、不能打包多条SQL语句,按照规定在使用方法execSQL这个方法的时候,按理应该只有一条语句。如果我们非要插入多条语句,并且使用分号隔开,那么以第一个分号结尾以后的语句将被忽略。

  • C、不能使用query相关的语句,execSQL这个方法是没有返回值的,所以涉及到查询语句将不能被使用。并且当我们使用execSQL这个方法查询时候,会抛出异常,提示要使用别的方法。这里会出现一个rawSQL的方法,其实这个方法也属于低级的方法。

一般来说,execSQL、rawSQL这两个方法来说就像是一起冷兵器,开发人员很少使用,最多execSQL用来创建表结构,触发器结构,而rawSQL开发人员基本就没有用过。不是作为编程执行SQL的一般性工具,相反,它们应该是用作访问不常用的SQL的最后手段,

为了替代应用程序代码中嵌入未经检查、非类型化的SQL字符串,Android SQLite API把SQL语义放入到API方法当中,主要分为四类,删除、更新、插入、查询。

2、删除

2.1、对外提供接口

通过观察SQLiteDatabase的API,我们不难发现有一个方法,是删除表里面的数据的,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 删除数据库表当中的一条数据,
* @param table :要操作的表名字
* @param whereClause :where条件语句,其中的值可以使用占位符来代替
* @param whereArgs : 对前面占位的实体值
* @return : 该操作对一个表结构来说影响了几行
*/
public int delete(String table, String whereClause, String[] whereArgs) {
acquireReference();
try {
SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table +
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

通过源码,我们不难看出,这里SQLiteDatabase把相应的操作是委托给了SQLiteStatement,让其进行了相应的删除操作,并且相应的SQL语句也是在这里拼接的。具体内部是如何实现的,不是本章的内容。如果感兴趣可以给下层追一下。

3、更新

实现更新操作的Android API这一次列操作也是比较少的,包括两个常用方法update、updateWithOnConflict,这两个的具体实现如下所示:

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
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
}

/**
* 数据库表字段的更新操作
*
* @param table : 要操作的表名
* @param values : 要修改的值
* @param whereClause : where的查询条件
* @param whereArgs : 前面查询条件的替换值
* @param conflictAlgorithm 当破坏主键或者其他字段,所采取的措施,有以下取值
*
* CONFLICT_FAIL :当一个发生违反约束的情况,命令将以返回码中止 SQLITE_CONSTRAINT。
* CONFLICT_IGNORE :当一个约束int违例发生,包含约束的一行 不会插入或更改违规。
* CONFLICT_NONE :U如果没有指定冲突行为,请参阅以下内容。
* CONFLICT_REPLACE :当发生UNIQUE约束冲突时,他预先存在的行是 导致在插入或更新之前删除约束条件 当前行。
* CONFLICT_ROLLBACK :发生约束冲突时,会立即发生ROLLBACK 结束当前交易,该命令将以一个返回码中止 SQLITE_CONSTRAINT。
*
* @return 影响的行数
*/
public int updateWithOnConflict(String table, ContentValues values,
String whereClause, String[] whereArgs, int conflictAlgorithm) {
if (values == null || values.size() == 0) {
throw new IllegalArgumentException("Empty values");
}

acquireReference();
try {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(table);
sql.append(" SET ");

// move all bind args to one array
int setValuesSize = values.size();
int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
Object[] bindArgs = new Object[bindArgsSize];
int i = 0;
for (String colName : values.keySet()) {
sql.append((i > 0) ? "," : "");
sql.append(colName);
bindArgs[i++] = values.get(colName);
sql.append("=?");
}
if (whereArgs != null) {
for (i = setValuesSize; i < bindArgsSize; i++) {
bindArgs[i] = whereArgs[i - setValuesSize];
}
}
if (!TextUtils.isEmpty(whereClause)) {
sql.append(" WHERE ");
sql.append(whereClause);
}

SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

可以看出这里,update属于updateWithOnConflict的子集,上述做了两件事,和上述的删除操作类似,拼接SQL语句,创建一个SQLiteStatement,最后通过statement去执行相应的操作,这里说一下这两个更新方法的区别是什么?

updateWithOnConflict方法含义呢,就是在如果修改中破坏了相应的主键约束,或者说是一定的约束条件,不仅仅是主键约束,需要底层需要如何处理,也就是当发生破坏表字段的约束的时候,需要数据库底层去准备哪些策略,具体策略,可以参考上述代码的注释

ContentValuesd,Android SQLite官方所引入的新的一种数据结构,当我们去修改,或者插入的时候,通过构造着一种数据结构,来整理数据,以便高效的插入。其实它的内部的实现就是一个简单的HashMap<K,V>,那么仔细想一下,为什么不直接使用Map就可以插入,还要多封装一种数据结构呢,其实很简单,只是为了适配相应的SQLite数据库,因为数据库是弱类型的,所以需要这种数据结构,我们看几个它的方法,相信各位看客就明白为什么需要它的原因了,

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
public final class ContentValues implements Parcelable {
public static final String TAG = "ContentValues";

/** Holds the actual values */
private HashMap<String, Object> mValues;


/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Short value) {
mValues.put(key, value);
}

/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Integer value) {
mValues.put(key, value);
}

/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Long value) {
mValues.put(key, value);
}

….

/**
* Gets a value and converts it to a String.
*
* @param key the value to get
* @return the String for the value
*/
public String getAsString(String key) {
Object value = mValues.get(key);
return value != null ? value.toString() : null;
}

/**
* Gets a value and converts it to a Long.
*
* @param key the value to get
* @return the Long value, or {@code null} if the value is missing or cannot be converted
*/
public Long getAsLong(String key) {
Object value = mValues.get(key);
try {
return value != null ? ((Number) value).longValue() : null;
} catch (ClassCastException e) {
if (value instanceof CharSequence) {
try {
return Long.valueOf(value.toString());
} catch (NumberFormatException e2) {
Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key);
return null;
}
} else {
Log.e(TAG, "Cannot cast value for " + key + " to a Long: " + value, e);
return null;
}
}
}

/**
* Gets a value and converts it to an Integer.
*
* @param key the value to get
* @return the Integer value, or {@code null} if the value is missing or cannot be converted
*/
public Integer getAsInteger(String key) {
Object value = mValues.get(key);
try {
return value != null ? ((Number) value).intValue() : null;
} catch (ClassCastException e) {
if (value instanceof CharSequence) {
try {
return Integer.valueOf(value.toString());
} catch (NumberFormatException e2) {
Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key);
return null;
}
} else {
Log.e(TAG, "Cannot cast value for " + key + " to a Integer: " + value, e);
return null;
}
}
}
}

4、插入

插入系列的方法个更新系列的方法几乎相同,但是我们需要注意一点比较重要的就是插入方法可以捕获SQLite的异常,通过insertOrThrow这个方法。还有一个方法就是replace这个方法,其实这个方法很危险,为什么说这个方法危险呢,主要就是它的功能导致的,它的功能为插入一条数据转化为使用OR_EREPLACE_ONCONFLICT算法,已解决违反约束的行为,如果插入数据没有造成冲突,replace方法将插入一个新行,如果造成了冲突,现有行将被替代为新的值。下面看看这三个方法在底层如何实现的,相信大家也就懂了

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
public long insert(String table, String nullColumnHack, ContentValues values) {
try {
return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + values, e);
return -1;
}
}

public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
throws SQLException {
return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

public long replace(String table, String nullColumnHack, ContentValues initialValues) {
try {
return insertWithOnConflict(table, nullColumnHack, initialValues,
CONFLICT_REPLACE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + initialValues, e);
return -1;
}
}

public long replaceOrThrow(String table, String nullColumnHack,
ContentValues initialValues) throws SQLException {
return insertWithOnConflict(table, nullColumnHack, initialValues,
CONFLICT_REPLACE);
}

public long insertWithOnConflict(String table, String nullColumnHack,
ContentValues initialValues, int conflictAlgorithm) {
acquireReference();
try {
StringBuilder sql = new StringBuilder();
sql.append("INSERT");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(" INTO ");
sql.append(table);
sql.append('(');

Object[] bindArgs = null;
int size = (initialValues != null && initialValues.size() > 0)
? initialValues.size() : 0;
if (size > 0) {
bindArgs = new Object[size];
int i = 0;
for (String colName : initialValues.keySet()) {
sql.append((i > 0) ? "," : "");
sql.append(colName);
bindArgs[i++] = initialValues.get(colName);
}
sql.append(')');
sql.append(" VALUES (");
for (i = 0; i < size; i++) {
sql.append((i > 0) ? ",?" : "?");
}
} else {
sql.append(nullColumnHack + ") VALUES (NULL");
}
sql.append(')');

SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeInsert();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

5、查询

6、事物

7、批处理

这里还有三点,这个我们后面会说到

四、应用程序中的使用

1、数据库在应用程序中所处的地位

整个开发当中,数据无时无刻都起着很重要的作用,说白的了,程序就是操作数据的指令集而已,MVC,MVP等设计模式,也是将展示和数据分离,在Android当中已是如此,其实在很久以前,分层设计能很好的体现出数据源与视图的关系,熟悉J2EE开发人员应该清楚,程序的底层数据设计DAO,数据操作对象是以何种身份呈现说出来的,这种设计其实在Android当中也是适用的。统一数据源,通过特定的接口统一暴露给上层,然后通过相关的Adapter去设置数据。

网络数据的时候,由于是通过网络拿数据,很多开发者遍忽略了DAO,其实可以理解为网络数据,就是数据库在远端的情况,并且在很多的大型商业项目当中,也是通过ContentProvider去维护网络数据的,这个后面部分笔者应该也会提到,当我们去查找,或者删除的时候去触发远端数据的同步过程,

下面详细的讨论一下,数据库在Android应用程序当中所处的位置。

1.1、UI与DB

UI和DB就是MVP,MVC,MVVP这些设计模式当中提到的很重要的两个关键点,视图与数据源,Android当中的DB一般都会嵌入到ContentProvider当中使用,而相应的视图则有Activity来代替,而相应的中间交互对象则就是接口层CRUD和cursor,通常称之为REST-like模型,但是由于Android当中特殊的线程规定,主线程不能做耗时操作等,所以在从数据源当中拿去Corsur就有点曲折了。

1.2、Cursor与CursorFactory

Cursor就是我们从数据库当中查询到的数据集合,是一种二维形式的数据结构,有点类似于二维数组,包括行和列,还存在一个指向当前行的指针,该指针介于-1到getCount之间,

https://blog.csdn.net/italywj222/article/details/50418577
https://blog.csdn.net/howlaa/article/details/46707159

Android数据库(2)、SQLite数据库常用的数据结构

我们都知道通过DDL语言来描述数据库中包含的数据结构,在RDBMS中,定义了标准的几种数据结构,为四种类型,分别是表(table)、视图(View)、触发器(trigger)、索引(Index),这篇博客主要讲以上这四种数据结构的基本用法。

一、表(Table)

我们使用数据库最开始接触到的就是表,同样的也是最为常用的数据结构,最简单的数据结构,且其他数据结构都是建立在它之上的。

1、关系模型

表来自于数学模型,在数学模型上面称为关系模型,基于一阶谓词逻辑的分支而诞生。该模型重在描述关系,并不强调顺序所以也是无序元组集合,其类型主要由关系的特性来定义。直观上面有点类似于我们日常使用的电子表格。关系模型不仅仅只是描述关系,同时它还描述关系的若干操作,最重要的几个关系是我们日常开发当中可以感知的,比如:约束(主键约束,外键约束,列约束等)、投影(查询)、连接(多表通过主外键进行连接查询,左外连接、右外连接、全外连接等,在数学中也被成为叉积),

2、关系模型的操作

2.1、投影和约束

这两个操作非常的相似,除了它们分别影响关系的行和列之外,约束一个关系产生一个新关系,其行是原有关系中行的子集。投影几乎和约束一样,除了新关系是原关系列的子集以外。

二、触发器(Trigger)

1、触发器的定义:

我们定义的触发器最后都是会被SQLite引擎所回调的,相当于Java中我们写了一个回调函数,SQLite引擎会自动根据我们定义的事件去触发我们定义的触发器,然后执行我们所指定动作,该动作可能是该表其他值的改变,可能就是该表所引用其他表的数据的改变。这便是触发器。

2、触发器的分类:

在SQL server当中将触发器主要分为了两种,第一种就是“Instead of”触发器,第二种叫做“After”触发器。

2.1、Instead of触发器,

这种类型的触发器在执行真正的“插入之前”会被SQL引擎调用,除了表之外,还可以将该触发器运用于视图当中,用来让视图可以支持更新操作。

这种类型的触发器会替代所要执行的SQL语句,意思就是说本来要执行的SQL语句斌不会真正的执行。下面可以看一个例子。

1
2
3
4
5
6
7
8
9
create trigger tigger_users_delete
on users
instead of Delete
as
begin
select * from deleted
end

delete from users where userNo= 4;

上述触发器定义了,当我们去删除表users当中的数据时,进行触发,所要做的动作就是去查询user表,

但是当我们最后执行了delete语句之后,发现userNo=4的数据还在,并没有被删除,原因就是触发器代替了所有执行的delete,而在触发器当中并未真正意义上的删除。

2.2、After 触发器

After触发器是在DML语句执行之后被触发的。并且After触发器只能修饰表,不能修饰视图。
After触发器主要用途就是在表进行DML语句之后,用来修改其他表的值,可以通过这种触发器来判断外键约束,和引用完整性。

3、触发器的原理

SQL引擎为每一个触发器都会额外的去创建两张表,这两张表都是由系统来进行维护的,它们存在于数据库引擎的内存当中,而不是在数据库当中,可以理解为是两张虚拟的表。并且这两张表都是由SQL引擎进行维护。且这两张表的结构总是和触发该触发器的表结构相同,在触发器工作结束以后,这两张表第一时间被删除,
对表的操作

4、触发器的执行过程

4.1、如果一个DML因为语句违反了约束,那么这条SQL语句就没有执行成功,因此“After”触发器也不会激活
4.2、“Instead Of”触发器可以取代激发它的操作来执行,它在Inserted表和Delete表刚刚建立,其他任何操作还没有发生的时候被执行。因为“Instead Of”触发器在约束之前执行,所以它可以对约束进行一些预处理。

5、触发器的基础语法

5.1、创建触发器,

1
2
3
4
5
CREATE TRIGGER trigger_name [BEFORE|AFTER] event_name 
ON table_name
BEGIN
-- Trigger logic goes here....
END;

上面提到的event_name就是相对的插入操作还是删除操作,以及更新操作,我们可以在表名字后面定义是什么触发器,是行触发器还是语句触发器(FOR EACH ROW),最后我们可以在BEGIN和END中间填入,相应的动作语句就OK了。下面就是一个创建相应更新操作的例子。

1
2
3
4
5
6
create trigger name 
after update of _id
on users
begin
-----
end;

创建的触发器的功能为每当更新了_id这个字段以后,执行相应的动作。

5.2、删除触发器,

通过DML当中的drop关键字来删除相应的触发器,但是相对的必须事前知道触发器的名字

1
sqlite> DROP TRIGGER trigger_name;

5.3、查看表中所存在的触发器

SQLite引擎会把当前数据库中所有的触发器存储在一个名叫sqlite_master的表中,如果我们想要查找当前数据库当中所有的触发器,那么我们只需要查询着一张表即可

1
2
sqlite> SELECT name FROM sqlite_master
WHERE type = 'trigger’;

如果我们想看一个表上面到底被添加了多少张触发器,我们可以使用如下的查询语句进行查询

1
2
sqlite> SELECT name FROM sqlite_master
WHERE type = 'trigger' AND tbl_name = 'COMPANY';

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
34
35
36
37
38
39
40
41
42
43
44
45
46
--创建班级表
create table class(
id integer primary key autoincrement, --班级编号
className nvarchar(50) --班级名称
);

--创建学生表
create table student(
id integer primary key autoincrement, --编号
stuName nvarchar(20), --学生名称
stuSex bit, --性别
stuAge integer , --年龄
classId --班级编号
);

--创建插入触发器 (创建学生时要触发插入触发器去判断是否存在该班级,存在插入成功,反之插入失败)
create trigger fk_Insert
before insert on student
for each row
begin
select raise(rollback,'还没有该班级')
where (select id from class where id = new.classId ) is null;
end;

--创建更新触发器 (更新学生时要触发更新触发器去判断是否存在更新班级,存在更新成功,反之更新失败)
create trigger fk_Update
before update on student
for each row
begin
select raise(rollback,'还没有该班级')
where (select id from class where id = new.classId)is null;
end;

--创建删除触发器 (删除班级时,首先根据班级编号删除该班级学生)
create trigger fk_Delete
before delete on class
for each row
begin
delete from student where classId = old.classId;
end ;

insert into class(className) values('s1t64');
insert into student(stuName,stuSex,stuAge,classId)values('zhangsan',1,23,1);
update student set stuName='lishi',classId=1 where id = 1;
select * from class ;
select * from student limit 0,100 ; -- 分页查询从索引0开始查找,100条数据

一、表(table)

1、表是什么
2、表的使用场景
3、使用表的相关优缺点
4、相关语法
5、简单示例

二、触发器(trigger)

1、触发器是什么
2、使用场景
3、使用触发器的优缺点
4、相关语法
5、简单示例

三、视图(View)

1、视图是什么
2、使用场景
3、使用视图的优缺点
4、相关语法
5、简单的例子

四、索引(Index)

1、索引是什么
2、索引的分类
3、索引的原理
4、使用场景
5、使用索引的优缺点
6、简单的例子

五、事物和锁特性

1、事务
2、事务的范围
3、数据库的锁
4、数据库的死锁
5、事务的种类
6、使用注意点

备注:未完成,由于查看资料时,发现很大一部分内容是两种说法,所以本人以列出大纲,自行研究中,下面网址,可以供读者学习这一方面内容

http://blog.csdn.net/hustyangju/article/details/17488309
http://blog.csdn.net/lzq_it/article/details/6960176
http://www.runoob.com/sqlite/sqlite-trigger.html
http://www.cnblogs.com/rainman/p/3675834.html
https://www.cnblogs.com/richzhang/archive/2013/04/24/3041280.html
https://www.cnblogs.com/envoy/p/4751140.html
https://stackoverflow.com/questions/172303/is-there-a-regular-expression-to-detect-a-valid-regular-expression