1.异常同步现象-死锁
1.定义
所谓死锁,是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。[引用自某度]
2.产生条件
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。[引用自同样的某度]
3.示例代码
package com.dream.home_1104.thread;
//本程序演示一个简单的死锁现象
//产生死锁的原因:同步嵌套同步,但同步锁却不同
//自定义同步锁类-使用静态成员方便调用
class Lock{
public static Object aLock=new Object();
public static Object bLock=new Object();
}
//自定义线程类
class TestThread implements Runnable{
private Object aLock=Lock.aLock;
private Object bLock=Lock.bLock;
private boolean flag;//分配执行路径的标识
public TestThread(boolean flag){
this.flag=flag;
}
public void run() {
if(flag){
while(true){
synchronized(aLock){
System.out.println(Thread.currentThread().getName()+"执行路径1->alock...");
synchronized(bLock){
System.out.println(Thread.currentThread().getName()+"执行路径1->block...");
}
}
}
}else{
while(true){
synchronized(bLock){
System.out.println(Thread.currentThread().getName()+"执行路径2->block...");
synchronized(aLock){
System.out.println(Thread.currentThread().getName()+"执行路径2->alock...");
}
}
}
}
}
}
public class DeadthThreadManager {
public static void main(String[] args) {
TestThread t1=new TestThread(true);
TestThread t2=new TestThread(false);
Thread th1=new Thread(t1);
Thread th2=new Thread(t2);
th1.start();
th2.start();
}
}
运行结果:
Thread-0执行路径1->alock…
Thread-1执行路径2->block…
说明:在本例中,两条执行路径中,都存在着双重嵌套同步代码块,但syncheonized关键字中的同步锁顺序刚好相反。当两个线程分别进入两条执行路径后,分别获得aLock
,bLock
同步锁,都无法进入下层同步代码快,因此程序进入死锁状态。
2.线程间的通信
1.相关方法
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。void notify()
唤醒在此对象监视器上等待的单个线程。void notifyAll()
唤醒在此对象监视器上等待的所有线程。
说明:
- 以上方法继承自
Object
类 - 以上只能用于同步情况下,因为要对持有监视器[锁]的线程操作。而只有同步中才有锁
这些操作线程的方法为什么要定义在Object中呢?
- 因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的锁
- 只有同一个锁上的被等待线程,可以被同一个锁上的
notify
唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。(因此以上的方法必须由相应同步锁对象调用)
2.等待-唤醒机制
简单说明:
- 在某些具体场景下,运用
wait
与notify
实现线程间的切换和通信需求 - 等待状态线程都存在线程池中,
notify
一般唤醒线程池中的第一个冻结线程
示例代码[双线程数据存取操作]:
package com.dream.home_1104.thread;
//本例演示一个简单地线程通信程序
//数据读取类
class DataOutput implements Runnable{
private Data data;//Data类成员
//构造方法
public DataOutput(Data data){
this.data=data;
}
public void run() {
while(true){
data.showData();//调用Data类中的数据输出方法
}
}
}
//数据输入类
class DataInput implements Runnable{
private Data data;//Data类成员
private int x=0;//数据输入标识
//构造方法
public DataInput(Data data){
this.data=data;
}
public void run() {
while(true){
if(x==0){
//调用Data类中的数据写入方法
data.setData("数据A",12);
x=1;
}else{
调用Data类中的数据写入方法
data.setData("数据B",24);
x=0;
}
}
}
}
//数据类
class Data{
private String name;
private int age;
private boolean flag=false;//是否具有数据的标识
//数据写入方法-进行同步
public void setData(String name,int age){
synchronized(this){
if(flag){
try {this.wait();}catch(InterruptedException e){}
}
//不再添加else判断语句
this.name=name;
this.age=age;
this.flag=true;
this.notify();
}
}
//数据输出方法-进行同步
public synchronized void showData(){
if(!flag){
try {this.wait();} catch (InterruptedException e) {}
}
//不再添加else判断语句
System.out.println(name+":"+age);
flag=false;
this.notify();
}
public void setFlag(boolean flag){
this.flag=flag;
}
public boolean getFlag(){
return flag;
}
}
public class DataThreadManager {
public static void main(String[] args) {
Data data=new Data();
//数据输入线程
Thread t1=new Thread(new DataInput(data));
//数据读取线程
Thread t2=new Thread(new DataOutput(data));
t1.start();
t2.start();
}
}
运行结果:
….
数据B:24
数据A:12
数据B:24
数据A:12
数据B:24
数据A:12
….
所谓线程通信,其实就是应用于多个线程对同一对象进行操作,但各自的具体操作不同(如本例中数据的读与写)。多个线程之间通过wait
与notify
进行状态切换,达到线程安全的目的。在本例中即体现为先写入、后读取,循环往复。
3.生产者与消费者
特点:对于某一资源,生产者线程(可以有多个)生产一个,消费者线程(可以有多个)才消费一个。按生产-消费顺序先后进行。
示例代码:
package com.dream.home_1104.thread;
//本例演示生产者-消费者模式
//生产者线程类
class Create implements Runnable{
private Resource re;//资源类对象
public Create(Resource re){
this.re=re;
}
public void run(){
while(true){
re.setData("示例名称");
}
}
}
//消费者线程类
class Use implements Runnable{
private Resource re;//资源类对象
//构造方法
public Use(Resource re){
this.re=re;
}
public void run(){
while(true){
re.getData();
}
}
}
//数据存放类-资源类
class Resource{
private boolean flag=false;//判断数据写入或者读取的标记
private int count=1;//记录生成的资源的编号
private String name;
//写入数据的方法
public void setData(String name){
synchronized(this){
if(flag){
try{
this.wait();
}catch(Exception e){}
}
this.name=name+count++; //将计数变量count加入name,便于后续区别
System.out.println(Thread.currentThread().getName()+" create->"+this.name);
flag=true;
this.notify();
}
}
//读取数据的操作
public void getData(){
synchronized(this){
if(!flag){
try{
this.wait();
}catch(Exception e){}
}
System.out.println(Thread.currentThread().getName()+" use--->"+name);
flag=false;
this.notify();
}
}
}
//运行类
public class FactoryManager {
public static void main(String[] args) {
Resource re=new Resource();
Create cr=new Create(re);
Use us=new Use(re);
new Thread(cr).start();//生产者线程
new Thread(us).start();//消费者线程
}
}
运行结果:
Thread-0 create->示例名称8708
Thread-1 use—>示例名称8708
Thread-0 create->示例名称8709
Thread-1 use—>示例名称8709
说明:本例与上一示例的显著区别在于为数据加上编号进行区分,所想要体现的即是生产-消费的运行模式。但本例仅运行一个生产者线程和一个消费者线程,当生产者与消费者线程存在多个时,本例会发生异常的运行情况。请继续阅读…
异常情况引例(部分代码):
//运行类
public class FactoryManager {
public static void main(String[] args) {
Resource re=new Resource();
Create cr1=new Create(re);
Create cr2=new Create(re);
Use us1=new Use(re);
Use us2=new Use(re);
new Thread(cr1).start();//生产者线程
new Thread(us1).start();//消费者线程
new Thread(cr2).start();//生产者线程
new Thread(us2).start();//消费者线程
}
}
运行结果:
….
….
Thread-0 create->示例名称14521
Thread-2 create->示例名称14522
Thread-1 use—>示例名称14522
…
Thread-2 create->示例名称45523
Thread-1 use—>示例名称45523
Thread-3 use—>示例名称45523
…
…
分析:在本例中,仅仅是添加了一个生产者线程和一个消费者线程,运行结果却出现了特殊的运行情况。从部分运行结果中,可以看出存在生产一次、消费两次
与生产两次、消费一次
的异常情况。简单来说,是由于从线程池中通过notify
被唤醒的冻结线程并未对本例中的标记flag
进行判断造成的。
解决方案:将本例中对flag
的判断由条件结构改为循环结构,则每次被唤醒的线程都将再次进行判断,避免多次生产或消费。被修改的代码片段如下。
代码片段:
//写入数据的方法
public void setData(String name){
synchronized(this){
//改动:将if改为while
while(flag){
try{
this.wait();
}catch(Exception e){}
}
this.name=name+count++; //将计数变量count加入name,便于后续区别
System.out.println(Thread.currentThread().getName()+" create->"+this.name);
flag=true;
this.notify();
}
}
//读取数据的操作
public void getData(){
synchronized(this){
//改动:将if改为while
while(!flag){
try{
this.wait();
}catch(Exception e){}
}
System.out.println(Thread.currentThread().getName()+" use--->"+name);
flag=false;
this.notify();
}
}
运行结果:
Thread-0 create->示例名称1
Thread-3 use—>示例名称1
Thread-2 create->示例名称2
Thread-1 use—>示例名称2
Thread-2 create->示例名称3
Thread-1 use—>示例名称3
Thread-2 create->示例名称4
Thread-1 use—>示例名称4
…..
解决方案存在的不足:由于程序中每次只会通过notify
唤醒一个冻结中的线程,程序可能进入线程全部冻结状态,程序运行停止
优化解决方案:将notify
改为notifyAll
,每次将全部冻结中的线程唤醒。随后通过循环中的flag
标记判断,令不需要唤醒的线程休眠。这样处理后就不会出现线程全部冻结的状态
代码片段:
//写入数据的方法
public void setData(String name){
synchronized(this){
while(flag){
try{
this.wait();
}catch(Exception e){}
}
this.name=name+count++; //将计数变量count加入name,便于后续区别
System.out.println(Thread.currentThread().getName()+" create->"+this.name);
flag=true;
this.notifyAll();//改动:将notify修改为notifyAll
}
}
//读取数据的操作
public void getData(){
synchronized(this){
while(!flag){
try{
this.wait();
}catch(Exception e){}
}
System.out.println(Thread.currentThread().getName()+" use--->"+name);
flag=false;
this.notifyAll();//改动:将notify修改为notifyAll
}
}
总结:当出现多个生产者或消费者同时执行统一操作时,必须将对标记(本例中为flag)的if判断改为while循环,并使用notifyAll执行唤醒操作(以上代码使用notify只能用于生产者消费者仅有一个的情况)
问:对于多个生产者和消费者,为什么要定义while判断标记?
答:让运行中被唤醒的线程再进行一次标记判断
问:为什么要使用notifyAll?
答:因为需要唤醒对方线程;因为只用notify,容易出现只唤醒本方线程,导致程序中的所有线程都处于等待状态
3.Lock与Condition
1.概述
JDK1.5之后,增添了显式的锁机制。Lock
替代了synchronized
方法和语句的使用,Condition
替代了Object
监视器方法的使用。
新特性:一个锁上可以有多个Condition
对象(通过Lock
对象调用newCondition
方法)
使用方式:
Lock
接口实现类:ReentrantLock
(执行相应的上锁等操作)
Conditon
接口:执行相应的线程等待等操作
说明:
- 必须将释放锁的操作放入
finally
中,这样锁的释放操作一定执行。 - 若方法中需要进行锁
lock()
操作,则该异常操作不通过catch
处理,而在该方法后通过throws
抛出异常
2.常用方法
Lock:
void lock()
获取锁void unlock()
释放锁
Condition:
void await()
造成当前线程在接到信号或被中断之前一直处于等待状态。void signal()
唤醒一个等待线程。void signalAll()
唤醒所有等待线程。
3.示例代码
注:本例即对上例进行部分改写,其余部分不变
//数据存放类-资源类
//数据存放类-资源类
class Resources{
private boolean flag=false;//判断数据写入或者读取的标记
private int count=1;//记录生成的资源的编号
private String name;
private Lock lock=new ReentrantLock();//锁
private Condition proCondition=lock.newCondition();//对应生产者
private Condition cusCondition=lock.newCondition();//对应消费者
//写入数据的方法
public void setData(String name) throws InterruptedException{
lock.lock();//上锁
try{
while(flag)
proCondition.await();
this.name=name+count++; //将计数变量count加入name,便于后续区别
System.out.println(Thread.currentThread().getName()+" create->"+this.name);
flag=true;
cusCondition.signal();
}
finally{
lock.unlock();//解锁
}
}
//读取数据的操作
public void getData() throws InterruptedException{
lock.lock();//上锁
try{
while(!flag)
cusCondition.await();
System.out.println(Thread.currentThread().getName()+" use--->"+name);
flag=false;
proCondition.signal();
}
finally{
lock.unlock();//解锁
}
}
}
运行结果:
Thread-0 create->示例名称31091
Thread-1 use—>示例名称31091
Thread-2 create->示例名称31092
Thread-3 use—>示例名称31092
Thread-0 create->示例名称31093
Thread-1 use—>示例名称31093
Thread-2 create->示例名称31094
Thread-3 use—>示例名称31094
Thread-0 create->示例名称31095
Thread-1 use—>示例名称31095
…..
说明:在本例中,定义两个Condition
对象分别对应生产者和消费者线程,分别执行await
和signal
操作。这一方式相当于在线程池中开辟两个独立的空间存放生产者线程和消费者线程,独立进行冻结唤醒操作。
4.Lock总结
- 将同步
synchronized
替换成现实的Lock
操作,更加直观 - 将Object中的
wait,notify,notifyAll
替换为Condition
对象的相应方法 - 在一定程度上避免死锁现象的发生
- 该实例中,实现了本方只执行唤醒对方操作,而不再同时唤醒双方对象,有利于节省系统资源
4.停止线程
前提:线程类的run
方法中一般都会写出循环语句(因为这就是使用线程的原因)
停止原理(唯一的方式):
run
方法结束。开启多线程运行,运行代码通常是循环结构。因此只要控循环停止条件,就可以让run
方法结束,也就是线程结束
示例代码:
package com.dream.home_1104.thread;
//本例演示停止线程的方法
class StopThread implements Runnable{
private boolean flag=true;
private int count=0;//统计运行次数
public void run(){
while(flag){
System.out.println("本线程大人还在死命地运行着..么么哒");
count++;
}
}
//自定义停止线程的方法
public void stopThread(){
this.flag=false;
System.out.println("线程结束...");
}
//获取运行次数
public int getCount(){
return this.count;
}
}
public class StopThreadManager {
public static void main(String[] args) {
StopThread thread=new StopThread();
new Thread(thread).start();
//测试线程在运行五次后结束
while(true){
if(thread.getCount()==4){
thread.stopThread();
break;
}
}
}
}
运行结果:
本线程大人还在死命地运行着..么么哒
本线程大人还在死命地运行着..么么哒
本线程大人还在死命地运行着..么么哒
本线程大人还在死命地运行着..么么哒
本线程大人还在死命地运行着..么么哒
线程结束…
特殊情况:当线程处于冻结状态,它就不会读取到改变后的标记,那么线程就不会结束。
异常引例(部分代码):
public synchronized void run(){
while(flag){
try{
System.out.println("本线程休眠...");
wait();
}
catch(InterruptedException e){
System.out.println("本线程发生中断异常");
}
System.out.println("本线程大人还在死命地运行着..么么哒");
count++;
}
}
运行结果:
本线程休眠…
说明:在本例中,自定义线程对象在run
方法中进入冻结状态,即使通过主线程中的语句改变了flag
,线程也不会停止。程序进入挂起状态,但程序并未结束
解决方案:当没有指定的方式让冻结的线程恢复到运行状态时,需要用interrupt
方法对冻结状态进行清除,强制让冻结线程恢复到运行状态来,然后可以操作标记让线程结束。相关示例代码如下。
示例代码:
package com.dream.home_1104.thread;
//本例演示停止线程的方法
class StopThread implements Runnable{
private boolean flag=true;
private int count=0;//统计运行次数
public synchronized void run(){
while(flag){
try{
System.out.println("本线程休眠...");
wait();
}
catch(InterruptedException e){
System.out.println("本线程发生中断异常");
}
System.out.println("本线程大人还在死命地运行着..么么哒");
count++;
}
}
//自定义停止线程的方法
public void stopThread(){
this.flag=false;
System.out.println("线程结束...");
}
//获取运行次数
public int getCount(){
return this.count;
}
}
public class StopThreadManager {
public static void main(String[] args) {
StopThread thread=new StopThread();
Thread t=new Thread(thread);
t.start();
//测试线程在运行五次后结束
while(true){
t.interrupt();//将冻结中的线程强行恢复到正常运行状态
if(thread.getCount()==4){
thread.stopThread();
break;
}
}
}
}
运行结果:
本线程休眠…
本线程发生中断异常
本线程大人还在死命地运行着..么么哒
本线程休眠…
本线程发生中断异常
本线程大人还在死命地运行着..么么哒
本线程休眠…
本线程发生中断异常
本线程大人还在死命地运行着..么么哒
本线程休眠…
本线程发生中断异常
本线程大人还在死命地运行着..么么哒
本线程休眠…
本线程发生中断异常
本线程大人还在死命地运行着..么么哒
线程结束…
5.守护线程( 后台线程)
相关方法:
void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
该方法由线程对象
说明:
- 该方法将调用线程标记为守护线程或用户线程
- 当正在运行的线程都是守护线程时,Java 虚拟机退出
- 该方法必须在启动线程前调用
- 守护线程开启后会与前台线程抢夺CPU资源,但当前台线程全部结束后,后台线程自动结束
应用场景:某些线程依赖其他线程而存在时可以这样使用。如:输入程序结束后,读取线程也应该结束
6.join方法
相关方法:
void join()
等待该线程终止。
说明:调用该方法的线程强行抢夺CPU执行权,当前线程自动进入冻结状态。只有等到这一线程结束后,当前线程才会从冻结状态中恢复过来。
应用场景:某一线程需要临时加入线程执行或需要优先执行完成。
7.优先级与yield方法
相关方法:
void setPriority(int newPriority)
更改线程的优先级。static void yield()
暂停当前正在执行的线程对象,并执行其他线程。(临时释放执行权)String toString()
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
说明:
- ThreadGroup 线程组:若A开启B,则B属于A线程组;例:由main()开启的线程就属于main线程组
- 优先级(1-10):线程优先级越高,被执行的可能性越高。但一般1,5,10才有显著影响
- 优先级高低中对应为:
MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY
- 所有线程(包括main)默认优先级为5
8.实际开发中常见线程封装示例
思想:对于程序中需要同时执行的代码块(例如多个循环语句块),可以单独封装出多个线程。一般通过匿名内部类的方式实现,具体的有两种实现方式。示例代码如下。
示例代码:
package com.dream.home_1104.thread;
//本例通过三个循环结构演示实际开发中的多线程操作封装方式
public class RealThreadManager {
public static void main(String[] args) {
//第一个为正常的循环结构
for(int i=0;i<300;i++){
System.out.println(Thread.currentThread().getName()+"...第一个循环执行次数:-->"+i);
}
//第二个循环通过匿名内部类实现
new Thread(){
public void run(){
for(int i=0;i<300;i++){
System.out.println(Thread.currentThread().getName()+"...第二次循环执行次数:-->"+i);
}
}
}.start();
//第三个循环通过接口方式实现
Runnable r=new Runnable(){
public void run(){
for(int i=0;i<300;i++){
System.out.println(Thread.currentThread().getName()+"...第三次循环执行次数:-->"+i);
}
}
};
new Thread(r).start();
}
}
运行结果:
….
Thread-1…第三次循环执行次数:–>2
Thread-1…第三次循环执行次数:–>3
Thread-0…第二次循环执行次数:–>54
Thread-1…第三次循环执行次数:–>4
Thread-0…第二次循环执行次数:–>55
Thread-1…第三次循环执行次数:–>5
Thread-1…第三次循环执行次数:–>6
….
说明:本例中存在三个线程,分别进行循环输出。实际开发中可任选其中一种线程封装方式。