Java基础-线程操作共享数据的安全问题
Java基础-线程操作共享数据的安全问题
作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
一.引发线程安全问题
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
1>.售票案例
假设某人一次性买了20张关于周杰伦的演唱会,原计划是请全部门去看演唱会的,但是由于老板的临时任务来袭,被迫需要把这20张票放在交给三个人转卖出去,请你模拟这个买票的过程。我们可以用代码来实现一下:
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note; 8 9 class Tickets implements Runnable{ 10 //定义出售的票源 11 private int ticket = 20; 12 @Override 13 public void run() { 14 while(true) { 15 //对于票数大于0才可以出售 16 if( ticket > 0 ) { 17 try { 18 Thread.sleep(10); 19 System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 } 25 } 26 } 27 28 29 public class ThreadDemo { 30 public static void main(String[] args) { 31 //创建Runnable接口实现了对象 32 Tickets t = new Tickets(); 33 //创建三个Thread类对象,传递Runnable接口实现类 34 Thread t1 = new Thread(t,"窗口1"); 35 Thread t2 = new Thread(t,"窗口2"); 36 Thread t3 = new Thread(t,"窗口3"); 37 t1.start(); 38 t2.start(); 39 t3.start(); 40 41 } 42 } 43
运行我们写的程序之后,发现了两个问题,即(重复买票还有出现负数票的情况),如下:
2>.分享出现线程安全问题的原因
3>.同步代码块解决线程安全问题
我们解决上面案例的思路就是:当一个线程进入数据操作的时候,无论是否休眠,其它线程只能等待。这就需要引入Java的一个关键字,同步锁的概念:
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note; 8 9 class Tickets implements Runnable{ 10 //定义出售的票源 11 private int ticket = 20; 12 13 private Object obj = new Object(); 14 @Override 15 public void run() { 16 while(true) { 17 //线程共享数据,保证安全,可以把一段代码变成一个原子性操作,也就是说当某个线程在执行该操作时,其它的线程进不来! 18 synchronized (obj) { 19 //对于票数大于0才可以出售 20 if( ticket > 0 ) { 21 try { 22 Thread.sleep(20); 23 System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 } 29 } 30 } 31 } 32 33 34 public class ThreadDemo { 35 public static void main(String[] args) { 36 //创建Runnable接口实现了对象 37 Tickets t = new Tickets(); 38 //创建三个Thread类对象,传递Runnable接口实现类 39 Thread t1 = new Thread(t,"窗口1"); 40 Thread t2 = new Thread(t,"窗口2"); 41 Thread t3 = new Thread(t,"窗口3"); 42 t1.start(); 43 t2.start(); 44 t3.start(); 45 46 } 47 }
执行结果如下:
4>.同步代码块的执行原理
二.同步方法
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 package cn.org.yinzhengjie.note; 7 8 class Tickets implements Runnable{ 9 //定义出售的票源 10 private int ticket = 20; 11 @Override 12 public void run() { 13 while(true) { 14 payTicket(); 15 } 16 } 17 //同步方法需要在方法上面用关键字synchronized声明,同步方法中也有对象锁,该锁就是本类对象引用(this). 18 //如果同步方法有静态修饰,那么成员变量也需要用静态修饰,静态同步方法中的锁对象是:“类名.class”,即"Tickets.class"。 19 public synchronized void payTicket() { 20 //对于票数大于0才可以出售 21 if( ticket > 0 ) { 22 try { 23 Thread.sleep(20); 24 System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 } 30 } 31 32 public class ThreadDemo { 33 public static void main(String[] args) { 34 //创建Runnable接口实现了对象 35 Tickets t = new Tickets(); 36 //创建三个Thread类对象,传递Runnable接口实现类 37 Thread t1 = new Thread(t,"窗口1"); 38 Thread t2 = new Thread(t,"窗口2"); 39 Thread t3 = new Thread(t,"窗口3"); 40 t1.start(); 41 t2.start(); 42 t3.start(); 43 } 44 }
代码执行结果如下:
三.Lock接口改进售票案例
我们用synchronized关键字实现同步锁方法,我们也知道非静态默认所就是本类对象引用(this).如果同步方法有静态修饰,那么成员变量也需要用静态修饰,静态同步方法中的锁对象是:“类名.class”。但是我们很难实现在程序看出来它是在哪上锁,又是在哪解锁。这个时候我们Java开发者在JDK1.5版本后退出了Lock接口,该接口就可以清晰的表示程序应该在哪个位置上上锁,又是在哪个位置上解锁。我们用实现Lock接口的子类ReentrantLock来进行模拟,代码如下:
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 package cn.org.yinzhengjie.note; 7 8 import java.util.concurrent.locks.Lock; 9 import java.util.concurrent.locks.ReentrantLock; 10 11 class Tickets implements Runnable{ 12 //定义出售的票源 13 private int ticket = 20; 14 //在类的成员位置创建lock获取锁 15 private Lock lock = new ReentrantLock(); 16 17 @Override 18 public void run() { 19 while(true) { 20 payTicket(); 21 } 22 } 23 //用lock锁也可以进行锁操作,可以和synchronzied实现同样的效果,并且可以清晰的在程序中看出在哪个位置上锁和解锁。 24 public void payTicket() { 25 //调用Lock接口方法获取锁 26 lock.lock(); 27 //对于票数大于0才可以出售 28 if( ticket > 0 ) { 29 try { 30 Thread.sleep(50); 31 System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 }finally { 35 //释放锁,调用Lock接口方法unlock 36 lock.unlock(); 37 } 38 } 39 } 40 } 41 42 public class ThreadDemo { 43 public static void main(String[] args) { 44 //创建Runnable接口实现了对象 45 Tickets t = new Tickets(); 46 //创建三个Thread类对象,传递Runnable接口实现类 47 Thread t1 = new Thread(t,"窗口1"); 48 Thread t2 = new Thread(t,"窗口2"); 49 Thread t3 = new Thread(t,"窗口3"); 50 t1.start(); 51 t2.start(); 52 t3.start(); 53 } 54 }
四.线程的死锁问题
线程的死锁前提是:必须是多线程出现同步嵌套。多线程场景下,多个线程互相等待对方释放锁的现象。
在实际生活中,死锁就好比两个小孩子打架,两个人彼此扯住对方的头发,谁也不撒手,都等着对方先松手为止。再比如我们看电影,尤其是成龙的动作片,经常出现两个搭档,电影中两个搭档去执行任务,一个人拿到了 抢,成龙达到了子弹,然后两个人分别跑到了走廊的两侧,发现彼此都达到了对方想要的东西,他们无法完成开枪的操作。在代码里,就变现为同步的嵌套,即拿着一个锁的同时,想要获取另外一个锁,此时另外一个锁拿走了当前线程需要锁并且等待着当前锁释放锁,即两个线程彼此等待对方释放锁的情况。我们可以用代码来模仿一下
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note; 8 9 class MyLock{ 10 //构造方法私有化 11 private MyLock() {} 12 //由于构造方法私有化,让用户只能通过"类名.静态方法"的方式调用(此处我们不考虑反射的情况!) 13 public final static MyLock lockA = new MyLock(); 14 public final static MyLock lockB = new MyLock(); 15 16 } 17 18 class Deadlock implements Runnable{ 19 private int i = 0; 20 @Override 21 public void run() { 22 while(true) { 23 if( i % 2 == 0 ) { 24 //先进去A同步,在进入B同步 25 synchronized(MyLock.lockA) { 26 System.out.printf("【%s】已经拿到了枪,准备去拿子弹!\n",Thread.currentThread().getName()); 27 synchronized(MyLock.lockB) { 28 System.out.printf("【%s】成功拿到子弹!\n",Thread.currentThread().getName()); 29 } 30 } 31 }else { 32 //先进入B同步,在进入A同步 33 synchronized(MyLock.lockB) { 34 System.out.printf("【%s】已经拿到子弹,准备去拿枪!\n",Thread.currentThread().getName()); 35 synchronized(MyLock.lockA) { 36 System.out.printf("【%s】成功拿到枪!\n",Thread.currentThread().getName()); 37 } 38 } 39 } 40 i++; 41 } 42 } 43 } 44 45 46 47 public class DeadLockDemo { 48 public static void main(String[] args) { 49 50 Deadlock dead = new Deadlock(); 51 52 Thread t1 = new Thread(dead,"成龙"); 53 Thread t2 = new Thread(dead,"李连杰"); 54 55 t1.start(); 56 t2.start(); 57 } 58 }
以上代码执行结果如下:
五.线程等待案例展示
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note1; 8 9 public class Resource { 10 public String name; 11 public String sex; 12 //定义一个标志位,让其默认值为false 13 public boolean flag = false; 14 }
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note1; 8 9 //定义一个输入的线程,对资源对象Resource中成员变量赋值 10 public class Input implements Runnable { 11 //让用户在调用时手动传入,若传入的对象是同一个,那么多个线程就可以实现对同一个线程进行操作 12 private Resource r ; 13 public Input(Resource r) { 14 this.r = r; 15 } 16 17 public void run() { 18 while(true) { 19 int i = 0; 20 while(true) { 21 synchronized (r) { 22 //表示是true时,表示赋值完成,我们可以让线程进入休眠状态 23 if(r.flag) { 24 try { 25 //让检查进入等待状态,也就是不会执行其下面的代码! 26 r.wait(); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 } 31 //如果标志位的值为false,则说明Resource对象并没有赋值,我们需要做的是赋值操作! 32 if(i % 2 == 0) { 33 r.name = "尹正杰"; 34 r.sex = "男"; 35 }else { 36 r.name = "yinzhengjie"; 37 r.sex = "man"; 38 } 39 //以上操作完成了赋值,标记改为true! 40 r.flag = true; 41 //此时将Output线程唤醒,让对方知道赋值已经完成,可以来取值啦! 42 r.notify(); 43 } 44 i++; 45 } 46 } 47 } 48 49 }
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note1; 8 9 //定义输出线程,对资源对象Resource中成员变量,输出值。 10 public class Output implements Runnable { 11 //让用户在调用时手动传入,若传入的对象是同一个,那么多个线程就可以实现对同一个线程进行操作 12 private Resource r ; 13 public Output(Resource r) { 14 this.r = r; 15 } 16 17 public void run() { 18 while(true) { 19 //注意,在选择锁的时候,若锁不相同,可能存在和我们期望的结果有偏差! 20 synchronized (r) { 21 //判断标志位的值是否为false,如果是则说明其是等待状态,我们需要的就是去取值! 22 if(!r.flag) { 23 try { 24 r.wait(); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 System.out.println(r.name+"==="+r.sex); 30 //标记改为false, 31 r.flag = false; 32 //表示赋值完成,唤醒Input线程。 33 r.notify(); 34 } 35 } 36 } 37 38 }
1 /* 2 @author :yinzhengjie 3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/ 4 EMAIL:y1053419035@qq.com 5 */ 6 7 package cn.org.yinzhengjie.note1; 8 9 public class ThreadDemo { 10 public static void main(String[] args) { 11 Resource r = new Resource(); 12 13 Input in = new Input(r); 14 Output out = new Output(r); 15 16 Thread t1 = new Thread(in); 17 Thread t2 = new Thread(out); 18 19 t1.start(); 20 t2.start(); 21 } 22 }
本文来自博客园,作者:尹正杰,转载请注明原文链接:https://www.cnblogs.com/yinzhengjie/p/9005858.html,个人微信: "JasonYin2020"(添加时请备注来源及意图备注,有偿付费)
当你的才华还撑不起你的野心的时候,你就应该静下心来学习。当你的能力还驾驭不了你的目标的时候,你就应该沉下心来历练。问问自己,想要怎样的人生。