一、什么是线程安全

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

  内部锁

  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

  • 打印42

  • 什么都不打印

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

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

  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作为键值。 
  下面是各对象之间的引用关系图,实线表示强引用,虚线表示弱引用: 
   
  综上,在每个线程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());
}
}
}

  运行结果:

 
可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

三、参考资料

    1. 深入研究java.lang.ThreadLocal类
    2. Java并发编程:深入剖析ThreadLocal
    3. [Java并发包学习七]解密ThreadLocal

【Java并发编程一】线程安全和共享对象的更多相关文章

  1. Java并发编程:线程的同步

    Java并发编程:线程的同步 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} J ...

  2. Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  3. Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  4. Java并发编程:线程池的使用(转)

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  5. Java并发编程:线程控制

    在上一篇文章中(Java并发编程:线程的基本状态)我们介绍了线程状态的 5 种基本状态以及线程的声明周期.这篇文章将深入讲解Java如何对线程进行状态控制,比如:如何将一个线程从一个状态转到另一个状态 ...

  6. Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  7. (转)Java并发编程:线程池的使用

    背景:线程池在面试时候经常遇到,反复出现的问题就是理解不深入,不能做到游刃有余.所以这篇博客是要深入总结线程池的使用. ThreadPoolExecutor的继承关系 线程池的原理 1.线程池状态(4 ...

  8. Java并发编程:线程池的使用(转载)

    转载自:https://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...

  9. Java并发编程:线程池的使用(转载)

    文章出处:http://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...

  10. [转]Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

随机推荐

  1. python公司内部语言规范与语言风格

    一.python语言规范 1.1导入 Tip: 仅对包和模块使用导入 定义: 模块间共享代码的重用机制. 优点: 命名空间管理约定十分简单.每个标识符的源都用一种一致的方式指示.x.obj 表示obj ...

  2. SQLSERVER中统计所有表的记录数

    SQLSERVER中统计所有表的记录数 利用系统索引表sysindexes中索引ID indid<1的行中的rows列存有该表的行数这一特点.    方法是利用隐藏未公开的系统存储过程sp_MS ...

  3. CSS之float vs position:absolute

    补充:ul 应该设置下 list-style: none; 题外话:看了张鑫旭的视频,这家伙把简单的css玩出了新花样,绝对大神级的存在.膜拜下先~ float的作用前面一章已经说过了,但没考虑过的是 ...

  4. (转)st(state-threads) coroutine调度

    目录(?)[-] EPOLL和TIMEOUT TIME TIMEOUT Deviation   st(state-threads) https://github.com/winlinvip/state ...

  5. Storm概念、原理详解及其应用(一)BaseStorm

    本文借鉴官文,添加了一些解释和看法,其中有些理解,写的比较粗糙,有问题的地方希望大家指出.写这篇文章,是想把一些官文和资料中基础.重点拿出来,能总结出便于大家理解的话语.与大多数“wordcount” ...

  6. 近期全国各地联通线路无法访问OA的解决方案

    最近有多地区使用联通线路的用户无法访问easyradius控制台,即oa.ooofc.com,其主要的原因是由于联通的DNS解析错误,导致的 oa.ooofc.com的解析IP是115.239.252 ...

  7. unity3d 资源打包加密 整理

    资源打包脚本,放到Assets\Editor 文件夹下 using UnityEngine; using System.Collections; using UnityEditor; using Sy ...

  8. 微信小程序/支付宝小程序 WxParse解析富文本(html)代码

    小程序本身并不太支持html代码,比如html的img.span.p这个时候改这么办呢?需要用到一个小插件WxParse来实现. 小程序高级交流群:336925436  微信小程序支持富文本编辑器代码 ...

  9. font-awesome 使用方法

    需要引入文件 font-awesome.css <link rel="stylesheet" href="{$yf_theme_path}public/font-a ...

  10. iOS模拟(糟糕的)网络环境

    有时候为了模拟在糟糕的网络环境下app的表现,会故意拔网线(断wifi),苹果其实提供了专门的工具来精确地模拟你在几个预设的场景下的网络连接情况:Network Link Conditioner 点击 ...