Thread - 同步
问题引出
我们现在来通过Runnable接口实现多线程,产生3个线程对象,模拟卖票的场景!
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
if (ticket > 0) {
try {
Thread.sleep(300); // 加入延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = " + ticket--);
}
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我们执行下这段代码,结果如下:
卖票:ticket = 5
卖票:ticket = 4
卖票:ticket = 5
卖票:ticket = 3
卖票:ticket = 2
卖票:ticket = 2
卖票:ticket = 1
卖票:ticket = 0
卖票:ticket = -1 // 票数竟然还能负数?
为什么会出现“负数”的情况:在上面的操作中,我们可以发现,因为加入了“延迟操作”一个线程很有可能在还没对票数进行减操作之前,其他线程就已经将票数减少了,这样就会出现票数为负的情况。
有没有方法解决?肯定是有的!想解决这样的问题,就必须使用同步!所谓同步,就是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行。
解决问题
解决资源共享的同步操作,有两种方法:同步代码块 和 同步方法 !
同步代码块
所谓代码块就是指使用“{}”括起来的一段代码,如果在代码块上加上synchronized关键字,则此代码块就成为同步代码块。
【同步代码块 - 格式】
synchronized(同步对象) {
需要同步的代码 ;
}
我们对代码进行修改:
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) { // 加入同步操作
if (ticket > 0) {
try {
Thread.sleep(300); // 加入延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我们重新执行下这段代码,结果如下:
卖票:ticket = 5
卖票:ticket = 4
卖票:ticket = 3
卖票:ticket = 2
卖票:ticket = 1
同步方法
除了可以将需要的代码设置成同步代码块外,也可以使用synchronized关键字将一个方法声明成同步方法。
【同步方法 - 格式】
synchronized 方法返回值 方法名称(参数列表) {
}
我们采用同步方法对代码进行修改:
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
this.sale(); // 调用同步方法
}
}
public synchronized void sale() { // 声明同步方法
if (ticket > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = " + ticket--);
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我们重新执行下这段代码,结果如下:
卖票:ticket = 5
卖票:ticket = 4
卖票:ticket = 3
卖票:ticket = 2
卖票:ticket = 1
从以上程序的运行结果可以发现,此代码完成了与之前同步代码块同样的功能。
总结
多个线程共享同一资源时需要进行同步,以保证资源操作的完整性。
Thread - 死锁
通过上面的例子,我们发现,同步还是很有好处的,它可以保证资源共享操作的正确性,但是过多的同步也会产生问题,这就是我们接下来要讨论“死锁”问题!
什么是死锁?
多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题 -- 死锁。
在编写多线程的时候,必须要注意资源的使用问题,如果两个或多个线程分别拥有不同的资源,而同时又需要对方释放资源才能继续运行时,就会发生死锁。
简单来说:死锁就是当一个或多个进程都在等待系统资源,而资源本身又被占用时,所产生的一种状态。
造成死锁的原因
多个线程竞争共享资源,由于资源被占用,资源不足或进程推进顺序不当等原因造成线程处于永久阻塞状态,从而引发死锁。
造成死锁的四个条件
1、互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2、请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3、不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4、循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
问题引出
现在张三想要李四的画,李四想要张三的书,于是产生了以下对话:
张三对李四说:“把你的画给我,我就给你书”
李四对张三说:“把你的书给我,我就给你画”
此时,张山在等着李四的答复,李四也在等着张三的答复,那么这样下去的结果就是,两个人都在等待,但是都没有结果,这就是“死锁”!
从线程角度来说,所谓死锁就是指两个线程都在等待彼此先完成,造成了程序的停滞,一般程序的死锁都是在程序运行时出现,比如我们通过一个代码范例来看看发生死锁的场景。
class Zhangsan {
public void say() {
System.out.println("Zhangsan say: give me your painting, i will give you my book!");
}
public void get() {
System.out.println("Zhangsan got Lisi's painting!");
}
}
class Lisi {
public void say() {
System.out.println("Lisi say: give me your book, i will give you my painting!");
}
public void get() {
System.out.println("Lisi got Zhangsan's book!");
}
}
public class ThreadDeadLock implements Runnable {
private static Zhangsan zs = new Zhangsan(); // 实例化static型对象,数据共享
private static Lisi ls = new Lisi(); // 实例化static型对象,数据共享
private boolean flag = false; // 声明标记,用于判断哪个对象先执行
public void run() {
if (flag) { // 判断标志位,flag为true,Zhangsan先执行
synchronized (zs) { // 同步第一个对象
zs.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ls) { // 同步第二个对象
zs.get();
}
}
} else { // Lisi先执行
synchronized (ls) { // 同步第二个对象
ls.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (zs) { // 同步第一个对象
ls.get();
}
}
}
}
public static void main(String[] args) {
ThreadDeadLock t1 = new ThreadDeadLock();
ThreadDeadLock t2 = new ThreadDeadLock();
t1.flag = true;
t2.flag = false;
Thread thA = new Thread(t1);
Thread thB = new Thread(t2);
thA.start();
thB.start();
}
}
我们执行下这段代码,结果如下:
Zhangsan say: give me your painting, i will give you my book!
Lisi say: give me your book, i will give you my painting!
从程序的运行结果中可以看出,两个线程都在彼此等待着对方的执行完成,这样,程序就无法向下继续执行,从而造成了死锁的现象。
解决问题
要预防和避免死锁的发生,只需将上面所讲到的4个条件破坏掉其中之一即可。
如上面的代码当中,有四个同步代码块,只需要将其中一个同步代码块去掉,即可解决死锁问题,一般而言破坏“循环等待”这个条件是解决死锁最有效的方法。