并发编程知识点总结

Posted boy快快长大

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程知识点总结相关的知识,希望对你有一定的参考价值。

并发编程知识点总结

1. 什么是进程?是什么线程?

线程是处理器任务调度和执行的基本单位。
进程是操作系统资源分配的基本单位。
进程是程序的一次执行过程,是系统运行的基本单位。

线程是一个比进程更小的执行单位,一个进程可以包含多个线程。

2. 进程和线程的关系?

定义:线程是处理器任务调度和执行的基本单位;进程是操作系统资源分配的基本单位。

包含关系:一个进程可以包含多个线程。

从Java虚拟机的角度来理解:Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器,如图所示,假设某个进程包含三个线程。

由上面可知以下进程和线程在以下几个方面的区别:

内存分配:进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。

资源开销:每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。

3.并行和并发的区别?

并行:单位时间多个处理器同时处理多个任务。
并发:一个处理器处理多个任务,按时间片轮流处理多个任务。

4. 多线程的优缺点(为什么使用多线程、多线程会引发什么问题)

优点:当一个线程进入等待状态或者阻塞时,CPU可以先去执行其他线程,提高CPU的利用率。

缺点:
1. 上下文切换:频繁的上下文切换会影响多线程的执行速度。
2. 死锁
3. 资源限制:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。在并发编程中,程序执行变快的原因是将程序中串行执行的部分变成并发执行,如果因为资源限制,并发执行的部分仍在串行执行,程序执行将会变得更慢,因为程序并发需要上下文切换和资源调度。

5.线程的上下文切换

即便是单核的处理器也会支持多线程,处理器会给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给每个线程的执行时间,一般来说时间片非常的短,所以处理器会不停地切换线程。

CPU会通过时间片分配算法来循环执行任务,当前任务执行完一个时间片后会切换到下一个任务,但切换前会保存上一个任务的状态,因为下次切换回这个任务时还要加载这个任务的状态继续执行,从任务保存到在加载的过程就是一次上下文切换。

6. Java中守护线程和用户线程的区别?

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolean) 设置,true则是将该线程设置为守护线程,false则是将该线程设置为用户线程。同时,Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

用户线程:平时使用到的线程均为用户线程。
守护线程:用来服务用户线程的线程,例如垃圾回收线程。
守护线程和用户线程的区别主要在于Java虚拟机是后存活。
用户线程:当任何一个用户线程未结束,Java虚拟机是不会结束的。
守护线程:如何只剩守护线程未结束,Java虚拟机结束。

7.线程死锁是如何产生的,如何避免

死锁:由于两个或两个以上的线程相互竞争对方的资源,而同时不释放自己的资源,导致所有线程同时被阻塞。

死锁产生的条件:
1. 互斥条件:一个资源在同一时刻只由一个线程占用。
2. 请求与保持条件:一个线程在请求被占资源时发生阻塞,并对已获得的资源保持不放。
3. 循环等待条件:发生死锁时,所有的线程会形成一个死循环,一直阻塞。
4. 不剥夺条件:线程已获得的资源在未使用完不能被其他线程剥夺,只能由自己使用完释放资源。

避免死锁的方法主要是破坏死锁产生的条件。
1.破坏互斥条件:这个条件无法进行破坏,锁的作用就是使他们互斥。
2.破坏请求与保持条件:一次性申请所有的资源。
3.破坏循环等待条件:按顺序来申请资源。
4. 破坏不剥夺条件:线程在申请不到所需资源时,主动放弃所持有的资源。

8.用Java实现死锁,并给出避免死锁的解决方案

package com.company.multi.thread;

/**
 * @version 1.0
 * @date 2023/3/28
 */
public class DeadLockDemo 
	private static Object resource1 = new Object();
	private static Object resource2 = new Object();

	public static void main(String[] args) 
		new Thread(() -> 
			synchronized (resource1) 
				System.out.println(Thread.currentThread().getName() + " ==>get resource1");
				try 
					Thread.sleep(1000);   //线程休眠,保证线程2先获得资源2
				 catch (InterruptedException e) 
					e.printStackTrace();
				
				System.out.println(Thread.currentThread().getName() + " ==>waiting get resource2");
				synchronized (resource2) 
					System.out.println(Thread.currentThread().getName() + " ==>get resource2");
				
			
		, "线程 1").start();

		new Thread(() -> 
			synchronized (resource2) 
				System.out.println(Thread.currentThread().getName() + " ==>get resource2");
				try 
					Thread.sleep(1000); //线程休眠,保证线程1先获得资源1
				 catch (InterruptedException e) 
					e.printStackTrace();
				
				System.out.println(Thread.currentThread().getName() + " ==>waiting get resource1");
				synchronized (resource1) 
					System.out.println(Thread.currentThread().getName() + " ==>get resource1");
				
			
		, "线程 2").start();
	


