《Java多线程编程核心技术(第2版)》 —1.2.8 实例变量共享造成的非线程安全问题与解决方案
1.2.8 实例变量共享造成的非线程安全问题与解决方案
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间交互时是很重要的技术点。
1.不共享数据的情况
不共享数据的情况如图1-18所示。
图1-18 不共享数据的情况
下面通过一个示例来看一下不共享数据的情况。
创建实验用的Java项目,名称为t3,MyThread.java类代码如下:
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);//设置线程名称
}
@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由 " + this.currentThread().getName()
+ " 计算,count=" + count);
}
}
}
运行类Run.java代码如下:
public class Run {
public static void main(String[] args) {
MyThread a=new MyThread("A");
MyThread b=new MyThread("B");
MyThread c=new MyThread("C");
a.start();
b.start();
c.start();
}
}
程序运行结果如图1-19所示。
由图1-19可以看到,该程序一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值,这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。
如果想实现3个线程共同去对一个count变量进行减法操作,则代码该如何设计呢?
2.共享数据的情况
共享数据的情况如图1-20所示。
共享数据的情况就是多个线程可以访问同一个变量,例如,在实现投票功能的软件时,多个线程同时处理同一个人的票数。
下面通过一个示例来看下共享数据的情况。
创建t4测试项目,MyThread.java类代码如下:
public class MyThread extends Thread {
private int count=5;
@Override
public void run() {
super.run();
count--;
//此示例不要用while语句,会造成其他线程得不到运行的机会
//因为第一个执行while语句的线程会将count值减到0
//一直由一个线程进行减法运算
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}
运行类Run.java代码如下:
public class Run {
public static void main(String[] args) {
MyThread mythread=new MyThread();
Thread a=new Thread(mythread,"A");
Thread b=new Thread(mythread,"B");
Thread c=new Thread(mythread,"C");
Thread d=new Thread(mythread,"D");
Thread e=new Thread(mythread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
程序运行结果如图1-21所示。
从图1-21可以看到,A线程和B线程输出的count值都是3,说明A和B同时对count进行处理,产生了“非线程安全”问题。而我们想要得到输出的结果却不是重复的,应该是依次递减的。
出现非线程安全的情况是因为在某些JVM中,count--的操作要分解成如下3步,(执行这3个步骤的过程中会被其他线程所打断):
1)取得原有count值。
2)计算count-1。
3)对count进行赋值。
在这3个步骤中,如果有多个线程同时访问,那么很大概率出现非线程安全问题,得出重复值的步骤如图1-22所示。
A线程和B线程对count执行减1计算后得出相同值4的过程如下。
1)在时间单位为1处,A线程取得count变量的值5。
2)在时间单位为2处,B线程取得count变量的值5。
3)在时间单位为3处,A线程执行count--计算,将计算后的值4存储到临时变量中。
4)在时间单位为4处,B线程执行count--计算,将计算后的值4也存储到临时变量中。
5)在时间单位为5处,A线程将临时变量中的值4赋值给count。
6)在时间单位为6处,B线程将临时变量中的值4也赋值给count。
7)最终结果就是A线程和B线程得到相同的计算结果4,非线程安全问题出现了。
其实这个示例就是典型的销售场景,5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每个销售员卖完一个货品后,其他销售员才可以在新的剩余物品数上继续减1操作,这时就需要使多个线程之间进行同步操作,即用按顺序排队的方式进行减1操作,更改代码如下:
public class MyThread extends Thread {
private int count=5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}
重新运行程序后,便不会出现值一样的情况了,如图1-23所示。
通过在run()方法前加入synchronized关键字,使多个线程在执行run()方法时,以排队的方式进行处理。一个线程调用run()方法前,先判断run()方法有没有被上锁,如果run()方法被上锁,则说明其他线程正在调用run()方法,必须等其他线程对run()方法调用结束后,该线程才可以执行run()方法,这样也就实现了排队调用run()方法的目的,从而实现按顺序对count变量减1的效果。synchronized可以对任意对象及方法加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程会首先尝试去申请这把锁,如果能够申请到这把锁,那么这个线程就会执行synchronized里面的代码。如果不能申请到这把锁,那么这个线程就会不断尝试去申请这把锁,直到申请到为止,而且多个线程会同时去争抢这把锁。
- 点赞
- 收藏
- 关注作者
评论(0)