[Java基础]多线程基础2--线程生命周期,线程安全与线程死锁

本文介绍了Java中线程的生命周期,包括新建、就绪、运行、阻塞和死亡状态。接着讨论了线程安全问题,通过窗口卖票的例子展示了线程不安全现象,并讲解了同步代码块、同步方法和锁机制的实现来解决线程安全。最后探讨了线程死锁的概念、产生条件和处理方法,包括死锁预防、避免及检测恢复策略。
摘要由CSDN通过智能技术生成

一. 线程的生命周期

新建
就绪
阻塞
运行
结束

新建:
new关键字创建了一个线程以后,该线程处于新建状态
这个状态下,JVM为线程分配的内存,初始化成员变量值
就绪:
当线程调用Start()方法,该线程处于就绪状态,这个状态下线程创建方法栈和程序计数器,等待线程调度器调度
运行
就绪状态下线程获得了CPU资源,开始运行run()方法
阻塞:
以下情况下都会使线程进入阻塞状态
1.线程调用了sleep()方法
2.线程调用了一个阻塞式IO方法,该方法返回之前线程都会处于阻塞状态
3.线程试图获取一个同步锁,但该锁被其他线程占用着,没有被释放
4.线程在等待某个通知(线程中通信)
5.程序调用了suspend()方法将该线程挂起,但是这个方法容易导致死锁,应该尽量减少使用这种方法

死亡:
正常结束:run() ,call()方法执行完成,正常结束
异常结束:线程抛出未捕获异常,调用了该线程的stop()方法(该方法使线程死掉了也不放锁,容易导致死锁,应该避免使用)

二.线程安全问题

2.1线程安全概念

多线程多次重复执行的结果与单线程执行的结果是一样的叫做线程安全,否则叫做线程不安全

2.2例子:多线程窗口卖票

public class SellTicket implements Runnable {
	private int ticketNum = 100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			if(ticketNum>0) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				String name = Thread.currentThread().getName();
				System.out.println("线程"+name+"售票"+ticketNum--);
			}
		}
	}

}

		SellTicket ticket = new SellTicket();
		Thread thread1 = new Thread(ticket,"Thread1");
		Thread thread2 = new Thread(ticket,"Thread2");
		Thread thread3 = new Thread(ticket,"Thread3");
		Thread thread4 = new Thread(ticket,"Thread4");
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();

打印结果:
在这里插入图片描述
不仅不安全还出现了重复,原因是多线程在共享数据且有多条代码对数据有写操作

2.3实现线程同步:

针对上述问题,解决思路是:当某个线程在修改共享资源时,其他线程不能修改该资源,要等某个线程修改结束之后才能获取CPU资源去完成相应的操作
为了实现线程同步,Java引入了七种线程同步机制

-机制备注
1同步代码块synchronized
2同步方法synchronized
3同步锁reenactmentLock
4特殊域变量volatile
5局部变量ThreadLocal
6阻塞队列LinkedBlockingQueue
7原子变量Atomic

这里暂时先介绍前三种

2.3.1同步代码块实现

首先需要一把锁,这个锁可以是任意一个对象,比如可以是Object对象

Object object = new Object();

然后加上同步代码块

while(true) {
			synchronized (object) {
				if(ticketNum>0) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					String name = Thread.currentThread().getName();
					System.out.println("线程"+name+"售票"+ticketNum--);
				}
			}
			
		}

结果是;
在这里插入图片描述
不会重复卖票了,但是变成了一个单线程

2.3.2同步方法实现

public void run() {
		// TODO Auto-generated method stub
		while(true) {
			sellTicket();
		}
	}
	
	public synchronized void sellTicket() {
			synchronized (object) {
				if(ticketNum>0) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					String name = Thread.currentThread().getName();
					System.out.println("线程"+name+"售票"+ticketNum--);
				}
			}
			
		}
	