result:
线程 1 ==>get resource1
线程 2 ==>get resource2
线程 1 ==>waiting get resource2
线程 2 ==>waiting get resource1

上面代码产生死锁的原因主要是线程1获取到了资源1,线程2获取到了资源2,线程1继续获取资源2而产生阻塞,线程2继续获取资源1而产生阻塞。解决该问题最简单的方式就是两个线程按顺序获取资源,线程1和线程2都先获取资源1再获取资源2,无论哪个线程先获取到资源1,另一个线程都会因无法获取线程1产生阻塞,等到先获取到资源1的线程释放资源1,另一个线程获取资源1,这样两个线程可以轮流获取资源1和资源2。代码如下:

package com.company.multi.thread;

/**
 * @author 
 * @version 1.0
 * @date 2023/3/28
 */
public class DeadLockDemo 
	private static Object resource1 = new Object();
	private static Object resource2 = new Object();

	public static void main(String[] args) 
		new Thread(() -> 
			synchronized (resource1) 
				System.out.println(Thread.currentThread().getName() + " ==>get resource1");
				try 
					Thread.sleep(1000);
				 catch (InterruptedException e) 
					e.printStackTrace();
				
				System.out.println(Thread.currentThread().getName() + " ==>waiting get resource2");
				synchronized (resource2) 
					System.out.println(Thread.currentThread().getName() + " ==>get resource2");
				
			
		, "线程 1").start();

		new Thread(() -> 
			synchronized (resource1) 
				System.out.println(Thread.currentThread().getName() + " ==>get resource1");
				try 
					Thread.sleep(1000);
				 catch (InterruptedException e) 
					e.printStackTrace();
				
				System.out.println(Thread.currentThread().getName() + " ==>waiting get resource2");
				synchronized (resource2) 
					System.out.println(Thread.currentThread().getName() + " ==>get resource2");
				
			
		, "线程 2").start();
	


result:
线程 1 ==>get resource1
线程 1 ==>waiting get resource2
线程 1 ==>get resource2
线程 2 ==>get resource1
线程 2 ==>waiting get resource2
线程 2 ==>get resource2

9.Java中的死锁、活锁、饥饿有什么区别?

活锁:任务或者执行者没有被阻塞,由于某些条件没有被满足,导致线程一直重复尝试、失败、尝试、失败。例如,线程1和线程2都需要获取一个资源,但他们同时让其他线程先获取该资源,两个线程一直谦让,最后都无法获取。

活锁和死锁的区别:

  • 活锁是在不断地尝试、死锁是在一直等待。
  • 活锁有可能自行解开、死锁无法自行解开。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源, 导致一直无法执行的状态。以打印机打印文件为例,当有多个线程需要打印文件,系统按照短文件优先的策略进行打印,但当短文件的打印任务一直不间断地出现,那长文件的打印任务会被一直推迟,导致饥饿。活锁就是在忙式等待条件下发生的饥饿,忙式等待就是不进入等待状态的等待。

产生饥饿的原因:

  • 高优先级的线程占用了低优先级线程的CPU时间
  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait()方法),因为其他线程总是被持续地获得唤醒。

死锁、饥饿的区别:饥饿可自行解开,死锁不行。

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

参考:第六节

11.创建线程一共有哪几种方法?

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 使用CallableFuture创建线程
  • 使用线程池例如用Executor框架

使用线程池例如用Executor框架: Executors可提供四种线程池,分别为:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

创建一个定长线程池

static class ThreadDemo extends Thread 
		@Override
		public void run() 
			System.out.println(Thread.currentThread().getName() + "正在执行");
		
	

	class TestFixedThreadPool 
		public static void main(String[] args) 
			//创建一个可重用固定线程数的线程池
			ExecutorService pool = Executors.newFixedThreadPool(2);
			//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
			Thread t1 = new ThreadDemo();
			Thread t2 = new ThreadDemo();
			Thread t3 = new ThreadDemo();
			Thread t4 = new ThreadDemo();
			Thread t5 = new ThreadDemo();
			//将线程放入池中进行执行
			pool.execute(t1);
			pool.execute(t2);
			pool.execute(t3);
			pool.execute(t4);
			pool.execute(t5);
			//关闭线程池
			pool.shutdown();
		
	
	
