1. 实现多线程

1.1 创建多线程的方式

1.1.1 实现Runnable接口

1
2
3
4
5
6
7
8
public class RunnableThread implements Runnable {

@Override
public void run() {
System.out.println('用实现Runnable接口实现线程');
}

}

1.1.2 继承Thread类

1
2
3
4
5
6
7
8
public class ExtendsThread extends Thread {

@Override
public void run() {
System.out.println('用Thread类实现线程');
}

}

1.1.3 线程池创建线程

本质还是通过new Thread()实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static class DefaultThreadFactory implements ThreadFactory {

DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}

public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}

}

1.1.4 有返回值的Callable创建线程

Callable、FutureTask、Runnable ,都是一个任务,是需要被执行的,而不是说它们本身就是线程;它们可以放到线程池中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CallableTask implements Callable<Integer> {

@Override
public Integer call() throws Exception {
return new Random().nextInt();
}

}

//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);

//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());

1.1.5 定时器Timer

1
2
3
4
5
class TimerThread extends Thread {

//具体实现

}

1.1.6 其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*描述:匿名内部类创建线程
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();

/**
* lamabda 表达式创建线程
*/
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

1.2 实现线程只有一种方式

本质上,实现线程只有一种方式—-构造一个Thread类,想要实现线程执行的内容却有两种方式,通过 实现Runnable方式、继承Thread类重写run方法,把我们想要的代码传入,让线程去执行。

1
2
3
4
5
6
7
8
@Override
public void run() {
// target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象
if (target != null) {
target.run();
}

}

1.3 实现 Runnable 接口比继承 Thread 类实现线程要好?

  • 代码的架构考虑;实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
  • 可以提高性能;使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类。
  • Java 语言不支持双继承;如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类。

2. 启动线程

调用start()方法

3. 停止线程

3.1 interrupt 停止线程

1
2
3
4
5
while (!Thread.currentThread().isInterrupted() && more work to do) {

do more work

}

休眠的的线程(sleep、wait)是否可以感受到中断?可以,被中断后会抛出InterruptedException异常

两种处理中断的方式:

  • try/catch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private void reInterrupt() {

    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    // 休眠期被中断会自动清除中断信号
    Thread.currentThread().interrupt();
    e.printStackTrace();
    }

    }
  • throws InterruptedException

    1
    2
    3
    4
    5
    void subTask2() throws InterruptedException {

    Thread.sleep(1000);

    }

3.2 volitile方式停止线程

stop(): 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题.

suspend() 和 resume(): 如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的.

  • volitile适用的场景
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
public class VolatileCanStop implements Runnable {

private volatile boolean canceled = false;

@Override
public void run() {
int num = 0;
try {
while (!canceled && num <= 1000000) {
if (num % 10 == 0) {
System.out.println(num + "是10的倍数。");
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
VolatileCanStop r = new VolatileCanStop();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(3000);
r.canceled = true;
}

}
  • volitile不适用的场景
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
class Producer implements Runnable {

public volatile boolean canceled = false;
BlockingQueue storage;

public Producer(BlockingQueue storage) {
this.storage = storage;
}

@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 50 == 0) {
storage.put(num);
System.out.println(num + "是50的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}

}

class Consumer {

BlockingQueue storage;

public Consumer(BlockingQueue storage) {
this.storage = storage;
}

public boolean needMoreNums() {
if (Math.random() > 0.97) {
return false;
}
return true;
}

}

public static void main(String[] args) throws InterruptedException {

ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(500);

Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");

//一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
producer.canceled = true;
System.out.println(producer.canceled);
}

}

尽管已经把 canceled 设置成 true,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理

4. 线程的6种状态

  1. New(新创建)
  2. Runnable(可运行)
  3. Blocked(被阻塞)
  4. Waiting(等待)
  5. Timed Waiting(计时等待)
  6. Terminated(被终止)

4.1 New新建

当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New

img

4.2 Runnable 可运行

Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源

img

4.3 阻塞状态

在 Java 中阻塞状态通常不仅仅是 Blocked,实际上它包括三种状态,分别是 Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待),这三 种状态统称为阻塞状态

4.3.1 Blocked 被阻塞

img

进入 synchronized 保护的代码时没有抢到 monitor 锁

4.3.2 Waiting 等待

img

线程进入waiting状态的可能:

  1. 没有设置 Timeout 参数的 Object.wait() 方法。
  2. 没有设置 Timeout 参数的 Thread.join() 方法。
  3. LockSupport.park() 方法。

Blocked 与 Waiting 的区别:

Blocked 在等待其他线程释放 monitor 锁;

Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() ;

4.3.2 Timed Waiting 限期等待

对于 Timed Waiting 而言,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark(),会直接恢复到 Runnable 状态,而无需经历 Blocked 状态

img

4.4 Terminated 终止

  • run() 方法执行完毕,线程正常退出。
  • 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止

img

5. wait/notify/sleep/join等重要方法

5.1 为什么 wait 必须在 synchronized 保护的同步代码中使用?

必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁

1
2
3
4
5
6
7
8
9
synchronized (obj) {

while (condition does not hold)

obj.wait();

... // Perform action appropriate to condition

}

如果不要求 wait 方法放在 synchronized 保护的同步代码中使用,而是可以随意调用,会出现什么问题?

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

Queue<String> buffer = new LinkedList<String>();

public void give(String data) {
buffer.add(data);
notify(); //Since someone may be waiting in take
}

public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}

}

