最近在看《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. 用sklearn计算卡方检验P值

    情形: 1. 对于一批分类变量,我们通常要评价两两之间的相关程度. 2. 因变量是分类变量,衡量其他分类变量和因变量的相关性高低. 来源:https://blog.csdn.net/snowdropt ...

  2. http://code52.org/DownmarkerWPF/

    http://code52.org/DownmarkerWPF/ http://kb.cnblogs.com/page/132209/

  3. vue+vuex+axios+echarts画一个动态更新的中国地图

    一. 生成项目及安装插件 # 安装vue-cli npm install vue-cli -g # 初始化项目 vue init webpack china-map # 切到目录下 cd china- ...

  4. 网络协议之UDP

    前言 TCP协议在不可靠的网络环境上提供了可靠的通信通道,隐藏了大量的底层细节,使应用程序更加简洁.但有些应用并不需要这么高的可靠性,并不需要按序交付,而且TCP为了提高可靠性也增加了延时,在某些对延 ...

  5. 面试经典问题---数据库索引B+、B-树

    具体讲解之前,有一点,再次强调下:B-树,即为B树.因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解.如人们可能会以为B-树 ...

  6. Java中使用google.zxing快捷生成二维码(附工具类源码)

    移动互联网时代,基于手机端的各种活动扫码和收付款码层出不穷:那我们如何在Java中生成自己想要的二维码呢?下面就来讲讲在Java开发中使用 google.zxing 生成二维码. 一般情况下,Java ...

  7. JSON解析代码

    /** * 解析有数据头的纯数组 */ private void parseHaveHeaderJArray() { //拿到本地JSON 并转成String String strByJson = J ...

  8. Web Api 的 路由机制

    ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动设备)的 HTTP 服务. ASP.NET Web API 是一种用于在 .NET Framework 上构 ...

  9. 安装caffe框架所需文件

    安装caffe框架所需文件: 1.微软提供的快速卷积神经网络框架caffe-master安装包或者windows提供的caffe-windows安装包. 链接:http://pan.baidu.com ...

  10. "characterEncoding" must end with the ';' delimiter.

    17/04/20 17:27:10 FATAL conf.Configuration: error parsing conf file:/usr/local/apache-hive-1.2.2-bin ...