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

在线程安全类中封装了必要的同步机制,因此客户端无需采取进一步的同步措施。

原子性


要么不执行,要么执行到底。原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其他线程对i进行任何操作。也就是说保证某个线程对i的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者CAS(Compare And Set 需要硬件CPU的支持)操作可以保证操作的原子性。

当多个线程访问某个状态变量,并且其中有一个线程执行写入操作时,必须采用同步机制来协调这些线程对变量的访问。无状态对象一定是线程安全的。

如果我们在无状态的对象中增加一个状态时,会出现什么情况呢?假设我们按照以下方式在servlet中增加一个"命中计数器"来管理请求数量:在servlet中增加一个long类型的域,每处理一个请求就在这个值上加1。

public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;

public long getCount() {
return count ;
}

@Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// do something
count++;
}
}

不幸的是,以上代码不是线程安全的,因为count++并非是原子操作,实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。如果线程A读到count为10,马上线程B读到count也为10,线程A加1写入后为11,线程B由于已经读过count值为10,执行加1写入后依然为11,这样就丢失了一次计数。

在 count++例子中线程不安全是因为 count++并非原子操作,我们可以使用原子类,确保确保操作是原子,这样这个类就是线程安全的了。

public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);

public long getCount() {
return count .get() ;
}

@Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// do something
count.incrementAndGet();
}
}

AtomicLong是java.util.concurrent.atomic包中的原子变量类,它能够实现原子的自增操作,这样就是线程安全的了。   同样,上述情况还会出现在 单例模式的懒加载过程中,当多个线程同时访问 getInstance()函数时。这篇文章中有讲解:实现优雅的单例模式

加锁机制


线程在执行被synchronized修饰的代码块时,首先检查是否有其他线程持有该锁,如果有则阻塞等待,如果没有则持有该锁,并在执行完之后释放该锁。

除了使用原子变量的方式外,我们也可以通过加锁的方式实现线程安全性。还是UnsafeCountingFactorizer,我们只要在它的service方法上增加synchronized关键字,那么它就是线程安全的了。当然在整个方法中加锁在这里是效率很低的,因为我们只需要保证count++操作的原子性,所以这里只对count++进行了加锁,代码如下:

public class UnsafeCountingFactorizer implements Servlet {private long count = 0;public long getCount() {return count ;}@Overridepublic void service(ServletRequest arg0, ServletResponse arg1)throws ServletException, IOException {// do somethingsynchronized(this){count++;}}
}

Synchronized代码块使得一段程序的执行具有 原子性,即每个时刻只能有一个线程持有这个代码块,多个线程执行在执行时会互不干扰。

java 内存模型及 可见性


Java的内存模型没有上面这么简单,在Java Memory Model中,Memory分为两类,main memory和working memory,main memory为所有线程共享,working memory中存放的是线程所需要的变量的拷贝(线程要对main memory中的内容进行操作的话,首先需要拷贝到自己的working memory,一般为了速度,working memory一般是在cpu的cache中的)。被volatile修饰的变量在被操作的时候不会产生working memory的拷贝,而是直接操作main memory,当然volatile虽然解决了变量的可见性问题,但没有解决变量操作的原子性的问题,这个还需要synchronized或者CAS相关操作配合进行。

每个线程内部都保有共享变量的副本,当一个线程更新了这个共享变量,另一个线程可能看的到,可能看不到,这就是可见性问题。

下面这段代码中 main 线程中 改变了 ready的值,当开启多个子线程时,子线程的值并不是马上就刷新为最新的ready的值(这里的中间刷新的时间间隔到底是多长,或者子线程的刷新机制,自己也不太清楚。当开启一个线程去执行时,ready值改变时就会立刻刷新,循环立刻就结束,但是当开启多个线程时,就会有一定的延迟)。

public class SelfTest {private  static boolean ready;private static int number;private static long time;public static class ReadThread extends Thread {public void run() {while(!ready ){System. out.println("*******  "+Thread.currentThread()+""+number);Thread. yield();}System. out.println(number+"   currentThread: "+Thread.currentThread());}}public static void main(String [] args) {time = System.currentTimeMillis();new ReadThread().start();new ReadThread().start();new ReadThread().start();new ReadThread().start();try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}number = 42;ready = true ;System.out.println("赋值时间:ready = true     ");}
}

上面这段代码的执行结果:可以看出赋值后,循环还是执行了几次。

