多线程数据安全问题与三种解决方式

1,多线程数据安全问题引入

      多线程可以提高程序的使用率,也可以提高cpu的使用使用率,同时也会带来一些弊端,比如:

  • 数据安全问题

      今天我们就先说一下多线程会产生什么样的数据安全问题;在讲之前,我们需要明确:

  • cpu执行的操作是原子性的,是不可拆分的
  • 这里说的数据安全针对的数据是多个线程共享的数据,所以我们会使用在第一部分中说的方式2来实现多线程

      由此,我们可以总结出会造成多线程数据安全的必要条件

  • 多线程环境
  • 多个线程操作共享数据
  • 操作共享数据的语句不是原子性的(多条)

2,代码演示

       先举一个抢钱的小案例,就是一共有1000元钱,现在开三个线程,分别是张三,李四,王五来抢这1000元钱;首先需要一个存储数据的类:Data.java,其代码如下:

 
  1. /**

  2.  * @author why

  3.  * @date 2018年3月4日

  4.  * @description:

  5.  */

  6. package com.why.test;

  7.  
  8. import java.util.concurrent.locks.Lock;

  9. import java.util.concurrent.locks.ReentrantLock;

  10.  
  11. public class Data {

  12.  
  13. public static int zsMoney = 0;

  14. public static int lsMoney = 0;

  15. public static int wwMoney = 0;

  16.  
  17. public static int zsHitNum = 0;

  18. public static int lsHitNum = 0;

  19. public static int wwHitNum = 0;

  20.  
  21. public static Object lockObject1 = new Data();

  22. public static Object lockObject2 = new Data();

  23.  
  24. public static Lock lock=new ReentrantLock();

  25.  
  26. }

里面有很多元素,目前不会全用到,在后面会用到;除了数据类之外,我们还需要一个实现Runnable接口的功能类,实现抢钱的功能:MoneyRunnable.java,其代码如下:

/**

 * @author why

 * @date 2018年2月25日

 * @description:

 */

package com.why.test;


