当前位置: 首页 > news >正文

java并发处理 (同步与原子性)

为什么80%的码农都做不了架构师?>>>   hot3.png

3、线程同步与原子性

 

线程安全:

 

每一个线程只做自己的工作固然好,但是线程之间经常会相互影响(竞争或者合作),多个线程需要同时操作同一个资源(比如一个对象)是常有的事。这个时候,线程安全问题就出现了。

 

举一个《thinking in java》第四版中的例子。有一个EvenGenerator类,它的next()方法用来生成偶数。如下:

public class EvenGenerator {

 

  private int currentValue = 0;

  private boolean cancled = false;

 

  public int next() {

      ++currentValue;       //危险!

      ++currentValue;

      return currentValue;

  }

 

  public boolean isCancled() {

      return cancled;

  }

  public void cancle() {

      cancled = true;

  }

}

另外有一个EvenChecker类,用来不断地检验EvenGeneratornext()方法产生的是不是一个偶数,它实现了Runnable接口。

 

public class EvenChecker implements Runnable {

 

  private EvenGenerator generator;

 

  public EvenChecker(EvenGenerator generator) {

      this.generator = generator;

  }

 

  @Override

  public void run() {

      int nextValue;

      while(!generator.isCancled()) {

          nextValue = generator.next();

          if(nextValue % 2 != 0) {

              System.out.println(nextValue + "不是一个偶数!");

              generator.cancle();

          }

      }

  }

}

然后创建两个EvenChecker来并发地对同一个EvenGenerator对象产生的数字进行检验。

 

public class Foo {

 

  public static void main(String[] args) {

EvenGenerator generator = new EvenGenerator();

while(i-->0){

       new Thread(new EvenChecker(generator)).start();

       new Thread(new EvenChecker(generator)).start();

       }

  }

}

显然,在一般情况下,EvenGeneratornext()方法产生的数字肯定是一个偶数,因为在方法体里进行两次”++currentValue”的操作。但是运行这个程序,输出的结果竟然像下面这样(并不是每次都是这个一样的结果,但是程序总会因这样的情况而终止):

 

849701不是一个偶数!

错误出在哪里呢?程序中有“危险”注释的哪一行便可能引发潜在的错误。因为很可能某个线程在执行完这一行只进行了一次递增之后,CPU时间片被另外一个线程夺去,于是就生产出了奇数。

 

解决的办法,就是给EvenGeneratornext()方法加上synchronized关键字,像这样:

public synchronized int next() {

      ++currentValue;

      ++currentValue;

      return currentValue;

}

这个时候这个方法就不会在并发环境下生产出奇数了。因为synchronized关键字保证了一个对象在同一时刻,最多只有一个synchronized方法在执行。

 

synchronized

 

每一个对象本身都隐含着一个锁对象,这个锁对象就是用来解决并发问题的互斥量(mutex)。要调用被synchronized修饰的Method的线程,必须持有这个对象的锁对象,执行完后,必须释放这个锁对象,以便别的线程能够得到这个锁对象。一个对象仅有一个锁对象,这就保证了在同一时刻,最多只有一个线程能够调用并执行这个被synchronized修饰的方法。其他想调用这个方法的线程必须等待当前线程释放锁。就像上面举的例子,在同一时刻,最多只有一个EvenChecker能调用EvenGeneratornext()方法,这就保证了不会出现currentValue只递增一次,CPU时间片就被别的线程夺去的情况。

 

synchronized除了能修饰方法之外,还能用来创建同步块。直接用来修饰方法并不是一个好的办法,这会锁住比较多的代码行。所以大多数情况下,使用同步块是一个更好的选择。

public void doSomething() {

     //一些操作

     synchronized(this) {

          //一些需要被同步的操作

     }

     //另外一些操作

}

其中,synchronized后的括号内必须是一个对象。表示:要执行同步块里的这些操作一定要当前线程取得括号内的这个对象的锁才行。常用的就是this,表示当前对象。在一些高级应用中,可能会用到其他对象的锁。

 

