成员变量与线程安全
2015-12-01 00:00
435 查看
摘要: 本文主要讨论成员变量对线程安全的影响。在多线程共享对象时,线程安全问题尤为突出、重要。在web开发中,碰到最多的还是多线程与单例模式之间的结合使用。
通过集中情况来观察成员变量对线程安全的影响:
测试类代码如下:
分析:在主程序中分别创建了三个线程实例对象,三个实例对象有自己的内容空间,有自己的成员变量,内存模型如下图所示:
三个线程a.b.c都有各自的count成员变量,三者运行互不影响。因此在数据不共享的情况下是不会出现线程安全问题的。
程序输出:
测试类不变:
分析:三个实例对象的成员变量都是使用指向的同一个成员变量,内存结构如下图所示:
三个线程修改的是同一个count变量,那么执行结果就不再是每一个线程都会循环5次了。除此之外,当线程a执行到了count--时,cpu切换去执行线程b,线程同样执行到count--然后输出,就会出现输出两次3,而没有结果4。这就出现了线程安全问题。其他的线程修改了本线程中还未处理完的数据(这里指的是输出)。
输出结果为:
由结果可以看出,Thread-0和Thread-1都输出了3,而正确的结果应该是输出4,3,2,1,0这几个数组都有的
解决方案:可以将方法定义为同步方法,在方法前加synchronized关键字??这种解决当然不正确,因为三个线程实例是三个对象,方法级别的synchronized是对对象加锁,所以对象各不相同因此在方法上加同步是没有任何效果的。正确的做法是使用synchronized语句块对MyThread的class文件加锁,程序修改如下:
测试类不修改,执行结果如下:
当然解决的方案有很多,这里不细讲解决方案。
有的书本中的讲解都会提及count++, count--会被分成三步操作的问题,这里我个人认为存在这一方面的原因,但也存在count--后时间片到了的情况,去执行其他线程的代码块,导致了count的值不准确。验证这一说法的办法就是将count--修改为--count,--count课时寄存器自减操作不会分成三步操作了吧。结果同样会出现相同的值。这个验证大家自行测试。
测试类:
通过传递同一个对象给两个线程,这两个线程共用这一个计数器。因为没有同步操作,这个线程执行还会出现线程安全问题。
输出结果:
解决方案:可以使用LocalThread解决,也可以使用sychronized解决,此处不细讲。
如果此处将计数器简单的使用Integer类型,观察会有什么不同?为什么?
通过以上实验可以得出,不管是同一个class文件产生的多个线程实例还是多个class文件产生的多个线程实例,只要对同一个对象进行处理就会出现线程安全问题。
如果不慎在Servlet中使用成员变量保存前台传输过来的数据,那么后台数据将会产生错乱(为什么?查看共享数据中的第二个例子,将MyNum想象成Servlet,使用count接收前台的数据)。因此在Servlet中都是在doGet或者doPost方法中使用局部变量来接收前台的数据,因为每次调用方法时,都会为此次方法调用开辟空间,方法中的各个局部变量之间没有影响。
因此在Servlet中很少使用成员变量。我将单独列出一个模块讨论多线程和单例之间的关系,这里就不深入研究了。
用自己的话总结一下:线程安全问题就是指应该成为原子操作的模块没有完整的执行。
通过集中情况来观察成员变量对线程安全的影响:
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中很少使用成员变量。我将单独列出一个模块讨论多线程和单例之间的关系,这里就不深入研究了。
用自己的话总结一下:线程安全问题就是指应该成为原子操作的模块没有完整的执行。
相关文章推荐
- .NET中保证线程安全的高级方法Interlocked类使用介绍
- Java线程安全中的单例模式
- 深入线程安全容器的实现方法
- PHP 线程安全与非线程安全版本的区别深入解析
- 深入理解线程安全与Singleton
- 多线程问题及处理方法【转】
- Java之线程安全的简单理解
- android的线程安全
- ConcurrentHashMap并不是绝对线程安全的
- Springmvc并发访问的线程安全性问题
- Servlet和 Struts线程安全问题
- 线程安全的单例模式
- Spring AOP的案例
- String,StringBuffer与StringBuilder的区别??
- struts2、spring多用户线程混乱
- 多线程-02,线程安全问题解决
- 通过Java反射在运行时修改TimerTask的执行周期并且立即生效
- 线程安全与可重入
- 多线程技术
- Servlet的线程安全问题