Java多线程——线程封闭
线程封闭:当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)
线程封闭技术一个常见的应用就是JDBC的Connection对象,JDBC规范并没有要求Connection对象必须是线程安全的,在服务器应用程序中,线程从连接池获取一个Connection对象,使用完之后将对象返还给连接池。下面介绍几种线程封闭技术:
1、Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程的封闭性的职责完全由程序实现承担,是非常脆弱的,因此在程序中尽量少使用,一般使用更强的线程封闭技术,比如栈封闭或者ThreadLocal类。
2、栈封闭
栈封闭是线程封闭的一种特列,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行栈中,其他线程无法访问这个栈,栈封闭也称为线程内部使用或者线程局部使用。简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
比如下面的例子:
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null; //animals被封装在方法中,不要使它们溢出
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for(Animal a:animals){
if(candidate==null || !candidate.isPotentialMate(a)){
candidate = a;
}else{
ark.load(new AnimalPair(candidate,a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
在loadTheArk中实例化一个TreeSet对象,并将该对象的一个引用保存到animals中。此时,只有一个引用指向集合animals,这个引用被封闭到局部变量中,因此也被封闭到局部变量中。然而,如果发布了对集合animals(或者该对象中的任何内部数据)的引用,那么封闭性将被破坏,并导致对象animals的逸出。
3、ThreadLocal类
维持线程封闭性的一种更加规范方法是使用ThreadLocal类,这个类能使线程中某个值与保存值的对象关联起来。ThreadLocal类提供了get和set等访问接口或者方法,这些方法为每个使用该变量的线程都存在一份独立的副本,因此get总是放回当前执行线程在调用set设置的最新值。看一下下面代码例子:
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}; public static Connection getConnection() {
return connectionHolder.get();
} public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
}
通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是自己独立拥有的一个的Connection对象副本,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。
每个线程是怎么和Connection对象副本绑定的?这个对象副本保存在哪里。当某个线程初次调用ThreadLocal类的get方法时,就会调用initialValue来获取初始值,从概念上看,我们可以将ThreadLocal<T>视为包含了Map<thread, T>对象,其中保存了特定于该线程的值,但是ThreadLocal的实现并非如此,这样只是为了我们方便理解而已。
下面我们来分析一下ThreadLocal类的源码。ThreadLocal类的方法很简单,只有四个,分别为set,get,remove, initialValue,从字面上我们也能理解这些方法的作用。
public T get():返回当前线程所对应的局部变量。
public void set(T arg0):设置当前线程局部变量的值。
public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。注意,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected T initialValue(): 对当线程局部变量进行初始化,并返回该初始值。是protected 属性,显然是让子类进行对其覆盖重写的,只有第一次调用set和get方法时才调用。
下面我们对这四个方法的源码进行分析,看看ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”。
3.1 set方法
以下是set方法的源码
public void set(T arg0) {
Thread arg1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
if (arg2 != null) {
arg2.set(this, arg0);
} else {
this.createMap(arg1, arg0);
} }
从set方法中可以看到,首先获取当前线程:Thread arg1 = Thread.currentThread();
再获取当前线程的ThreadLocalMap:ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
判断ThreadLocalMap是否为空,不为空,则以键值对的形式设置值,key为this,value就是局部变量的副本,this是当前线程持有的ThreadLocal类实例化对象。
假如为空,则通过createMap方法创建。
我们看下getMap和createMap方法的源码:
ThreadLocal.ThreadLocalMap getMap(Thread arg0) {
return arg0.threadLocals;
} void createMap(Thread arg0, T arg1) {
arg0.threadLocals = new ThreadLocal.ThreadLocalMap(this, arg1); }
从代码上已经写的非常清楚,每个线程都有自己的局部变量的副本,该副本是存在ThreadLocalMap 中,其中键值就是ThreadLocal类实例化对象。也就是说每个线程都拥有自己的ThreadLocalMap,ThreadLocalMap保存的就是局部变量副本。我们看一下java.lang.Thread源码。
private static int threadInitNumber;
ThreadLocalMap threadLocals = null;
ThreadLocalMap inheritableThreadLocals = null;
3.2 get方法
public T get() {
Thread arg0 = Thread.currentThread();
ThreadLocal.ThreadLocalMap arg1 = this.getMap(arg0);
if (arg1 != null) {
ThreadLocal.ThreadLocalMap.Entry arg2 = arg1.getEntry(this);
if (arg2 != null) {
Object arg3 = arg2.value;
return arg3;
}
} return this.setInitialValue();
}
从代码上看,前两步和set方法是一个样的,分别获取当前线程和当前线程的ThreadLocalMap,第三步判断ThreadLocalMap是否为空,不为空根据this键值获取value,为空调用setInitialValue()方法。
以下是setInitialValue方法代码:
private T setInitialValue() {
Object arg0 = this.initialValue();
Thread arg1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
if (arg2 != null) {
arg2.set(this, arg0);
} else {
this.createMap(arg1, arg0);
} return arg0;
}
在setInitialValue里调用了initialValue()方法,也就是子类要重写覆盖的方法,对应上面的例子的代码是:
protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
然后获取当前线程和当前线程的ThreadLocalMap,ThreadLocalMap为空则调用createMap,否则调用set方法。
3.3 总结
ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。
进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
也就说,每个线程都有一个ThreadLocalMap,该线程访问到某个局部变量,且该局部变量是用ThreadLocal类进行声明时,该线程就会new ThreadLocal(),然后将该ThreadLocal类的对象作为key值,所对应的局部变量作为value值保存到ThreadLocalMap中。当线程访问多个ThreadLocal类进行声明局部变量时,在ThreadLocalMap中就有多个键值对。而每个线程都有自己的ThreadLocalMap,从而达到隔离的目的了。
当某个线程终止后,该线程里的ThreadLocalMap也被回收了,所以完全不用担心内存泄漏的问题。
假如多线程访问的对象实例是单例的,或者说只能创建一个,那就老老实实的使用同步机制(synchronized)了.
Java多线程——线程封闭的更多相关文章
- java 多线程—— 线程让步
java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...
- java 多线程—— 线程等待与唤醒
java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...
- Java多线程--线程及相关的Java API
Java多线程--线程及相关的Java API 线程与进程 进程是线程的容器,程序是指令.数据的组织形式,进程是程序的实体. 一个进程中可以容纳若干个线程,线程是轻量级的进程,是程序执行的最小单位.我 ...
- Java多线程-线程的同步(同步方法)
线程的同步是保证多线程安全访问竞争资源的一种手段.线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源.什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些 ...
- Java多线程——线程的优先级和生命周期
Java多线程——线程的优先级和生命周期 摘要:本文主要介绍了线程的优先级以及线程有哪些生命周期. 部分内容来自以下博客: https://www.cnblogs.com/sunddenly/p/41 ...
- Java多线程——线程的创建方式
Java多线程——线程的创建方式 摘要:本文主要学习了线程的创建方式,线程的常用属性和方法,以及线程的几个基本状态. 部分内容来自以下博客: https://www.cnblogs.com/dolph ...
- Java多线程——线程之间的协作
Java多线程——线程之间的协作 摘要:本文主要学习多线程之间是如何协作的,以及如何使用wait()方法与notify()/notifyAll()方法. 部分内容来自以下博客: https://www ...
- Java多线程——线程的死锁
Java多线程——线程的死锁 摘要:本文主要介绍了Java多线程中遇到的死锁问题. 部分内容来自以下博客: https://www.cnblogs.com/wy697495/p/9757982.htm ...
- Java多线程——线程之间的同步
Java多线程——线程之间的同步 摘要:本文主要学习多线程之间是如何同步的,如何使用volatile关键字,如何使用synchronized修饰的同步代码块和同步方法解决线程安全问题. 部分内容来自以 ...
随机推荐
- 生产消费者队列(TaskCompletionSource)的应用
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Li ...
- abp AutoMap Custom Mapping
[DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule { public override void Pr ...
- 安卓开发解决android.os.NetworkOnMainThreadException异常方法(主线程不能直接调用webservice)
安卓开发解决android.os.NetworkOnMainThreadException异常方法 2013-01-07 14:01:04| 分类: 技术 | 标签:安卓 技术 java | ...
- android 的AlertDialog对话框
private int selectedFruitIndex = 0; private void showMsg2() {// Dialog alertDialog = new AlertDial ...
- ps 中添加一张图片
// 测试打开一个文件var fileref = new File ("/E/work/没有图片提交/2014/2014.5.19/G20/部件渲染测试/png/tianji_1-41001 ...
- Redis安装部署、Jedis的使用
一.NoSQL概述 为什么需要NoSQL High performance -高并发读写 Huge Storage - 海量数据的高效率存储和访问 High Scalability && ...
- 如何把App放在服务器上供用户下载
如何把App放在服务器上供用户下载 有时候做了个简单的App想把App给朋友帮忙测试一下,却发现上传到各种平台很麻烦,肿么办?难道一个个拷贝,那也太low啦,不是咱程序员该干的事儿,好的话不多说,开搞 ...
- JSP中的一个树型结构
看方力勋的javaWeb,采用左右值来表示树型结构(就是俺门的多级分类)表结构 页面代码 <%@ page language="java" import="java ...
- 线上服务 CPU 100%?一键定位 so easy!
转自: https://my.oschina.net/leejun2005/blog/1524687 摘要: 本文主要针对 Java 服务而言 0.背景 经常做后端服务开发的同学,或多或少都 ...
- spring注解@Value取不到值【转】
spring注解@Value取不到值 今天在一个项目中发现一个情况,在Service中取不到name值,直接输出了{name}字符串,找了好久,最后在一篇文章中找到解决方案. 解决这个问题的一篇文章( ...