死锁问题的出现和解决
同步弊端:
- 效率低
- 如果出现了同步嵌套,就容易产生死锁问题
死锁问题及其代码重现
死锁:
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象
举例:
中国人、美国人吃饭案例
正常情况:
中国人:筷子两支
美国人:刀和叉
现在:
中国人:筷子一支,刀一把
美国人:筷子一支,叉一把
产生死锁问题:
中国人拿着刀的同时等着美国人把另一只筷子给他,美国人拿着一支筷子的同时等着中国人把刀给他
同步代码块的嵌套案例–重现死锁现象
//定义锁对象
public class MyLock {
public static final Object objA = new Object();
public static final Object objB = new Object();
}
//定义Thread类的run()方法
public class DeadLock extends Thread {
//定义一个flag变量,用来标记此时持有的是哪个锁
private boolean flag;
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
}else{
synchronized (MyLock.objB) {
System.out.println("if objB");
synchronized (MyLock.objA) {
System.out.println("if objA");
}
}
}
}
}
//测试用例
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock dl1 = new DeadLock(true);
DeadLock dl2 = new DeadLock(false);
dl1.start();
dl2.start();
}
}
- 两个线程一旦启动,则将陷入互相等待的循环之中,就成为了死锁
死锁问题的解决
解决方式1:线程间通信–通过构造方法共享数据
有一个Student类作为资源(锁)
public class Student {
/*
* 默认权限修饰符,是为了让同一个包下的其他类也能访问到其成员变量
*/
String name;
int age;
boolean flag; //默认情况下false:没有数据,如果是true:说明有数据
}
模拟生产者-消费者模型:
创建两个线程对象,一个是设置 Student 属性的线程 SetThread ,另一个是获取 Student 属性的线程 GetThread
public class SetThread implements Runnable {
private Student st ;
private int x=0;
public SetThread(Student stu) {
this.st = stu;
}
@Override
public void run() {
while(true){
synchronized (st) {
//判断有没有数据
if(st.flag){
try {
st.wait(); //如果有数据就等待消费者消费
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(x%2 == 0){
st.name = "紫霞仙子";
st.age=27;
}else{
st.name = "刘意";
st.age=30;
}
x++;
//修改标记
st.flag = true;
//唤醒线程
st.notify();
}
}
}
}
public class GetThread implements Runnable {
private Student st ;
public GetThread(Student stu) {
this.st = stu;
}
@Override
public void run() {
while(true){
synchronized (st) {//加锁,防止出现姓名--年龄不匹配的问题
if(!st.flag){ //没有数据等待
try {
st.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(st.name+"--------"+st.age);
//修改标记
st.flag = true;
//唤醒线程
st.notify();
}
}
}
}
测试用例:
public class StudentDemo {
public static void main(String[] args) {
Student stu = new Student();
SetThread st = new SetThread(stu);
GetThread gt = new GetThread(stu);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
运行结果:
...
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
刘意--------30
刘意--------30
刘意--------30
刘意--------30
刘意--------30
...
- 发现结果并不是那么理想,同一组数据出现多次
出现这种状况的原因:
CPU的一点点时间片执行时间,足以让一个线程中的代码执行多次
解决方式2:改进1:通过构造方法共享数据,并使用 (等待–唤醒) 机制实现线程间通信
存在问题:
等待唤醒机制
A:生产者—先看看是否有数据,有就等待;没有就生产,然后通知消费者进行消费
B:消费者—先看看是否有数据,有就消费,消费完通知生产者继续生产;没有就等待
Object类中提供了3个方法:
wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程
问:wait和notify方法为什么不定义在Thread类上而是Object类上呢?
这些方法的调用必须通过锁对象来调用,而我们刚才使用的锁对象是任意对象
所以,这些方法必须定义在Object类中。
解决方式3:改进2:私有化锁对象的成员变量,自己提供get和set方法供外界调用
方式2中,同样也存在不适用的情况:一旦Student类中的成员变量被private修饰符私有化,那么其他Thread类就无法直接访问到其成员变量了。
解决方法:
Student自己实现get和set方法,外部Thread类直接调用其方法进行数据交换即可。
public class Student {
/*
* 一旦权限修饰符改为private,其他类就无法访问到
* 改进:
* 将set和get方法写在 Student类中
*/
String name;
int age;
boolean flag; //默认情况下没有数据,如果是true,说明有数据
//student的set和get过程(包括同步)都是由自己来操作,外界调用就行
public synchronized void set(String name, int age){
//如果有数据就等待
if(this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//设置数据
this.name = name;
this.age = age;
//修改标记并唤醒线程
this.flag = true;
this.notify();
}
public synchronized void get(){
//如果没有数据则等待
if(!this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//获取数据
System.out.println(this.name +"---"+this.age);
//修改标记并唤醒线程
this.flag = false;
this.notify();
}
}
/**
* 生产者类
* @author llj
*
*/
public class SetThread implements Runnable {
private Student st ;
private int x=0;
public SetThread(Student stu) {
this.st = stu;
}
@Override
public void run() {
while(true){
if(x%2 == 0){
st.set("紫霞仙子", 27) ;
}else{
st.set("刘意", 30) ;
}
x++;
}
}
}
/**
* 消费者类
* @author llj
*
*/
public class GetThread implements Runnable {
private Student st ;
public GetThread(Student stu) {
this.st = stu;
}
@Override
public void run() {
while(true){
st.get();
}
}
}
测试用例:
public class StudentDemo {
public static void main(String[] args) {
Student stu = new Student();
//将student引用作为参数传递实现线程间通信
SetThread st = new SetThread(stu);
GetThread gt = new GetThread(stu);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
- 运行结果:
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
...
A.姓名和年龄出现不匹配
原因:
线程运行的随机性
解决方法:
加锁,在一个线程对共享资源操作的时候,其他线程只能等待该锁释放
* 注意: A: 不同种类的线程都要加锁
B: 不同种类的线程加的锁都必须是同一把
B.执行结果重复出现
原因:
CPU的一点点时间片执行时间,足以让一个线程中的代码执行多次
CPU执行程序时的随机性
解决方法:
利用 等待 – 唤醒 机制,让两个线程交替执行