文档章节

java并发-对象的共享

贲大侠
 贲大侠
发布于 2017/09/05 19:32
字数 4134
阅读 13
收藏 0
点赞 1
评论 0

1.可见性

    在单线程环境中,如果向某变量写入值,然后在没有其他操作的情况下读取这个变量,那么总能的到相同的值,没毛病。但是在多线程环境下,当读、写操作在不同的线程中执行时候,得到的结果便很容易出现问题,因为在线程进行读操作的时候,很难看得到其他线程在写入值。所以为了确保可见性,可以使用同步机制。在  java并发-线程安全性 中的目录3有介绍过可见性问题的例子,可以看下。

1.1 volatile变量

    volatile变量可以确保一个变量在更新时用一种可预见的方式来告知其他线程。当一个域声明为volatile类型后,编译器运行时会监视这个变量,它是共享的,针对它的操作不会与其他的内存操作一起被重排序。volatile不会被缓存在寄存器或者对其他处理器隐藏的地方。(重排序:从JVM并发看CPU内存指令重排序(Memory Reordering))

    我们可以把volatile想象成如下代码:

	private int value;
	
	public synchronized int get(){    //把volatile变量的读操作看做为get()方法
		return value;
	}
	
	public synchronized void set(int value){ //把volatile变量的写操作看做为set()方法
		this.value = value;
	}

    把volatile的读写分别视为get()和set()方法。当然,这也只是想象,在访问volatile时不会进行加锁的操作,没了锁,就没了线程阻塞,所以可以把volatile看为更轻量级的同步机制。

    volatile提供了可见性,假设线程A写入一个volatile变量并且线程B随后读取该变量,在A写入volatile变量之前,volatile变量对A都是可见的,在B读取了volatile变量之后,对B也是可见的。也就是说从内存可见性的角度来看,写入volatile相当于退出同步,读取相当于进入同步。但是,volatile内存可见性上的作用不是最强的,以后会介绍更加牛逼闪闪的。

下面列一个volatile的经典用法:

private volatile boolean asleep;

public  void test(String str) {
	while (!this.asleep) {
         
	}
}

     检查asleep是否退出循环,当asleep被修改时,如果不是用volatile修饰的变量,执行线程在判断时无法发现当前asleep被修改为true。在使用volatile修饰以后,增强了可见性,就会避免一些问题的发生。

    volatile变量通常被当做是标识完成、中断、状态的标记使用。它也存在一些限制。比如用volatile修饰的变量在进行递增操作的时候是无法满足  操作 的原子性。volatile变量只能保证可见性,而加锁可以保证可见性和原子性。 

    只有满足下面所有的标准后,你才能使用volatile变量:

    1.写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

    2.变量不需要与其它的状态量共同参与不变约束

    3.访问变量时,没有其他的原因需要加锁

2.发布与逸出

    发布一个对象的意思是指,让对象能够在当前作用域之外被使用。比如将指向该对象的引用保存到其他代码可以访问的地方,或者在一个非私有的反方返回该引用,或者把引用当作参数传递到其他类的方法中。举个例子:

public class A {
	public static List<C> list;
	
	public void initialize(){
		list = new ArrayList<>();
	}
	
	public List<C> getList(){//在一个非私有的反方返回该引用
		return list;
	}
	
	public void setBList(){
		B b  = new B();
		b.setList(list);//把引用当做参数传递到其他类的方法中
	}
}

    在很多情况下,我们需要确保对象以及其内部状态不被发布。而有时候又要发布某个对象,想要确保线程安全,则可能需要同步。发布内部状态可能会破坏封装性,难以维持程序的不变性条件。如果在对象构造完之前就发布该对象,就破坏了线程安全性。如果将一个C对象添加到list中,那么C也被发布了,任何代码都可以遍历这个list来获取C的引用。

    当一个不该被发布的对象被发布的时候,这种情况被称之为逸出。

public class D {
	private String[] str = new String[] { "a", "b" };//私有的String数组

	public String[] getStr() {//公共方法把私有的数组发布了,尴尬不。
		return this.str;
	}
}

    上面的代码 方法getStr()发布了 str数组,也就是说任何调用getStr()方法的代码都有可以修改str数组内的内容,这种情况就是逸出,str已经逸出了所在的作用域,一个私有的数组被发布了以后可以被任何调用getStr()的代码蹂躏,毫无尊严可言。在发布一个对象的时候,只要是不是私有的都会被发布。

    还有一种this逸出就是在发布一个内部的类实例的时候:

