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

      最近在做与下载相关的APK的时候,需要用到多线程的一些知识,之前用的不是很多很深入,所以现在重新翻出来学习并且记录一下,这部分内容目前准备三个阶段完成;第一部分是一些基本概念与多线程几种常见的实现方式;第二部分是线程相关的一些方法以及使用过程中的一些注意事项;由于学习的调整,内容安排上优点变化,所以第三部分先简单说一下多线程中数据安全问题与相应的解决方法。

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

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

  • 数据安全问题

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

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

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

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

2,代码演示

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

/**
 * @author why
 * @date 2018年3月4日
 * @description:
 */
package com.why.test;

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

public class Data {

public static int zsMoney = 0;
public static int lsMoney = 0;
public static int wwMoney = 0;

public static int zsHitNum = 0;
public static int lsHitNum = 0;
public static int wwHitNum = 0;

public static Object lockObject1 = new Data();
public static Object lockObject2 = new Data();

public static Lock lock=new ReentrantLock();

}

里面有很多元素,目前不会全用到,在后面会用到;除了数据类之外,我们还需要一个实现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中的下面两行注释对调一下:

   MoneyRunnable my1 = new MoneyRunnable();

// 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肯定大于这个版本啦。同步可以改进数据安全问题,但与此同时付出的代价就是效率降低,这是一个由来已久的话题了。下一章我们会介绍互锁与相应的解决办法,就会使用到这部分和第二部分的知识了。

猜你喜欢

转载自blog.csdn.net/hfut_why/article/details/79449647