此时如果把 ready的属性加上 volatile 结果便是如下的效果:

由此可见Volatile可以解决内存可见性的问题。

上面讲的加锁机制同样可以解决内存可见性的问题,加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

注:由于System.out.println的执行仍然需要时间,所以这面打印的顺序还是可能出现错乱。

参考:

http://www.mamicode.com/info-detail-245652.html

并发编程实战

http://www.cnblogs.com/NeilZhang/p/7979629.html

梦想不是浮躁,而是沉淀和积累

Java 并发基础——线程安全性相关推荐

  1. JAVA并发编程-线程安全性

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

  2. Java并发基础(六) - 线程池

    Java并发基础(六) - 线程池 1. 概述 这里讲一下Java并发编程的线程池的原理及其实现 2. 线程池的基本用法 2.1 线程池的处理流程图 该图来自<Java并发编程的艺术>: ...

  3. java线程抢占式执行,Java并发基础(一)-线程基础

    原标题:Java并发基础(一)-线程基础 只要涉及到线程,其运行结果就是不确定的,虽然说java很早就提供了线程以及并发的支持,但是我们需要知道,线程是完全交给调度器的.有很多同学在编写书上的代码时, ...

  4. Java 并发编程 -- 线程池源码实战

    一.概述 小编在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写的太简单,只写了一点皮毛,要么就是是晦涩难懂,看完之后几乎都 ...

  5. Java并发编程-线程安全基础

    线程安全基础 1.线程安全问题 2.账户取款案例 3.同步代码块synchronized synchronized的理解 java中有三大变量的线程安全问题 在实例方法上使用synchronized ...

  6. java 线程安全的原因_Java并发编程——线程安全性深层原因

    线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因. 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的 ...

  7. Java并发基础学习(八)——好好聊聊死锁

    文章目录 前言 什么是死锁 发生死锁的条件 发生死锁的必要条件 如何定位修复和避免 如何定位 1.jstack 2.ThreadMXBean 如何修复 如何避免 活锁与饥饿 活锁 饥饿 一些面试题 总 ...

  8. Java并发基础构建模块简介

    在实际并发编程中,可以利用synchronized来同步线程对于共享对象的访问,用户需要显示的定义synchronized代码块或者方法.为了加快开发,可以使用Java平台一些并发基础模块来开发. 注 ...

  9. Java并发基础框架AbstractQueuedSynchronizer初探(ReentrantLock的实现分析)

    AbstractQueuedSynchronizer是实现Java并发类库的一个基础框架,Java中的各种锁(RenentrantLock, ReentrantReadWriteLock)以及同步工具 ...

最新文章

  1. Java项目:仓库管理系统设计和实现(java+ssm+springboot+layui)
  2. Couldn't start MySQL! while starting XAMPP
  3. 使用c:foreach时链接失效
  4. 用代码的方式增加servlet mapping定义
  5. 转:权限管理——用户认证和用户授权
  6. 飞鸽传书 桌面上常见的窗口:
  7. LeetCode 164. 最大间距
  8. 使用Eclipse将项目上传至远程GitLab
  9. 弃用数据库自增ID,曝光一下我自己用到的解决方法之---终结篇
  10. python 干什么工作具有明显优势-Python在数据分析方面有什么独特优势
  11. python代码阅读_莫烦Python RL 代码阅读一
  12. MySQL的函数-窗口函数
  13. java web 播放flv,实现网页中播放FLV文件的源代码
  14. linux查看隐藏文件命令
  15. [CTF]CTFSHOW文件包含练习笔记
  16. 微型计算机百度云,STONE_百度云资源_盘多多如风搜_盘搜搜_哎哟喂啊
  17. 什么是resultful 以及为什么要使用它
  18. 浅谈对dao层的理解
  19. 三相异步电机------坐标变换(二)
  20. python 二级 —— turtle

热门文章

  1. RabbitMQ/pika模块
  2. 如何获取枚举字符串,值及遍历枚举(转)
  3. python 如何在一个for循环中遍历两个列表
  4. delphi中的copy,delete,pos和leftstr,RightStr的用法
  5. 转为win64后, MS的lib问题
  6. codeforces #236 div2 简洁题解
  7. [高效时间管理]复盘篇
  8. [Kali][VMware][2020][窗口大小][分辨率]高分辨率自适应
  9. 图论中的基础概念总结
  10. 【解决问题】IDEA配置Tomcat添加Deployment时没有Artifact