pool-1-thread-2正在执行
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-2正在执行

12. runnable 和 callable 有什么区别?

相同点:

  1. 两者都是接口
  2. 两者都需要调用Thread.start启动线程

不同点:

  1. callable的核心是call()方法,允许返回值,runnable的核心是run()方法,没有返回值
  2. call()方法可以抛出异常,但是run()方法不行
  3. callablerunnable都可以应用于executorsthread类只支持runnable

13. 线程的run()和start()有什么区别?

  • 线程是通过Thread对象所对应的方法run()来完成其操作的,而线程的启动是通过start()方法执行的。
  • run()方法可以重复调用,start()方法只能调用一次

14. 为什么调用start()方法时会执行run()方法,而不直接执行run()方法?

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

run()方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run()方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

调用start()方法可以开启一个线程,而run()方法只是thread类中的一个普通方法,直接调用run()方法还是在主线程中执行的。

15. 线程同步以及线程调度相关的方法有哪些?

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

  • sleep():使当前线程进入指定毫秒数的休眠,暂停执行,需要处理InterruptedException。

  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关。

  • notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。

  • jion():与sleep()方法一样,是一个可中断的方法,在一个线程中调用另一个线程的join()方法使得当前的线程挂起,知直到执行join()方法的线程结束。例如在B线程中调用A线程的join()方法,B线程进入阻塞状态,直到A线程结束或者到达指定的时间。

  • yield():提醒调度器愿意放弃当前的CPU资源,使得当前线程从RUNNING状态切换到RUNABLE状态。

16.线程的sleep()方法和yield()方法有什么不同?

  • sleep()方法使得当前线程暂停指定的时间,没有消耗CPU时间片。
  • sleep()使得线程进入到阻塞状态,yield()只是对CPU进行提示,如果CPU没有忽略这个提示,会使得线程上下文的切换,进入到就绪状态。
  • sleep()一定会完成给定的休眠时间,yield()不一定能完成。
  • sleep()需要抛出InterruptedException,而yield()方法无需抛出异常。

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

相同点:

  • wait()方法和sleep()方法都可以使得线程进入到阻塞状态。
  • wait()sleep()方法都是可中断方法,被中断后都会收到中断异常。

不同点:

  • wait()是Object的方法,sleep()是Thread的方法。
  • wait()必须在同步方法中进行,sleep()方法不需要。
  • 线程在同步方法中执行sleep()方法,不会释放monitor的锁,而wait()方法释放monitor的锁。
  • sleep()方法在短暂的休眠之后会主动退出阻塞,而wait()方法在没有指定wait时间的情况下需要被其他线程中断才可以退出阻塞。

18. wait()方法一般在循环块中使用还是if块中使用?

在JDK官方文档中明确要求了要在循环中使用,否则可能出现虚假唤醒的可能。官方文档中给出的代码示例如下:

synchronized (obj) 
			while (<condition does not hold>)
				obj.wait();
			    //满足while中的条件后执行业务逻辑
		

如果讲while换成if

synchronized (obj) 
			if (<condition does not hold>)
				obj.wait();
			    //满足while中的条件后执行业务逻辑
		

当线程被唤醒后,可能if()中的条件已经不满足了,出现虚假唤醒。

19. 线程通信的方法有哪些?

  • 锁与同步
  • wait()/notify()notifyAll()
  • 信号量
  • 管道

20. 为什么wait()、notify()、notifyAll()被定义在Object类中而不是在Thread类中?

因为这些方法在操作同步线程时,都必须要标识他们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify()或notifyAll()唤醒,不可以对不同锁中的线程进行唤醒,也就是说等待和唤醒必须是同一锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。

如果把wait()、notify()、notifyAll()定义在Thread类中,则会出现一些难以解决的问题,例如如何让一个线程可以持有多把锁?如何确定线程等待的是哪把锁?既然是当前线程去等待某个对象的锁,则应通过操作对象来实现而不是操作线程,而Object类是所有对象的父类,所以将这三种方法定义在Object类中最合适。

21. 为什么wait(),notify()和notifyAll()必须在同步方法或者同步块中被调用?

因为wait()暂停的是持有锁的对象,notify()或notifyAll()唤醒的是等待锁的对象。所以wait()、notify()、notifyAll()都需要线程持有锁的对象,进而需要在同步方法或者同步块中被调用。

