Java中的线程同步与ThreadLocal无锁化线程封闭实现_java

Synchronized关键字

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
以上规则对其它对象锁同样适用.
代码示例

package test160118;

public class TestSynchronized {
  public static void main(String[] args) {
    Sy sy = new Sy(0);
    Sy sy2 = new Sy(1);
    sy.start();
    sy2.start();
  }
}

class Sy extends Thread {
  private int flag ;

  static Object x1 = new Object();
  static Object x2 = new Object();

  public Sy(int flag) {
    this.flag = flag;
  }
  @Override
  public void run() {
    System.out.println(flag);
    try {
      if (flag == 0) {
        synchronized (x1) {
          System.out.println(flag+"锁住了x1");
          Thread.sleep(1000);
          synchronized (x2) {
            System.out.println(flag+"锁住了x2");
          }
          System.out.println(flag+"释放了x1和x2");
        }
      }
      if(flag == 1) {
        synchronized (x2) {
          System.out.println(flag+"锁住了x2");
          Thread.sleep(1000);
          synchronized (x1) {
            System.out.println(flag+"锁住了x1");
          }
          System.out.println(flag+"释放了x1和x2");
        }
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

ThreadLocal无锁化线程封闭实现原理
ThreadLocal能做什么呢?

这个一句话不好说,我们不如来看看实际项目中遇到的一些困解:当你在项目中根据一些参数调用进入一些方法,然后方法再调用方法,进而跨对象调用方法,很多层次,这些方法可能都会用到一些相似的参数,例如,A中需要参数a、b、c,A调用B后,B中需要b、c参数,而B调用C方法需要a、b参数,此时不得不将所有的参数全部传递给B,以此类推,若有很多方法的调用,此时的参数就会越来越繁杂,另外,当程序需要增加参数的时候,此时需要对相关的方法逐个增加参数,是的,很麻烦,相信你也遇到过,这也是在C语言面向对象过来的一些常见处理手段,不过我们简单的处理方法是将它包装成对象传递进去,通过增加对象的属性就可以解决这个问题,不过对象通常是有意义的,所以有些时候简单的对象包装增加一些扩展不相关的属性会使得我们class的定义变得十分的奇怪,所以在这些情况下我们在架构这类复杂的程序的时候,我们通过使用一些类似于Scope的作用域的类来处理,名称和使用起来都会比较通用,类似web应用中会有context、session、request、page等级别的scope,而ThreadLocal也可以解决这类问题,只是他并不是很适合解决这类问题,它面对这些问题通常是初期并没有按照scope以及对象的方式传递,认为不会增加参数,当增加参数时,发现要改很多地方的地方,为了不破坏代码的结构,也有可能参数已经太多,已经使得方法的代码可读性降低,增加ThreadLocal来处理,例如,一个方法调用另一个方法时传入了8个参数,通过逐层调用到第N个方法,传入了其中一个参数,此时最后一个方法需要增加一个参数,第一个方法变成9个参数是自然的,但是这个时候,相关的方法都会受到牵连,使得代码变得臃肿不堪。

上面提及到了ThreadLocal一种亡羊补牢的用途,不过也不是特别推荐使用的方式,它还有一些类似的方式用来使用,就是在框架级别有很多动态调用,调用过程中需要满足一些协议,虽然协议我们会尽量的通用,而很多扩展的参数在定义协议时是不容易考虑完全的以及版本也是随时在升级的,但是在框架扩展时也需要满足接口的通用性和向下兼容,而一些扩展的内容我们就需要ThreadLocal来做方便简单的支持。

简单来说,ThreadLocal是将一些复杂的系统扩展变成了简单定义,使得相关参数牵连的部分变得非常容易,以下是我们例子说明:

Spring的事务管理器中,对数据源获取的Connection放入了ThreadLocal中,程序执行完后由ThreadLocal中获取connection然后做commit和rollback,使用中,要保证程序通过DataSource获取的connection就是从spring中获取的,为什么要做这样的操作呢,因为业务代码完全由应用程序来决定,而框架不能要求业务代码如何去编写,否则就失去了框架不让业务代码去管理connection的好处了,此时业务代码被切入后,spring不会向业务代码区传入一个connection,它必须保存在一个地方,当底层通过ibatis、spring jdbc等框架获取同一个datasource的connection的时候,就会调用按照spring约定的规则去获取,由于执行过程都是在同一个线程中处理,从而获取到相同的connection,以保证commit、rollback以及业务操作过程中,使用的connection是同一个,因为只有同一个conneciton才能保证事务,否则数据库本身也是不支持的。

其实在很多并发编程的应用中,ThreadLocal起着很重要的重要,它不加锁,非常轻松的将线程封闭做得天衣无缝,又不会像局部变量那样每次需要从新分配空间,很多空间由于是线程安全,所以,可以反复利用线程私有的缓冲区。

如何使用ThreadLocal?

在系统中任意一个适合的位置定义个 ThreadLocal 变量,可以定义为 public static 类型(直接new出来一个ThreadLocal对象),要向里面放入数据就使用set(Object),要获取数据就用get()操作,删除元素就用remove(),其余的方法是非 public 的方法,不推荐使用。

下面是一个简单例子(代码片段1):

public class ThreadLocalTest2 {

 public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>();

 public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>();

 public static void main(String[]args) {
 for(int i = 0 ; i < 100 ; i++) {
  final String name = "线程-【" + i + "】";
  final String value = String.valueOf(i);
  new Thread() {
  public void run() {
   try {
   TEST_THREAD_NAME_LOCAL.set(name);
   TEST_THREAD_VALUE_LOCAL.set(value);
   callA();
   }finally {
   TEST_THREAD_NAME_LOCAL.remove();
   TEST_THREAD_VALUE_LOCAL.remove();
   }
  }
  }.start();
 }
 }

 public static void callA() {
 callB();
 }

 public static void callB() {
 new ThreadLocalTest2().callC();
 }

 public void callC() {
 callD();
 }

 public void callD() {
 System.out.println(TEST_THREAD_NAME_LOCAL.get() + "/t=/t" + TEST_THREAD_VALUE_LOCAL.get());
 }
}

这里模拟了100个线程去访问分别设置 name 和 value ,中间故意将 name 和 value 的值设置成一样,看是否会存在并发的问题,通过输出可以看出,线程输出并不是按照顺序输出,说明是并行执行的,而线程 name 和 value 是可以对应起来的,中间通过多个方法的调用,以模实际的调用中参数不传递,如何获取到对应的变量的过程,不过实际的系统中往往会跨类,这里仅仅在一个类中模拟,其实跨类也是一样的结果,大家可以自己去模拟就可以。

相信看到这里,很多程序员都对 ThreadLocal 的原理深有兴趣,看看它是如何做到的,尽然参数不传递,又可以像局部变量一样使用它,的确是蛮神奇的,其实看看就知道是一种设置方式,看到名称应该是是和Thread相关,那么废话少说,来看看它的源码吧,既然我们用得最多的是set、get和remove,那么就从set下手:

set(T obj)方法为(代码片段2):

public void set(T value) {
 Thread t = Thread.currentThread();
 ThreadLocalMap map = getMap(t);
 if (map != null)
 map.set(this, value);
 else
 createMap(t, value);
}

首先获取了当前的线程,和猜测一样,然后有个 getMap 方法,传入了当前线程,我们先可以理解这个map是和线程相关的map,接下来如果   不为空,就做set操作,你跟踪进去会发现,这个和HashMap的put操作类似,也就是向map中写入了一条数据,如果为空,则调用createMap方法,进去后,看看( 代码片段3 ):

void createMap(Thread t, T firstValue) {
 t.threadLocals = new ThreadLocalMap(this, firstValue);
}

返现创建了一个ThreadLocalMap,并且将传入的参数和当前ThreadLocal作为K-V结构写入进去( 代码片段4 ):

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
 table = new Entry[INITIAL_CAPACITY];
 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 table[i] = new Entry(firstKey, firstValue);
 size = 1;
 setThreshold(INITIAL_CAPACITY);
}

这里就不说明ThreadLocalMap的结构细节,只需要知道它的实现和HashMap类似,只是很多方法没有,也没有implements Map,因为它并不想让你通过某些方式(例如反射)获取到一个Map对他进一步操作,它是一个ThreadLocal里面的一个static内部类,default类型,仅仅在java.lang下面的类可以引用到它,所以你可以想到Thread可以引用到它。

我们再回过头来看看getMap方法,因为上面我仅仅知道获取的Map是和线程相关的,而通过 代码片段3 ,有一个t.threadLocalMap = new ThreadLocalMap(this, firstValue)的时候,相信你应该大概有点明白,这个变量应该来自Thread里面,我们根据getMap方法进去看看:

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

是的,是来自于Thread,而这个Thread正好又是当前线程,那么进去看看定义就是:

ThreadLocal.ThreadLocalMap threadLocals = null;

这个属性就是在Thread类中,也就是每个Thread默认都有一个ThreadLocalMap,用于存放线程级别的局部变量,通常你无法为他赋值,因为这样的赋值通常是不安全的。

好像是不是有点乱,不着急,我们回头先摸索下思路:

1、Thread里面有个属性是一个类似于HashMap一样的东西,只是它的名字叫ThreadLocalMap,这个属性是default类型的,因此同一个package下面所有的类都可以引用到,因为是Thread的局部变量,所以每个线程都有一个自己单独的Map,相互之间是不冲突的,所以即使将ThreadLocal定义为static线程之间也不会冲突。

2、ThreadLocal和Thread是在同一个package下面,可以引用到这个类,可以对他做操作,此时ThreadLocal每定义一个,用this作为Key,你传入的值作为value,而this就是你定义的ThreadLocal,所以不同的ThreadLocal变量,都使用set,相互之间的数据不会冲突,因为他们的Key是不同的,当然同一个ThreadLocal做两次set操作后,会以最后一次为准。

3、综上所述,在线程之间并行,ThreadLocal可以像局部变量一样使用,且线程安全,且不同的ThreadLocal变量之间的数据毫无冲突。

我们继续看看get方法和remove方法,其实就简单了:

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

通过根据当前线程调用getMap方法,也就是调用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本结构,因为你set写入的仅仅有值,所以,它会设置一个e.value来返回你写入的值,因为Key就是ThreadLocal本身。你可以看到map.getEntry也是通过this来获取的。

同样remove方法为:

public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
  m.remove(this);
}

同样根据当前线程获取map,如果不为空,则remove,通过this来remove。

补充下(2013-6-29),搞忘写有什么坑了,这个ThreadLocal有啥坑呢,大家从前面应该可以看出来,这个ThreadLocal相关的对象是被绑定到一个Map中的,而这个Map是Thread线程的中的一个属性,那么就有一个问题是,如果你不自己remove的话或者说如果你自己的程序中不知道什么时候去remove的话,那么线程不注销,这些被set进去的数据也不会被注销。

反过来说,写代码中除非你清晰的认识到这个对象应该在哪里set,哪里remove,如果是模糊的,很可能你的代码中不会走remove的位置去,或导致一些逻辑问题,另外,如果不remove的话,就要等线程注销,我们在很多应用服务器中,线程是被复用的,因为在内核分配线程还是有开销的,因此在这些应用中线程很难会被注销掉,那么向ThreadLocal写入的数据自然很不容易被注销掉,这些可能在我们使用某些开源框架的时候无意中被隐藏用到,都有可能会导致问题,最后发现OOM得时候数据竟然来自ThreadLocalMap中,还不知道这些数据是从哪里设置进去的,所以你应当注意这个坑,可能不止一个人掉进这个坑里去过。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索java
, 线程
, threadlocal
, 线程同步
, synchronized

threadlocal 线程池、threadlocal线程安全、threadlocal与线程池、threadlocal 子线程、threadlocal 多线程,以便于您获取更多的相关知识。

时间: 2024-10-31 10:34:22

Java中的线程同步与ThreadLocal无锁化线程封闭实现_java的相关文章

ThreadLocal实现方式&amp;amp;使用介绍---无锁化线程封闭

虽然现在可以说很多程序员会用ThreadLocal,但是我相信大多数程序员还不知道ThreadLocal,而使用ThreadLocal的程序员大多只是知道其然而不知其所以然,因此,使用ThreadLocal的程序员很多时候会被它导入到陷进中去,其实java很多高级机制系列的很多东西都是一把双刃剑,也就是有利必有其弊,那么我们的方法是找到利和弊的中间平衡点,最佳的方式去解决问题.   本文首先说明ThreadLocal能做什么,然后根据功能为什么要用它,如何使用它,最后通过内部说明讲解他的坑在哪里

高效线程池之无锁化实现(Linux C)

笔者之前照着通用写法练手写过一个小的线程池版本,最近几天复习了一下,发现大多数线程池实现都离不开锁的使用,如互斥量pthread_mutex*结合条件变量pthread_cond*.众所周知,锁的使用对于程序性能影响较大,虽然现有的pthread_mutex*在锁的申请与释放方面做了较大的优化,但仔细想想,线程池的实现是可以做到无锁化的,于是有了本文. 1.常见线程池实现原理 如上图所示,工作队列由主线程和工作者线程共享,主线程将任务放进工作队列,工作者线程从工作队列中取出任务执行.共享工作队列

多线程 java 同步 锁-java中多线程访问同步问题

问题描述 java中多线程访问同步问题 public class SyschronizedSample{ private int value; public synchronized int get(){ return value;} public synchronized void set(int value) { this.value=value; } } 以上的代码中,要使得访问value时具有线程安全,所以在set和get方法中都加了synchronized同步语句,如果只在set方法前

java中一个void修饰的方法无返回值,那么有是不是返回一个空对象,有空对象这种说法吗

问题描述 java中一个void修饰的方法无返回值,那么有是不是返回一个空对象,有空对象这种说法吗 java中一个void修饰的方法无返回值,那么有是不是返回一个空对象,有空对象这种说法吗 解决方案 加入你有一个对象 Object 里面有个方法:public void method1(); 那你调用这个方法的时候就是 object.method1(); 那么这里就表示 没有返回值. 所以我里面的void就是 :没有返回值,这个方法不能做他用! 对比 有一个object 里面有一个方法: publ

Java中的ReentrantLock和synchronized两种锁机制的对比

原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言.核心类库包含一个 Thread 类,可以用它来构建.启动和操纵线程,Java 语言包括了跨线程传达并发性约束的构造 -- synchronized 和 volatile.在简化与平台无关的并发类的开发的同时,它决没有使并发

使用JAVA实现高并发无锁数据库操作步骤分享_java

1. 并发中如何无锁.一个很简单的思路,把并发转化成为单线程.Java的Disruptor就是一个很好的例子.如果用java的concurrentCollection类去做,原理就是启动一个线程,跑一个Queue,并发的时候,任务压入Queue,线程轮训读取这个Queue,然后一个个顺序执行. 在这个设计模式下,任何并发都会变成了单线程操作,而且速度非常快.现在的node.js, 或者比较普通的ARPG服务端都是这个设计,"大循环"架构.这样,我们原来的系统就有了2个环境:并发环境 +

解析Java中的定时器及使用定时器制作弹弹球游戏的示例_java

  在我们编程过程中如果需要执行一些简单的定时任务,无须做复杂的控制,我们可以考虑使用JDK中的Timer定时任务来实现.下面LZ就其原理.实例以及Timer缺陷三个方面来解析java Timer定时器. 一.简介      在java中一个完整定时任务需要由Timer.TimerTask两个类来配合完成. API中是这样定义他们的,Timer:一种工具,线程用其安排以后在后台线程中执行的任务.可安排任务执行一次,或者定期重复执行.由TimerTask:Timer 安排为一次执行或重复执行的任务

Java中Json字符串直接转换为对象的方法(包括多层List集合)_java

使用到的类:net.sf.json.JSONObject  使用JSON时,除了要导入JSON网站上面下载的json-lib-2.2-jdk15.jar包之外,还必须有其它几个依赖包:commons-beanutils.jar,commons-httpclient.jar,commons-lang.jar,ezmorph.jar,morph-1.0.1.jar 下面是例子代码: // JSON转换 JSONObject jsonObj = JSONObject.fromObject(jsonSt

Java中的日期和时间类以及Calendar类用法详解_java

Java日期和时间类简介 Java 的日期和时间类位于 java.util 包中.利用日期时间类提供的方法,可以获取当前的日期和时间,创建日期和时间参数,计算和比较时间. Date 类 Date 类是 Java 中的日期时间类,其构造方法比较多,下面是常用的两个: Date():使用当前的日期和时间初始化一个对象. Date(long millisec):从1970年01月01日00时(格林威治时间)开始以毫秒计算时间,计算 millisec 毫秒.如果运行 Java 程序的本地时区是北京时区(