public class ThisEscape {
	public ThisEscape(EventSource source) {
		source.registerListener(new EventListnener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		});
	}
}

    在ThisEscape的构造方法进行初始化的时候,registerListener执行完毕后发布了一个EventListnener对象,这个时候已经是this逸出了,问题在于初始化还没完毕。也就是从ThisEscape的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。

2.1 安全的对象构造过程

public class ThisEscape {
	private final EventListnener listener;//在ThisEscape的作用域内声明私有的引用listener

	public ThisEscape() {//构造方法初始化
		listener = new EventListnener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		};
	}
	
	public static ThisEscape newInstance(EventSource source){
		ThisEscape escape = new ThisEscape();//这一行跑完,保证escape的构造方法初始化完毕,是一个完整的对象
		source.registerListener(escape.listener);//在进行registerListener()的发布
		return escape;
	}
}

    把ThisEscape的构造方法定位私有的,其次构造未完成时不进行发布,然后定义一个工厂方法,在工厂方法 new一个ThisEscape,ThisEscape 的构造方法执行完毕后,在进行registerListener的发布,来防止this逸出。

    简单地说上述代码就是:不要在ThisEscape构造初始化的时候发布(即调用registerListener),设立工厂方法的作用就是为了保证ThisEscape构造完毕后在发布。

3.线程封闭

    当访问共享可变数据时,通常需要加同步。一种避免使用同步的方式就是不共享数据。这种技术被称之为线程封闭,它是实现线程安全性最简单的方式之一。当某个对象被封闭在一个线程的时候,这种方法自动实现线程安全线,即使这个对象不是线程安全的。

3.1 ThreadLocal

    维持线程封闭性的一种规范的方式是使用ThreadLocal,简单的说这个类能够使线程中的某个值与保存值的对象关联起来。其中有get和set的方法,这些方法为每个使用该变量的线程都存有一份独立的副本,互不干扰,从而达到一种隔离的效果。

3.1.1 使用

    ThreadLocal对象通常用户防止可变的变量进行共享,举个栗子:

public class TestThreadLocal {
	private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
		@Override
		protected Integer initialValue() {
			return 0;//ThreadLocal value初始化值为0
		}
	};
	
	
	public  static void main(String[] args) {
		for(int i=0;i<5;i++){
			new Thread(new MyThread(i)).start();
		}
	}
	
	static class MyThread implements Runnable{
		private int i ;
		
		public  MyThread(int i) {
			this.i = i;
		}
		
		@Override
		public  void run() {
			System.out.println("线程"+i+"初始value:"+value.get());
			for(int x=0; x<10;x++){
				value.set(value.get()+x);//给当前线程set变量值
			}
			System.out.println("线程"+i+"累计后value:"+value.get());
		}
		
	}
}

    结果:

线程0初始value:0
线程0累计后value:45
线程1初始value:0
线程1累计后value:45
线程2初始value:0
线程2累计后value:45
线程4初始value:0
线程4累计后value:45
线程3初始value:0
线程3累计后value:45

    就像上面说的,每个使用该变量的线程都存有一份独立的副本,无论什么操作都是互不干扰的。达到了线程内部的隔离效果。

    再举个大家比较熟悉的栗子:

public class TestThreadLocal2 {
	private static ThreadLocal<Connection> local = new ThreadLocal<Connection>(){
		protected Connection initialValue() {
			return DriverManager.getConnection(DBURL);
		};
	};

	public static Connection getConnection(){
		return local.get();
	}
}

    单线程应用程序中可能会维持一个全局的数据库连接,启动时会初始化这个链接,从而避免在调用每个方法时都要传递Connection对象。但是JDBC的链接对象不一定是线程安全的,如果多线程程序在没有同步的情况下使用全局的Connection对象时候,线程安全性就是个问题,这时候如果把Connection存在ThreadLocal里的话,让每个线程都拥有自己的链接,那么就安全多了。

    当某个频繁执行的操作要加一个临时对象,同时又希望每次执行时不重复分配的时候,也可以用到ThreadLocal。

