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 @Override13 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 @Override15 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 @Override12 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 @Override18 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接口方法unlock36 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 @Override21 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 //定义一个标志位,让其默认值为false13 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 }