java线程对单个对象的共享的一些方式

Posted 占用我名字

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java线程对单个对象的共享的一些方式相关的知识,希望对你有一定的参考价值。

 最近看了关于java多线程的一些知识,今天总结一下。主要总结的是java多线程对于单个对象共享的控制,主要从可见性、发布逸出、线程封闭、不变性、安全发布5个方面来进行总结,看的书籍为《Java并发边编程实战》。

1、可见性

可见性简单的理解就是,一个线程对某个变量的更改,其他的线程可以看到这个变量更改的值。通过下面的程序分析一下。

public class VisibilityTest 


	private static boolean flag;
	public static void main(String[] args) throws InterruptedException 
		
		new Thread(new ExcuteThread()).start();
		Thread.sleep(1000);
		flag=true;
		System.out.println(flag);
	
	private static class ExcuteThread implements Runnable
	
		@Override
		public void run() 
			while(!flag)
		
	

	

最后的运行结果是:控制台会输出true,但是程序不会停止。 

上述图片是JVM的内存模型(JVM的一些知识在随后的博客中会写),是我从别人博客中复制过来的,这里说明一下。 jvm运行时有一个虚拟机栈和 堆区、方法区,其中虚拟机栈是线程私有的,而堆区和方法区是共享的。上述图片的工作内存可以简单的理解为虚拟机栈,主内存理解为堆区、方法区。也就是所有线程共享的内存。再看上面的代码,其中
private static boolean flag;
这个flag是类的静态成员变量,所以存在与方法区,是线程共享的,所以当有一个线程在执行 某个方法(如上述代码的run方法)使用这个变量时,这个线程就会通过一系列的操作,将主内存的flag复制到自己的工作内存。而当主线程MAIN()方法中修改了主内存flag,但是修改完之前,原来的线程已经将flag的值调用到了自己的工作内存,此时原来的线程就不会再去主内存中访问该变量,直接就从工作内存中访问该变量的缓存。所以就造成了这个flag变量值还是原先的变量。这个变量就是不可见的。如果将变量写成volidate类型,该变量就是可见的。更详细的的请参考下面的链接,写的挺全面 http://www.th7.cn/Program/java/201312/166504.shtml

1.1 非原子的64位操作

在jvm内存模型中,从内存往工作内存中复制都是原子性的,比如int型的数据在内存中占32位的空间,从内存往工作内存中复制时32个字节要一起全部复制到工作内存中,而long和double类型的数据,在内存中占用64位字节,JVM允许将64位的读写操作分两次32位的操作。所以当内存中有个变量long number=1;如果当一个线程要访问这个变量时,而同时另一个线程对number变量修改为20,此时第一个线程可能只读到number前32位,而后再读的时候,可能是修改后的值的后32,所以得到的值可能既不是1,也不是20.

1.2 Volidate修饰变量


java提供了一个稍弱的同步机制,用volidate修饰变量,此时变量具备了可见性,当线程读取该变量时,他会从内存中去读取,不会再读取工作内存中的变量副本。修改该变量时也会更新内存中该变量的值。 但是,volidate修饰的变量,是无法保证变量的同步性的。这里就不写代码了,简单的解释下。具体的知识的上面的地址中有写到,同时推荐《深入理解JAVA虚拟机》这本书,这本书中也有详细介绍。因为volidate修饰的变量虽然可以保证变量的可见性,也就是每次读取该变量的值的时候都会从主内存中去读取。当A线程读取该变量时,在A线程还未讲该变量修改的值同步到主内存中的时候,线程B此时也要读取该变量的值,所以就会造成该变量的不同步问题。 Volidate修饰的变量也可以解决指针重排序的问题(在上述链接和推荐的那本书有详细描述)。 理解Volidate对多线程的理解是很有帮助的。

2.发布和逸出

所谓发布,简单的解释就是A线程创建了一个对象,而其他的线程可以看到这个对象,那么该对象就被发布了。
public class PublishEscape 

	private static PublishEscape pe = null;
	
	private PublishEscape()
	
	
	public static <span style="font-family: Arial, Helvetica, sans-serif;">PublishEscape getInstance()</span>
		if(pe == null)
			pe = new PublishEscape();
		
		return pe;
	


上述代码为一个最简单的单例模式,存在的问题显而易见,缺少同步控制,当A线程调用getInstance方法是,发现pe==null,此时线程B同时也调用该方法,也会发现pe==null,此时就会创建两个PublishEscape实例。本来单例模式只准创建一个对象实例。这个算是对象的逸出。还有一种是对象发布出去后,他的状态可以随意发生改变,或者状态不一致等。都算逸出(这个概念有点模糊,因为水平有限,我也没办法说的太细)
public class Escape 
	private String []state = new String[]"A","B";
	public String[] getSate()
		return state;
	
如上面的代码,当Escape对象发布出去后,任何Escape对象的调用者都可以随意更改state的内容,所以state就已经逸出了它的作用域

2.1 this指针逸出

