想要使用多线程编程,有一个很重要的前提,那就是必须保证操纵的是线程安全的类.

那么如何构建线程安全的类呢? 1. 使用同步来避免多个线程在同一时间访问同一数据. 2. 正确的共享和安全的发布对象,使多个线程能够安全的访问它们.

那么如何正确的共享和安全的发布对象呢? 这正是这篇博客要告诉你的.

1. 多线程之间的可见性问题.

为什么在多线程条件下需要正确的共享和安全的发布对象呢?

这要说到可见性的问题:

在多线程环境下,不能保证一个线程修改完共享对象的数据,对另一个线程是可见的.

一个线程读到的数据也许是一个过期数据,这会导致严重且混乱的问题,比如意外的异常,脏的数据结构,错误的计算和无限的循环.

举个例子:

    private static class RenderThread extends Thread{
@Override
public void run(){
while(!ready){
Thread.yield();
}
System.out.println("num = " + num);
} } public static void main(String [] args) throws InterruptedException {
new RenderThread().start();
num = 42;
ready = true; }
}

new RenderThread().start()表示创建一个新线程,并执行线程内的run()方法 ,如果ready的值是false,执行Thread.yield()方法(当前线程休息一会让其他线程执行),这时候再交给main方法的主线程执行,给num赋值42,ready赋值true,然后在任务线程中输出num的值.因为可见性的问题,任务线程可能没有看到主线程对num赋值,而输出0.

我们接下来来看看发布对象也会引发的可见性问题.

2. 什么是发布一个对象

发布: 让对象内被当前范围之外的代码所使用.

public class Publish {
public int num1; private int num2; public int getNum2(){
return this.num2;
}
}

无论是 publish.num1 还是 publish.getNum2()哪种方法,只要能在类以外的地方获取到对象,我们就称对象被发布了.

如果一个对象在没有完成构造的情况下就发布了,这种情况叫逸出.逸出会导致其他线程看到过期值,危害线程安全.

常见的逸出的情况:

1.最常见的逸出就是将对象的引用放到公共静态域(public static Object obj),发布对象的引用,而在局部方法中实例化这个对象.

public class Test {
public static Set<Object> set; public void initialize(){
set = new HashSet<>();
}
}

2.发布对象的状态,而且状态是可变的(没用final修饰),或状态里包含其他的可变数据.

public class UnsafeStates {
private String [] states = new String[]{"a","b","c"}; public String[] getStates(){
return states;
}
}

3.在构造方法中使用内部类. 内部类的实例包含了对封装实隐含的引用.

public class UnsafeStates {

    private Runnable r;

    public UnsafeStates() {
r = new Runnable() {
@Override
public void run() {
// 内部类在对象没有构造好的情况下,已经可以this引用,逸出了
// do something;
}
};
}
}

逸出主要会导致两个方面的问题:

  1. 发布线程以外的任何线程都能看到对象的域的过期值,因而看到的是一个null引用或者旧值,即使此刻对象已经被赋予了新值.
  2. 线程看到对象的引用是最新的,但是对象的状态却是过期的.

我们已经了解了逸出的问题,那么如何安全的发布一个对象呢?

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见(也就是说安全发布就是保证对象的可见性),一个正确创建的对象可以通过下列条件安全发布:

  1. 通过静态初始化器初始化对象的引用.
public class NoVisibility {

    public static Object obj = new Object();

}
  1. 将它的引用存储到volatile域或AtomicReference;
public class NoVisibility {

    public volatile Object obj = new Object();

}

Volatile可以保证可见性.性能消耗也只比非volatile多一点,但是不要过度依赖volatile变量,它比使用锁的代码更脆弱,更难以理解,

使用volatile的最佳方式就是用它来做退出循环的条件.

使用volatile的例子:

public class Cycle {
private boolean condition; public void loop(){
while (condition){
//do something..
}
} public void changeCondition(){
if(condition == true){
condition = false;
}else{
condition = true;
}
}
}

3.将它的引用储存到正确创建的对象的final域中.

public class NoVisibility {

    public final Object obj = new Object();

}

4.或者将它的引用存储到由锁正确保存的域中.

public class NoVisibility {

    private Hashtable<String,Object> hashtable = new Hashtable<>();

    public void  setHashtable(){
Object obj = new Object();
hashtable.put("obj",obj);
} }

不限于HashTable,只要是线程安全的容器都行.

现在我们了解了如何安全的发布一个对象,那么问题来了,是否所有对象都需要安全发布?安全发布的对象是否就是线程全的了?

让我们继续往下看.

3. 如何构建一个线程安全的类.

我们先来回答上面的第一个疑问,是否所有对象都需要安全发布?答案都是否定的.

要回答这个问题,我们先简单了解一下以下的三种对象:

1.不可变对象

2.高效不可变对象

3.可变对象

1.不可变对象:创建后不能被修改的对象叫不可变对象,不可变对象天生是线程安全的.

不可变对象不仅仅是所有域都是final类型的,只有满足如下状态才是不可变对象:

