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锁的优化实现