Java 多线程编程4---同步与死锁
一个多线程的程序如果是通过实现Runable接口实现的,实现类中的属性可以被多个线程共享,这样就造成一个问题,如果这个多线程程序操作同一资源时就有可能出现资源同步的问题。例如之前的买票程序,如果多个线程同时操作时,就有可能出现卖出的票为负数的问题。
问题的引出
下面通过Runable接口实现多线程,并产生3个线程对象,同时卖掉这5张票。
实例:有问题的买票程序
package my.thread.sync;
class MyThread implements Runnable
{
//有5张票
private int ticket = 5;
public void run()
{
for (int i = 0; i < 100; i++)
{
if (ticket > 0)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--);
}
}
}
}
public class SyncDemo01
{
public static void main(String args[])
{
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"售票员A");
Thread t2 = new Thread(mt,"售票员B");
Thread t3 = new Thread(mt,"售票员C");
//启动3个线程进行买票
t1.start();
t2.start();
t3.start();
}
}
一种运行结果:
售票员B卖票掉一张,余票5
售票员C卖票掉一张,余票4
售票员A卖票掉一张,余票4
售票员B卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员A卖票掉一张,余票3
售票员B卖票掉一张,余票1
售票员C卖票掉一张,余票0
售票员A卖票掉一张,余票-1
从运行结果中看一共有5张票,但是却卖掉了9次,而且结果出现了余票为负数的情况。下面来分析查产生这样我问题的原因。
上面卖票的操作步骤:
(1)判断票数是否大于0,如果票数大于0,则表示还有票可以卖。
(2)如果可以卖票,就把余票减一
但是,我们在上面的代码中,加入了延迟操作,那么一个线程有可能还没来得及把票数减1,其他线程就已经把票数减1了,这样就有可能出现票数为负数的情况。
使用同步解决问题
这里有两种方式可以结局资源的同步问题,一种是使用同步代码块完成,一种是使用同步方法完成。
使用同步代码块
所谓代码块就是使用{}
括起来的一段代码,根据其位置和声明的不同,可以分为普通代码块,构造块,静态代码块3中,如果在代码块前面加上synchronized关键字,则称该代码块为同步代码块。同步代码块的格式如下
synchronized(同步对象)
{
需要同步的代码;
}
从上面可以同步代码块的格式,可以看出,在使用同步代码块的时候必须指定一个需要同步的对象,一般都将当前对象this设置成同步对象。
实例:使用同步代码块解决上述买票问题
使用同步代码块把上面买票的if语句包裹起来即可,完整代码如下。
package my.thread.sync;
class MyThread implements Runnable
{
//有5张票
private int ticket = 5;
public void run()
{
for (int i = 0; i < 100; i++)
{
//使用同步代码块,同步对象设置为当前对象
synchronized (this)
{
if (ticket > 0)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--);
}
}
}
}
}
public class SyncDemo01
{
public static void main(String args[])
{
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"售票员A");
Thread t2 = new Thread(mt,"售票员B");
Thread t3 = new Thread(mt,"售票员C");
//启动3个线程进行买票
t1.start();
t2.start();
t3.start();
}
}
运行结果:
售票员A卖票掉一张,余票5
售票员A卖票掉一张,余票4
售票员A卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员C卖票掉一张,余票1
多次运行,不管你怎么运行,结果都是只卖掉5张票。
上面的代码将判断余票if (ticket > 0)
和修改票数ticket--
这两个操作进行了同步,所以不会出现多次卖票的情况。
这里一定要注意,同步代码块中一定要包括取值和修改值两个操作,如果单独同步一个操作,将不是同步,错误的代码如下:
package my.thread.sync;
class MyThread implements Runnable
{
// 有5张票
private int ticket = 5;
public void run()
{
for (int i = 0; i < 100; i++)
{
// 使用同步代码块,同步对象设置为当前对象
if (ticket > 0)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
synchronized (this)
{
System.out.println(Thread.currentThread().getName()
+ "卖票掉一张,余票" + ticket--);
}
}
}
}
}
public class SyncDemo01
{
public static void main(String args[])
{
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "售票员A");
Thread t2 = new Thread(mt, "售票员B");
Thread t3 = new Thread(mt, "售票员C");
// 启动3个线程进行买票
t1.start();
t2.start();
t3.start();
}
}
运行结果:
售票员C卖票掉一张,余票5
售票员B卖票掉一张,余票4
售票员A卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员B卖票掉一张,余票1
售票员A卖票掉一张,余票0
售票员C卖票掉一张,余票-1
上面的同步代码块只同步了对票数减1
的操作,而不同步票数判断
的操作,所以达不到同步的效果。使用时一定要在把判断操作
和修改操作
成对放入到同步代码块中个,不然达不到同步的效果。
使用同步方法
除了可以将需要的代码设置成同步代码块之外,还可使用synchronized
关键字将一个方法声明成同步方法。声明同步方法的格式如下。
synchronized 方法返回值 方法名称(参数列表)
{
//方法体
}
实例:使用同步方法实现卖票的同步操作
package my.thread.sync;
class MyThread1 implements Runnable
{
// 有5张票
private int ticket = 5;
public void run()
{
for (int i = 0; i < 100; i++)
{
saleTicket();
}
}
public synchronized void saleTicket()
{
if (ticket > 0)
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + "卖票掉一张,余票" + ticket--);
}
}
}
public class SyncDemo2
{
MyThread1 mt=new MyThread1();
Thread th1=new Thread(mt,"售票员A");
Thread th2=new Thread(mt,"售票员B");
Thread th3=new Thread(mt,"售票员C");
}
一次运行结果:
售票员A卖票掉一张,余票5
售票员C卖票掉一张,余票4
售票员C卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员B卖票掉一张,余票1
从程序的运行结果中可以发现,上面的代码完成了与之前同步代码块同样的功能。
使用同步代码块还是使用同步方法
同步代码块,同步方法,静态同步方法使用的锁
- 同步代码块使用的锁是任意的对象。
- 同步方法使用的锁是当前对象this
- 使用static关键字修饰的静态同步方法使用的是该类所在的字节码文件对象,格式为类名.class。
同步代码块和同步方法的区别
- 同步方法默认用this或者当前类class对象作为锁;
- 同步代码块可以选择加锁的对象;
- 同步代码块比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的关键代码而不是整个方法;
使用同步代码块还是使用同步方法比较好
同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点。
而且同步是一个开销很大的操作,因此尽量减小同步的区域。所以通常没有必要同步整个方法,使用同步代码块同发生同步问题的关键代码即可。
所以考虑性能,最好使用同步代码块从而减少锁定范围以提高并发效率。
死锁
同步可以保证资源共享操作的正确性,但是过多同步也会产生问题,例如会产生死锁。所谓死锁,就是指两个线程都在等待彼此先完成,造成程序的卡着无法往下运行。一般死锁都是在程序运行时出现的,发生在两个线程相互持有对方正在等待的东西(实际是两个线程共享的东西)。只要有两个线程和两个对象就可能产生死锁。
实例:死锁例子
package my.thread.deadlock;
public class DeadLock implements Runnable
{
public String name;
public boolean flag;
// 静态对象是类的所有对象共享的
private static Object object1 = new Object();
private static Object object2 = new Object();
@Override
public void run()
{
System.out.println("flag=" + flag);
if (flag)
{
// 同步代码块1
synchronized (object1)
{
System.out.println(Thread.currentThread().getName()
+ "成功持有object1对象的锁,成功进入同步代码块1中");
try
{
System.out.println(
Thread.currentThread().getName() + "睡眠500毫秒");
Thread.sleep(500);
} catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "睡眠结束,正在获取object2对象的锁...");
// 同步代码块2
synchronized (object2)
{
System.out.println(Thread.currentThread().getName()
+ "成功持有object2对象的锁,成功进入同步代码块2中");
System.out.println("1");
}
}
}
if (!flag)
{
// 同步代码块3
synchronized (object2)
{
System.out.println(Thread.currentThread().getName()
+ "成功持有object2对象的锁,成功进入同步代码块3");
try
{
System.out.println(
Thread.currentThread().getName() + "睡眠500毫秒");
Thread.sleep(500);
} catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "睡眠结束,正在获取object1对象的锁...");
// 同步代码块4
synchronized (object1)
{
System.out.println(Thread.currentThread().getName()
+ "成功获取object1对象的锁,成功进入同步代码块4中");
System.out.println("0");
}
}
}
}
public static void main(String[] args)
{
DeadLock A = new DeadLock();
DeadLock B = new DeadLock();
A.flag = true;
B.flag = false;
new Thread(A, "线程A").start();
new Thread(B, "线程B").start();
}
}
运行结果:
分析
- 线程A启动,由于A对象的flag为true,且此时
object1对象的锁
还没有任何被线程持有,所以线程A就马上持有object1对象的锁
,然后进入同步代码块1
中去执行里面的代码,然后线程A睡眠500毫秒。 - 然后线程B启动,由于B对象的flag为false,且此时
object2对象的锁
还没有被任何线程持有,所以线程B很愉快的持有object2对象的锁
,然后进入同步代码块3
中去执行里面的代码,然后线程B睡眠500毫秒。 - 线程A睡眠结束后,就需要进入
同步代码块2
中去执行,此时就需要持有object2对象的锁
,但是由于线程B还没走出同步代码块3
中,也就是说object2对象的锁还被线程B持有。只有等到线程B执行执行完毕同步代码块3
中的代码,线程B才会释放object2对象的锁。所以,此时线程A无法获取到object2对象的锁,线程A要等待线程B执行完同步代码块3中的所有代码,然后把object2的锁释给线程A。 - 线程B睡眠结束后,想要进入
同步代码块4
中去执行,此时就需要持有object1对象的锁
,但是此时线程A还在同步代码块中等待线程B释放object2给它,所以线程A没有执行完同步代码块1
中的内容,线程A将继续占有object1对象的锁。所以线程B需要等待线程A执行完同步块1中的所有代码,然后吧object1对象的锁释放给线程B. -
好的,现在的解说是,线程A等着线程B执行完毕释放object2对象的锁,而线程B也在等待线程A执行完毕释放object2对象的锁。线程A等待线程B,线程B等待线程A。线程A和线程B相互等待,这样造成了死锁。
产生死锁的四个必要条件
虽然进程(线程)在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
1.互斥条件
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程(线程)占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2.请求和保持条件
指进程(线程)已经持有至少一个资源,但又提出了新的资源请求,而该资源已被其它进程(线程)占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3.不可剥夺条件
指进程(线程)已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4.循环等待条件
指在发生死锁时,必然存在一个相互等待的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个被P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源,也就是若干进程之间形成一种头尾相接的循环等待资源关系。
分析上述的代码,使用使用了同步代码块,就满足了1.互斥条件,2.请求与和保持条件,3.不可剥夺条件。此时在使用两个相互嵌套的同步代码块,
第一个嵌套的同步代码块的锁对象由外到内的顺序是:object1,object2.
第二个嵌套的同步代码块的锁对象由外到内的顺序是:object2,object1.
进入第一个嵌套的同步代码块中的线程A必然持有object1对象的锁了,之后他想要持有object2对象的锁。
而进入第二个嵌套的同步代码块的线程B必然持有object2对象的锁了,之后他又想要持有object1对象的锁。
这样就造成了 线程A等待线程B,线程B等待线程A 这就产生死锁的第四个条件:循环等待,所以上面程序出现了死锁。
死锁的避免
上面四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
参考博客:https://blog.csdn.net/silence723/article/details/52036609
上一篇: java中死锁定位分析
下一篇: Java面试——死锁编码及定位分析
推荐阅读
-
Java多线程编程(2)--多线程编程的术语与概念
-
Java多线程编程(2)--多线程编程的术语与概念
-
C#多线程编程之使用ReaderWriterLock类实现多用户读与单用户写同步的方法
-
java多线程死锁实例,线程锁并不可怕 多线程Javathread编程活动
-
java多线程死锁实例,线程锁并不可怕 多线程Javathread编程活动
-
JAVA并发编程(三):同步的辅助类之闭锁(CountDownLatch)与循环屏障(CyclicBarrier)
-
总结java多线程之互斥与同步解决方案
-
Java并发编程之同步容器与并发容器详解
-
Java多线程与线程同步
-
史上最强多线程面试44题和答案:线程锁+线程池+线程同步等 Java编程语言架构多线程面试