BATJ面试必会之多线程上篇(附答案)

出自:已或授权https://blog.csdn.net/sky_dsy/article/details/78912461

一. 多线程的实现

1.1 实现多线程的三种方式

  • (1)继承Thread类
  • (2)实现Runnable接口
  • (3)使用ExecutorService、Callable、Future实现有返回结果的多线程

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

public class MyRunnable implements Runnable {
    
public void run() {
// ...
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Tread thread = new Thread(instance);
thread.start();
}
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable {
    
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}

继承 Thread 类

同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。

public class MyThread extends Thread {
    
public void run() {
// ...
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

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


1.2 Thread和Runable的区别和联系

(1)联系:

  • 1、Thread类实现了Runable接口。
  • 2、都需要重写里面Run方法。


(2)不同:

  • 1、实现Runnable的类更具有健壮性,避免了单继承的局限。
  • 2、Runnable更容易实现资源共享,能多个线程同时处理一个资源。


二.基础线程机制

sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。

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

public void run() {
    
try {
// ...
Thread.sleep(1000);
// ...
} catch (InterruptedException e) {
System.err.println(e);
}
}

yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。

public void run() {
    
// ...
Thread.yield();
}

join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。

可以加一个超时参数。

deamon

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

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

main() 属于非后台线程。

使用 setDaemon() 方法将一个线程设置为后台线程。

三.线程的状态转换

BATJ面试必会之多线程上篇(附答案)

  • yield方法:使当前线程从执行状态变为就绪状态。
  • sleep方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回到可运行状态。
  • join方法:通常用于在main()主线程内,等待其它线程完成再结束main()主线程。

线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • (1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  • (2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • (3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep是不会释放持有的锁)


四.结束线程

4.1阻塞

一个线程进入阻塞状态可能有以下原因:

  • 1. 调用 Thread.sleep() 方法进入休眠状态;
  • 2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息;
  • 3. 等待某个 I/O 的完成;
  • 4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。

4.2中断

使用中断机制即可终止阻塞的线程。

使用 interrupt() 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。

应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。

1. 不可中断的阻塞

不能中断 I/O 阻塞和 synchronized 锁阻塞。

2. Executor 的中断操作

Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。

如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。

3. 检查中断

通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。

interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。

interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。

4 如何中断线程的执行?

(1)设置退出标志(boolean类型的变量),使线程正常退出,也就是当run()方法完成后线程终止。

(2)使用 interrupt() 方法中断线程。

(3)使用 stop 方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,可能出现数据不一致的问题。

五、线程之间的协作

5.1同步与通信的概念理解

在操作系统中,有三个概念用来描述进程间的协作关系:

  1. 互斥:多个进程在同一时刻只有一个进程能进入临界区;
  2. 同步:多个进程按一定顺序执行;
  3. 通信:多个进程间的信息传递。

通信是一种手段,它可以用来实现同步。也就是说,通过在多个进程间传递信息,可以控制多个进程以一定顺序执行。

而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。

总结起来就是:通信 --> 同步 --> 互斥。

进程和线程在一定程度上类似,也可以用这些概念来描述。

在 Java 语言中,这些概念描述有些差别:

  1. 同步:可以和操作系统的互斥等同;
  2. 通信:可以和操作系统的同步等同。


5.2线程同步

给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。

5.2.1. synchronized

同步一个方法

使多个线程不能同时访问该方法。

public synchronized void func(String name) {
    
// ...
}

同步一个代码块

public void func(String name) {
    
synchronized(this) {
// ...
}
}

synchronized关键字

  • (1)synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
  • 1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  • 2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
  • (2)Synchronized的作用主要有三个:
  • (1)确保线程互斥的访问同步代码
  • (2)保证共享变量的修改能够及时可见
  • (3)有效解决重排序问题。
  • (3)Synchronized 原理 在编译的字节码中加入了两条指令来进行代码的同步。monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
  • 1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  • 2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  • 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  • monitorexit:
  • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  •  通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

5.2.2. Lock

实现更细粒度的控制。

private Lock lock;
public int func(int value) {
try {
lock.lock();
// ...
} finally {
lock.unlock();
}
}

5.2.3 synchronized与Lock的区别

lock是一个类,主要有以下几个方法:

lock():获取锁,如果锁被暂用则一直等待

unlock():释放锁

tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true

tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间

BATJ面试必会之多线程上篇(附答案)_第1张图片

(1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。

(2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。

(3)Lock能提高多个线程读操作的效率;

(4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。如果面试问起,你就说底层主要靠volatile和CAS操作实现的。


5.3线程通信

5.3.1. wait() notify() notifyAll()

它们都属于 Object 的一部分,而不属于 Thread。

wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。

sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。

实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。

private boolean flag = false;
public synchronized void after() {
while(flag == false) {
wait();
// ...
}
}
public synchronized void before() {
flag = true;
notifyAll();
}

wait() 和 sleep() 的区别

  1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法;
  2. wait() 会放弃锁,而 sleep() 不会。


1.wait/notify机制

  • wait():该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结构。
  • notify():该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。
  • notifyAll():notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

5.3.2.同步

(多个线程通过synchronized关键字这种方式来实现线程间的通信)

本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

5.3.3.while轮询的方式

尽管线程A一直在while中执行,需要占用CPU。但是,线程的调度是由JVM或者说是操作系统来负责的,并不是说线程A一直在while循环,然后线程B就占用不到CPU了。对于线程A而言,它就相当于一个“计算密集型”作业了。如果我们的while循环是不断地测试某个条件是否成立,那么这种方式就很浪费CPU。如果同步快中代码进入了死循环,则可能导致多线程无法继续执行下去。

5.3.4.管道通信

(通过管道,将一个线程中的消息发送给另一个)

5.3.5. BlockingQueue

java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:

  • FIFO 队列 :LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
  • 优先级队列 :PriorityBlockingQueue


提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将一直阻塞到队列中有内容,如果队列为满 put() 将阻塞到队列有空闲位置。它们响应中断,当收到中断请求的时候会抛出 InterruptedException,从而提前结束阻塞状态。

阻塞队列的 take() 和 put() 方法是线程安全的。

使用 BlockingQueue 实现生产者消费者问题

// 生产者
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
private BlockingQueue queue;
public Producer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is making product...");
String product = "made by " + Thread.currentThread().getName();
try {
queue.put(product);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费者
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
private BlockingQueue queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
String product = queue.take();
System.out.println(Thread.currentThread().getName() + " is consuming product " + product + "...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 客户端
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Client {
public static void main(String[] args) {
BlockingQueue queue = new LinkedBlockingQueue<>(5);
for (int i = 0; i < 2; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
}
for (int i = 0; i < 5; i++) {
// 只有两个 Product,因此只能消费两个,其它三个消费者被阻塞
new Thread(new Producer(queue), "Producer" + i).start();
}
for (int i = 2; i < 5; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
}
}
}
// 运行结果
Producer0 is making product...
Consumer0 is consuming product made by Consumer0...
Producer1 is making product...
Consumer1 is consuming product made by Consumer1...
Producer2 is making product...
Producer3 is making product...
Producer4 is making product...
Consumer2 is consuming product made by Consumer2...
Consumer3 is consuming product made by Consumer3...
Consumer4 is consuming product made by Consumer4...


你可能感兴趣的:(面试,java,操作系统)