线程的安全性可能是非常复杂的,在没有充足同步的情况下,由于多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果(非预期的)。下面的Tools工具类的plus方法会使计数加一,为了方便,这里的num和plus()都是static的:

public class Tools {

    private static int num = 0;

    public  static int plus() {
num++;
return num;
} }

  我们再编写一个任务,调用这个plus()方法并输出计数:

public class Task implements Runnable {

    @Override
public void run(){
int num = Tools.plus();
System.out.println(num);
}
}

  最后创建10个线程,驱动任务:

public class Main {

    public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Task()).start();
}
}
}

  输出:

2
4
3
1
5
6
7
8
9
10

  看起来一切正常,10个线程并发地执行,得到了0累加10次的结果。我们把10次改为10000次:

public class Main {

    public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(new Task()).start();
}
}
}

  输出:

...
9994
9995
9996
9997
9998

  在我的电脑上,这个程序只能偶尔输出10000,为什么?

  问题在于,如果执行的时机不对,那么两个线程会在调用plus()方法时得到相同的值,num++看上去是单个操作,但事实上包含三个操作:读取num,将num加一,将计算结果写入num。由于运行时可能多个线程之间的操作交替执行,因此这多个线程可能会同时执行读操作,从而使它们得到相同的值,并将这个值加1,结果就是,在不同的线程调用中返回了相同的数值。

A线程:num=9→→→9+1=10→→→num=10
B线程:→→→→num=9→→→9+1=10→→→num=10

  如果把这个操作换一种写法,会看的更清晰,num加一后赋值给一个临时变量tmp,并睡眠一秒,最后将tmp赋值给num:

public class Tools {

    private static int num = 0;

    public static int plus() {
int tmp = num + 1;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = tmp;
return num;
} }

  这次我们启动两个线程就能看出问题:

public class Main {

    public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Task()).start();
}
}
}

  启动程序后,控制台1s后输出:

1
1
A线程:num=0→→→0+1=1→→→num=1
B线程:→num=0→→→0+1=1→→→num=1

  上面的例子是一种常见的并发安全问题,称为竞态条件(Race Condition),在多线程环境下,plus()是否会返回唯一的值,取决于运行时对线程中操作的交替执行方式,这并不是我们希望看到的情况。

  由于多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量,线程会由于无法预料的数据变化而发生错误。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生彼此干扰。幸运的是,java提供了各种同步机制来协同这种访问。

  将plus()修改为一个同步方法,同一时间只有一个线程可以进入该方法,可以修复错误:

public class Tools {

    private static int num = 0;

    public synchronized static int plus() {
int tmp = num + 1;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = tmp;
return num;
} }

  控制台先后输出:

1
2

  这时如果将plus()方法改为num++,驱动10000个线程去执行,也可以保证每次都能输出到10000了。

  那么如何设计一个线程安全的类避免出现此类问题呢?

  如果我们写了这样一个Tools工具类,没有考虑并发的情况,其他调用者可能就会在多线程调用plus()方法中产生问题,我们也不希望在多线程调用其他开发者编写的类时产生和单线程调用不一样的结果,我们希望无论单线程还是多线程调用一个类时,无须使用额外的同步,这个类即能表现出正确的行为,这样的类是线程安全的。

  观察上面程序,我们在对num变量进行操作时出了问题,首先,num变量具有两个特点:共享的(Shared)和可变的(Mutable)。“共享”意味着变量可以由多个线程同时访问,而“可变”意味着变量的值在其生命周期内可以发生变化;其次,看一下我们对num的操作,读取num,将num加一,将计算结果写入num,这是一个“读取-修改-写入”的操作,并且其结果依赖于之前的状态。这是一种最常见的竞态条件——“先检查后执行(Check-Then-Act)”操作,首先观察某个条件为真(num为0),然后根据观察结果采取相应的动作(将num加1),但是,当我们采取相应动作的时候,系统的状态就可能发生变化,观察结果可能变得无效(另一个线程在这期间将num加1),这样的例子还有很多,比如观察某路径不存在文件夹X,线程A开始创建文件夹X,但是当线程A开始创建文件夹X的时候,它先前观察的结果就失效了,可能会有另一个线程B在这期间创建了文件夹X,这样问题就出现了。

  因此,我们可以从两个方面来考虑设计线程安全的类

  一、状态变量方面:(对象的状态是指存储在实例变量与静态域成员中的数据,还可能包括其他依赖对象的域。例如,某HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry对象中。)多线程访问同一个可变的状态变量没有使用合适的同步会出现问题,因此:

  1.不在线程之间共享该状态变量(即每个线程都有独自的状态变量)

  2.将状态变量修改为不可变的变量

  3.在访问状态变量时使用同步

  二、操作方面:在某个线程需要复合操作修改状态变量时,通过某种方式防止其它线程使用这个变量,从而确保其它线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。

