Thread_多线程


        

Thread-多线程

什么是进程,什么是线程,为什么我们需要编写多线程程序?

进程:进程是程序的一次执行过程,是系统运行程序的基本单位

线程:线程与进程相似,但是线程是一个比进程更小的执行单位,一个进程在执行过程中可以产生多个线程。

使用多线程编程的好处:使用多线程编写程序,可以解决负载均衡问题,以及充分的利用CPU资源,提高CPU的使用率,也可能提高程序的运行效率。为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等


线程的使用

使用线程的俩种方法:

  • 实现 Runnable 接口
  • 继承 Thread 类

实现 Runnable 接口:

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。
1
2
3
4
5
6
7
8
9
10
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:  
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
类可能只要求可执行就行,继承整个 Thread 类开销过大。

Java线程的五种基本状态

线程的五种基本状态.png

1. 新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2. 就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3. 运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4. 阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5. 死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java多线程的就绪、运行和死亡状态:

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

线程状态转换.png


基础线程机制

Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

主要有三种 Executor:

CachedThreadPool:一个任务创建一个线程;
FixedThreadPool:所有任务只能使用固定大小的线程;
SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
1
2
3
4
5
6
7
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}

Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

main() 属于非守护线程。

在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。

1
2
3
4
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}

sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

1
2
3
4
5
6
7
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

1
2
3
public void run() {
Thread.yield();
}

线程安全问题

线程安全问题出现的根本原因:

1.存在两个或者两个以上的线程对象共享同一个资源

2.多线程操作共享资源代码有多个语句

线程安全问题的解决方案:

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

方式一:同步代码块

1
2
3
4
5
格式:synchronize(锁对象){

需要被同步的代码

}

同步代码块需要注意的事项:

1.锁对象可以是任意的一个对象;

2.一个线程在同步代码块中sleep了,并不会释放锁对象;

3.如果不存在线程安全问题,千万不要使用同步代码块;

4.锁对象必须是多线程共享的一个资源,否则锁不住。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void run() {
while(true) {
synchronized (obj) {
if(tickets > 0) {
tickets--;
System.out.println(Thread.currentThread().getName() + ":售出了一张票,还剩" + tickets);
}else
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};

方式二:同步函数

(同步函数就是使用synchronized修饰一个函数)

同步函数注意事项:

1.如果函数是一个非静态的同步函数,那么锁对象是this对象

2.如果函数是静态的同步函数,那么锁对象是当前函数所属的类的字节码文件(class对象)

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
/*
同步:
指发送一个请求,需要等待返回,然后才能发送下一个请求,有一个等待的过程。
异步:
指发送一个请求,不需要等待返回,随时可以发送下一个请求,不需要等待。
*/
public class SyncDemo3 {
public static void main(String[] args) throws InterruptedException {
Calculate c = new Calculate();
Runnable task = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
c.increment();
}
}
};

Runnable task2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
c.addOne();
}
}
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
Thread t4 = new Thread(task2);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(c.getValue());
}
}
public class Calculate {
private static int value;

/*同步方法:
锁对象: this
进入时上锁
退出时释放锁
*/
public void increment() {
synchronized (getClass()) {
value++;
}
}

/*同步静态方法:
锁对象:Calculate.class 类的字节码文件对象
进入时上锁
退出时释放锁
*/
public synchronized static void addOne() {
value++;
}
public int getValue() {
return value;
}
}

// 设置锁后结果为 40000
// 未设置的结果 < 4000

方式三:Lock(锁)

Lock接口下,void lock() –> 获取锁, void unlock() –> 释放锁

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

