线程的同步与死锁
在多线程中,同步与死锁概念很重要,在本章中必须了解以下几点:
1)哪里需要同步。
2)如何实现同步,了解代码即可。
3)及实现同步后有哪些副作用。
代码并不要求可以完整编写,但是概念必须清楚。
具体内容
1.1问题引出
以买火车票为例,不管多少地方可以买火车票,最终一趟列车的车票数量是固定的,如果把各个售票点理解为线程的话,则所有线程应该共同拥有同一份票数。
package Thread1; class MyThread implements Runnable{ private int ticket = 5 ; // 假设一共有5张票 public void run(){ for(int i=0;i<100;i++){ if(ticket>0){ // 还有票 System.out.println("卖票:ticket = " + ticket-- ); } } } }; public class demo1{ public static void main(String args[]){ MyThread mt = new MyThread() ; // 定义线程对象 Thread t1 = new Thread(mt) ; // 定义Thread对象 Thread t2 = new Thread(mt) ; // 定义Thread对象 Thread t3 = new Thread(mt) ; // 定义Thread对象 t1.start() ; t2.start() ; t3.start() ; } };
运行结果:
卖票:ticket = 5 卖票:ticket = 3 卖票:ticket = 2 卖票:ticket = 1 卖票:ticket = 4
因为网络操作可能会有延迟,所以这里增加一个延迟操作sleep();修改如下;
package Thread1; class MyThread implements Runnable{ private int ticket = 5 ; // 假设一共有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 demo1{ public static void main(String args[]){ MyThread mt = new MyThread() ; // 定义线程对象 Thread t1 = new Thread(mt) ; // 定义Thread对象 Thread t2 = new Thread(mt) ; // 定义Thread对象 Thread t3 = new Thread(mt) ; // 定义Thread对象 t1.start() ; t2.start() ; t3.start() ; } };
运行结果:
卖票:ticket = 5 卖票:ticket = 3 卖票:ticket = 4 卖票:ticket = 2 卖票:ticket = 1 卖票:ticket = 0 卖票:ticket = -1
此时结果发现卖出的票数成负数,程序代码出现问题。
例如,在线程2取出数据,但是还没有减一送回去之前,线程n也取出了这个数据,也执行了减一操作,这样这个数据同时执行了两次减一操作,导致数据为负。
问题解决
要解决这种问题就要使用同步,所谓同步就是指多个操作在同一时间段内只能有一个线程进行,其他线程要等这个线程完成之后才能继续执行。
2. 使用同步解决问题。
要想解决资源共享的同步操作问题,可以使用同步代码块及同步方法两种方式完成。
2.1同步代码块
之前已经介绍过代码块分四种:
1)普通代码块,直接定义在方法中。
2)构造块,是直接定义在类中,优先于构造方法执行,重复调用。
3)静态块,是使用static关键字声明的,优先于构造块执行,只执行一次。
4)同步代码块,使用synchronized关键字声明的代码块,叫做同步代码块。
syschronized关键字相关知识:
java线程同步: synchronized详解(转)
同步代码块的格式:
synchronized(同步对象){ 需要同步的代码。 }
同步的时候必须指明同步的对象,一般情况下,会将当前对象作为同步对象,使用this表示。
package Thread1; class MyThread implements Runnable{ private int ticket = 5 ; // 假设一共有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 demo1{ public static void main(String args[]){ MyThread mt = new MyThread() ; // 定义线程对象 Thread t1 = new Thread(mt) ; // 定义Thread对象 Thread t2 = new Thread(mt) ; // 定义Thread对象 Thread t3 = new Thread(mt) ; // 定义Thread对象 t1.start() ; t2.start() ; t3.start() ; } };
运行结果:
卖票:ticket = 5 卖票:ticket = 4 卖票:ticket = 3 卖票:ticket = 2 卖票:ticket = 1
从运行结果可以发现,程序加入了同步操作,所以不会产生负数的情况,但是程序的执行效率明显降低很多。
2.2同步方法
使用了synchronized声明的方法为同步方法。
同步方法定义格式:
synchronized 方法返回值类型 方法名称(参数列表) { }
同步方法解决如下:
package Thread1; class MyThread implements Runnable{ private int ticket = 5 ; // 假设一共有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 demo1{ public static void main(String args[]){ MyThread mt = new MyThread() ; // 定义线程对象 Thread t1 = new Thread(mt) ; // 定义Thread对象 Thread t2 = new Thread(mt) ; // 定义Thread对象 Thread t3 = new Thread(mt) ; // 定义Thread对象 t1.start() ; t2.start() ; t3.start() ; } };
运行结果:
卖票:ticket = 5 卖票:ticket = 4 卖票:ticket = 3 卖票:ticket = 2 卖票:ticket = 1
3.3死锁
资源共享时候需要进行同步操作。
程序中过多的同步会产生死锁。
死锁一般情况下就是表示互相等待,是在程序运行时候出现的一种文体。
下面通过一个代码来模拟一下死锁的概念,本代码读者只需了解其效果即可,对于代码不必深入了解。
package Thread1; class Zhangsan{ // 定义张三类 public void say(){ System.out.println("张三对李四说:“你给我画,我就把书给你。”") ; } public void get(){ System.out.println("张三得到画了。") ; } }; class Lisi{ // 定义李四类 public void say(){ System.out.println("李四对张三说:“你给我书,我就把画给你”") ; } public void get(){ System.out.println("李四得到书了。") ; } }; public class demo1 implements Runnable{ private static Zhangsan zs = new Zhangsan() ; // 实例化static型对象,保证两个操作的对象是同一个,这样syschronized(对象)才能起作用,能够起同步的作用。 private static Lisi ls = new Lisi() ; // 实例化static型对象 private boolean flag = false ; // 声明标志位,判断那个先说话 public void run(){ // 覆写run()方法 if(flag){ synchronized(zs){ // 同步张三,占用了zs对象。 zs.say() ; try{ Thread.sleep(500) ; }catch(InterruptedException e){ e.printStackTrace() ; } synchronized(ls){ //因为ls对象已经被t2占用了,所以同步ls的时候被阻塞,所以在等待 zs.get() ; } } }else{ synchronized(ls){ //同步李四,占用了ls对象。 ls.say() ; try{ Thread.sleep(500) ; }catch(InterruptedException e){ e.printStackTrace() ; } synchronized(zs){ //因为zs已经被t1占用了,所以同步zs的时候被阻塞,所以等待 ls.get() ; } } } } public static void main(String args[]){ demo1 t1 = new demo1() ; // 控制张三 demo1 t2 = new demo1() ; // 控制李四 t1.flag = true ; t2.flag = false ; Thread thA = new Thread(t1) ; Thread thB = new Thread(t2) ; thA.start() ; thB.start() ; } };
运行结果:
张三对李四说:“你给我画,我就把书给你。”
李四对张三说:“你给我书,我就把画给你”
此时双方说完话都在等待对方回应,此时处于停滞状态,都在互相等待着对方回答。