3.1.2 原理解析

    先看下ThreadLocal的源码

protected T initialValue() {
        return null;
}

    initialValue()默认返回当前线程的初始值,默认为null。可以在实例化的时候初始化initialValue。

    ThreadLocal的构造方法啥也没做。

public ThreadLocal() {

}

    ThreadLocal最重要的两个方法就是get()和set(),让每个线程都存有一份独立的副本,互不干扰,从而达到一种隔离的效果。

    set()和get()实现涉及到一下几个类:

    Thread 线程类,代表一个线程

    ThreadLocalMap    可以把它看成一个hashmap,但和java.util.Map没什么关系,只是跟hashmap实现方式类似,都是采用哈希表的方式进行存储。

    ThreadLocalMap.Entry:把它看成是保存键值对的对象。

3.1.2.1 ThreadLocal.set()解析

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    set()方法的整体流程很简单,先通过Thread.currentThread()获得当前线程的引用,然后获取当前ThreadLocalMap的实例map,判断map是否为空,不为空的话,以当前threadlocal为key,再把value传入,如果为空则创建一个ThreadLocalMap,把键和值保存在其中。

    整体流程讲完,深入一下ThreadLocal,ThreadLocalMap,Thread之间的关系,它们是如果工作的。可以看到set()方法刚进入的时候,获取到了当前线程的引用也就是Thread t = Thread.currentThread()方法,这个方法是Thread的方法,那么进入Thread看下源码:

    /**
     * Returns a reference to the currently executing thread object.
     *
     * @return  the currently executing thread.
     */
    public static native Thread currentThread();

    Thread.currentThread直接返回了当前线程,那么回到ThreadLocal往下看,ThreadLocalMap map = getMap(t);这里根据当前线程获取了当前ThreadLocalMap的实例map,那么看下getMap(t)是怎么实现的:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

    可以看到getMap直接返回了当前线程的threadLocals,那么这个threadLocals是什么呢,再看下Thread的源码:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    Thread在初始化的时候就声明了一个ThreadLocalMap引用,也就是每个线程在创建的时候都会有一个ThreadLocalMap的引用。这里才是关键的地方,简单的说之所以能够让每个线程都实现从内部隔离,单独副本就是因为每个线程在创建的时候就有了自己的ThreadLocalMap。

    画面再回到ThreadLocal的set源码,当map为空时候创建一个ThreadLocalMap,接下来就很好理解了,直接调用createMap(t, value)为当前线程创建一个ThreadLocalMap对象用来存储:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3.1.2.2 ThreadLocal.get()解析

    解析完set(),get()就好理解多了,看下get源码:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    get()的整体流程:先通过Thread.currentThread()获得当前线程的引用,然后获取当前ThreadLocalMap的实例map,判断map是否为空,不为空的话,map调用getEntry(this)来获取当前保存键和值的对象,在对象中获取值来返回存储的值。如果为空,则调用一个初始化set的方法setInitialValue(),setInitialValue()获取了当前ThreadLocal的初始化默认值,创建一个ThreadLocal进行存储。

    这里有必要看下setInitialValue()这个方法,看下源码:

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    可以看到其实这个初始化set()的方法就是获取了当前ThreadLocal的初始值,然后判断了ThreadLocal  是否为空,不为空把初始值放入map,为空直接创建个ThreadLocalMap然后再存储初始值。

    以上就是ThreadLocal的存取机制。

4.不变性

    只要对象的状态不改变,保持不变性,那么对象一定是线程安全的。

    当对象满足一下条件的时候,对象才是不可变的:

    1.对象创建以后其状态就不能修改。

    2.对象的所有域都是final类型

    3.对象都是正确创建的,在对象创建期间this引用没有逸出

    4.对所有的变量不提供set方法。

    5.所有变量都是私有的

4.1 final

    final是java中的保留关键字,可以适用于类,方法,变量。一旦引用声明为final,改引用将不能被改变。 

4.1.1 final变量

    如果将变量声明为final变量,那么这个变量是只读的。

public class F {
	private final int i = 0;
	public void a() {
		i++;//编译器会在这一行报错 The final field F.i cannot be assigned
	}
}

4.1.2 final方法

    如果在方法前加上final证明这个方法不可以被子类的方法重写。

public class F {
	public final void test() {
		System.out.println("666");
	}

