成员变量与线程安全

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

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();
	}

}

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

155154_CvJ5_2309504.jpg

三个线程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);
		}
	}	
}

测试类不变:

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

161621_YwL4_2309504.jpg

三个线程修改的是同一个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中很少使用成员变量。我将单独列出一个模块讨论多线程和单例之间的关系,这里就不深入研究了。

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


转载于:https://my.oschina.net/u/2309504/blog/537944

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值