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<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();
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() { 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异常
两种处理中断的方式:
3.2 volitile方式停止线程
stop(): 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题.
suspend() 和 resume(): 如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的.
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; }
}
|
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种状态
- New(新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(被终止)
4.1 New新建
当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New
4.2 Runnable 可运行
Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源
4.3 阻塞状态
在 Java 中阻塞状态通常不仅仅是 Blocked,实际上它包括三种状态,分别是 Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待),这三 种状态统称为阻塞状态
4.3.1 Blocked 被阻塞
进入 synchronized 保护的代码时没有抢到 monitor 锁
4.3.2 Waiting 等待
线程进入waiting状态的可能:
- 没有设置 Timeout 参数的 Object.wait() 方法。
- 没有设置 Timeout 参数的 Thread.join() 方法。
- LockSupport.park() 方法。
Blocked 与 Waiting 的区别:
Blocked 在等待其他线程释放 monitor 锁;
Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() ;
4.3.2 Timed Waiting 限期等待
对于 Timed Waiting 而言,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark(),会直接恢复到 Runnable 状态,而无需经历 Blocked 状态
4.4 Terminated 终止
- run() 方法执行完毕,线程正常退出。
- 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止
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(); }
public String take() throws InterruptedException { while (buffer.isEmpty()) { wait(); } return buffer.remove(); }
}
|
会存在以下场景:
- 首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
- 此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
- 此时,刚才被调度器暂停的消费者线程回来继续执行 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 类中?
- 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
- 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
5.3 wait/notify 和 sleep 方法的异同?
- 它们都可以让线程阻塞。
- 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
- wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
- 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
- sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
- wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
6. 生产者、消费者模式的实现方式
生产者消费者模式:
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. 多线程带来的性能问题的原因?