1.1 它的状态不能在创建后改变.(包括状态包含的其他值也不可做修改,比如状态是一个集合list,list里面的值也不可以修改,或者状态是一个对象,那么对象的状态也不更改)

1.2.所有域都是final类型的.

1.3.它被正确创建(创建期间没有this引用的逸出)

2.不可变对象: 技术上是可以改变的,但是实际应用程序中,不会被改变

用高效不可变对象可以简化开发,并由于减少了同步的使用,还会提高性能.

3.可变对象: 就是可变对象.

下面就是三种对象的发布机制,发布对象的必要条件依赖于对象的可变性:

  1. 不可变对象可以通过任意机制发布;
  2. 高效不可变对象必须要安全地发布;
  3. 可变对象必须要安全发布,同时必须要线程安全或者是被锁保护.

最后一个问题安全发布的对象是否就是线程全的了?

安全发布只能保证对象发布时的可见性,所以要保证线程的安全就要根据对象的可变性,通过同步+安全发布来保证线程安全.

关于同步和线程安全的知识可以看我的上一篇博客从零开始学多线程之线程安全(一)

这两篇博客的知识点加在一起就可以构建线程安全类了.

在下一篇博客中,我会为大家介绍一些构建线程安全类的模式,这些模式让类更容易成为线程安全的,并且不会让程序意外破坏这些类的线程安全性.

本期分享就到这了,我们下篇再见!

并发编程学习笔记之可见性&过期数据(二)的更多相关文章

  1. 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题

    再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁. 什么是内置锁? 在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对 ...

  2. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  3. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  4. 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理

    (一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...

  5. 并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用

    (一)什么是AQS? 阅读java文档可以知道,AbstractQueuedSynchronizer是实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量.事件,等等)提供一个框架, ...

  6. 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理

    (1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...

  7. 并发编程学习笔记(15)----Executor框架的使用

    Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...

  8. 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理

    1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...

  9. 并发编程学习笔记(13)----ConcurrentLinkedQueue(非阻塞队列)和BlockingQueue(阻塞队列)原理

    · 在并发编程中,我们有时候会需要使用到线程安全的队列,而在Java中如果我们需要实现队列可以有两种方式,一种是阻塞式队列.另一种是非阻塞式的队列,阻塞式队列采用锁来实现,而非阻塞式队列则是采用cas ...

随机推荐

  1. 使用bat文件实现批量重命名功能

    在生活中我们总会碰到对大量文件进行重命名操作,这时如果一个一个的,选取文件→右键→重命名→选取文件,这样操作势必会浪费大量时间. 现在小编就告诉大家一个使用bat文件(命令行)的方法,快速对文件进行重 ...

  2. 异常:java.lang.IllegalStateException: No instances found of configserver(里面是一个微服务名)

    今天本地测试代码时出现了个异常,该异常出现的原因是:微服务启动的顺序出现了问题: 应该先启动本地eureka,然后在启动本地配置中心,然后在启动具体的微服务.

  3. 接口方式[推荐]/动态SQL语句

    MVC目录结构: Src -- com.shxt.servlet[控制层] --com.shxt.service[业务逻辑层] --com.shxt.model[实体Bean,用来承载数据] --co ...

  4. (转)红帽 Red Hat Linux相关产品iso镜像下载【百度云】【更新6.7 Boot Disk】

    不为什么,就为了方便搜索,特把红帽EL 5.EL6 的各版本整理一下,共享出来. RedHat Enterprise Server 6.7 for i386 Boot Disk:rhel-server ...

  5. 【转】LTE 全过程流程

    LTE 过程全流程 1. UE处于关闭状态 2. 打开UE电源. 3. 搜索附近的频率 4. 同步时间 5. 小区搜索 6. 小区选择 7. 解码MIB 8. 解码SIB 9. 初始化RACH过程 1 ...

  6. HTML 和 CSS

    HTML html是英文hyper text mark-up language(超文本标记语言)的缩写,它是一种制作万维网页面标准语言.   内容摘要   Doctype 告诉浏览器使用什么样的htm ...

  7. Jmeter 分布式压测及可能出现的问题;

    (注:master与slave机的jmeter版本必须保持一致) master机器上的准备工作如下: 1.先准备一个调试通过的下单接口: 2.找到jmeter的bin目录下的jmeter.proper ...

  8. d-s证据理论

    证据理论是Dempster于1967年首先提出,由他的学生Shafer于1976年进一步发展起来的一种不精确推理理论,也称为Dempster/Shafer 证据理论(D-S证据理论),属于人工智能范畴 ...

  9. C# 实现快捷键几种方法

    本文讲解了三种方法实现C# button快捷键,如Alt + *(按钮快捷键),Ctrl+*及其他组合键等. 一. C# button快捷键之第一种:Alt + *(按钮快捷键) 在大家给button ...

  10. 侯捷STL学习(一)--顺序容器测试

    开始跟着<STL源码剖析>的作者侯捷真人视频,学习STL,了解STL背后的真实故事! 视频链接:侯捷STL 还有很大其他视频需要的留言 第一节:STL版本和重要资源 STL和标准库的区别 ...