【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
- 打印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值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。
三、参考资料
【Java并发编程一】线程安全和共享对象的更多相关文章
- Java并发编程:线程的同步
Java并发编程:线程的同步 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} J ...
- Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- Java并发编程:线程池的使用(转)
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
- Java并发编程:线程控制
在上一篇文章中(Java并发编程:线程的基本状态)我们介绍了线程状态的 5 种基本状态以及线程的声明周期.这篇文章将深入讲解Java如何对线程进行状态控制,比如:如何将一个线程从一个状态转到另一个状态 ...
- Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- (转)Java并发编程:线程池的使用
背景:线程池在面试时候经常遇到,反复出现的问题就是理解不深入,不能做到游刃有余.所以这篇博客是要深入总结线程池的使用. ThreadPoolExecutor的继承关系 线程池的原理 1.线程池状态(4 ...
- Java并发编程:线程池的使用(转载)
转载自:https://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...
- Java并发编程:线程池的使用(转载)
文章出处:http://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...
- [转]Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
随机推荐
- Android Studio 出现 Build gradle project info
导入Android Studio,一直停留在Build gradle project info.主要是因为google被墙,下载gradle很慢,有时候设置下载不成功. 参考链接 http://blo ...
- Linux操作_磁盘管理_增加虚拟磁盘
环境:虚拟机 VM 12,Linux版本号 CentOS 7.3 1,在当前的虚拟机选项卡点击鼠标右键,选择“设置” 2,在弹出的对话框中左侧选中“磁盘”->点击下方“添加”按钮,在弹出的“添加 ...
- (转)ffmpeg 从mp4上提取H264的nalu
出自:http://blog.csdn.net/gavinr/article/details/7183499 1.获取数据ffmpeg读取mp4中的H264数据,并不能直接得到NALU,文件中也 ...
- Python装饰器、metaclass、abc模块学习笔记
(博客原创作品,转载请注明出处!) 最近接触到了Python中的decorator,metaclass,abc Module,six.add_metaclass等内容,这里做一个简单的笔记. 主要资源 ...
- com.ibm.msg.client.jms.DetailedJMSSecurityException: JMSWMQ2013: 为队列管理器提供的安全性认证无效
com.ibm.msg.client.jms.DetailedJMSSecurityException: JMSWMQ2013: 为队列管理器“zm_queue_manager”提供的安全性认证无效, ...
- HTML5 3D爱心动画及其制作过程
之前有看到过很多基于HTML5或者CSS3制作的爱心动画,不过基本上都是2D平面的,今天在国外的网站上看到一个基于HTML5 3D的爱心动画,对于HTML5爱好者来说,不免兴奋了一把.下面将分享一下这 ...
- Godray
软管的这个有点蛋疼..应该是我材质没弄好 最后发现不是材质,是法线不正确,调整后
- unity3d 使用GL 方式画线
这个是画线部分 private Vector3[] linePoints; public int m_LineCount; public int m_PointUsed; public void Re ...
- Unity3D Shader基础教程
原文地址:http://bbs.9ria.com/thread-212557-1-1.html 此教程将指引你如何建立自己的Shaders,让你的游戏场景看起来更好.Unity配备了强大的阴影和材料的 ...
- Junit结合Spring对Dao层进行单元测试
关于单元测试,上一次就简单的概念和Mock基础做了,参考:http://60.174.249.204:8888/in/modules/article/view.article.php/74 实际开发过 ...