	class F1 extends F {
		@Override
		public void test() {   
			// 方法报错
			// Multiple markers at this line
		    // - Cannot override the final method from F
			// - overrides com.ben.test1.F.test
		}
	}
}

4.1.3 final类

    使用final来修饰的类叫做final类,final类除了不能被继承以外,其余都和普通类一样。

public final class F {
	public final void test() {
		System.out.println("666");
	}

	class F1 extends F { //The type F1 cannot subclass the final class F
	}
}

     创建不可变类就需要使用final关键字,对象一旦创建便不可以修改,且需要满足上面不变性的条件。但是对于集合对象,如果声明为final类型,那么是可以进行删除,增加等操作的,如果想要保证不变性,那就要对类进行“正确的构造对象”,比如:    

public final class F {//final类
	private final List<String> list = new ArrayList<>();
	
	public F() { //正确滴构造对象
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
	}
	
	public List<String> getList(){ //只对外提供get方法
		return list;
	}
}

5.安全发布

//不安全发布
public Holder holder;

public void init(){
      holder = new Holder(10);
}

public class Holder {
	private int n;
	public Holder(int n) {
		this.n = n;
	}
	
	public void assertSanity() {
		if(n!=n) {
			throw new AssertionError("This statement is false.");
		}
	}
}

    上述代码比较有疑惑的地方就是Holder里的  if(n!=n) 看起来屌,n怎么可能不等于n呢,其实却是有可能不等于n。假设有两个线程A和B,A调用了init(),B调用了assertSanity(),那么此时会出现这样两种情况:

    首先有可能会出现由于线程A调用的init()还没有创建完毕对象,线程B调用了assertSanity(),也就是说线程B看到了线程A还未创建完毕的对象,那么结果肯定会报出空指针的问题。

    其次在A调用了init(),如果Holder的构造方法还没构造完毕,B就调了assertSanity(),那么此时Holder的n变量就是0,在if(n!=n)时,第一次获取n为0,而第二次获取n变量的时候,这时候构造方法执行完毕了,n变成了10,那么结果很明显,尴尬了。

    我们可以改造下代码:

public valetile Holder holder; //使用volatile声明来保证holder的更新状态是随时可见的。

public void init(){
      holder = new Holder(10);
}

public class Holder {
    //使用final来防止n在构造方法没有执行完毕被赋值为0的情况(用final声明的变量是必须等待构造完毕才能使用的 嘿嘿嘿...)
	private final int n;

	public Holder(int n) {
		this.n = n;
	}
	
	public void assertSanity() {
		if(n!=n) {
			throw new AssertionError("This statement is false.");
		}
	}
}

                                  

© 著作权归作者所有

贲大侠
粉丝 1
博文 18
码字总数 17703
作品 0
海淀
程序员
java面试必备之ThreadLocal

按照传统的经验,如果某个对象是非线程安全的,在多线程环境下对象的访问需要采用synchronized进行同步。但是模板类并未采用线程同步机制,因为线程同步会降低系统的并发性能,此外代码同步解...

编程老司机 ⋅ 05/16 ⋅ 0

Java 面试知识点解析(三)——JVM篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部...

我没有三颗心脏 ⋅ 05/16 ⋅ 0

11、Java并发性和多线程-Java内存模型

以下内容转自http://ifeve.com/java-memory-model-6/: Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内...

easonjim ⋅ 2017/06/15 ⋅ 0

计算机科学中抽象的好处与问题—伪共享实例分析

David John Wheeler有一句名言“计算机科学中的任何问题都可以通过加上一层间接层来解决”,一层不够就再加一层。后半句是我加的 (* ̄︶ ̄) ,虽然有点玩笑的意思,但是也的确能说明一些问题...

MageekChiu ⋅ 01/10 ⋅ 0

JAVA虚拟机 JVM 详细分析 原理和优化(个人经验+网络搜集整理学习)

JVM是java实现跨平台的主要依赖就不具体解释它是什么了 ,简单说就是把java的代码转化为操作系统能识别的命令去执行,下面直接讲一下它的组成 1.ClassLoader(类加载器) 加载Class 文件到内...

小海bug ⋅ 06/14 ⋅ 0

悲观的并发策略——synchronized互斥锁