这个方法其实也有锁对象,只不过没有显示出来,如果synchronized不是加到静态方法上,那么该方法会调用该方法对象的实例,这个实例相当于锁对象。

public synchronized void sellTicket()
     -->public synchronized(new SellTicket() ) void sellTicket()

如果是静态方法,那么这个锁对象是类对象。

public static synchronized void sellTicket()
     -->public synchronized(SellTicket) void sellTicket()

2.3.3同步锁

这个方法可以检测锁的状态,主要的方法是lock()和unLock(),这个位于包 java.util.concurrent.locks.Lock下,还需要用到包java.util.concurrent.locks.ReentrantLock;
使用步骤:
1.首先需要一个锁对象,构造方法中的参数代表是否是公平锁,公平锁是指多个线程都拥有公平的执行权,非公平就是独占锁

private Lock lock = new ReentrantLock(true);//公平锁

2.使用锁,注意要用一个try和finally的代码块,在finally中释放锁,一定要记得释放,否则会出现死锁

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			lock.lock();
			try {
				if(ticketNum>0) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					String name = Thread.currentThread().getName();
					System.out.println("线程"+name+"售票"+ticketNum--);
				}
				
			}
			finally {
				lock.unlock();
			}
		
		}
	}

运行结果:
在这里插入图片描述
每个窗口都可以卖票了

2.3.4synchronized关键字和lock的区别

synchronized关键字lock
JVM层面代码层面
无法获取锁的状态能获取锁
能自动释放锁需要手动释放
如果两个线程,线程1获得锁,线程2会等待,线程1阻塞的话线程2会一直等下去不一定会等待,如果该线程获取不到锁,线程可以不用等待就结束
可重入,不可中断,非公平可重入,可判断,可公平
适合少量代码同步适合大量代码同步

三.线程的死锁

3.1什么是死锁

多个线程在竞争资源产生的问题,不借助外力的条件下无法进行下去。

3.2死锁产生的必要条件

条件例子
互斥条件一段时间内的某个资源只能被一个进程占有,此时有其他进程想占有就只能
不可剥夺条件每个线程获得的资源都不会被强行剥夺,只有在自己使用完成之后才会主动释放
请求与保持条件线程占用了一定的资源不释放并还需要获得其他资源才能执行但是又得不到
循环等待条件1等2,2等3,3等4,4等1,形成了

3.3死锁演示

