Java并发(理论知识)—— 线程安全性
1、什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步错失。
2、原子性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。当多个线程访问某个状态变量,并且其中有一个线程执行写入操作时,必须采用同步机制来协调这些线程对变量的访问。无状态对象一定是线程安全的。
如果我们在无状态的对象中增加一个状态时,会出现什么情况呢?假设我们按照以下方式在servlet中增加一个"命中计数器"来管理请求数量:在servlet中增加一个long类型的域,每处理一个请求就在这个值上加1。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; }} |
不幸的是,以上代码不是线程安全的,因为count++并非是原子操作,实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。如果线程A读到count为10,马上线程B读到count也为10,线程A加1写入后为11,线程B由于已经读过count值为10,执行加1写入后依然为11,这样就丢失了一次计数。
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步操作,延迟初始化是竞态条件的常见情形:
|
1
2
3
4
5
6
7
8
|
public class LazyInitRace { private SomeObject instance = null; public SomeObject getInstance() { if(instance == null) instance = new SomeObject(); return instance ; }} |
在LazyInitRace中包含竞态条件:首先线程A判断instance为null,然后线程B判断instance也为null,之后线程A和线程B分别创建对象,这样对象就进行了两次初始化,发生错误。
要避免静态条件,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
在UnsafeCountingFactorizer 例子中,线程不安全的原因是count ++并非原子操作,我们可以使用原子类,确保加操作是原子的,这样类就是线程安全的了:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count .get() ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count.incrementAndGet(); }} |
AtomicLong是java.util.concurrent.atomic包中的原子变量类,它能够实现原子的自增操作,这样就是线程安全的了。
3、加锁机制
除了使用原子变量的方式外,我们也可以通过加锁的方式实现线程安全性。还是UnsafeCountingFactorizer,我们只要在它的service方法上增加synchronized关键字,那么它就是线程安全的了:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; } @Override public synchronized void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; }} |
在方法上增加synchronized关键字后,它能够保证,同一时间只会有一个线程进入方法体,这样每个线程就可以全部执行完方法后再退出,方法体内操作就相当于是原子操作了,避免了竞态条件错误。
以上代码是线程安全的,但是性能很糟糕,因为我们把整个service都给锁起来了,同一时刻只能一个线程执行service,并发任务变成了串行任务。其实我们本意只是想把count++变成原子操作,根本就没必要把整个方法锁住,只需锁住count++操作即可:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; }<br> @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something synchronized(this){ count++; } }} |
我们缩小了锁的范围,这样可以更好的增加并发性。
4、可见性
每个线程内部都保有共享变量的副本,当一个线程更新了这个共享变量,另一个线程可能看的到,可能看不到,这就是可见性问题,以下面的代码为例:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class NoVisibility { private static boolean ready; private static int number; public static class ReadThread extends Thread { public void run() { while(!ready ) Thread. yield(); System. out.println(number); } } public static void main(String [] args) { new ReadThread().start(); number = 42; ready = true ; }} |
以上代码可能输出0或者什么也不能输出。为什么会什么也不能输出呢?因为我们在主线程中把ready置为true,但是ReadThread中却不一定能够读到我们设置的ready值,所以在ReadThread中Thread.yield()将一直执行下去。为什么可能为0呢?如果ReadThread能够读到我们的值,可能先读到ready值为true,还未读取更新number值,ReadThread就把保有的number值输出了,也就是0。
注意,上面的所有内容都是假设,在缺乏同步的情况下,ReadThread和主线程会如何交互,我们是无法预期的,以上两种情况只是两种可能性。那么如何避免这种问题呢?很简单,只要有数据在多个线程之间共享,就使用正确的同步。
4.1、加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式查看另一个线程的执行结果,当线程A进入某同步代码块时,线程B随后进入由同一个锁保护的同步代码块,此时,线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一同步代码块中的所有操作结果,如果没有同步,那么就无法实现上述保证。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
4.2、volatile变量
volatile是一种比synchronized关键字轻量级的同步机制,volatile关键字可以确保变量的更新操作通知到其他线程。
下面是volatile的典型用法:
|
1
2
3
4
|
volatile boolean asleep;...while(!asleep) doSomeThing(); |
加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。
5、总结
编写线程安全的代码,其核心在于要对状态访问操作进行管理。编写线程安全的代码时,有两个关注点,一个是原子性问题,一个是可见性问题,要尽量避免竞态条件错误。
from:https://www.cnblogs.com/timlearn/p/4012501.html
Java并发(理论知识)—— 线程安全性的更多相关文章
- 【Java并发.2】线程安全性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)和可变的(Mutable)状态的访问. “共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生 ...
- Java并发编程 (四) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...
- JAVA并发编程之线程安全性
1.一个对象是否是线程安全的,取决于它是否被多个线程访问.想要使得线程安全,需要通过同步机制来协同对对象可变状态的访问. 2.修复多线程访问可变状态变量出现的错误:1.程序间不共享状态变量 2.状态变 ...
- Java并发编程 (五) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.安全发布对象-发布与逸出 1.发布与逸出定义 发布对象 : 使一个对象能够被当前范围之外的代码所使用 ...
- Java并发编程:线程控制
在上一篇文章中(Java并发编程:线程的基本状态)我们介绍了线程状态的 5 种基本状态以及线程的声明周期.这篇文章将深入讲解Java如何对线程进行状态控制,比如:如何将一个线程从一个状态转到另一个状态 ...
- Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- Spring并发访问的线程安全性问题
Spring并发访问的线程安全性问题 http://windows9834.blog.163.com/blog/static/27345004201391045539953/ 由于Spring MVC ...
- Java并发编程:线程池的使用(转)
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
随机推荐
- Win7 x64 svn 服务器搭建
SVN服务器搭建和使用 Subversion是优秀的版本控制工具,其具体的的优点和详细介绍,这里就不再多说. 首先来下载和搭建SVN服务器. 现在Subversion已经迁移到apache网站上了 ...
- java多线程快速入门(九)
多线程安全问题(卖火车票案例) package com.cppdy; class MyThread5 implements Runnable{ private Integer ticketCount= ...
- LeetCode(60): 第k个排列
Medium! 题目描述: 给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列. 按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下: "123" ...
- C++ code:prime decision
1 判断一个数是否为素数 对于判断一个数m是否为素数,最朴素的方式是按照素数的定义,试除以从2开始到m-1的整数,倘若无一例外地不能整除,则该数必为素数. #include<iostream&g ...
- 性能测试二十一:环境部署之mysql
在正常工作中,mysql应该部署到 一台独立的服务器上,不与tomcat共用服务器,由于成本原因,现部署到一起 为避免出错引起麻烦,先备份: 一:环境清理:先卸载系统自带的mysql 停止mysql: ...
- python 全栈开发,Day72(昨日作业讲解,昨日内容回顾,Django多表创建)
昨日作业讲解 1.图书管理系统 实现功能:book单表的增删改查 1.1 新建一个项目bms,创建应用book.过程略... 1.2 手动创建static目录,并在目录里面创建css文件夹,修改set ...
- python 全栈开发,Day30(第一次面向对象考试)
月考题: python 全栈11期月考题 一 基础知识:(70分) 1.文件操作有哪些模式?请简述各模式的作用(2分) 2.详细说明tuple.list.dict的用法,以及它们的特点(3分) 3.解 ...
- MVC开发中的常见错误-04-“System.NullReferenceException”类型的异常在 BBFJ.OA.WebApp.dll 中发生,但未在用户代码中进行处理
未将对象引用设置到对象实例,又名空指针异常,伴随程序员开发的一生. 查看详细信息得知: SetUserRoleInfo() 首先想到的是 IBLL.IRoleInfoService RoleInfo ...
- Ext.js入门:常用组件与综合案例(七)
一:datefield简单示例 二:timefield简单示例 三:numberfield简单示例 四:FormPanel提交 datefield简单示例: <html xmlns=&quo ...
- Hibernate之一级缓存和二级缓存
1:Hibernate的一级缓存: 1.1:使用一级缓存的目的是为了减少对数据库的访问次数,从而提升hibernate的执行效率:(当执行一次查询操作的时候,执行第二次查询操作,先检查缓存中是否有数据 ...