转自 https://blog.csdn.net/suifeng3051/article/details/52164267

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/suifeng3051/article/details/52164267

线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

产生线程不安全的原因

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

多线程同时执行下面的代码可能会出错:
1 public class Counter {
2     protected long count = 0;
3     public void add(long value){
4         this.count = this.count + value;
5     }
6 }

想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:

1 从内存获取 this.count 的值放到寄存器
2 将寄存器中的值增加value
3 将寄存器中的值写回内存

观察线程A和B交错执行会发生什么:

1    this.count = 0;
2    A:   读取 this.count 到一个寄存器 (0)
3    B:   读取 this.count 到一个寄存器 (0)
4    B:   将寄存器的值加2
5    B:   回写寄存器值(2)到内存. this.count 现在等于 2
6    A:   将寄存器的值加3
7    A:   回写寄存器值(3)到内存. this.count 现在等于 3

两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。

竞态条件 & 临界区
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。
共享资源
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。
局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
1 public void someMethod(){
2   long threadSafeInt = 0;
3   threadSafeInt++;
4 }

局部的对象引用

上面提到的局部变量是一个基本类型,如果局部变量是一个对象类型呢?对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内,所有的对象都存在共享堆中,所以对于局部对象的引用,有可能是线程安全的,也有可能是线程不安全的。

那么怎样才是线程安全的呢?如果在某个方法中创建的对象不会被其他方法或全局变量获得,或者说方法中创建的对象没有逃出此方法的范围,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:
1 public void someMethod(){
2   LocalObject localObject = new LocalObject();
3   localObject.callMethod();
4   method2(localObject);
5 }
6 public void method2(LocalObject localObject){
7   localObject.setValue("value");
8 }

上面样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象,始终在someMethod()方法内部。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。

对象成员对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:
1 public class NotThreadSafe{
2     StringBuilder builder = new StringBuilder();
3     public add(String text){
4         this.builder.append(text);
5     }
6 }

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:

 1 NotThreadSafe sharedInstance = new NotThreadSafe();
 2 new Thread(new MyRunnable(sharedInstance)).start();
 3 new Thread(new MyRunnable(sharedInstance)).start();
 4 public class MyRunnable implements Runnable{
 5   NotThreadSafe instance = null;
 6   public MyRunnable(NotThreadSafe instance){
 7     this.instance = instance;
 8   }
 9   public void run(){
10     this.instance.add("some text");
11   }
12 }

注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。

当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:
1 new Thread(new MyRunnable(new NotThreadSafe())).start();
2 new Thread(new MyRunnable(new NotThreadSafe())).start();

现在两个线程都有自己单独的NotThreadSafe对象,访问的不是同一资源,不满足竞态条件,是线程安全的。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。

判断资源对象是否是线程安全

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

1 如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象,数组,文件,数据库连接,套接字等等。Java中我们无需主动销毁对象,所以“销毁”指不再有引用指向对象。

注意即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:
1 检查记录X是否存在,如果不存在,插入X

如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:

1 线程1检查记录X是否存在。检查结果:不存在
2 线程2检查记录X是否存在。检查结果:不存在
3 线程1插入记录X
4 线程2插入记录X

同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

不可变的共享资源
当多个 线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:
1 public class ImmutableValue{
2     private int value = 0;
3     public ImmutableValue(int value){
4         this.value = value;
5     }
6     public int getValue(){
7         return this.value;
8     }
9 }

如果你需要对ImmutableValue类的实例进行操作,如添加一个类似于加法的操作,我们不能对这个实例直接进行操作,只能创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:

 1 public class ImmutableValue{
 2     private int value = 0;
 3     public ImmutableValue(int value){
 4         this.value = value;
 5     }
 6     public int getValue(){
 7         return this.value;
 8     }
 9     public ImmutableValue add(int valueToAdd){
10         return new ImmutableValue(this.value + valueToAdd);
11     }
12 }

请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。
引用不是线程安全的!
重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:
public void Calculator{private ImmutableValue currentValue = null;public ImmutableValue getValue(){return currentValue;}public void setValue(ImmutableValue newValue){this.currentValue = newValue;}public void add(int newValue){this.currentValue = this.currentValue.add(newValue);}
}

Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用,因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,多个线程访问Calculator实例时仍可通过setValue()和add()方法改变它的状态,因此Calculator类不是线程安全的。
换句话说:ImmutableValue类是线程安全的,但使用它的类则不一定是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。
Java中实现线程安全的方法
在Java多线程编程当中,提供了多种实现Java线程安全的方式:
最简单的方式,使用Synchronization关键字:Java Synchronization介绍
使用java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
使用java.util.concurrent.locks 包中的锁
使用线程安全的集合ConcurrentHashMap
使用volatile关键字,保证变量可见性(直接从内存读,而不是从线程cache读)
 ————————————————
