【Java并发编程一】线程安全和共享对象

一、什么是线程安全

  当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

  内部锁

  Java提供了强制性的内置锁机制:synchronized块。一个synchronized块有两个部分:锁对象的引用,以及这个锁保护的代码块。执行线程进入synchronized块之前会自动获得锁,无论通过正常控制路径退出还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。 
内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁。

  重进入

  内部锁是可重进入的,这意味着锁的请求是基于“每线程”,而不是基于“每调用”的,重进入的实现是通过为每个锁关联一个请求技术和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将锁记录锁的占有者且将请求计数值置为1,。如果同一线程再次请求这个锁,计数 将递增,每次占用线程退出同步块,计数值将递减。直到计数器达到0时,锁被释放。

二、共享对象

  synchronized不仅用于原子操作,划定“临界区”,另外还可以确保当一个线程修改对象的状态后,其他线程能够真正看到变化。

  可见性

  当读与写发生在不同的线程时,通常不能保证读线程及时读取其他线程写入的值。例如下面这个例子:

public class TestMain
{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread
{
@Override
public void run()
{ while(!ready)
{
System.out.println("读线程");
Thread.yield(); //暂停当前正在执行的线程对象
System.out.println(number);
}
}
}
public static void main(String[] args) throws InterruptedException
{
new ReaderThread().start();
System.out.println("main线程");
number=42;
ready=true; }
}

  在上面的代码中,主线程启动读进程,然后把number设为42,ready设为true,读进程进行循环。经测试,该程序运行的结果有下面几种可能:

  • 打印0

【Java并发编程一】线程安全和共享对象

  • 打印42

【Java并发编程一】线程安全和共享对象

  • 什么都不打印

【Java并发编程一】线程安全和共享对象

  出现上面的错误是因为在没有同步的情况下,线程运行的顺序不同导致了不同的运行结果。有一个简单的方法来避免这样的问题:只要数据需要被跨线程共享,就进行恰当的同步。

  上面的例子能够引起意外的后果:过期数据。过期数据不会发生在全部变量上,也不完全不出现。 
锁不仅仅是关于同步和互斥的,也是关于内存可见的。为了保证所有的线程都能看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

  Volatile变量

  volatile变量,确保对一个变量的更新以可预见的方式告知其它的线程。当一个域声明为volatile类型后,编译器与运行时会监视这个变量,它是共享的,而且对它的操作不会与其它的内存操作一起被重排序。volatile变量不会缓存在寄存器或者缓存在对其它处理器隐藏的地方。 
  volatile变量的操作不会加锁,也不会引起执行线程的阻塞,这使得volatile变量相对于sychronized而言是一种轻量级的同步机制。 
  volatile变量通常被当做是标识完成、中断、状态的标记使用。它也存在一些限制。volatile变量只能保证可见性,而加锁可以保证可见性和原子性。 
  只有满足下面所有的标准后,你才能使用volatile变量:

  • 写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其它的状态量共同参与不变约束
  • 访问变量时,没有其他的原因需要加锁

  ThreadLocal

  ThreadLocal不是一个线程的本地实现版本,它不是一个Thread,而是线程布局变量,为每一个使用该变量的线程都提供了一个变量值的副本。 
  从线程的角度看,每一个线程都保持一个对其线程局部变量副本的隐式引用,只要线程时活动的并且ThreadLocal实例是可访问的。 
  JVM为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境出现的并发问题提供了一种隔离机制。这种隔离机制与同步机制不同,同步机制才用了“以时间换空间”的方式,而ThreadLocal才用了“以空间换时间”的方式,前者仅提供一份变量,让不同的线程排队访问,后者则为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  ThreadLocal的实现思路

  查看Thread的源码,我们可以看到,

public
class Thread implements Runnable
{
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
} private volatile char name[];
private int priority;
private Thread threadQ;
private long eetop; ... /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null; /*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  Thread类中有一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,就是用它来存储当前线程变量的副本。ThreadLocalMap是ThreadLocal类的静态内部类,ThreadLocal类有一个函数public T get():

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

  该函数首先获取当前线程,然后通过getMap(t)方法获取ThreadLocalMap类型的map,这个map也就是当前线程的变量threadLocals。接下来获取key-value键值对,如果获取成功,则返回value值,若map为空,则调用setInitialValue方法返回value。

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

  下面看一下ThreadLocalMap的实现:

static class ThreadLocalMap
{
static class Entry extends WeakReference<ThreadLocal<?>>
{
Object value;
Entry(ThreadLocal<?> k, Object v)
{
super(k);
value = v;
}
}
....
}

  ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。 
  下面是各对象之间的引用关系图,实线表示强引用,虚线表示弱引用: 
  【Java并发编程一】线程安全和共享对象 
  综上,在每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前的ThreadLocal变量,value为变量副本。 
  初始化时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 
  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。例子:

public class TestThreadLocal
{
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>()
{
@Override
protected Integer initialValue()
{
return 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 index; public MyThread(int index)
{
this.index = index;
}
public void run()
{
System.out.println("线程" + index + "的初始value:" + value.get());
for (int i = 0; i < 10; i++)
{
value.set(value.get() + i);
}
System.out.println("线程" + index + "的累加value:" + value.get());
}
}
}

  运行结果:

【Java并发编程一】线程安全和共享对象 
可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

三、参考资料

    1. 深入研究java.lang.ThreadLocal类
    2. Java并发编程:深入剖析ThreadLocal
    3. [Java并发包学习七]解密ThreadLocal
上一篇:WebApi和MVC的区别


下一篇:iOS 视图控制器转场详解