面试快答(线程,进程,线程安全,syn和lock,死锁,wait方法,sleep方法)

本文详细探讨了线程和进程的概念,线程安全问题及其解决方案,深入比较了syn关键字锁与ReentrantLock的区别,并阐述了wait、sleep、notify和notifyAll的方法差异以及死锁的产生原因和避免策略。内容涵盖了Java实现多线程的多种方式,线程的生命周期,线程优先级和线程池的使用价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.线程进程?

线程是执行顺序,执行流程,进程是线程集合,是一个正在运行的程序(需要多个线程才搞得定)
创建线程的几种方式:
继承thread类,实现runnable接口,使用匿名内部类方式创建线程thread t = new thread(new runnable){…重写run方法},用线程池(常用)
线程常用的api

getid,得到当前线程的id(thread-编号,编号从0开始)
getname,得到当前线程的名称。
sleep(时间毫秒值),休眠线程。
stop,停止线程。

线程还分守护线程和非守护线程(用户线程)
守护线程:

守护线程和主线程共存亡,Thread.setDamon(true),设置线程为守护线程。gc线程,就是一个守护线程,gc主要回收主线程的垃圾。

用户线程:

和主线程无关,是主线程创建的子线程。

在这里插入图片描述

二.多线程安全问题?

什么是多线程的安全呢?

当多个线程共享同一个全局变量(成员变量)的时候,A线程对变量做+操作,B线程对变量做-操作,可能就会导致数据不误差。

解决方法:

用syn(自动锁),lock(jdk1.5并发包,手动锁)

三.syn关键字锁和lock的那些事?

答:如果可用在方法和代码块的区别

用在方法上,当某个线程调用这个方法的时候,会获取该实例的对象锁,方法没有结束之前,其他线程只能去等待了。
只有释放对象锁的的时候,其他线程才有机会抢占这个锁,执行里面的方法,
但是发生的这一切的条件应该是所有的线程都是使用的是这个对象的实例,才能出现这个互斥的现象,不然syn没意义了。
但是如果这个方法是类方法,即修饰符是static,那么意味某个调用此方法的线程当前会拥有该类的锁,
只要线程持续在当前方法内运行,其他线程依然是无法获得方法的使用权的。

为什么常使用的是syn代码块呢?

优势,其实就是可以最对需要同步的地方使用就好了,
常和object类的方法wait,notify,nitifyAll方法一起使用,注意这三个方法不是线程方法,属于object里面的方法。

四.方法wait,sleep,notify,nitifyAll各有什么区别呢?

wait方法

释放对象锁,线程进入到线程等待池,释放CPU,其他正在等待的线程可以抢占这把锁

sleep

sleep方法只是休眠,休眠时,释放CPU,不会释放对象锁,其他线程不能进入到方法内部,休眠结束后,重新获得cpu,执行代码。

notify方法

会唤醒因为调用对象的wait方法而等待的线程,其实就是将对对象的锁进行唤醒,使wait的线程可以有机会获取对象锁

notifyall方法

唤醒所有等待的线程。需要注意的是:wait方法和notify方法必须在syn代码块中调用(切记)。

死锁:

什么是死锁?
多线程竞争资源造成互相等待,没外力,进程无法推进。
产生死锁原因:
(1)资源竞争:
打印机,磁带机这种不刻剥夺资源竞争,会产生死锁。
(2)进程推进顺序非法:
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
(3)信号量使用不当也会造成死锁:
进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
解决死锁:
1)加锁的顺序(线程按照一定的顺序加锁)

多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁很容易发生,假设A线程需要一些锁,按照指定的顺序获取锁,先得到之前的锁,才能得到后面的锁。

2)加锁的时限

获取锁的过程中超过时限放弃对锁的请求,若一个线程没有在指定的时限内成功获取全部的锁,回退并释放之前获取到的锁,然后等待一段随机时间再重试。

3)死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每单一个线程获取到锁,用map记录下来

那么当检测出死锁时,这些线程该做些什么呢?
给这些线程设置优先级,让一个或多个线程回退,剩下的线程就会像没发生死锁一样保持着他们需要的锁。如果这些线程的优先级是固定不变的,同一批线程总会拥有更高的优先级,为了避免这种情况的出现,可以在死锁发生的时候设置随机的优先级。

总结:

wait方法全释放,释放锁,释放CPU;  
sleep方法只是释放CPU,不释放对象锁,只有释放对象锁才能进入到方法内部。
因为锁资源要竞争,可能出现死锁(占着茅坑不拉屎)

死锁解决方案:

五.syn和reentrantlock区别?

ReentrantLock 与synchronized有相同的并发性和内存语义,还包含了中断锁等候和定时锁等候,意味着线程A如果先获得了对象obj的锁,那么线程B可以在等待指定时间内依然无法获取锁,那么就会自动放弃该锁。
总结:syn,A不释放,B只能等待。reentrantlock是等待一下,等到不耐烦了,不等了,去干别的事情。

使用建议?

并发量小,用syn,并发量高,syn性能下降严重,用reentrantlock

												20200515中午11点更新

并行

两个线程同时做事情。

并发

一会做这个,一会做哪个,存在调度,单核cpu不可能存在并行(微观上)。  

java实现多线程的两种方式:继承thread类和实现runnable接口。

临界区:

	用来表示一种公共的资源或者是共享的数据,可以被多个线程使用。但是每一次,只能有一个线程去使用。一旦临界区资源被占用,其他线程就得等待。