会存在以下场景:

  1. 首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
  2. 此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
  3. 此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。

问题:没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 give 方法内的 notify 的唤醒

原因:因为 wait 方法所在的 take 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作

改动:确保 notify 方法永远不会在 buffer.isEmpty 和 wait 方法之间被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void give(String data) {

synchronized (this) {
buffer.add(data);
notify();
}

}

public String take() throws InterruptedException {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}

}

5.2 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

  1. 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
  2. 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

5.3 wait/notify 和 sleep 方法的异同?

  • 相同点:
  1. 它们都可以让线程阻塞。
  2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
  • 不同点:
  1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  2. 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
  3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  4. wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

6. 生产者、消费者模式的实现方式

生产者消费者模式:

img

6.1 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
public static void main(String[] args) {

BlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);

Runnable producer = () -> {

while (true) {

queue.put(new Object());

}

};

new Thread(producer).start();

new Thread(producer).start();

Runnable consumer = () -> {

while (true) {

queue.take();

}

};

new Thread(consumer).start();

new Thread(consumer).start();

}

6.2 Condition实现生产者消费者模式

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
public class MyBlockingQueueForCondition {

private Queue queue;

private int max = 16;

private ReentrantLock lock = new ReentrantLock();

private Condition notEmpty = lock.newCondition();

private Condition notFull = lock.newCondition();

public MyBlockingQueueForCondition(int size) {

this.max = size;
queue = new LinkedList();

}

public void put(Object o) throws InterruptedException {

lock.lock();
try {
while (queue.size() == max) {
notFull.await();
}
queue.add(o);
notEmpty.signalAll();
} finally {
lock.unlock();
}

}

public Object take() throws InterruptedException {

lock.lock();
try {
while (queue.size() == 0) {
notEmpty.await();
}
Object item = queue.remove();
notFull.signalAll();
return item;
} finally {
lock.unlock();
}
}

}

6.3 wait/notify实现生产者消费者模式

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
class MyBlockingQueue {

private int maxSize;

private LinkedList<Object> storage;

public MyBlockingQueue(int size) {

this.maxSize = size;
storage = new LinkedList<>();

}

public synchronized void put() throws InterruptedException {

while (storage.size() == maxSize) {
wait();
}

storage.add(new Object());
notifyAll();

}

public synchronized void take() throws InterruptedException {

while (storage.size() == 0) {
wait();
}

System.out.println(storage.remove());
notifyAll();

}

}

public class WaitStyle {

public static void main(String[] args) {

MyBlockingQueue myBlockingQueue = new MyBlockingQueue(10);

Producer producer = new Producer(myBlockingQueue);

Consumer consumer = new Consumer(myBlockingQueue);

new Thread(producer).start();

new Thread(consumer).start();

}

}

class Producer implements Runnable {

private MyBlockingQueue storage;

public Producer(MyBlockingQueue storage) {
this.storage = storage;
}

@Override
public void run() {

for (int i = 0; i < 100; i++) {
try {
storage.put();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

}

class Consumer implements Runnable {

private MyBlockingQueue storage;

public Consumer(MyBlockingQueue storage) {

this.storage = storage;

}

@Override
public void run() {

for (int i = 0; i < 100; i++) {
try {
storage.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

}

7. 线程安全

7.1 什么是线程安全问题?

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的;

如果某个对象是线程安全的,那么对于使用者而言,在使用时就不需要考虑方法间的协调问题,比如不需要考虑不能同时写入或读写不能并行的问题,也不需要考虑任何额外的同步问题,比如不需要额外自己加 synchronized 锁,那么它才是线程安全的。

7.2 线程不安全问题

  • 运行结果错误:多线程同时操作一个变量导致的运行结果错误
  • 发布和初始化导致的安全问题:在错误的时间或地点发布或初始化造成的线程安全问题
  • 活跃性问题
    • 死锁:两个线程之间相互等待对方资源,但同时又互不相让,都想自己先执行
    • 活锁:每次报错后又会被放到队列头进行重试,周而复始,最终导致线程一直处于忙碌状态,但程序始终得不到结果
    • 饥饿:饥饿是指线程需要某些资源时始终得不到,尤其是CPU 资源,就会导致线程一直不能运行而产生的问题

7.3 哪些场景需要额外注意线程安全问题?

  • 访问共享变量或者资源:访问共享对象的属性,访问 static 静态变量,访问共享的缓存,等等
  • 依赖时序的操作
  • 不同数据之间存在绑定关系
  • 没有生命自己是线程安全的

8. 多线程带来的性能问题的原因?

  • 调度开销
  • 上下文切换
  • 缓存失效

本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.