Java并发编程实战2-线程安全

原创 2017年05月24日 13:59:08

1. 定义

一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。

2. WHY

我们想要的是线程安全的程序,为什么在线程安全的开始讲线程安全的类呢?

编写线程安全的代码,本质上就是管理对状态的访问,而且通常是共享的、可变的状态

我们讨论的的线程安全性,看起来好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中保护数据

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方不必作其它的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

对与线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态。

3. 无状态对象是线程安全的

无状态对象:它不包含域也没有引用其他类的域。

例如:

public class MathAdd {
    public int add(int a, int b){
        return a + b;
    }
}

对于MathAdd的实例来说,它只有一个计算两个int数值的的和的add()方法,每个执行线程在运行时,本地变量存储在线程栈中,只有执行线程能够访问,那么无论这个示例被多少个线程并发执行,不同的线程之间并不会相互影响,原因就是:两个线程不共享状态,它们如同在访问不同的实例。

4. 原子性

仍然使用MathAdd来说,假设我们现在需要统计一下,一个MathAdd类的实例被使用的次数,修改后的类如下:

public class MathAdd {

    private long count = 0;

    public long getCount(){
        return count;
    }

    public int add(int a, int b){
        ++count;
        return a + b;
    }

}

此时,MathAdd的实例不再是无状态的对象了,因为其中增加了一个count属性,而多个线程之间又要在add()方法中对count属性进行操作,因此count属性被多个线程共享并操作。

那么这个对象是线程安全吗?不是,因为在add()方法中,有 ++count; 语句,正是这个语句导致对象不是线程安全的。++count; 的执行过程是:先获取当前count的值,然后对当前值加1,将新值写回count,也就是有三个操作,不是一个原子操作

我自己给原子操作下了一个定义:一个操作是不可分割的,就是原子操作。

5. 竞争条件

当计算的正确性依赖于运行时相关的时序或者多线程的交替时,就会产生竞争条件。也就是说,计算的正确性依赖时序,会产生竞争条件。