阻塞与非阻塞

	通常用来形容多线程之间的相互影响。比如一个线程占用了临界区资源,那么其他需要这个资源的线程就必须在临界区等待,等待会导致线程的挂起。这种情况就是阻塞。
	占用方一直不愿意释放资源,其他线程都不能工作。阻塞性能不好,一般需要8w个时钟周期来做调度。
	非阻塞则允许多个线程同时进入临界区。

死锁:

	多个线程循环等待他方占有资源而无限的僵持下去的局面。例如打印机,磁带机这种不可谋夺的资源。

活锁:

	线程12都需要资源A/B.假设1线程占有A资源,2线程占有B资源。由于两个线程都需要用到A和B两种资源。为了避免死锁。各自释放资源,AB空闲,两个线程同时抢锁。再次出现上述情况,此时发生了活锁。这是很难排查的。

饥饿:

	是指某一个或者多个线程因为种种的原因无法获得需要的资源,导致一直无法执行。

线程的生命周期

	线程周期中,要经历创建,可运行,不可运行几种状态。

	创建状态:
		new创建一个线程的时候,线程处于创建状态。这时候只是一个空的线程对象,系统不为他分配资源。
	
	可运行状态:
		执行线程的start方法将为线程分配需要的系统资源。安排上去。调用线程体--run方法。这样线程就处于可运行状态(runnable)。这个状态并不是运行中的状态,因为线程实际上并没有真正的执行,可能还在等待cpu的调度。
	
	不可运行状态
		发生下面的事件的时候,处于运行状态的线程会转入到不可运行的状态:
			- 调用了sleep方法;
			- 线程调用wait方法等待特定的条件满足。
			- 线程输入/输出阻塞、
			- 返回可运行状态。
			- 处于睡眠状态的线程在指定的时间过去后。
			- 如果线程在等待某一个条件,另外一个对象必须通过notify或者notifyall方法通知等待线程条件的改变。
			- 如果线程是因为输入输出阻塞,等待输入输出完成。

线程优先级

线程的优先级及设置
			线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级高的线程将优先执行。
线程的调度策略
			线程的调度器选择优先级最高的线程运行。但是,如果发生下面的情况,就会终止线程的运行:
				1.线程体中调用了yield方法,让出对CPU的占用权。
				2.线程体中调用了sleep方法,使线程进入了睡眠的状态。
				3.线程由于I/O操作而发生的阻塞。
				4.另外一个更加高级的线程出现。
				5.在支持时间片的系统中,该线程的时间片用完了。  

单线程的创建方式

两种方式:
			1.继承thread类
			2.实现runnable接口。

		注意:
			- 不管是继承的还是实现接口的,业务逻辑是写在run方法里面的,线程启动的时候是调用start方法。
			- 开启新的线程的时候,不影响主线程代码的执行的顺序也不会发生阻塞主线程的执行。
			- 新的线程和主线程的代码的执行的顺序是不能够保证先后顺序的。
			- 对于多线程的顺序问题。从微观上讲某一时刻只有一个线程在工作,多线程的目的就是为了让CPU忙起来。
			- 通过thread类的源码可以看到,也是实现了runnable接口的,所以两种方法本质上是一个。

为什么要使用线程池?

为什么不开发一个多线程的程序,为什么还要引入线程池?
				主要是因为上面的单线程方式存在几个问题:
					1.线程的工作周期:线程创建时间为T1。线程执行任务所需时间是T2.线程销毁需要时间T3.
					但是现实中T1+T3的时间还要大于T2.所以频繁的创建,销毁线程是很消耗时间的。
					2.如果任务来了,再去创建线程效率就比较低。但是有一个池子,直接给你去不就很好嘛。
					3.线程池可以用来管理和控制线程。因为线程是一种稀缺的资源,如果无限制的创建,
					不仅会消耗系统的资源,还会降低系统的稳定性。线程池达到线程的统一分配,调优和监控。
					4.线程池提供队列,存放缓冲等待执行的任务。
			通过线程池创建从调用的api角度来说分为两种:
			一种:原生的线程池。
			第二种:通过java提供的并发包来创建。后者简单,拿来即用。后者其实是对原生的线程池的创建方式做了一次简化包装。道理还是原生的线程池的创建。  
			通过ThreadPoolExecutor创建线程池,APi如下:
				public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,  long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue); 

代码解释:

			corePoolSize:
				核心池的大小。
				创建线程池后,默认情况下,线程池中没有任何线程,而是等待有任务到来创建线程去执行任务,除非调用了prestartAllCoreThreads方法,预创建线程的方式,没有任务到来之前创建corePoolSize个线程或者一个线程。
				默认情况下,创建线程池后,线程池中的线程数为0,任务来之后,创建线程,当线程池中的线程的数目达到了corePoolSize之后,就会把到达的任务放到缓存队列中。

			maximumPoolSize:
				线程池最大线程数,这个参数很重要。表示可以在线程池中最多创建多少个线程。

			keepAliveTime:
				表示线程没有任务执行时候最多保持多少时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时候,keepAliveTime才会起到作用。

			unit:
				keepAliveTime的单位时间。

			workQueue:
				一个阻塞队列,用来存储等待执行的任务,这个参数也非常重要。会对线程池的运行过程产生很大的影响。
				这里的阻塞队列有以下的几种的选择:
					ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值