2022届秋招Java后端高频知识点汇总③--多线程

1.线程和进程有什么区别

①进程是系统资源调度的最小单位,线程是CPU调度的最小单位

②一个线程从属于一个进程,一个进程可以包含多个线程

③一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。

④进程在执行时拥有独立的内存单元,多个线程共享进程的内存。

⑤进程的系统开销大于线程的开销,线程需要的系统资源较少

⑥进程和线程的通信方式不一样。

2. 进程之间的通信方式

进程间通信主要有以下7种方式:匿名管道、有名管道、信号、消息队列、共享内存、信号量、Socket

①管道/匿名管道(Pipes):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系.⽤于具有亲缘关系的⽗⼦进程之间的通信。

②有名管道(Names Pipes): (半双工)匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据 ,有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信。有名管道的名字存在于文件系统中,内容存放在内存中。

③信号(Signal):信号是进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

④消息队列(Message Queuing):消息队列是消息的链表,具有写权限的进程可以按照一定的规则向消息队列中添加新数据,堆消息队列有读权限的进程则可以从消息队列中读取数据。

⑤信号量(Semaphores) :信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。 (信号量也用来解决互斥和同步问题)

对信号量的操作分为P操作和V操作,P操作是将信号量的值减1,V操作是将信号量的值加1。当信号量的值小于等于0之后,再进行P操作,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时,锁也是用的这种原理实现的。

⑥共享内存(Shared memory):进程间本身的内存是相互隔离的,而共享内存机制相当于给两个进程开辟了一块二者均可以访问的内存空间,这时两个进程便可以共享一些数据了,但是多进程同时占用资源会带来一些意料之外的情况,这时,往往需要用互斥锁和信号量来控制对内存空间的访问。可以说这是最有⽤的进程间通信⽅式。

⑦套接字(Sockets) : 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。用于网络中不同机器之间进程间的通信。

套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。

3. 线程之间的通信方式

Java中线程通信主要用以下3种方式:

①wait()、notify()、notifyAll()

如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。

wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

②await()、signal()、signalAll()

如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。

这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

③BlockingQueue

Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

4. 创建线程的四种方式

1.继承Thread类

2.实现Runnable接口

3.实现Callable接口

4.使用线程池创建

几种线程创建方式的优缺点

2022届秋招Java后端高频知识点汇总③--多线程

5. 线程中start和run的区别

start() :

它的作用是启动一个新线程。

通过Thread类中的start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。

run() :

run()就和普通的成员方法一样,可以被重复调用。

如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里。

6. 线程的生命周期和状态

①新建:线程对象已经创建,但是还没有调用start()方法

②就绪:线程对象调用start()方法,但是还未开始运行就是就绪状态。一旦获取CPU时间片,就开始运行。

③运行:当线程对象调用了start()方法,并且获取了CPU时间片,就是运行状态。

③阻塞:等待获取一个排它锁,如果其线程释放了锁就会结束此状态。如调用wait()方***进入阻塞状态,调用sleep方***进入睡眠状态。

⑥死亡:可以是线程结束任务后自己结束,或者产生了异常而结束。

7. 什么是线程死锁?如何避免死锁?

死锁:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。

死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,这些线程都陷入无限的等待中。

产生死锁的4个必要条件:

①互斥条件:该资源在任意⼀个时刻只由⼀个线程占⽤。

②请求与保持条件:⼀个线程因请求资源⽽阻塞时,对已获得的资源保持不放。

③不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。

④循环等待条件:若⼲线程之间形成⼀种头尾相接的循环等待资源关系。

如何避免线程死锁?

产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。

①破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。

②破坏请求与保持条件 :⼀次性申请所有的资源。

③破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

④破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

避免死锁最简单的方法就是 阻止循环等待条件 ,将系统中所有的资源设置标志位、排序, 规定所有的进程申请资源必须以一定的顺序 (升序或降序) 做操作来避免死锁 。

8. sleep和wait的区别

sleep():sleep 方法是属于Thread 类中的方法。它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,但是在此期间线程不会释放锁,只会阻塞线程,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,高优先级和低优先级的线程都有机会执行。

wait():wait 方法是属于Object 类中的方法,wait 过程中线程会释放对象锁,只有当其他线程调用 notify()或notifyAll() 才能唤醒此线程。wait使用时必须先获取对象锁,即必须在 synchronized修饰的方法或代码块中使用,那么相应的notify方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常。

9. wait()方法为什么要在while()循环中使用

wait()方法之所以要用while而不是if是因为: 当多个线程并发访问同一个资源的时候, 若消费者同时被唤醒,但是只有一个资源可用, 那么if会导致资源被用完后直接去获取资源(发生越界异常等),而while则会让每个消费者获取之前再去判断一下资源是否可用.可用则获取,不可用则继续wait住。

10. 线程池

2022届秋招Java后端高频知识点汇总③--多线程

11. 为什么要使用线程池

2022届秋招Java后端高频知识点汇总③--多线程

12. 线程池有哪些参数?每个参数的作用是什么?

一共有7个参数

2022届秋招Java后端高频知识点汇总③--多线程

7.时间的单位

keepAliveTime的时间单位

13. 线程池的拒绝策略和阻塞队列

拒绝策略

默认的拒绝策略:AbortPolicy

2022届秋招Java后端高频知识点汇总③--多线程

阻塞队列

①第一种阻塞队列是 LinkedBlockingQueue。 对应的线程池:newSingleThreadExecutor( )和newFixedThreadPool(int n)

LinkedBlockingQueue,它的容量是 Integer.MAX_VALUE,为 231 -1 ,是一个非常大的值,可以认为是*队列。

FixedThreadPool 和 SingleThreadExecutor 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。

②第二种阻塞队列是 SynchronousQueue。 对应的线程池是 newCachedThreadPool( )

线程池 CachedThreadPool 的最大线程数是 Integer.MAX_VALUE,可以理解为线程数是可以无限扩展的。

CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

③第三种阻塞队列是 DelayedWorkQueue。 对应的线程池分别是 newScheduledThreadPool (int n)和 newSingleThreadScheduledExecutor( )

这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。

DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构(堆的应用之一就是 优先级队列)。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

上一篇:并发编程-wait/notify原理


下一篇:操作系统——经典进程同步问题