最近在看《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. error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory

    一般我们在Linux下执行某些外部程序的时候可能会提示找不到共享库的错误, 比如: tmux: error while loading shared libraries: libevent-1.4.s ...

  2. 你真的了解js伪数组吗?深入js伪数组

    关于js伪数组 具有length属性: 按索引方式存储数据: 不具有数组的push().pop()等方法: 你可能知道怎么把伪数组转换为数组,但是你知道这里边的原理吗? 假如页面有一组li元素 < ...

  3. matlab随笔

    主要是记录一些函数.(博客园的一些操作实在是太不方便了) cat函数:http://blog.sina.com.cn/s/blog_6b7dfd9d0100mnz7.html 联结两个数组 magic ...

  4. nio复习总结

    观察者: 多个对象依赖一个对象的状态, 当这个对象状态发生改变时,依次通知多个对象. 消息的分发和处理 事件驱动 / IO多路复用 借助select  epoll等 reactor: io事件触发时, ...

  5. 进程同步——哲学家进餐问题Java实现

    哲学家就餐问题是1965年由Dijkstra提出的一种线程同步的问题. 问题描述:一圆桌前坐着5位哲学家,两个人中间有一只筷子,桌子中央有面条.哲学家思考问题,当饿了的时候拿起左右两只筷子吃饭,必须拿 ...

  6. Centos之字符串搜索命令grep

    grep [选项] 字符串 文件名 在文件当中匹配符合条件的字符串 选项: -i 忽略大小写 -v 排除指定字符串 [root@localhost ~]# grep "work" ...

  7. Java事务管理之JDBC

    前言 关于Java中JDBC的一些使用可以参见: Java 中使用JDBC连接数据库例程与注意事项 在使用JDBC的使用, 如何进行事务的管理.直接看一下代码 示例代码 /** * @Title: J ...

  8. SQL SERVER2008 存储过程、表、视图、函数的权限

    EXEC sp_addrolemember N'db_owner', N'db'----将db 设置为 db_owner 角色中的一员 EXEC sp_droprolemember N'db_owne ...

  9. Linux命令之远程登录与执行远程主机命令

    实现远程登录的命令 ssh.telnet.rlogin (1)ssh命令 ssh命令是openssh套件中的客户端连接工具,可以给予ssh加密协议实现安全的远程登录服务器.ssh命令用于远程登录上Li ...

  10. Linux基础入门学习笔记之三

    第四节 Linux目录结构及文件基本操作 Linux目录结构 Linux 的目录与 Windows 的目录的区别 目录与存储介质(磁盘,内存,DVD 等)的关系 Windows 一直是==以存储介质为 ...