最近在看《java并发编程实战》,希望自己有毅力把它读完。

  线程本身有很多优势,比如可以发挥多处理器的强大能力、建模更加简单、简化异步事件的处理、使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题。看下面例子:

public class UnsafeSequence {
private int value; /*返回一个唯一的数值*/
public int getNext(){
return value++;
}
}

  UnsafeSequence的问题在于,如果执行时机不对,那么两个线程在调用getNext时会得到相同的值,图1给出了这种错误情况。虽然递增运算value++看上去是单个操作,但事实上它包含三个独立的操作:  读取value、将value加1、将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使它们得到相同的值,并都将这个值加1。结果就是,在不同线程的调用中返回了相同的值。

在UnsafeSequence中说明的是一种常见的并发安全问题,称为竞态条件。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

再举个例子,延迟初始化中的竞态条件:

public class LazyInitRace {
private HashMap<String, String> instance = null; public HashMap<String, String> getInstance(){
if (instance == null) {
instance = new HashMap<String, String>();
} return instance;
}
}

  在LazyInitRace中包含一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance,A看到instance为空,因而创建一个新的HashMap实体,B同样需要判断instance是否为空,此时的instance是否为空,要取决于不可预测的时序,如果当B检查时,instance也为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例。

  java提供了锁机制来解决这一问题,但这些终归只是一些机制,要编写线程安全的代码,其核心在于要对对象的状态进行管理。

  对象的状态是指存储在状态变量(例如实例或者静态域)中的数据。

一、线程封闭

  如果一个对象无状态,它一定是线程安全的。  

public class StatelessServlet implements Servlet {
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
int i = 1;
i++;
...
}
}

  与大多数servlet相同,StatelessServlet是无状态的:它即不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中(这块需要对jvm内存分配有基础了解),并且只能由正在执行的线程访问。线程之间没有共享状态,由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

  像上面的例子,仅在线程内访问数据,自然也就安全,这种技术称为线程封闭。java提供了一些机制来实现线程封闭,例如局部变量(上面的例子)和ThreadLocal类。

  1.栈封闭

  也就是局部变量,这块要理解为什么局部变量是线程安全的。jvm运行时的数据分配如图2所示。

  

  java虚拟机栈是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这部分会抛出两种异常:如果线程请求的栈深度大于虚拟机允许的栈深度,抛出StackOverflowError;如果栈扩展时无法申请到足够内存,抛出OutOfMemoryError异常。

  2.ThreadLocal类

  ThreadLocal对象通常用于防止对可变的单实例变量或者全局变量进行共享。例如JDBC的Connection对象,JDBC并不要求Connection对象必须是线程安全的。伪代码如下:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
};
}; public static Connection getConnection(){
return connectionHolder.get();
}

二、用锁来保护状态

  1.内置锁

  java提供了一种内置的锁机制来支持原子性:同步代码块。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象,一般不要这么做,这样会影响效率。

  synchronized(lock){

  //访问或修改由锁保护的共享状态

  }

  每一个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。

  对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁以后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式的创建锁对象。

  开发中常见的内置锁的使用方法是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态饿代码路径进行同步,使得在该对象上不会发生并发访问,例如,Vector和其他的同步集合类。

  2.Volatile变量

  同步还有另外一层意思:我们不仅希望防止某个线程正在使用对象状态而另一个线程正在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。java提供了一种削弱的同步机制,即volatile变量,用来确保将变量的更新操作通知其他线程。

  volatile变量的典型用法:  

volatile boolean asleep;
...
while(!asleep){
...
}

volatile变量通常用做某个操作完成,发生中断或者作为状态的标志。volatile的语义不足以确保递增操作的原子性。也就是说,加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

关于volatile后补:

  这块的讲解不是很详细,这里重新整理下,首先要达成一个共识:

  1、每个线程都有自己的线程存储空间

  2、线程何时同步本地存储空间的数据到主存是不确定的。

  正是由于这种不确定性,一个线程修改了数据其他线程不能及时看到,而使用volatile以后,做了如下事情

  1、每次修改volatile变量都会同步到主存中  

  2、每次读取volatile变量的值都强制从主存读取最新的值(强制JVM不可优化volatile变量,如JVM优化后变量读取会使用cpu缓存而不从主存中读取)

  通过直接读取主存保证了可见性,无论哪个线程读取volatile类型变量都是最新数据。但是这不意味着volatile修饰的变量是线程安全的,多线程交替执行还是会存在数据不一致的问题。看到某个成员变量被修饰成volatile类型,可以理解为下面代码的行为:

