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中没有关联
} } }

Java并发编程实战2-线程安全的更多相关文章

  1. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

  2. java并发编程实战之线程安全性(一)

    1.1什么是线程安全性 要对线程安全性给出一个确切的定义是非常复杂的.最核心的概念就是正确性.正确性:某个类的行为与其规范完全一致.在良好的规范中通常会定义各种不变性条件来约束对象的状态,以及定义各种 ...

  3. Java并发编程实战 之 线程安全性

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

  4. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  5. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  6. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  7. 【Java并发编程实战】-----“J.U.C”:CountDownlatch

    上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...

  8. 【Java并发编程实战】-----“J.U.C”:CyclicBarrier

    在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...

  9. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  10. 【Java并发编程实战】-----“J.U.C”:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...

随机推荐

  1. python 实现字符串的切片功能

    '''string切片''' def string_split(stringone,split): m = [] if type(split)!=str: return False if split ...

  2. Macaca环境搭建全教程

    首先想要会Macaca,还得先会用github,不然你得死……因为各种例子都在git上,官网也一样,蛋疼的很…… #基础环境 1.JDK:jdk1.8-配置变量 2.Android SDK:sdk   ...

  3. Java Comparator接口学习笔记

    Comparator是一个泛型函数式接口,T表示待比较对象的类型: @FunctionalInterface public interface Comparator<T> { } 本文将主 ...

  4. 使用Xshell远程访问tensorboard

    在使用tensorflow时,由于本地资源的限制,一般在远程服务器上训练模型,而服务器没有图形界面,那么在训练过程中如何实时地访问tensorboard可视化数据呢? 如果服务器和本地电脑连接在同一个 ...

  5. leetcode-填充同一层的兄弟节点Ⅱ

    给定一个二叉树 struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *next; } 填充它的每个 ...

  6. CF100015C

    主要找到环上任意一条边,有比较dis(u,v),dis(u,a)+w+dis(b,v),dis(u,b)+w+dis(a,u) 然后,然后没了 lca求dis(u,v):dis(u,v)=dis[u] ...

  7. 使用vbox构建局域网络

    update: 也可以启用DHCP自动分配IP地址.(看到过的某一篇博文写过要使用这个服务还得自己搭--就没有动手去实践一下直接手动分配了静态的IP.偶然尝试了一下发现动态IP分配和手动静态IP分配都 ...

  8. runlevel 命令详解

    基础命令学习目录首页 原文链接:https://blog.csdn.net/PecoVio/article/details/82428883 runlevel 知识扩展 linux操作系统自从开始启动 ...

  9. Python发送邮件(最全)

    简单邮件传输协议(SMTP)是一种协议,用于在邮件服务器之间发送电子邮件和路由电子邮件. Python提供smtplib模块,该模块定义了一个SMTP客户端会话对象,可用于使用SMTP或ESMTP侦听 ...

  10. idea最常使用的快捷键

    撤销 反撤销 : Ctrl+Z / Ctrl+Shift+Z 删除一行 : Ctrl+Y 跳到实现类 : Ctrl+Alt+B 重命名文件:   shift+F6 控制台放大缩小: ctrl+shif ...