public class ThisEscape 
	
	private int a = 0;

	public ThisEscape()
		EventClass event = new EventClass();
		new Thread(event).start();
		a = 10;
	

	class EventClass implements Runnable
		
		@Override
		public void run() 
			System.out.println(a);
		
		
	
	public static void main(String args[])
		 new ThisEscape();
	
如上面的例子,因为内部类EventClass包含了对外部类ThisEscape的引用,当内部类的线程输出a变量的时候,外部类的a可能还没有进行a=10这一步操作,造成了状态不一致。所以就造成了ThisEscape这个类的this逸出。以后要防止在构造函数中this逸出的情况。

3 线程封闭

线程封闭主要是将某个变量封装在某个线程内,其他线程无法访问到该变量,例如局部变量,ThreadLocal维持的变量。主要介绍下ThreadLocal。

3.1 ThreadLocal类


ThreadLocal类主要是线程将某个内存共享的类或变量,在堆内存中创建一份只有当前线程可以访问的对象。这个对象是其他线程所看不到的。下面先看一下ThreadLocal 类
public class ThreadLocal<T> 

	public void set(T value) 
	        Thread t = Thread.currentThread(); //得到当前线程
			/**
			得到当前线程下对应的ThreadLocal对象。Thread类中有一个ThreadLocal.ThreadLocalMap变量 threadLocals。
			*/

		   ThreadLocalMap map = getMap(t);    //如果当前线程第一次执行这个方法,map肯定等于null,所以程序会走到createMap这个方法。
	        if (map != null)
	            map.set(this, value);
	        else
	            createMap(t, value); 
	 
	 
	 void createMap(Thread t, T firstValue) 
		 
		/**
			这一步是创建一个ThreadLocalMap对象,然后放到当前线程的threadLocals这个变量中。
			而这个ThreadLocalMap 是ThreadLocal类的一个静态内部类,见下面的代码
		*/
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    
	
	
	  static class ThreadLocalMap 
		private Entry[] table;
      
		ThreadLocalMap(ThreadLocal firstKey, Object firstValue) 
			/**
				Entry 类又是ThreadLocalMap类的一个静态内部类。看下面的Entry类,
				而真正我们起初调用的set(T value)这个方法的value值是存在了Entry类中的value变量中。
			*/
            table = new Entry[INITIAL_CAPACITY]; 
			/**
				这个i很重要,他通过当前的这个ThredLocal对象中的threadLocalHashCode来的得到的I值。
				因为现在是set()方法的一系列操作,当get()时候,也是通过这样得到i,进而取到table[i]里面的Entry.
				所以如果我们把当前的ThreadLocal对象设为null,就得到不i值了,就可能会造成内存泄露。
			*/
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        
		/**
				Entry 类继承了弱引用,这个弱引用指向的是当前的这个ThreadLocal对象。
				所以ThreadLocal有内存泄露的可能,这个分析在接下来的图中进行分析。
				
			*/
	 static class Entry extends WeakReference<ThreadLocal> 

			Object value;

            Entry(ThreadLocal k, Object v) 
                super(k);
                value = v;
            
        
		
		
	 
	 

上面是ThreadLocal的源码中截取的一部分,接下来我用图分析一下。
首先我们创建一个
//粗略代码
ThreadLocal<Connection> threadL=new ThreadLocal<Connection>();
Connection conn = new Connection();//模拟创建
thread.set(conn);

橘黄色部分是每个线程私有的,也就是每个线程在java堆内创建的对象,白色部分为所有线程共有的。
当一个线程第一次操作ThreadLocal时(也就是所谓的ThreadLocal对象实例的set()方法),首先会在堆内存中创建橘黄色中显示的一系列的对象。其中白色箭头的意思是弱引用,在table数组到Enter实例对象这一步,他是根据当前的TreadLocal实例中的一个threadLocalHashCode变量来得到 i 的值,进而取到table[i]对应的Enter实例,进而去得到Enter实例西面的Connection实例。 当我们把threadL置为空时。意思是堆中分配的ThreadLocal对象失去了强引用,因为Enter对象对ThreadLocal实例是软引用,所以当垃圾回收时就会回收ThreadLocal实例, 此时要再获取Conncetion实例时,因为ThreadLocal对象实例已找不到,所以就得不到上面所说的threadLocalHashCode得值,进而得不到table数组的下标,所以有可能造成内存泄露。 虽然JAVA对ThreadLocal这个对象在调用get()和set()方法时候会进行一系列的清除工作,但是当这个线程执行完毕后,我们把Connection对象置为null,此时这个线程回到线程池中,并不清除。以后这个线程不会再执行ThreadLocal的一系列操作,但是这个线程的threadLocal变量还存在。所以这个时候就会造成内存泄露。
 
  
 
  

以上是关于java线程对单个对象的共享的一些方式的主要内容,如果未能解决你的问题,请参考以下文章

002-多线程-锁-同步锁-synchronized几种加锁方式Java对象头和MonitorMutex LockJDK1.6对synchronized锁的优化实现

Java多线程与并发库7.多个线程之间共享数据的方式探讨

JAVA中的多线程

java类内多个函数如何同步

单个 java 进程的队列/工作线程架构

java多线程-线程通信