22. 为什么Thread类的sleep()和yield()方法是静态的?*

sleep()yield()都是需要正在执行的线程调用的,那些本来就阻塞或者等待的线程调用这个方法是无意义的,所以这两个方法是静态的。

23.如何停止一个正在运行的线程? * *

  1. 中断:Interrupt方法中断线程
  2. 使用volatile boolean标志位停止线程:在线程中设置一个boolean标志位,同时用volatile修饰保证可见性,在线程里不断地读取这个值,其他地方可以修改这个boolean值。
  3. 使用stop()方法停止线程,但该方法已经被废弃。因为这样线程不能在停止前保存数据,会出现数据完整性问题。

Java并发总结-全景图

Java并发编程知识总结——开篇词

这篇博客是开始总结Java并发的开篇,主要说下下面总结Java并发编程相关知识的思路、呈现出自己理解的Java并发知识的全景图,为后面Java并发知识总结的复习提供线索和记忆脉络。主要学习和借鉴的主要资料有:《极客时间》并发编程专栏、《Java并发实战》等。

Java并发编程总结思路

  • 之前有粗有细的看了一遍《Java并发实战》,看完后过段时间又全部忘记了;究其原因,是因为没有看到Java并发的全局,只看到了局部零散的知识点,记住的是点,难有线和面。因此需要从一个个单一的知识和技术中“跳出来”,建立一张全景图。
  • 并发编程领域主要可以抽象成三个核心问题:分工、同步、互斥
  1. 分工:

    在多核处理器时代,利用多核处理优势,将一个大的任务拆解成一个个小任务分工去完成提高效率。比如要建一条马路,划分成不同的任务:打地基、铺路、铺砖、土建等不同的子任务、分工去完成,提供了工作的效率(性能)。抽象到Java并发上,比如:生产者-消费者模式、Fork/Join、Future(同异步分工协同)等。

  2. 同步(协作):

    在分工的基础上,子任务之间可能存在依赖关系,比如在造路过程中,土建完成之后需要通知铺路小组去完成铺路,这就是子任务之间的同步。

    像这类问题抽象到Java并发上就是:一个线程执行完成了一个任务、如何通知执行后续任务的线程开工的问题。Java提供的Future、同步工具类(CountDownLatch、Notify机制、栏栅CyclicBarrier)等都是解决同步问题的工具。

    工作中遇到的线程协作问题,基本可以描述成:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行

  3. 互斥(安全性):

    分工、同步主要强调的是性能,而并发程序里还有一部分关于正确性的的问题,叫“线程安全”

    并发程序里多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定意味着可能正确,也可能错误,事先是不不确定的。而不确定的主要源头是可见性问题有序性问题、和原子性问题。为了解决这三个问题,Java语言引入内存模型,内存模型提供了一系列的规则,利用这些规则,可以避免可见性问题、有序性问题,但还是不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。

    互斥,指同一时刻,只允许一个线程访问共享变量

    互斥的核心就是锁,Java中的synchronized、SDK里的各种Lock都能解决互斥问题。虽说锁解决了安全问题,但同时也带来了性能问题,如何保证安全性的同时有能尽可能提高性能呢?

    可以分场景优化,Java SDK里提供ReadWriteLock、StampedLock就可以优化读多写少场景下的锁性能。当然还可以使用无锁的数据结构,例如Java SDK里提供的原子类都是基于无锁技术实现的。

    除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读。这方面,Java提供了ThreadLocal和final关键字,还有一种Copy-on-write模式。

?

Java并发编程全景图

  • 主要根据《极客时间》并发编程专栏的全景图的基础上加上自己的理解补充完善出来的。后面总结主要会围绕全景图的脉络并以全景图的部分和一个标题分支来命名博客标题。这样我觉得会更方便记忆和查找。

  • 后面全景图会根据反复阅读《并发编程实战》来补充更新完善。

  • 全景图:
    技术图片

  • 基本概念图:
    技术图片

  • 并发工具图
    技术图片

(图片不知道能不能看清楚,思维导图附件我会放到我的博客后台根目录下的xmind目录下)。

以上是关于并发编程知识点总结的主要内容,如果未能解决你的问题,请参考以下文章

Java知识全面总结:并发编程+JVM+设计模式+常用框架+....

Java知识全面总结:并发编程+JVM+设计模式+常用框架+....

Java 知识全面总结:并发编程+JVM+设计模式+常用框架+....

Java 知识全面总结:并发编程+JVM+设计模式+常用框架+....

并发编程之学习总结

Spark系列