代码演示:

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
public class LockDemo1 {
public static void main(String[] args) throws InterruptedException {
Runnable task = new Runnable() {
private int tickets = 100;
private Lock lock = new ReentrantLock();
private final Object obj = new Object();
@Override
public void run() {
while(true) {
lock.lock();
try {
if (tickets > 0) {
tickets--;
System.out.println(Thread.currentThread().getName() + ":售出了一张票,还剩" + tickets);
}
else {
break;
}
}finally {
lock.unlock();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Thread.currentThread().getName() + ": End");
}
}

关于 synchronized 与 ReentrantLock 的比较:

  1. 锁的实现
    synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

  2. 性能
    新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

  3. 等待可中断
    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
    ReentrantLock 可中断,而 synchronized 不行。

  4. 公平锁
    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
    synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

  5. 锁绑定多个条件
    一个 ReentrantLock 可以同时绑定多个 Condition 对象


DeadLock-死锁

死锁的定义: 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
更加规范的定义: 集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。

K4gm9K.png

死锁现象出现的原因:

1. 互斥
2. 占有且等待,线程 T 占有共享资源 X,等待共享资源 YX
3. 不可抢夺资源
4. 循环等待 线程 T1 占有共享资源 X,等待共享资源 Y,线程 T2 占有共享资源 Y,等待共享资源

死锁的预防:
1. 确定顺序获得锁
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
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
public class DeadLockDemo {
public static void main(String[] args) throws InterruptedException {
Account account1 = new Account("wx",1000);
Account account2 = new Account("wc",1000);
Thread t1 = new Thread() {
@Override
public void run() {
account1.transfer(account2,500);
}
};
Thread t2 = new Thread() {
@Override
public void run() {
account2.transfer(account1,200);
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("账户一余额:" + account1.getBalance());
System.out.println("账户二余额:" + account2.getBalance());
}
}
// 转账方法
public void transfer(Account other, int money) {

if(allocator.apply(this,other)) {
synchronized (this) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (other) {
if (money <= 0 || balance < money) return;
balance -= money;
// other.balance += money;
int a = other.balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a += money;
other.balance = a;
}
}
allocator.free(this, other);
}
}

// 调整锁的顺序以预防死锁
/* public void transfer(Account other, int money) {
Account littleAccount = this;
Account bigAccount = other;
if(littleAccount.compareTo(bigAccount) > 0) {
Account tmp = littleAccount;
littleAccount = bigAccount;
bigAccount = tmp;
}
synchronized(littleAccount) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (bigAccount) {
if (money <= 0 || balance < money) return;
balance -= money;
// other.balance += money;
int a = other.balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a += money;
other.balance = a;
}
}
}

private int compareTo(Account account) {
return name.compareTo(account.name);
}*/
class Allocator {
// collection 集合中存储的是已经被申请的资源
private Collection collection = new ArrayList();

/*
如果申请成功,就等待
*/
public synchronized boolean apply(Object obj1, Object obj2) {
while (collection.contains(obj1) || collection.contains(obj2)) {
try {
// 被唤醒只能说明条件曾经成立,并不代表运行的时候还成立。
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
collection.add(obj1);
collection.add(obj2);
return true;
}

/*public synchronized boolean apply(Object obj1, Object obj2) {
if(collection.contains(obj1) || collection.contains(obj2)) return false;
collection.add(obj1);
collection.add(obj2);
return true;
}*/

public synchronized void free(Object obj1, Object obj2) {
collection.remove(obj1);
collection.remove(obj2);
notifyAll(); // 唤醒线程
}
}

线程间的通信(协作)

Object:

1
2
3
4
5
6
7
8
9
10
11
12
13
void wait()
当前线程释放锁资源, 进入等待状态。直到被其他线程唤醒。

void wait(long timeout)
当前线程释放锁资源, 进入等待状态。直到被其他线程唤醒,或者是等待超时。

void wait(long timeout, int nanos)

void notify()
随机唤醒一个等待 this 锁对象的线程。

void notifyAll()
唤醒所有等待 this 锁对象的线程。

注意事项:

  • notify() 和 notifyAll() 不会释放锁资源,也不会放弃 CPU 的时间片
  • 慎用 notify() 方法, 因为它可能会让一个线程永远等待下去。

问题: 为什么等待唤醒的方法不定义在 Thread类 中?

a. 只有同步情况下,线程之间才需要通信。
b. 锁对象是线程之间通信的媒介。
c. 锁对象可以是任意对象。

所以 wait(), notify() 方法才定义在 Object 类中。


生产者消费者模型

代码演示:

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
/* 
生产者:
如果生产抵达上线,等待
生产产品,唤醒其他线程
消费者:
如果没有产品,等待
消费产品,唤醒其他线程
*/
public class Demo2 {
public static void main(String[] args) {
Store store = new Store("藤原包子店");
Runnable task1 = new Runnable() {
@Override
public void run() {
while(true) {
store.produce();
}
}
};
Runnable task2 = new Runnable() {
@Override
public void run() {
while(true) {
store.consume();
}
}
};
Thread t1 = new Thread(task1,"拓海");
Thread t2 = new Thread(task1,"拓海他爸");
Thread t3 = new Thread(task2,"夏树");
Thread t4 = new Thread(task2,"夏树叔叔");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Store {
private static final int MAX_SIZE = 100;
private String name;
private Queue<Bun> queue = new ArrayDeque();

public Store(String name) {
this.name = name;
queue = new ArrayDeque(MAX_SIZE);
}

public void produce() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
while(queue.size() >= MAX_SIZE){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(new Bun());
System.out.println(Thread.currentThread().getName() + ":生产了一个包子,还剩" + queue.size() + "个");
notifyAll();
}
}

public void consume() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
while(queue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bun bun = queue.poll();
System.out.println(Thread.currentThread().getName() + ":消费了一个包子,还剩" + queue.size() + "个");
notifyAll();
}
}
}
class Bun {
}

结果演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    ...
拓海:生产了一个包子,还剩13个
拓海他爸:生产了一个包子,还剩14个
夏树叔叔:消费了一个包子,还剩13个
夏树:消费了一个包子,还剩12个
拓海:生产了一个包子,还剩13个
拓海他爸:生产了一个包子,还剩14个
拓海:生产了一个包子,还剩15个
拓海他爸:生产了一个包子,还剩16个
夏树叔叔:消费了一个包子,还剩15个
夏树:消费了一个包子,还剩14个
拓海:生产了一个包子,还剩15个
拓海他爸:生产了一个包子,还剩16个
拓海:生产了一个包子,还剩17个
...

BlockingQueue

阻塞队列是一种队列,一种可以在多线程环境下使用,并且支持阻塞等待的队列。也就是说,阻塞队列和一般的队列的区别就在于:

  1. 多线程环境支持,多个线程可以安全的访问队列

  2. 支持生产和消费等待,多个线程之间互相配合,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线 程就会阻塞直到队列不满。

阻塞队列在java中的一种典型使用场景是线程池,在线程池中,当提交的任务不能被立即得到执行的时候,线程池就会将提交的任务放到一个阻塞的任务队列中来,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
```

**实现一个阻塞队列:**
<span id="inline-purple">代码演示:</span>

``` java
/* 阻塞队列
阻塞队列就是一个生产者消费者模型
JDK:
ArrayBlockingQueue
LinkedBlockingQueue
*/
public class MyBlockingQueue<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
private int front;
private int rear;
private int size;

@SuppressWarnings("unchecked")
public MyBlockingQueue(){
elements = (E[]) new Object[DEFAULT_CAPACITY];
}

@SuppressWarnings("unchecked")
public MyBlockingQueue(int initialCapacity){
if (initialCapacity < 0) {
throw new IllegalArgumentException("initialCapacity :" + initialCapacity);
}
elements = (E[]) new Object[initialCapacity];
}

public synchronized void put(E e) {
while (size == elements.length) {
try {
wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
elements[rear] = e;
rear = (rear + 1) % elements.length;
size++;
notifyAll();
}

public synchronized E take() {
while(size == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E e = elements[front];
elements[front] = null;
front = (front + 1) % elements.length;
size--;
notifyAll();
return e;
}
}

Some Questions

  1. 同步有几种方式,分别是什么?

    Lock
    synchronized(同步代码块,同步方法,静态同步方法)

  2. 启动一个线程是run() 还是 start() ? 他们的区别?

    run(): 线程要执行的任务
    start(): 调用系统的 API 启动线程,该线程会执行run()

  3. sleep() 和 wait() 方法的区别?

    相同:线程都会进入阻塞状态

    sleep:

    不会释放资源  
    Thread中的静态方法  
    必须有一个时间限制,可以被interrupt() 

    wait:

    只能在同步代码块中通过锁对象调用  
    释放资源  
    Object中的成员方法  
    可以无限制地等待,等待被唤醒  
  4. 线程的生命周期
    见上图

参考文档(一) ,参考文档(二)

---------------- The End ----------------

本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
本文地址:https://philxin.top/2019/10/28/Thread-多线程/
转载请注明出处,谢谢!

0%