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

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

原子性


要么不执行,要么执行到底。原子性就是当某一个线程修改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 ;
} @Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// do something
synchronized(this){
count++;
}
}
}

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

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并发基础】安全性、活跃性与性能问题

    前言 Java的多线程是一把双刃剑,使用好它可以使我们的程序更高效,但是出现并发问题时,我们的程序将会变得非常糟糕.并发编程中需要注意三方面的问题,分别是安全性.活跃性和性能问题. 安全性问题 我们经 ...

  2. Java并发基础--线程通信

    java中实现线程通信的四种方式 1.synchronized同步 多个线程之间可以借助synchronized关键字来进行间接通信,本质上是通过共享对象进行通信.如下: public class S ...

  3. Java并发基础--线程安全

    一.线程安全 1.线程安全的概念 线程安全:某个类被单个线程,或者多个线程同时访问,所表现出来的行为是一致,则可以说这个类是线程安全的. 2.什么情况下会出现线程安全问题 在单线程中不会出现线程安全问 ...

  4. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  5. java并发编程 线程基础

    java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...

  6. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  7. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  8. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

  9. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

随机推荐

  1. A Simple Problem with Integers~POJ - 3468

    You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of op ...

  2. 在VCS仿真器中使用FSDB

    FSDB(Fast Signal Database)是Verdi支持的文件格式,用于保存仿真产生的信号波形.据Verdi文档说明,FSDB比标准的VCD格式节省磁盘空间,处理速度更快.要用VCS仿真器 ...

  3. [Luogu3768]简单的数学题

    题面戳我 题意:求 \[\sum_{i=1}^{n}\sum_{j=1}^{n}ij\gcd(i,j)\] \(n\le10^{10}\) sol \[ans=\sum_{d=1}^{n}d\sum_ ...

  4. C#开发Open-Webkit-Sharp浏览器并支持前端alert显示

    看了网上的很多教程,但是总是总是只言片语的,可能不同的人遇到的问题不一样,他们就只列举了自己的问题,那么这里我来做一下总结吧,跟大家分享一下我的完整的开发过程 首先你需要准备Visual Studio ...

  5. HBase新的客户端接口

    最近学习接触HBase的东西,看了<Habase in Action>,但里面关于HBase接口都是过时的接口,以下为HBase新的客户端接口: package com.n10k; imp ...

  6. gradle build docker image

    前言:其实gradle-docker插件干的事和我们手动制作镜像是一样的,只不过它封装了一些步骤而已. eg:如果我们要将项目打包成镜像,首先我们要写Dockerfile,这是制作镜像的不可或缺的第一 ...

  7. shell脚本中文件测试

    shell脚本中文件测试 author:headsen chen  2017-10-17  14:35:19 个人原创,转载请注明作者,否则 依法追究法律责任 [ -f  filename  ]   ...

  8. iOS 用户体验之音频

    早期某知名公司的应用有这么一个问题,如果我在听音乐时打开该应用,播放一段小视频,视频播放完成之后,音乐没有继续播放.这个问题被很多用户吐槽,很久以后,该公司终于修复了这个问题. 无论声音是应用体验的重 ...

  9. python编程中的if __name__ == 'main与windows中使用多进程

    if __name__ == 'main 一个python的文件有两种使用的方法,第一是直接作为程序执行,第二是import到其他的python程序中被调用(模块重用)执行. 因此if __name_ ...

  10. response重定向的网址中有中文乱码的解决办法

    在servlet都是使用response对象来重定向到另一个页面:response.sendRedirect("--").现在遇到一个问题,网页的地址是带中文的,当执行到跳转命令的 ...