Java基础-线程操作共享数据的安全问题

                    Java基础-线程操作共享数据的安全问题

                                        作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.引发线程安全问题

  如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

1>.售票案例

  假设某人一次性买了20张关于周杰伦的演唱会,原计划是请全部门去看演唱会的,但是由于老板的临时任务来袭,*需要把这20张票放在交给三个人转卖出去,请你模拟这个买票的过程。我们可以用代码来实现一下:

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note; class Tickets implements Runnable{
//定义出售的票源
private int ticket = 20;
@Override
public void run() {
while(true) {
//对于票数大于0才可以出售
if( ticket > 0 ) {
try {
Thread.sleep(10);
System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
} public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现了对象
Tickets t = new Tickets();
//创建三个Thread类对象,传递Runnable接口实现类
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
Thread t3 = new Thread(t,"窗口3");
t1.start();
t2.start();
t3.start(); }
}

  运行我们写的程序之后,发现了两个问题,即(重复买票还有出现负数票的情况),如下:

Java基础-线程操作共享数据的安全问题

2>.分享出现线程安全问题的原因

Java基础-线程操作共享数据的安全问题

3>.同步代码块解决线程安全问题

  我们解决上面案例的思路就是:当一个线程进入数据操作的时候,无论是否休眠,其它线程只能等待。这就需要引入Java的一个关键字,同步锁的概念:

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note; class Tickets implements Runnable{
//定义出售的票源
private int ticket = 20; private Object obj = new Object();
@Override
public void run() {
while(true) {
//线程共享数据,保证安全,可以把一段代码变成一个原子性操作,也就是说当某个线程在执行该操作时,其它的线程进不来!
synchronized (obj) {
//对于票数大于0才可以出售
if( ticket > 0 ) {
try {
Thread.sleep(20);
System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
} public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现了对象
Tickets t = new Tickets();
//创建三个Thread类对象,传递Runnable接口实现类
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
Thread t3 = new Thread(t,"窗口3");
t1.start();
t2.start();
t3.start(); }
}

  执行结果如下:

Java基础-线程操作共享数据的安全问题

4>.同步代码块的执行原理

Java基础-线程操作共享数据的安全问题

二.同步方法

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/
package cn.org.yinzhengjie.note; class Tickets implements Runnable{
//定义出售的票源
private int ticket = 20;
@Override
public void run() {
while(true) {
payTicket();
}
}
//同步方法需要在方法上面用关键字synchronized声明,同步方法中也有对象锁,该锁就是本类对象引用(this).
//如果同步方法有静态修饰,那么成员变量也需要用静态修饰,静态同步方法中的锁对象是:“类名.class”,即"Tickets.class"。
public synchronized void payTicket() {
//对于票数大于0才可以出售
if( ticket > 0 ) {
try {
Thread.sleep(20);
System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现了对象
Tickets t = new Tickets();
//创建三个Thread类对象,传递Runnable接口实现类
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
Thread t3 = new Thread(t,"窗口3");
t1.start();
t2.start();
t3.start();
}
}

  代码执行结果如下:

Java基础-线程操作共享数据的安全问题

三.Lock接口改进售票案例

Java基础-线程操作共享数据的安全问题

  我们用synchronized关键字实现同步锁方法,我们也知道非静态默认所就是本类对象引用(this).如果同步方法有静态修饰,那么成员变量也需要用静态修饰,静态同步方法中的锁对象是:“类名.class”。但是我们很难实现在程序看出来它是在哪上锁,又是在哪解锁。这个时候我们Java开发者在JDK1.5版本后退出了Lock接口,该接口就可以清晰的表示程序应该在哪个位置上上锁,又是在哪个位置上解锁。我们用实现Lock接口的子类ReentrantLock来进行模拟,代码如下:

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/
package cn.org.yinzhengjie.note; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; class Tickets implements Runnable{
//定义出售的票源
private int ticket = 20;
//在类的成员位置创建lock获取锁
private Lock lock = new ReentrantLock(); @Override
public void run() {
while(true) {
payTicket();
}
}
//用lock锁也可以进行锁操作,可以和synchronzied实现同样的效果,并且可以清晰的在程序中看出在哪个位置上锁和解锁。
public void payTicket() {
//调用Lock接口方法获取锁
lock.lock();
//对于票数大于0才可以出售
if( ticket > 0 ) {
try {
Thread.sleep(50);
System.out.printf(Thread.currentThread().getName()+"出售第[%d]张票\n",ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放锁,调用Lock接口方法unlock
lock.unlock();
}
}
}
} public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现了对象
Tickets t = new Tickets();
//创建三个Thread类对象,传递Runnable接口实现类
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
Thread t3 = new Thread(t,"窗口3");
t1.start();
t2.start();
t3.start();
}
}

四.线程的死锁问题

  线程的死锁前提是:必须是多线程出现同步嵌套。多线程场景下,多个线程互相等待对方释放锁的现象。

  在实际生活中,死锁就好比两个小孩子打架,两个人彼此扯住对方的头发,谁也不撒手,都等着对方先松手为止。再比如我们看电影,尤其是成龙的动作片,经常出现两个搭档,电影中两个搭档去执行任务,一个人拿到了 抢,成龙达到了子弹,然后两个人分别跑到了走廊的两侧,发现彼此都达到了对方想要的东西,他们无法完成开枪的操作。在代码里,就变现为同步的嵌套,即拿着一个锁的同时,想要获取另外一个锁,此时另外一个锁拿走了当前线程需要锁并且等待着当前锁释放锁,即两个线程彼此等待对方释放锁的情况。我们可以用代码来模仿一下

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note; class MyLock{
//构造方法私有化
private MyLock() {}
//由于构造方法私有化,让用户只能通过"类名.静态方法"的方式调用(此处我们不考虑反射的情况!)
public final static MyLock lockA = new MyLock();
public final static MyLock lockB = new MyLock(); } class Deadlock implements Runnable{
private int i = 0;
@Override
public void run() {
while(true) {
if( i % 2 == 0 ) {
//先进去A同步,在进入B同步
synchronized(MyLock.lockA) {
System.out.printf("【%s】已经拿到了枪,准备去拿子弹!\n",Thread.currentThread().getName());
synchronized(MyLock.lockB) {
System.out.printf("【%s】成功拿到子弹!\n",Thread.currentThread().getName());
}
}
}else {
//先进入B同步,在进入A同步
synchronized(MyLock.lockB) {
System.out.printf("【%s】已经拿到子弹,准备去拿枪!\n",Thread.currentThread().getName());
synchronized(MyLock.lockA) {
System.out.printf("【%s】成功拿到枪!\n",Thread.currentThread().getName());
}
}
}
i++;
}
}
} public class DeadLockDemo {
public static void main(String[] args) { Deadlock dead = new Deadlock(); Thread t1 = new Thread(dead,"成龙");
Thread t2 = new Thread(dead,"李连杰"); t1.start();
t2.start();
}
}

  以上代码执行结果如下:

Java基础-线程操作共享数据的安全问题

五.线程等待案例展示

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note1; public class Resource {
public String name;
public String sex;
//定义一个标志位,让其默认值为false
public boolean flag = false;
}

Resource.java 文件内容

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note1; //定义一个输入的线程,对资源对象Resource中成员变量赋值
public class Input implements Runnable {
//让用户在调用时手动传入,若传入的对象是同一个,那么多个线程就可以实现对同一个线程进行操作
private Resource r ;
public Input(Resource r) {
this.r = r;
} public void run() {
while(true) {
int i = 0;
while(true) {
synchronized (r) {
//表示是true时,表示赋值完成,我们可以让线程进入休眠状态
if(r.flag) {
try {
//让检查进入等待状态,也就是不会执行其下面的代码!
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果标志位的值为false,则说明Resource对象并没有赋值,我们需要做的是赋值操作!
if(i % 2 == 0) {
r.name = "尹正杰";
r.sex = "男";
}else {
r.name = "yinzhengjie";
r.sex = "man";
}
//以上操作完成了赋值,标记改为true!
r.flag = true;
//此时将Output线程唤醒,让对方知道赋值已经完成,可以来取值啦!
r.notify();
}
i++;
}
}
} }

Input.java 文件内容

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note1; //定义输出线程,对资源对象Resource中成员变量,输出值。
public class Output implements Runnable {
//让用户在调用时手动传入,若传入的对象是同一个,那么多个线程就可以实现对同一个线程进行操作
private Resource r ;
public Output(Resource r) {
this.r = r;
} public void run() {
while(true) {
//注意,在选择锁的时候,若锁不相同,可能存在和我们期望的结果有偏差!
synchronized (r) {
//判断标志位的值是否为false,如果是则说明其是等待状态,我们需要的就是去取值!
if(!r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(r.name+"==="+r.sex);
//标记改为false,
r.flag = false;
//表示赋值完成,唤醒Input线程。
r.notify();
}
}
} }

Output.java 文件内容

 /*
@author :yinzhengjie
Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
EMAIL:y1053419035@qq.com
*/ package cn.org.yinzhengjie.note1; public class ThreadDemo {
public static void main(String[] args) {
Resource r = new Resource(); Input in = new Input(r);
Output out = new Output(r); Thread t1 = new Thread(in);
Thread t2 = new Thread(out); t1.start();
t2.start();
}
}
上一篇:The type String cannot be constructed. You must configure the container to supply this value.


下一篇:旺财速啃H5框架之Bootstrap(一)