线程安全性:num++操作为什么也会出问题?的更多相关文章

  1. Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

  2. Java并发编程(五):Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

  3. Java 并发基础——线程安全性

    当线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么久称这个类是线程安全的. 在线程 ...

  4. 【Java并发.2】线程安全性

    要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)和可变的(Mutable)状态的访问. “共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生 ...

  5. Java中各种集合(字符串类)的线程安全性!!!

    Java中各种集合(字符串类)的线程安全性!!! 一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读 ...

  6. Java并发编程原理与实战二十:线程安全性问题简单总结

    一.出现线程安全性问题的条件 •在多线程的环境下 •必须有共享资源 •对共享资源进行非原子性操作   二.解决线程安全性问题的途径 •synchronized (偏向锁,轻量级锁,重量级锁) •vol ...

  7. Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

    前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 packa ...

  8. java并发编程实战:第二章----线程安全性

    一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问 ...

  9. 分享和探讨——如何测试Java类的线程安全性?

    缺乏线程安全性导致的问题很难调试,因为它们是零星的,几乎不可能有意复制.你如何测试对象以确保它们是线程安全的? 我在最近的学习中和优锐课老师谈到了这个问题.现在,是时候以书面形式进行解释了.线程安全是 ...

随机推荐

  1. console的高级使用

    1.console.table()用来表格化展示数据. var people = { zqz: { name: 'zhaoqize', age: 'guess?' }, wdx: { name: 'w ...

  2. SuperMap iClient for JavaScript 新手入门

    地理信息系统(英语:Geographic Information System,缩写:GIS)是一门综合性学科,结合地理学与地图学,已经广泛的应用在不同的领域,是用于输入.存储.查询.分析和显示地理数 ...

  3. Web安全相关(三):开放重定向(Open Redirection)

    简介 那些通过请求(如查询字符串和表单数据)指定重定向URL的Web程序可能会被篡改,而把用户重定向到外部的恶意URL.这种篡改就被称为开发重定向攻击.   场景分析 假设有一个正规网站http:// ...

  4. SharePonit 2010 更改另存为列表模板的语言类型

    从朋友处得来一个列表模板:AccessApplicationSharePoint.stp 将其通过:网站操作----网站设置----列表模板,上传进去.然后去创建列表,发现找不到此模板. 根据多年老司 ...

  5. 【开源】专业K线绘制[K线主副图、趋势图、成交量、滚动、放大缩小、MACD、KDJ等)

    这是一个iOS项目雅黑深邃的K线的绘制. 实现功能包括K线主副图.趋势图.成交量.滚动.放大缩小.MACD.KDJ,长按显示辅助线等功能 预览图 最后的最后,这是项目的开源地址:https://git ...

  6. 开源 iOS 项目分类索引大全 - 待整理

    开源 iOS 项目分类索引大全 GitHub 上大概600个开源 iOS 项目的分类和介绍,对于你挑选和使用开源项目应该有帮助 系统基础库 Category/Util sstoolkit 一套Cate ...

  7. java.lang.NoSuchFieldError: org.apache.http.message.BasicLineFormatter.INSTANCE

    Android发出HTTP请求时出现了这个错误: java.lang.NoSuchFieldError: org.apache.http.message.BasicLineFormatter.INST ...

  8. Linux的locale、LC_ALL和LANG

    如果你是一个Linux新手,并且刚刚安装了一个新的英文系统但想要设置成中文系统,肯定会接触到上面几个变量,在网上搜索了一系列解决方法,给一些变量赋一下值,再export一下,或者写到配置文件里面,然后 ...

  9. Xamarin.Android之Fragment Walkthrough

    利用Fragment设计能够兼容不同屏幕的应用 这里我们先围观下最后的成果图,给读者打打气: 普通手机上显示的结果: 在平板上显示的结果: 笔者要郑重声明下,虽然看似是两种不同的显示效果,但是同一个应 ...

  10. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...