一. 线程的生命周期
新建:
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顺序加锁
比较符合几个进程都需要相同的一些锁的情况,但是不同的线程按照不同的顺序加锁就很容易发生死锁,如果所有的线程都按照相同的顺序加锁并获得锁,死锁就不会发生
比如:
Thread1 | Thread2 |
---|---|
lockA | wait for A |
lockB | wait for B |
wait for C | lock C |
Thread1锁住A之后在等C,而Thread2已经锁住C了,这种情况下就会死锁
如果按顺序加锁就不会死锁:
Thread1 | Thread2 |
---|---|
lockA | wait for A |
lockB | wait for B |
lockC | wait 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死锁恢复
方式 | 说明 |
---|---|
利用抢占恢复 | 临时将某个资源从它当前的进程转移到另一个进程 |
利用回滚恢复 | 周期性地将进程的状态进行备份,当发现当前进程死锁时,根据备份将其复位到上一个更早的,还没有获得所需资源的状态,然后把这些资源分配给其他死锁的进程 |
通过杀死进程恢复 | 这是最简单的办法但也是最不推荐的方法。需要谨慎,尽可能地保证杀死的进程可以从头再来而不带来副作用。 |