public class DeadThread implements Runnable {
	private static Object obj1 = new Object();
	private static Object obj2 = new Object();
	public  int flag;
	DeadThread(int flag){
		this.flag = flag;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		if(flag==1) {
			synchronized (obj1) {
				System.out.println(Thread.currentThread().getName()+"获得资源obj1,等待资源obj2");
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			synchronized (obj2) {
				System.out.println(Thread.currentThread().getName()+"获得资源obj1和资源obj2");
			}
		}else {
			synchronized (obj2) {
				System.out.println(Thread.currentThread().getName()+"获得资源obj2,等待资源obj1");
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			synchronized (obj1) {
				System.out.println(Thread.currentThread().getName()+"获得资源obj1和资源obj2");
			}
		}
		
	}

}

在main中测试:

		DeadThread dead1 = new DeadThread(1);
		DeadThread dead2 = new DeadThread(2);
		Thread thread1 = new Thread(dead1);
		Thread thread2 = new Thread(dead2);
		thread1.start();
		thread2.start();

运行结果;
在这里插入图片描述
emmm居然没死锁,扑街了
(原因后续更新)

3.4 死锁的处理

办法解释
预防死锁破坏四个必要条件
避免死锁在资源动态分配的过程中,用某种方法防止系统进入不安全状态
检测死锁运行系统运行过程中发生死锁,但是可以设置检测机制检测死锁的发生,并采取适当的办法清除死锁
解除死锁死锁发生后的解除

3.4.1死锁预防

破坏条件思路局限
破坏互斥条件破坏不了
破坏占有并等待条件1.一次性分配资源,要什么都一次性给完就不需要等了 2.要求每个线程申请新的资源时先放弃所有自己占有的资源需要提前知道需要什么资源,但是一般情况下都不能知道/2.不适用于有资源依赖的情况
破坏不可抢占条件1.A请求某些资源的请求被拒绝那么A需要释放占有的资源,有必要的话它以后可以继续请求2.如果A进程请求B进程的资源,那么操作系统可以让B释放占有的资源方法2在两个进程优先级不同的情况下才可以使用
破坏循环等待条件给资源设置编号,必须按顺序获取

3.4.2死锁避免

3.4.2.1有序资源算法

给资源编号,按顺序获取,比如线程必须获取了1号资源才能依次获取2号,3号等资源,存在问题是大多数情况下我们并不能提前知道进程所需要的资源,这样的话就不可行了

3.4.2.2银行家算法

分配资源之前先判断系统是否安全,如果安全才分配,这个算法是最有代表性的死锁算法
在这里插入图片描述
(图片来源于网络)
Java实现的参考代码:https://blog.csdn.net/qq_40693171/article/details/84780224

3.4.2.3顺序加锁

比较符合几个进程都需要相同的一些锁的情况,但是不同的线程按照不同的顺序加锁就很容易发生死锁,如果所有的线程都按照相同的顺序加锁并获得锁,死锁就不会发生
比如:

Thread1Thread2
lockAwait for A
lockBwait for B
wait for Clock C

Thread1锁住A之后在等C,而Thread2已经锁住C了,这种情况下就会死锁
如果按顺序加锁就不会死锁:

Thread1Thread2
lockAwait for A
lockBwait for B
lockCwait for C
3.4.2.3限时加锁

线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有自己已经获得的资源,等待一段随机时间后再重试
缺点是:
1.当线程数量较多时,两个线程重新获取锁的概率很高,可能会导致超时后重试的死循环。
2.Java不能对synchronized同步块设置时间,需要自定义的锁,或者使用java.util.concurrent包下的工具

3.4.3死锁检测

死锁的预防和避免是开销非常大的,所以更好的方法是不采取任何 限制性措施,而是通过死锁检测来检测和恢复

3.4.3.1死锁检测需要的数据结构:

1.现有的资源向量E,代表每种已存在的资源总数
(E1,E2,…,En)
2.可分配资源向量A,代表当前可供使用的资源数(即没有分配到的资源)
(A1,A2,…,An)
3.分配矩阵C,C的第i行代表Pi当前所持有的每一种类型资源的资源数
|C11,C12,…,C1n|
|C21,C22,…,C2n|

|Ci1,Ci2,…,Cin|

|Cm1,Cm2,…,Cmn|
4.资源请求矩阵R,R的每一行代表P所需要的资源的数量
|R11,R12,…,R1n|
|R21,R22,…,R2n|

|Ri1,Ri2,…,Rin|

|Rm1,Rm2,…,Rmn|

3.4.3.2死锁检测步骤:

step1:寻找一个没有结束标记的进程P,对于它而言,R矩阵的第i行向量小于或等于A
step2:如果找到了这样的进程,先执行它,然后将C矩阵的第i行向量加入到A中,标记该进程,并转到第1步
step3:如果没有这样的进程了,那么算法终止
step4:算法结束时,没有标记过的进程都是死锁进程

3.4.4死锁恢复

方式说明
利用抢占恢复临时将某个资源从它当前的进程转移到另一个进程
利用回滚恢复周期性地将进程的状态进行备份,当发现当前进程死锁时,根据备份将其复位到上一个更早的,还没有获得所需资源的状态,然后把这些资源分配给其他死锁的进程
通过杀死进程恢复这是最简单的办法但也是最不推荐的方法。需要谨慎,尽可能地保证杀死的进程可以从头再来而不带来副作用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值