您的位置:首页 > 其它

成员变量与线程安全

2015-12-01 00:00 435 查看
摘要: 本文主要讨论成员变量对线程安全的影响。在多线程共享对象时,线程安全问题尤为突出、重要。在web开发中,碰到最多的还是多线程与单例模式之间的结合使用。

通过集中情况来观察成员变量对线程安全的影响:

1.数据不共享

线程类代码如下:

package com.feng.example;

public class MyThread extends Thread {

private int count = 5;

@Override
public void run() {
// TODO Auto-generated method stub
while(count > 0)
{
count--;
System.out.println(Thread.currentThread().getName()+"==="+count);
}
}
}

‍测试类代码如下

package com.feng.example;

public class ThreadTest {

/**
* @param args
*/
public static void main(String[] args) {

Thread a = new MyThread();
Thread b = new MyThread();
Thread c = new MyThread();

a.start();
b.start();
c.start();
}

}

分析:在主程序中分别创建了三个线程实例对象,三个实例对象有自己的内容空间,有自己的成员变量,内存模型如下图所示:



三个线程a.b.c都有各自的count成员变量,三者运行互不影响。因此在数据不共享的情况下是不会出现线程安全问题的。

程序输出:

Thread-0===4
Thread-0===3
Thread-0===2
Thread-0===1
Thread-0===0
Thread-1===4
Thread-1===3
Thread-1===2
Thread-1===1
Thread-1===0
Thread-2===4
Thread-2===3
Thread-2===2
Thread-2===1
Thread-2===0


2.数据共享

在数据共享这一部分分为两个部分来讲:

(1)多个线程实例(同一个类的实例)的成员变量指向同一个对象,那就将成员变量改为static类型

线程类改写为:

package com.feng.example;

public class MyThread extends Thread {

private static int  count = 5;

@Override
public void run() {
// TODO Auto-generated method stub
while(count > 0)
{
count--;
System.out.println(Thread.currentThread().getName()+"==="+count);
}
}
}

测试类不变:

分析:三个实例对象的成员变量都是使用指向的同一个成员变量,内存结构如下图所示:



三个线程修改的是同一个count变量,那么执行结果就不再是每一个线程都会循环5次了。除此之外,当线程a执行到了count--时,cpu切换去执行线程b,线程同样执行到count--然后输出,就会出现输出两次3,而没有结果4。这就出现了线程安全问题。其他的线程修改了本线程中还未处理完的数据(这里指的是输出)。

输出结果为:

Thread-0===3
Thread-0===2
Thread-0===1
Thread-0===0
Thread-1===3

由结果可以看出,Thread-0和Thread-1都输出了3,而正确的结果应该是输出4,3,2,1,0这几个数组都有的

解决方案:可以将方法定义为同步方法,在方法前加synchronized关键字??这种解决当然不正确,因为三个线程实例是三个对象,方法级别的synchronized是对对象加锁,所以对象各不相同因此在方法上加同步是没有任何效果的。正确的做法是使用synchronized语句块对MyThread的class文件加锁,程序修改如下:

package com.feng.example;

public class MyThread extends Thread {

private static int  count = 5;

@Override
public void run() {
// TODO Auto-generated method stub
synchronized(MyThread.class)
{
while(count > 0)
{
count--;
System.out.println(Thread.currentThread().getName()+"==="+count);
}
}
}
}

测试类不修改,执行结果如下:

Thread-2===4
Thread-2===3
Thread-2===2
Thread-2===1
Thread-2===0

当然解决的方案有很多,这里不细讲解决方案。

有的书本中的讲解都会提及count++, count--会被分成三步操作的问题,这里我个人认为存在这一方面的原因,但也存在count--后时间片到了的情况,去执行其他线程的代码块,导致了count的值不准确。验证这一说法的办法就是将count--修改为--count,--count课时寄存器自减操作不会分成三步操作了吧。结果同样会出现相同的值。这个验证大家自行测试。

(2)不同的线程(不同的线程类)引用同一对象作为成员变量

定义两个线程MyThreadA,MyThreadB,自定义类MyNum用于计数

package com.feng.example;

public class MyNum {

int count;

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}
}

package com.feng.example;

public class MyThreadA extends Thread{

private MyNum count;

public MyThreadA(MyNum count)
{
this.count = count;
}

@Override
public void run() {
// TODO Auto-generated method stub
while(count.getCount()  >0)
{
count.setCount(count.getCount()-1);
System.out.println(Thread.currentThread().getName()+"====="+count.getCount());
}
}

}

package com.feng.example;

public class MyThreadB extends Thread{

private MyNum count;

public MyThreadB(MyNum count)
{
this.count = count;
}

@Override
public void run() {
// TODO Auto-generated method stub
while(count.getCount()  >0)
{
count.setCount(count.getCount()-1);
System.out.println(Thread.currentThread().getName()+"====="+count.getCount());
}
}

}

测试类:

package com.feng.example;

public class ThreadTest {

/**
* @param args
*/
public static void main(String[] args) {

MyNum count = new MyNum();
count.setCount(5);
Thread a = new MyThreadA(count);
Thread b = new MyThreadB(count);

a.start();
b.start();
}

}

通过传递同一个对象给两个线程,这两个线程共用这一个计数器。因为没有同步操作,这个线程执行还会出现线程安全问题。

输出结果:

Thread-1=====3
Thread-0=====3
Thread-1=====2
Thread-0=====1
Thread-1=====0

解决方案:可以使用LocalThread解决,也可以使用sychronized解决,此处不细讲。

如果此处将计数器简单的使用Integer类型,观察会有什么不同?为什么?

通过以上实验可以得出,不管是同一个class文件产生的多个线程实例还是多个class文件产生的多个线程实例,只要对同一个对象进行处理就会出现线程安全问题。

3.浅谈web中的Servlet

Servlet是单例的,意思就是说不管多少个请求,如果请求的是同一个Servlet,那么他们都会使用同一个Servlet对象。

如果不慎在Servlet中使用成员变量保存前台传输过来的数据,那么后台数据将会产生错乱(为什么?查看共享数据中的第二个例子,将MyNum想象成Servlet,使用count接收前台的数据)。因此在Servlet中都是在doGet或者doPost方法中使用局部变量来接收前台的数据,因为每次调用方法时,都会为此次方法调用开辟空间,方法中的各个局部变量之间没有影响。

因此在Servlet中很少使用成员变量。我将单独列出一个模块讨论多线程和单例之间的关系,这里就不深入研究了。

用自己的话总结一下:线程安全问题就是指应该成为原子操作的模块没有完整的执行。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息