文档章节

JUC系列二:对象的发布与共享

那位先生_
 那位先生_
发布于 2015/08/01 17:28
字数 2117
阅读 52
收藏 0

系列一讲了如何通过同步来避免多个线程访问共享的变量,这一节讲如何发布与共享对象,使它能够安全的被多个线程所访问。

###发布和逸出

所谓发布一个对象就是使对象能够被当前作用域外的代码所访问,例如在一个非私有方法中返回这个对象的引用

public class Test{
	private Student o=new Student();
	//....
	public Student getStudent(){
		return o;
	}
}

本来对象o是无法在Test类外被访问的,但getObject方法返回了这个对象的引用,这样就使得在任何一个类中,都可以通过这个方法来获取对象o,从而修改对象o的属性。我们称这种行为叫发布了对象o。

在程序开发过程中,虽然大多数情况下都需要确保对象不被外部访问,但在某些情况下确实需要发布一个对象,这时就可能带来两个线程的安全性问题:第一是当你发布一个对象时,其它的线程就能够修改这个对象的属性,就会破坏封装性,使得程序难以维持不变性条件;在上面的例子中,对于任何一个线程,都能够修改对象o的状态,如果对象o与Test的其它属性存在不变性关系时,这种关系就容易被打破。二是在发布过程中,可能会发布一个还没有正确构造完的对象,这种情况叫逸出,像下面的例子

public class Escape{
	private int thisCanBeEscape = 0;
	public Escape(){
	    new InnerClass();
	}
	private  class InnerClass {
		public InnerClass() {
			//这里可以在Escape对象完成构造前提前引用到Escape的private变量
			System.out.println(Escape.this.thisCanBeEscape);
		}
	}
}

很显然,在Escape对象没有构造完全时。InnerClass的构造方法中就能够访问到这个还没有构造完全的对象,从而产生线程安全问题,这种情况叫this引用逸出

###可变与不可变

如果某个对象在被创建后其状态不能被修改,那么这个对象就称为不可变对象,而不可变对象一定是线程安全的。虽然Java语法规范和内存模型并没有给出不可变的定义,但不可变并不等于将所有的域(状态)都声明为final,而且,就算全为final,对象也可能是可变的,因为在final类型的域中,可以保存对可变对象的引用。

当一个对象满足以下所有的条件时,该对象才是不可变的:

  • 对象创建后其状态就不能修改

  • 对象的所有域都被final修饰(并不是十分准确,比如String类型是不可变的,但其hash域就没有被final修饰)

  • 对象是正确创建的(创建是没有发生上面的this引用逸出)

而如果对象在构造后域可以被修改,那么就称之为可变对象。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对对象的访问时同样需要使用同步来确保后续修改操作的可见性,要安全的共享可变对象,这些对象就必须被安全地发布,并且访问时这个对象要么是线程安全的,要么由某个锁保护起来。

还有一种情况是,如果对象从技术上来看是可变的,但其状态在发布后就不会再改变,那么这种对象称之为事实不可变对象,例如,Date本身是可变的,但如果将它作为不可变对象来使用,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。假设需要维护一个Map对象,其中保存了每位用户的最近登录时间:

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

如果Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就足以使Date值被安全地发布,并且在访问这些Date值时不需要额外的同步。 (这里开始有点不明白,难道放在ArrayList中就不是被安全的发布么?后来想,放在ArrayList中并不能保证Date的可见性,所以不一定能被安全的发布,应该是这个道理)

###安全发布 要安全的发布一个对象,对象的引用以及对象的状态必须同时保持对其它线程可见,一个正确创建的对象(没有this引用逸出)可以通过以下方式来安全的发布,

  • 在静态初始化函数中初始化一个对象引用(也就是使用静态变量)

  • 将对象的引用使用volatile修饰或者保存到AtomicReference对象中

  • 将对象的引用保存到某个正确构造对象的final域中

  • 将对象引用保存到一个由锁保护的域中

对象的发布需求取决于它的可变性:

  • 对于不可变对象可以通过任意机制来发布

  • 对于事实不可变对象必须通过以上安全的方式来发布(比如上面的Date)

  • 对于可变的对象必须通过以上安全的方式来发布,并且该对象要么是线程安全的,要么被某个锁保护起来

###共享对象

在并发程序中使用和共享对象时,可以使用一下四个策略:

  • 线程封闭

  • 只读共享

  • 线程安全共享

  • 保护对象

####线程封闭 当访问共享的可变数据时,一种避免使用同步的方式就是不共享数据。对于发布的对象只能由一个线程所拥有,也只能由这个线程修改。这样就不存在线程安全的问题。比较常用的应用就是JDBC的Connection对象,Connection并不是线程安全的,当服务器收到一个请求时,请求所属的线程就向连接池申请一个Connection对象,并且在使用完Connection对象时,连接池不会将它分配给其它的线程,这样,Connection对象就被封闭在线程中,从而保证了Connection的线程安全性。

在Java的核心库中,也提供了一些机制来帮助维持线程的封闭性,例如栈封闭(局部变量)和ThreadLocal类。栈封闭自然不用说,在JVM系列中就有说明,栈是线程独有的,不会共享。所以我们来看看ThreadLocal类。