通过代码来进一步理解:

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if(null == instance){
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

上面的代码是常用的方法惰性初始化,由于某个对象的初始化代价比较昂贵(占用时间和资源),因此延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。

上述代码存在竞争条件:当两个线程操作同一个LazyInitRace对象时,A线程检查instace为空,于是new一个对象;这时,B线程也检查instance是否为null,如果A已经成功new了一个对象,那么B线程就直接返回A线程new的对象;但是,也存在这种情况,A正在new对象,但是没有完成,此时instance还是null,于是B线程也来new对象了,这时就存在两个instance对象了,可能直接导致后面所有的程序都是在两个不同的instance对象上操作,导致程序出错。

6. 复合操作

在MathAdd类和LazyInitRace类中,都是由于某个语句或某个语句块不是原子操作:++count;不是一个原子操作;if(null == instance) { instance = new ExpensiveObject(); }更是明显的非原子操作,也就是复合操作,但是我们在运行程序时,只有满足是原子操作操作时,才能保证运行结果的正确性。

正是由于多线程程序会用到复合操作的中间结果,导致了对象不是线程安全。对于多线程共用的对象来说,中间结果可能是正确的,也可能是不正确,任何一个线程都不能依赖于复合操作的中间结果。

俗语说“苍蝇不叮无缝的蛋”,对于多线程来说,Bug专叮依赖于复合操作中间结果的多线程,也可以说Bug专叮依赖于时序的多线程

7. 内部锁

通过前面的学习,我们发现导致多线程出错的原因,就是对象的某些操作不是原子操作,出现了中间结果,进而导致运行结果错误。因此,Java提供了强制原子性的内置锁机制:syschronized块,语法如下:

synchronized(loack) {
    //访问或修改被锁保护的共享状态
}

从synchronized的语法上可以看出,它有两部分:锁对象的引用,以及这个锁保护的代码块。

执行线程进入synchronized块前,需要获得锁,否则必须等待或阻塞;无论通过正常控制路径退出,还是从块中抛出异常,线程都会释放锁。以此,保证了原子性。实例代码:

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public synchronized ExpensiveObject getInstance() {
        if(null == instance){
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

注意一点,如果用synchronized修饰方法,那么锁的对象会有不同:

(1) 当方法不是静态方法时,锁是该方法所在的对象本身;

(2) 当方法是静态方法时,锁是该方法所在的class的class对象。

每个Java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁

8. 可重入性

当一个线程获得了锁之后,当该线程再次请求获得该锁时,是可以再次成功获得该锁,就是可重入。从逻辑上来说,这是合理的,如果不满足可重入,会出现什么情况呢:

public class Parent {

    public synchronized void doSomething(){
        System.out.println("doSomething in Parent");
    }

}

public class Child extends Parent {

    public synchronized void doSomething(){
        System.out.println("doSomething in Child");
        super.doSomething();
    }

}

如果锁不满足可重入性,那么当在Child中调用doSomething时,执行到了super.doSomething()时,永远无法进入,造成死锁。

9. 用锁来保护状态

通过对对象内部的可变状态变量加锁,可以保证对状态变量的操作是原子的,也就保护了状态。

只要保证多线程访问时,每次都用同一个锁来保护变量,就能避免竞争条件。因此,这个锁是否与对象有关都没有关系。但是,为了便利,每个对象都有一个内部锁,所以不需要显式创建锁对象。

对于每一个设计多个变量的不变约束,需要同一个锁来保护其所有的变量。

10. 性能

synchronized块是好的,可以保证操作是原子的。但是,如果将整个方法都加上synchronized的话,那么这个方式就是串行的了,失去了多线程的好处。因此,synchronized块的大小要根据需要设置,甚至可以将不要保证原子操作的两部分代码分别加上synchronized,从而保证多线程的性能。示例代码:

public class Test {

    public void doSomething(){

        //代码1,不需要保证同步

        synchronized(this){
            //代码2,保证同步
        }

        //代码3,不需要保证同步

        synchronized(this){
            //代码4,需要保证同步,但是跟代码2中没有关联
        }

    }

}

《Go并发编程实战》第2版 紧跟Go的1.8版本

终于来了!经过出版社的各位编辑、校对、排版伙伴与我的N轮PK和共同努力,《Go并发编程实战》第2版的所有内容终于完全确定,并于2017年3月24日交付印刷!当然,印刷也经历了若干流程,以尽量把出错概率...
  • turingbooks
  • turingbooks
  • 2017年04月10日 15:54
  • 6390

Java并发编程规则:不可变对象永远是线程安全的

创建后状态不能被修改的对象叫作不可变对象。不可变对象天生就是线程安全的。它们的常量(变量)是在构造函数中创建的,既然它们的状态无法被修改,那么这些常量永远不会被改变——不可变对象永远是线程安全的。 不...
  • boonya
  • boonya
  • 2016年12月12日 18:06
  • 2090

Java多线程看着一篇足够了!

引 如果对什么是线程、什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内。 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来...
  • zhangliangzi
  • zhangliangzi
  • 2016年05月24日 15:53
  • 10009

设计高效的线程安全的缓存(java并发编程实战5.6)

几乎每一个应用都会使用到缓存, 但是设计高效的线程安全的缓存并不简单. 如: Java代码   public interface Computable {        V compute(...
  • u012572955
  • u012572955
  • 2017年02月10日 16:58
  • 758

《Java并发编程实战》读书笔记二:构建线程安全

一、用组合来实现线性安全1.设计线程安全的类设计线程安全类的三个基本要素: 1. 找出构成对象状态的所有变量 2. 找出约束状态变量的不变性条件 3. 建立对象状态的并发访问管理策略要分析对象的...
  • jeffleo
  • jeffleo
  • 2016年12月24日 20:54
  • 421

利用对象限制和委托构建线程安全的类(java并发编程实战第四章内容)

设计线程安全的类需要考虑: 1. 确定组成对象状态的变量. 2. 确定约束对象状态的不变式. 3. 建立并发访问对象状态的规则.   后置条件: 由于某些变量的取值是有限...
  • u012572955
  • u012572955
  • 2017年02月09日 14:47
  • 181

Java并发编程实战笔记(1)- 线程安全

1、什么是线程安全? 线程安全是指多个线程同时访问同一个类时,如果不需要额外的同步,这个类的行为仍然是正确的。原子操作是线程安全的,锁就是要把复合操作变成原子操作。比较常见的复合操作有: 1)、读...
  • wgh1015398431
  • wgh1015398431
  • 2016年10月19日 17:07
  • 208

java并发实战第六章(2)非阻塞式线程安全列表与一般List集合多线程情况下的比较

这里我把ConcurrentLinkedDeque与List进行对比测试了一下,发现在多线程情况下一般的集合会出现很大的并发性问题,下面就一起探索一下 1.使用ConcurrentLinkedDeq...
  • u010504064
  • u010504064
  • 2015年08月30日 20:04
  • 1378

JAVA并发编程2_线程安全&内存模型

”你永远都不知道一个线程何时在运行!“ 在上一篇博客JAVA并发编程1_多线程的实现方式中后面看到多线程中程序运行结果往往不确定,和我们预期结果不一致。这就是线程的不安全。线程的安全性是非常复杂的,...
  • cauchyweierstrass
  • cauchyweierstrass
  • 2015年05月15日 23:16
  • 2519

java并发编程2:构建线程安全应用程序

线程安全性         调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图...
  • B_H_L
  • B_H_L
  • 2013年03月20日 10:13
  • 1577
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Java并发编程实战2-线程安全
举报原因:
原因补充:

(最多只允许输入30个字)