你也可以显式的使用锁对象来实现同步,Java提供了一些Lock类,此处不做描述。

 

原子性(atomicity

 

具有原子性的操作被称为原子操作。原子操作在操作完毕之前不会线程调度器中断。在Java中,对除了longdouble之外的基本类型的简单操作都具有原子性。简单操作就是赋值或者return。比如”a = 1;“和 return a;”这样的操作都具有原子性。”a += b”这样的操作不具有原子性,在某些JVM中”a += b”可能要经过这样三个步骤:

 

取出ab

 

计算a+b

 

将计算结果写入内存

 

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。

 

类似的,像”a++“这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。

有一些大牛可以利用原子性避免同步而写出“免锁”的代码。如果你能编写出一个牛逼的高性能的JVM,你就可以考虑考虑是否可以避免使用同步。

 

所以,在成为这样牛的大牛之前,还是老老实实使用同步吧。

 

Java SE引入了原子类,比如AtomicInter,AtomicLong等等。

 

volatile

 

上面提到了,对longdouble的简单操作不具有原子性。但是,一旦给这两个类型的属性加上volatile修饰符,对它们的简单操作就会具有原子性(当然这是说的在Java SE5之后的故事)。

 

在一些情况下即便是原子操作也可能会引发一些错误,特别是在多处理器的环境下。因为多处理器的计算机可以将内存中的值暂时储存在寄存器或者本地内存缓冲区中。所以,运行在不同处理器上的线程取同一个内存位置的值可能不相同。有一些编译器也会自作主张地优化指令,使得上述情况发生。你当然可以用同步锁来解决这些问题,不过volatile也能解决。

 

如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。

 

但是值得注意的是,除了对longdouble的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!


转载于:https://my.oschina.net/modi/blog/222027

相关文章:

  • Java入门项目:学生信息管理系统V2
  • IE6不支持a标签以外元素的hover的解决方案
  • 40个Java多线程问题详解复习
  • 【Linux】目录权限与文件权限
  • YUV数据格式与YUV_420_888
  • vue-cli3.0项目中使用vw——相比flexible更原生的移动端解决方案
  • rsync 数据镜像备份 记录
  • Lucene:基于Java的全文检索引擎简介
  • Android 其他特效展示
  • DataUml Design 教程7 - 数据库生成模型
  • request
  • luanet分布式lua框架
  • 解决 LLVM 错误 fatal error: ‘csignal’ file not found
  • 使用idea 搭建一个 SpringBoot + Mybatis + logback 的maven 项目
  • vmware 安装dos注意
  • [译] 怎样写一个基础的编译器
  • C++11: atomic 头文件
  • JSONP原理
  • Linux编程学习笔记 | Linux IO学习[1] - 文件IO
  • Meteor的表单提交:Form
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • 阿里云前端周刊 - 第 26 期
  • 技术发展面试
  • 聊聊flink的TableFactory
  • 前端设计模式
  • 前嗅ForeSpider中数据浏览界面介绍
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 数据结构java版之冒泡排序及优化
  • 微信开放平台全网发布【失败】的几点排查方法
  • 微信小程序:实现悬浮返回和分享按钮
  • 为什么要用IPython/Jupyter?
  • 线上 python http server profile 实践
  • const的用法,特别是用在函数前面与后面的区别
  • RDS-Mysql 物理备份恢复到本地数据库上
  • 通过调用文摘列表API获取文摘
  • ​低代码平台的核心价值与优势
  • # 数论-逆元
  • ###项目技术发展史
  • #Linux(权限管理)
  • $.each()与$(selector).each()
  • (11)MSP430F5529 定时器B
  • (Git) gitignore基础使用
  • (八)光盘的挂载与解挂、挂载CentOS镜像、rpm安装软件详细学习笔记
  • (第一天)包装对象、作用域、创建对象
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (南京观海微电子)——COF介绍
  • (三)elasticsearch 源码之启动流程分析
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (一)Java算法:二分查找
  • (一)UDP基本编程步骤
  • (转)C#调用WebService 基础
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**