public class SynchronizedInteger{
private int value; public synchronized int getValue() {
return value;
} public synchronized void setValue(int value) {
this.value = value;
}
}

  

三,不可变对象

  如果一个对象在被创建后其状态就不能被修改,那么这个对象是不可变对象,所以,不可变指的是状态不可变。不可变对象一定是线程安全的

  书中给出了一个判断不可变对象的原则:

  • 对象创建以后其状态不能修改(听着像废话)
  • 对象的所有域都是final类型
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)

  例子:

public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>(); public ThreeStooges(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
} public boolean isStooge(String name){
return stooges.contains(name);
}
}

  尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域来访问,最后一个要求是“正确的构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。

  至此,区分3个概念:

  (1)无状态对象:无成员变量,一定线程安全

  (2)不可变对象:一定线程安全,有状态,但状态不可变

  (3)可变对象:线程不安全,状态可变

总之,这部分的核心是理解对象的状态。

java并发基础(一)的更多相关文章

  1. Java 并发基础

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

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

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

  3. java并发基础(二)

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

  4. Java并发基础概念

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

  5. java并发基础及原理

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

  6. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

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

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

  8. Java并发基础:进程和线程之由来

    转载自:http://www.cnblogs.com/dolphin0520/p/3910667.html 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程. ...

  9. java并发基础(六)--- 活跃性、性能与可伸缩性

    <java并发编程实战>的第9章主要介绍GUI编程,在实际开发中实在很少见到,所以这一章的笔记暂时先放一放,从第10章开始到第12章是第三部分,也就是活跃性.性能.与测试,这部分的知识偏理 ...

随机推荐

  1. sed实例收集

    url:http://blog.csdn.net/hepeng597/article/details/7852468 一.元字符集    1)^锚定行的开始 如:/^sed/匹配所有以sed开头的行. ...

  2. MyEclipse文本对比界面样式修改

    MyEclipse刚安装好,使用文件对比的时候,发现两边的对比颜色非常浅,不同的地方不容易发现,可以通过以下配置将显示颜色调深一点. 配置 效果

  3. java基础45 IO流技术(输入字符流/缓冲输入字符流)

    一.输入字符流 1.1.输入字符流体系 ------| Reader:输入字符流的基类(抽象类)  ----------| FileReader:向指定文件读取数据的输入字符流(把硬盘上的数据读取到程 ...

  4. » Working Around JNI UTF-8 Strings Deprogramming

    private static native void printString(String text); ... void examplePrintString() { String str = &q ...

  5. 2016-2017-2 20155309南皓芯《java程序设计》第七周学习总结

    教材学习内容总结 Lambda 一种匿名方法 表达式构成 括号以及括号里用逗号分隔的参数列表 仅有一个参数的可以省略括号 ->符号 花括号以及花括号里的语句 仅有一条语句时可以省略花括号,并且这 ...

  6. sql server2012 企业版 百度云下载

    链接: https://pan.baidu.com/s/1j7a6RWwpvSzG-sF7Dnexfw 提取码: 关注公众号[GitHubCN]回复获取  

  7. CCF CSP 201709-2 公共钥匙盒

    CCF计算机职业资格认证考试题解系列文章为meelo原创,请务必以链接形式注明本文地址 CCF CSP 201709-2 公共钥匙盒 问题描述 有一个学校的老师共用N个教室,按照规定,所有的钥匙都必须 ...

  8. LoadRunner常用知识点-----LoadRunner日志输出

    在Windows环境下,日志文件output.txt保存在脚本目录中:在UNIX环境下,保存在标准输出中. [Vuser]——[Run Time Settings]——[General]——[Log] ...

  9. python3下django连接mysql数据库

    1.安装pymysql pip install pymysql 有一点需要注意,有的系统(比如ubuntu16.04)同时安装了python2和python3,而比较新的django需要在python ...

  10. js javascript 实现多线程

    在讲之前,大家都知道js是基于单线程的,而这个线程就是浏览器的js引擎. 首先来看一下大家用的浏览器都具有那些线程吧. 假如我们要执行一些耗时的操作,比如加载一张很大的图片,我们可能需要一个进度条来让 ...