Java并发编程实战 第3章 对象的共享
可见性
可见性是由于java对于多线程处理的内存模型导致的。这似乎是一种失败的设计,但是JVM却能充分的利用多核处理器的强大性能,例如在缺乏同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,同时,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器的特定缓存中。
可见性可以导致3个问题,失效数据,非原子的64位操作,重排序。
失效数据
如果一个共享的数据被多个线程读写,一个线程执行了写入,随后另外一个线程执行了读取,可能读取到的是未写入之前的数据。
也就是说,写入操作可能在寄存器缓存或者CPU缓存中,其他线程看不到。
非原子的64位操作
失效数据至少是之前某一个线程设置的值,只不过延迟了而已。而不是一个随机值,这种安全性叫做最低安全性。
最低安全性适用于大部分变量,但是存在一个例外,非volatile类型的64位数值变量(double和long)。
java内存模型要求,变量的读取操作和写入操作都必须是原子操作。但是对于非volatile类型的long和double类型,jvm允许将64位的度去操作或者写入操作分解成两个32位,
试想,当你读取或者写入long类型的时候,前32位是一个值的,后32位变成了另外一个值的了。那么这个值就是随机的。
重排序
如果在一个线程中执行下面的代码:
ready = true;
number = 42;
在另外一个线程中,有如下代码:
if(ready)
{
System.out.println(number);
}
这个时候即使是ready为true,number也可能不打印42.
一个线程不能依赖另外一个线程非同步的代码顺序。
因为java会对代码执行重排序,上面两句赋值代码的顺序可能会白java改变,因为这种改变在第一个线程本身中是不会有影响的。
加锁与可见性
加锁可以保证可见性,使用同一个锁加锁的两个代码块,他们是顺序执行的,所以不会有上面三种问题。
为了保证所有的线程都能看到共享数据的最新值,要保证所有的读写操作都要在一个共同的锁的管理下。
Volatile变量
一旦一个变量加入了volatile修饰符,那么在整个变量上,不会有可视性的三个问题:失效数据,非原子的64位操作,重排序。
编译器与运行时都会注意到volatile变量,它是共享的,所以不会被缓存在寄存器或者其他不会被其他线程看不到的地方,也不会对他进行重排序。读取volatile变量总能返回最新的 值。
理解volatile变量的的有效方法是:相当于在volatile变量上加入了synchronize的get和set方法。所有的读写都走这两个方法。
volatile变量的可见性影响比volatile变量本身更多。线程A写入了volatile变量,一旦线程B读取了这个volatile变量,在吸入volatile变量之前对A可见的所有变量的值,都会马山对B也可见。
不过不建议过度依赖volatile的这个作用,因为这种策略是脆弱的,也是难以理解的。
volatile的一个重要的用法是,检查状态标记,以判断是否退出循环。
while(!asleep)
{
//run
}
volatile只能保证可见性。若能用于i=3,但是不能用于i++;
对于i++这种:
如果只有一个线程在i++,也是没有问题的。++过程中没有其他写入操作与它冲突,一旦++完成,其他线程就能看到。也就是说,如果保证只有一个线程在做写操作,那么不论是多么复杂的写操作,只要写操作的中间过程不会对i赋予不合理的值,都是可以使用volatile的。
如果是多个线程进行写入,那么++操作是分为多步骤的,就可能会出现混乱。不过++逻辑比较简单,因为有可视性的保证,最坏的问题也就是,同时对一个i读取并且++了。还是可以保证最低安全性的。
发布与逸出
发布是对象能够在当前作用域之外的代码使用。
逸出:不应该发布的对象被发布了。
如get方法返回一个可变对象,如果这个对象不应该被更改,这个就叫逸出。
在构造器中发布对象的时候,如果包含了this的发布,那么在多线程环境中是有问题的,因为此时其他对象调用发布的对象,这是的this还没有构造完成。这是不正确的构造。
不要在构造器中使用this引用逸出。
线程封闭
线程封闭就是把数据封闭在县城内部,不共享数据。
有三种技术:
Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程封闭型的职责完全由程序来承担。
很少使用。
栈封闭
栈封闭就是说在发布的时候,拷贝一个副本。将变量的作用域限制在在栈里。暴露的只是副本。
ThreadLocal类
该类提供了线程局部变量。这些变量不同于它们的普通对应物,因为访问一个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的私有静态字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
方法摘要 |
|
get() |
|
protected T |
initialValue() |
void |
remove() |
void |
可以将ThreadLocal<T>视为包含Map<Thread,T>的对象。其中保存了特定于该线程的值。
我的理解:ThreadLocal本身是为了为每个线程提供独立拷贝,并不是为了解决锁争用的问题。不使用ThreadLocal,我们同样也可以在每个线程里都维护一个属性来实现拷贝,然后在主线程初始化新线程的时候通过新线程的构造函数传入。不过,如果只是线程在初始化一个属性的时候会访问到共享变量,根据这个共享变量初始化完自己的属性就不会再跟整个共享变量产生交互,那么是可以使用ThreadLocal来避免多个线程一起初始化自己的属性,同时访问共享变量产生的冲突的。
但是ThreadLocal还有一个作用,那就是一个线程终止之后,它的ThreadLocal就可以被垃圾回收。
- package com.zjf;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ThreadLocalTest {
- public static void main(String[] args) {
- ExecutorService es = Executors.newCachedThreadPool();
- for(int i = 0; i < 10; i++)
- {
- es.execute(new Runnable() {
- public void run() {
- System.out.println(ThreadLocalTest.get());
- }
- });
- }
- es.shutdown();
- }
- //nextSerialNum要在多个线程之间共享的 用++生成多个线程的序列号
- private static int nextSerialNum = 0;
- private static ThreadLocal serialNum = new ThreadLocal() {
- //initialValue方法用来初始化 在每个线程第一次调用serialNum.get()的时候触发
- protected synchronized Object initialValue() {
- return new Integer(nextSerialNum++);
- }
- };
- public static int get() {
- //serialNum.get()会返回initialValue或者set设置的值
- return ((Integer) (serialNum.get())).intValue();
- }
- }
结果:
出现了重复,这是为什么?
我们改变一下代码,将run中的内容改为:
- System.out.println(Thread.currentThread().getId() + ":" + ThreadLocalTest.get());
结果:
9:0
10:2
13:4
15:6
11:1
12:3
14:5
16:7
17:8
10:2
不变性
不可变对象一定是线程安全的。
不可变对象:
- 对象创建之后其状态不能被修改。
- 对象的所有属性都是final。
- 对象是正确创建的。在对象的创建期间,this没有逸出。
使用volatile来发布不可变对象
使用volatile和不可变对象来改写上一章的缓存程序:
- package com.zjf;
- import java.math.BigInteger;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- /**
- * 一个获取用于计算整数的平方的类 实现了缓存
- *
- * @author hadoop
- *
- */
- //这是一个不可变的对象
- //通过构造器初始化两个final域
- //然后通过get方法返回
- class Cache {
- private final BigInteger lastNumber;
- private final BigInteger lastPower;
- public Cache(BigInteger lastNumber,BigInteger lastPower) {
- //这里要保证对象的状态在构造之后不能被修改
- //因为lastNumber lastPower是外部传入的 其实如果传入之后外部修改了它的内容 这个对象就不是不可变对象了。
- //但是因为我们传入的是BigInteger类型的 他是不可变对象 所以不需要担心这个问题
- this.lastNumber = lastNumber;
- this.lastPower = lastPower;
- }
- public BigInteger get(BigInteger i)
- {
- BigInteger result = null;
- if(i.equals(lastNumber))
- {
- //返回的对象 也是返回了一个引用 但是由于BigInteger是不可变对象 所以我们也不需要创建拷贝
- result = lastPower;
- }
- return result;
- }
- }
- public class PowerCache2 {
- //使用volatile加不可变对象
- private volatile Cache cache = new Cache(null, null);
- public BigInteger power(BigInteger i) {
- BigInteger result;
- //在执行get的过程中 其他线程不能修改cache的内容 因为它是不可变的 只能重新给cache赋值 但是我正在使用 jvm总要等我用完再赋值吧
- result = cache.get(i);
- if(result == null)
- {
- result = i.pow(2);
- //因为是volatile的 所以这个改变立刻会被其他对象看到
- cache = new Cache(i, result);
- }
- return result;
- }
- public static void main(String[] args) {
- PowerCache2 pc = new PowerCache2();
- ExecutorService es = Executors.newCachedThreadPool();
- for(int i = 0; i < 100; i++)
- {
- es.execute(new Runnable() {
- public void run() {
- BigInteger bi = BigInteger.valueOf((long)(Math.random() * 10));
- System.out.println(bi + ":" + PowerCache.pc.power(bi));
- }
- });
- }
- es.shutdown();
- }
- }
Java并发编程实战 第3章 对象的共享的更多相关文章
- Java并发编程实战 第4章 对象的组合
Java监视器模式 java监视器模式就是在将共享的数据封装在一个类里面,然后然后所有访问或者修改这些数据的方法都标注为synchronize. 车辆追踪模拟: 使用监视器模式: CarTracker ...
- 读书笔记-----Java并发编程实战(二)对象的共享
public class NoVisibility{ private static boolean ready; private static int number; private static c ...
- 【JAVA并发编程实战】1、对象的共享
1.栈封闭 在栈封闭中,只能通过局部变量才能访问对象. 所谓栈封闭就是把变量的声明以及应用都局限在一个局部线程中,在这个局部线程中声明和实例化的对象对于线程外部是不可见的,这个局部线程的栈,无法被任何 ...
- Java并发编程实战---第六章:任务执行
废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...
- Java并发编程实战 第16章 Java内存模型
什么是内存模型 JMM(Java内存模型)规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见. JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Be ...
- 【java并发编程实战】第一章笔记
1.线程安全的定义 当多个线程访问某个类时,不管允许环境采用何种调度方式或者这些线程如何交替执行,这个类都能表现出正确的行为 如果一个类既不包含任何域,也不包含任何对其他类中域的引用.则它一定是无状态 ...
- Java并发编程实战 第8章 线程池的使用
合理的控制线程池的大小: 下面内容来自网络.不过跟作者说的一致.不想自己敲了.留个记录. 要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析: 任务的性质:CPU密集型任务.IO ...
- JAVA并发编程实战---第三章:对象的共享(2)
线程封闭 如果仅仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭,它是实现线程安全性的最简单的方式之一.当某个对象封闭在一个线程中时,这种方法将自动实现线程安全性,即使被封闭的对象本生不是线 ...
- JAVA并发编程实战---第三章:对象的共享
在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程序中,要对内存操作的执行顺序进行判断几乎无法得到正确的结果. 非原子的64位操作 当 ...
随机推荐
- OpenvSwitch/OpenFlow 架构解析与实践案例
目录 文章目录 目录 前言 软件定义网络(SDN) 虚拟交换机(vSwitch) 为什么说云计算时代的 SDN 非常重要 OpenFlow 简介 Open vSwitch Open vSwitch 的 ...
- Tomcat启动报错:“通配符的匹配很全面, 但无法找到元素 'tx:annotation-driven' 的声明“
从报错信息就可以明显察觉到是xml配置文件出现的问题 <?xml version="1.0" encoding="UTF-8"?> <bean ...
- selenium.common.exceptions.WebDriverException: Message: ‘geckodriver’ executable needs to be in PATH
问题,找不到’geckodriver’ 的环境path,解决方案 下载geckodriver.exe 放到Firefox的安装目录下,如:(D:\火狐\Mozilla Firefox): 将火狐安装目 ...
- HDU 1160 FatMouse's Speed (动态规划、最长下降子序列)
FatMouse's Speed Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) ...
- [转帖]Linux杂谈: 树形显示多级目录--tree
Linux杂谈: 树形显示多级目录--tree https://www.cnblogs.com/tp1226/p/8456539.html tree -L 最近写博客的时候偶尔会需要将文件目录结构直观 ...
- springboot笔记-thymeleaf
简介:Thymeleaf 是⾯向 Web 和独⽴环境的现代服务器端 Java 模板引擎,能够处理 HTML.XML.JavaScript.CSS 甚至纯文本.Thymeleaf 的作用域在 HTML ...
- HDU-4332-Constructing Chimney
题目描述 用\(1*1*2\)的砖头摆出如图所示的烟囱,可以横着摆也可以竖着摆,求摆出\(n\)层高的烟囱会有多少种不同的方案. Input 一共有\(T\)组数据. 每组数据包含一个\(n(1 \l ...
- MySQL总结(4)
insert 数据的插入 INSERT插入数据
- APT高持续渗透攻击-后门篇
APT是指高级持续性威胁, 利用先进的攻击手段对特定目标进行长期持续性网络攻击的攻击形式,APT攻击的原理相对于其他攻击形式更为高级和先进,其高级性主要体现在APT在发动攻击之前需要对攻击对象的业务流 ...
- java线程捕获异常
java多线程程序中,所有线程都不允许抛出未捕获的checked exception(比如sleep时的InterruptedException),也就是说各个线程需要自己把自己的checked ex ...