public class MoneyRunnable implements Runnable {

private int sumMoney = 1000;

@Override

public void run() {

while(true){

if (sumMoney > 0) {


/**

* sumMoney = sumMoney - 1;

* 放在前面就不会有问题

*/

//sumMoney = sumMoney - 1;

System.out.println(Thread.currentThread().getName() + "获得一元钱");

if (Thread.currentThread().getName().equals("张三")) {

Data.zsMoney++;

} else if (Thread.currentThread().getName().equals("李四")) {

Data.lsMoney++;

} else {

Data.wwMoney++;

}


/**

* sumMoney = sumMoney - 1;

* 放在后面就会出现数据安全问题(线程安全问题)

*/

sumMoney = sumMoney - 1;

} else {

System.out.println("钱抢完了:");

System.out.println("张三获得了:" + Data.zsMoney);

System.out.println("李四获得了:" + Data.lsMoney);

System.out.println("王五获得了:" + Data.wwMoney);

System.out.println("他们一共获得了:"+(Data.zsMoney+Data.wwMoney+Data.lsMoney));

try {

//防止数据刷的过快,休眠一段时间

Thread.sleep(4000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}


}
这些准备完了,就可以开始编写测试类了:ThreadTestDemo.java,其代码如下:
/**

 * @author why

 * @date 2018年2月25日

 * @description:

 */

package com.why.test;


import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

import java.util.concurrent.FutureTask;


public class ThreadTestDemo {


public static void main(String[] args) {


/**

* 线程数据安全问题出现条件:

* 

* (1)多线程环境 

* (2)多个线程操作共享数据 

* (3)每一个线程共享数据的语句有多条

* 

* 解决方法: 

* (1)同步代码块 

* (2)同步方法 

* (3)Lock对象锁

*/


/**

* 抢钱案例

*/


MoneyRunnable my1 = new MoneyRunnable();

// MoneyRunnableImp my1 = new MoneyRunnableImp();

Thread t1 = new Thread(my1);

Thread t2 = new Thread(my1);

Thread t3 = new Thread(my1);

t1.setName("张三");

t2.setName("李四");

t3.setName("王五");

t1.start();

t2.start();

t3.start();


/**

* 卖票案例

*/

// TicketRunnable tr=new TicketRunnable();

// TicketRunnableImp tr=new TicketRunnableImp();

// Thread t4=new Thread(tr);

// Thread t5=new Thread(tr);

// Thread t6=new Thread(tr);

// t4.setName("窗口1");

// t5.setName("窗口2");

// t6.setName("窗口3");

// t4.start();

// t5.start();

// t6.start();


/**

* 打小明案例

*/

// HitPeopleRunnable hr = new HitPeopleRunnable();

// HitPeopleRunnableImp hr = new HitPeopleRunnableImp();

// Thread t7 = new Thread(hr);

// Thread t8 = new Thread(hr);

// Thread t9 = new Thread(hr);

// t1.setName("张三");

// t2.setName("李四");

// t3.setName("王五");

// t1.start();

// t2.start();

// t3.start();


}



}

运行测试类,有下面运行结果:

 

      我们发现一共1000元,他们三人却抢到了1002元,为什么会出现这样的问题了?而且这里你可以多测试几次,他们最多也只能抢到1002元,主要只因为这里面的

sumMoney = sumMoney - 1

不是一个原子性的操作,所以很可能在执行了,sumMoney-1之后,赋值操作之前,另个线程进来了,所以,就会多执行一次抢钱动作,所以,你也可以开n个线程,最多他们可以抢到(1000+n-1)元,当然n<=1000;

 

3,解决方法

      既然遇到上面的问题,那么怎么解决了;解决的方式主要有下面三种:

  • 同步代码块
  • 同步方法
  • Lock对象(本身是一个接口,使用其子类)

就针对上面的案列,我们只用第一种方式来解决一下,这里我们把MoneyRunnable.java换成MoneyRunnableImp.java:其代码如下:

/**

 * @author why

 * @date 2018年3月5日

 * @description:

 */

package com.why.test;



public class MoneyRunnableImp implements Runnable {



private int sumMoney = 1000;

@Override

public void run() {


while (true) {

/**

* 同步代码块实现数据安全:

* 

* 这里面的this就是一把锁,使用这个类创建的线程使用同一把锁

* 

*/

synchronized (this) {

if (sumMoney > 0) {


/**

* sumMoney = sumMoney - 1; 放在前面就不会有问题

*/

// sumMoney = sumMoney - 1;

System.out.println(Thread.currentThread().getName() + "获得一元钱");

if (Thread.currentThread().getName().equals("张三")) {

Data.zsMoney++;

} else if (Thread.currentThread().getName().equals("李四")) {

Data.lsMoney++;

} else {

Data.wwMoney++;

}


/**

* sumMoney = sumMoney - 1; 放在后面就会出现数据安全问题(线程安全问题)

*/

sumMoney = sumMoney - 1;

} else {

System.out.println("钱分完了:");

System.out.println("张三获得了:" + Data.zsMoney);

System.out.println("李四获得了:" + Data.lsMoney);

System.out.println("王五获得了:" + Data.wwMoney);

System.out.println("他们一共获得了:" + (Data.zsMoney + Data.wwMoney + Data.lsMoney));

try {

// 防止数据刷的过快,休眠一段时间

Thread.sleep(4000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}


}

}


}

然后把测试ThreadTestDemo.java中的下面两行注释对调一下:

 
  1.   MoneyRunnable my1 = new MoneyRunnable();

  2.  
  3. // MoneyRunnableImp my1 = new MoneyRunnableImp();

再次运行,结果如下:

      

      这个时候发现数据正常了,原理也很简单,就是在进入同步代码块之前,必须要拿到this这个锁,当时当其他线程正常执行时,即便丢失cpu执行权,也不释放this这个锁,所以,其他线程无法执行,必须等待该线程执行完同步代码块,把锁释放了,其他的线程才可以拿着这个锁进入同步代码块。

 

4,扩展部分

       前面说了有三种方式可以解决这个问题,那么其他两种是什么了,为了加强练习,我又重新写了两个小案例,下面分别给出解决前代码和解决后的代码:

      打小明案例:

解决前代码:

/**


 * @author why

 * @date 2018年3月5日

 * @description:

 */

package com.why.test;



public class HitPeopleRunnable implements Runnable {



private int sumHit = 1000;


@Override

public void run() {

while (true) {


if (sumHit > 0) {


/**

* sumHit = sumHit - 1; 放在前面就不会有问题

*/

// sumMoney = sumMoney - 1;

System.out.println(Thread.currentThread().getName() + "打了小明一拳");

if (Thread.currentThread().getName().equals("张三")) {

Data.zsHitNum++;

} else if (Thread.currentThread().getName().equals("李四")) {

Data.lsHitNum++;

} else {

Data.wwHitNum++;

}


/**

* sumHit = sumHit - 1; 放在后面就会出现数据安全问题(线程安全问题)

*/

sumHit = sumHit - 1;

} else {

System.out.println("\n小明被打死了:");

System.out.println("张三打了小明:" + Data.zsHitNum + "拳");

System.out.println("李四打了小明:" + Data.lsHitNum + "拳");

System.out.println("王五打了小明:" + Data.wwHitNum + "拳");

System.out.println("他们一共打了小明:" + (Data.zsHitNum + Data.lsHitNum + Data.wwHitNum)+"拳");

try {

// 防止数据刷的过快,休眠一段时间

Thread.sleep(4000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}


}

}


}

解决后代码:

/**

 * @author why

 * @date 2018年3月5日

 * @description:

 */

package com.why.test;



public class HitPeopleRunnableImp implements Runnable {


private int sumHit = 1000;


@Override

public void run() {

while (true) {


// 这里加锁

Data.lock.lock();

if (sumHit > 0) {


/**

* sumHit = sumHit - 1; 放在前面就不会有问题

*/

// sumMoney = sumMoney - 1;

System.out.println(Thread.currentThread().getName() + "打了小明一拳");

if (Thread.currentThread().getName().equals("张三")) {

Data.zsHitNum++;

} else if (Thread.currentThread().getName().equals("李四")) {

Data.lsHitNum++;

} else {

Data.wwHitNum++;

}



/**

* sumHit = sumHit - 1; 放在后面就会出现数据安全问题(线程安全问题)

*/

sumHit = sumHit - 1;

} else {

System.out.println("\n小明被打死了:");

System.out.println("张三打了小明:" + Data.zsHitNum + "拳");

System.out.println("李四打了小明:" + Data.lsHitNum + "拳");

System.out.println("王五打了小明:" + Data.wwHitNum + "拳");

System.out.println("他们一共打了小明:" + (Data.zsHitNum + Data.lsHitNum + Data.wwHitNum) + "拳");

try {

// 防止数据刷的过快,休眠一段时间

Thread.sleep(4000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

Data.lock.unlock();

}

}

}

      卖票案例:

解决前代码:

/**

 * @author why

 * @date 2018年3月5日

 * @description:

 */

package com.why.test;


public class TicketRunnable implements Runnable {

private int number = 100;

private int count =0;


@Override

public void run() {

// TODO Auto-generated method stub

while (true) {

if (number > 0) {

System.out.println( "当前卖了"+(++count)+"张票");

System.out.println(Thread.currentThread().getName() + "出售了第:" + number + "张票");

number=number-1;

}

}

}

}

解决后代码:

/**

 * @author why

 * @date 2018年3月5日

 * @description:

 */

package com.why.test;



public class TicketRunnableImp implements Runnable {



private int number = 100;

private int count =0;


@Override

public void run() {

// TODO Auto-generated method stub

while (true) {

/**

* 同步方法实现数据安全:

* 

* 这里面的this就是一把锁,使用这个类创建的线程使用同一把锁

* 

*/

sysoInfo();

}

}


//同步方法实现,关键字synchronized也可以放在权限修饰符前面

private synchronized  void sysoInfo() {

if (number > 0) {

System.out.println( "当前卖了"+(++count)+"张票");

System.out.println(Thread.currentThread().getName() + "出售了第:" + number + "张票");

number=number-1;

}


}


}

      总结:测试代码还是ThreadTestDemo.java,只需要把想测试的案例注释放开即可,测试结果这里就不在给出。把所有的java类放在一个项目中,因为后面的案列需要使用到DATA这个数据类。后面通过Lock实现的是在jdk1.5之后才有,不过相信各位jdk肯定大于这个版本啦。同步可以改进数据安全问题,但与此同时付出的代价就是效率降低,这是一个由来已久的话题了。下一章我们会介绍互锁与相应的解决办法,就会使用到这部分和第二部分的知识了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值