互斥锁是最常见的同步手段,在并发过程中,当多条线程对同一个共享数据竞争时,它保证共享数据同一时刻只能被一条线程使用,其他线程只有等到锁释放后才能重新进行竞争。 对于Java开发人员,...

wangyangzhizhou ⋅ 04/16 ⋅ 0

Java 编程之美:并发编程基础晋级篇

本文来自作者 加多 在 GitChat 上分享 「Java 并发编程之美:并发编程基础晋级篇」 编辑 | Mc Jin 借用 Java 并发编程实践中的话,编写正确的程序并不容易,而编写正常的并发程序就更难了! ...

gitchat ⋅ 04/18 ⋅ 0

14、Java并发性和多线程-Java ThreadLocal

以下内容转自http://ifeve.com/java-theadlocal/: Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作。因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有...

easonjim ⋅ 2017/06/16 ⋅ 0

Java多线程学习(四)等待/通知(wait/notify)机制

系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Ja...

一只蜗牛呀 ⋅ 04/16 ⋅ 0

JVM自动内存管理机制—读这篇就够了

之前看过JVM的相关知识,当时没有留下任何学习成果物,有些遗憾。这次重新复习了下,并通过博客来做下笔记(只能记录一部分,因为写博客真的很花时间),也给其他同行一些知识分享。 Java自动内...

java高级架构牛人 ⋅ 06/13 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

笔试题之Java基础部分【简】【一】

基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法,虚拟机方面的语法,其他 1.length、length()和size() length针对...

anlve ⋅ 22分钟前 ⋅ 1

table eg

user_id user_name full_name 1 zhangsan 张三 2 lisi 李四 `` ™ [========] 2018-06-18 09:42:06 星期一½ gdsgagagagdsgasgagadsgdasgagsa...

qwfys ⋅ 46分钟前 ⋅ 0

一个有趣的Java问题

先来看看源码: public class TestDemo { public static void main(String[] args) { Integer a = 10; Integer b = 20; swap(a, b); System.out......

linxyz ⋅ 51分钟前 ⋅ 0

十五周二次课

十五周二次课 17.1mysql主从介绍 17.2准备工作 17.3配置主 17.4配置从 17.5测试主从同步 17.1mysql主从介绍 MySQL主从介绍 MySQL主从又叫做Replication、AB复制。简单讲就是A和B两台机器做主...

河图再现 ⋅ 今天 ⋅ 0

docker安装snmp rrdtool环境

以Ubuntu16:04作为基础版本 docker pull ubuntu:16.04 启动一个容器 docker run -d -i -t --name flow_mete ubuntu:16.04 bash 进入容器 docker exec -it flow_mete bash cd ~ 安装基本软件 ......

messud4312 ⋅ 今天 ⋅ 0

OSChina 周一乱弹 —— 快别开心了,你还没有女友呢。

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @莱布妮子 :分享吴彤的单曲《好春光》 《好春光》- 吴彤 手机党少年们想听歌,请使劲儿戳(这里) @clouddyy :小萝莉街上乱跑,误把我认错成...

小小编辑 ⋅ 今天 ⋅ 8

Java 开发者不容错过的 12 种高效工具

Java 开发者常常都会想办法如何更快地编写 Java 代码,让编程变得更加轻松。目前,市面上涌现出越来越多的高效编程工具。所以,以下总结了一系列工具列表,其中包含了大多数开发人员已经使用...

jason_kiss ⋅ 昨天 ⋅ 0

Linux下php访问远程ms sqlserver

1、安装freetds(略,安装在/opt/local/freetds 下) 2、cd /path/to/php-5.6.36/ 进入PHP源码目录 3、cd ext/mssql进入MSSQL模块源码目录 4、/opt/php/bin/phpize生成编译配置文件 5、 . ./...

wangxuwei ⋅ 昨天 ⋅ 0

如何成为技术专家

文章来源于 -- 时间的朋友 拥有良好的心态。首先要有空杯心态,用欣赏的眼光发现并学习别人的长处,包括但不限于工具的使用,工作方法,解决问题以及规划未来的能力等。向别人学习的同时要注...

长安一梦 ⋅ 昨天 ⋅ 0

Linux vmstat命令实战详解

vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。这个命令是我查看Linux/Unix最喜爱的命令...

刘祖鹏 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部