版权声明:本文为CSDN博主「Heaven-Wang」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/suifeng3051/article/details/52164267

转载于:https://www.cnblogs.com/yi-mu-xi/p/11399736.html

[转]线程安全java相关推荐

  1. 线程池java.util.concurrent.ThreadPoolExecutor总结

    http://uule.iteye.com/blog/1123185 线程池还具有提高系统性能的优点,因为创建线程和清除线程的开销比较大. 有两种不同类型的线程池:一是固定线程数量的线程池:二是可变数 ...

  2. android JNI层线程回调Java函数

    今天,简单讲讲android的jni如何使用jni回调java函数. 之前,我写了部分jni的博客,讲的都是如何从android的java代码调用jni的函数.最近,需要做一个新的功能,在jni的C函 ...

  3. JDK1.5中的线程池(java.util.concurrent.ThreadPoolExecut

    JDK1.5中的线程池(java.util.concurrent.ThreadPoolExecut 在多线程大师Doug Lea的贡献下,在JDK1.5中加入了许多对并发特性的支持,例如:线程池. 一 ...

  4. 线程加入 java 1615477815

    线程加入 java 1615477815 关于 演练 效果 子线程执行完成以后 join方法下一行的代码 后续的代码才会被执行

  5. 线程放弃 java 1615477619

    线程放弃 java 1615477619 关于 演练 线程类 测试类 结果 说明 当执行到yield的时候,线程就会放弃cpu,让给其他线程亨用

  6. 线程休眠 java 1615477264

    线程休眠 java 1615477264 关于 案例 线程类 测试类 结果

  7. 使用匿名内部类实现方式二线程创建 java 1615474836

    使用匿名内部类实现方式二线程创建 java 1615474836 有点多余的类 匿名内部类的应用 代码改造 测试类中直接用匿名内部类 就可以不用单独的新建上述那个只用一次的类了 最终代码

  8. 获取线程名称 java 1615387415

    获取线程名称 java 1615387415 多个线程一起跑的时候 有三个线程的运行 结果 无法明确子线程是哪个子线程的操作结果 获取与修改线程名称 演示 在线程的方法中 结果 获取当前线程对象 先获 ...

  9. java 5 线程 睡眠,Java线程之线程的调度-休眠

    Java线程之线程的调度-休眠 白玉 IT哈哈 Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率. 这里要明确的一点,不管程序员怎么编写调度,只能最大 ...

  10. 测试礼让线程(Java)

    测试礼让线程(Java) package src.State;//礼让不一定成功!看CPU心情 public class TestYield {public static void main(Stri ...

最新文章

  1. 随机顺序_SSD固态硬盘的顺序读写和随机读写区别,谁更重要?
  2. php中pre标签,html中pre标签与code标签的作用与用法
  3. python中list是什么意思_python中list和list [:]有什么区别?
  4. Spring boot中使用Swagger2
  5. php worker,问题 php不支持worker工作模式的报错
  6. java网络接口_java网络编程之识别示例 获取主机网络接口列表
  7. php返回支付状态,magento paypal返回支付状态
  8. php软件开发--linux进阶
  9. come back 继工作1年1一个月随想
  10. 制作软盘镜像文件的几种方法
  11. 接口测试基础思维导图
  12. 如何更快地渲染?深入了解3D渲染性能的指南!(6)
  13. lammps复杂形状建模案例——胶囊粒子分子模型
  14. 流逝的昨日,崭新的今天
  15. No ‘Access-Control-Allow-Origin‘ header is present on the requested resource Vue配置代理解决跨域问题
  16. 如何注册公司邮箱?公司邮箱邮件这样写98%的人都爱看
  17. 如何做CEdit中的Undo,Redo(和word类似的输入法输入一次为一个回退块)
  18. 仿天猫 购物车(Android studio 仿天猫 详情页面 添加购物车选择 颜色 尺寸demo)
  19. Intel新CEO敲定,斯旺终”转正“
  20. 前端进阶(12) - css 的弱化与 js 的强化

热门文章

  1. 线程状态以及sleep yield wait join方法
  2. ORACLE 查询日志
  3. 3D几何图形的生成算法
  4. 【分享】(性能优化)思考数据列表中“特殊的列”
  5. We Are The World -- 欧美群星 迈克尔.杰克逊
  6. Linux无法联网怎么办?解析VMware上的CentOS7(Linux)系统无法联网的解决办法
  7. 《LeetCode力扣练习》第10题 C语言版 (做出来就行,别问我效率。。。。)
  8. 二维数组更改vue_使用vue中的v-for遍历二维数组的方法
  9. oracle 云服务器cpu升级,Oracle 10g RAC 升级(CPU Update)之--升级CRS
  10. C++知识点17——使用C++标准库(顺序容器vector常用操作)