import java.util.concurrent.atomic.*;
import java.util.*;
public class ThreadLocalDemo{
	private static final AtomicInteger count=new AtomicInteger(0);
	public static ThreadLocal<Integer> integerHolder=new ThreadLocal<Integer>(){
		public Integer initialValue(){
			return count.incrementAndGet();
		}
	};
	public static void main(String []args){
		for(int i=0;i<2;i++){
			Thread thread=new Thread(new MyThread());
			thread.start();
		}
	}
}
class MyThread implements Runnable{
	private static int count=0;
	private static final Random rand=new Random();
	public void run(){
		try{
			while(true){
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName()+" value is "+ThreadLocalDemo.integerHolder.get());
				count++;
				if(count%5==0){
					ThreadLocalDemo.integerHolder.set(count);
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

从结果可以看的出来,同一个线程的value获取多少次都是同一个值,当某个线程初次调用ThreadLocal.get()方法时,就会调用initialValue来获取初始值,可以把ThreadLocal当作一个Map<Thread,T>对象来看,其键是当前线程,其值为特定于该线程的值。

####只读共享

在没有同步的情况下,共享只读对象可以保证线程的安全性,因为任何线程都不能修改对象的状态,也就不会存在线程安全问题。共享的只读对象包括不可变对象和事实不可变对象(需要使用安全的方式发布)。

####线程安全共享

线程安全的对象在其内部实现了对所有可变状态的操作进行了同步,从而使得多线程环境中可以通过对象的共有接口来进行访问而不需要外加同步处理。JavaAPI中很多这样的类,比如Vector。但这种对象遇到判断添加的情况时就可能会遇到原子性的问题。

####保护对象

保护对象就是在访问时通过同步来限制对象在同一时刻只能被一个线程访问,在访问对象时,每个线程都需要获取特定的锁。

© 著作权归作者所有

那位先生_

那位先生_

粉丝 131
博文 109
码字总数 242433
作品 0
深圳
后端工程师
私信 提问
加载中

评论(0)

Java 多线程系列目录(共43篇)

Java多线程系列目录(共43篇) 最近,在研究Java多线程的内容目录,将其内容逐步整理并发布。 (一) 基础篇 01. Java多线程系列--“基础篇”01之 基本概念 02. Java多线程系列--“基础篇”02之 ...

foxeye
2016/02/29
346
0
1、JUC系列之---线程基础

一、多线程概述 1、进程:正在进行中的程序 2、线程:就是进程中一个负责程序执行的控制单元(执行路径) 一个进程中,可以有多个执行路径,即多线程 一个进程中,至少有一个执行路径。 (多...

李李李李格尔楞
2018/05/13
28
0
JUC系列三:对象的委托与组合

在讲之前,我们先看一个Java监视器模式示例,这个示例是用于调度车辆的车辆追踪器,首先使用监视器模式来构建车辆追踪器,然后再尝试放宽某些封装性需求同时又保持线程安全性。每台车都由一个...

那位先生
2015/08/05
115
0
java程序猿技术栈

一、java 基础知识 1.1 java基础集合类 1.2 jdk1.5、1.6、1.7、1.8 特效比较 1.3 java异常处理 1.4 jvm原理及常见问题 1.5 log4j等日志收集 1.6 jdbc驱动 1.7 jdk反射机制使用和原理 1.8 ja...

南寒之星
2016/11/30
17
0
使用数据库悲观锁实现不可重入的分布式锁

一、前言 在同一个jvm进程中时,可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题,但是当多个不同机器上的不同jvm进程共同竞争同一个共享资源时候,juc包的锁就...

阿里加多
2018/06/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

渲染学习笔记——渲染管线介绍及CPU应用阶段

1.GPU优越性及缺点 注意:GPU并行结构if/else两边都会进行计算(现在有改观) 2.渲染流水线 3.CPU应用阶段 Unity有一些资源可以开启read/write选项,当开启后,加载到现存中的数据不会在内存...

myctrd
32分钟前
68
0
在C#中使用Global Mutex的良好模式是什么?

Mutex类非常容易被误解,而全局互斥体更是如此。 创建全局互斥锁时,可以使用哪种良好,安全的模式? 一个会起作用的 无论我的机器位于哪个区域 保证正确释放互斥锁 如果没有获取互斥,可以选...

javail
33分钟前
56
0
从开发到生产上线,如何确定集群规划大小?

在 Flink 社区中,最常被问到的问题之一是:在从开发到生产上线的过程中如何确定集群的大小。这个问题的标准答案显然是“视情况而定”,但这并非一个有用的答案。本文概述了一系列的相关问题...

阿里云官方博客
33分钟前
74
0
Linux就该这么学 -- 命令 -- top&uptime&free

top top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况,常用于服务端性能分析。 在top命令中按f键后,可进入设置页面,可设置显示或隐藏对应的列,可设置按某...

jionzhao
37分钟前
49
0
Go - atomic包使用及atomic.Value源码分析

1. Go中的原子操作 原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到...

Java天天
40分钟前
54
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部