在并发编程中,基本上离不开这三个东西,如何实现多线程之间的数据共享,能够用 volatile; 每一个线程维护本身的变量,则采用 ThreadLocal; 为了保证方法or代码块的线程安全,就该 synchronized 上场。这里将主要说明下这三个能够怎么用,以及内部的实现细节html
java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保经过排他锁单独得到这个变量。Java语言提供了volatile,在某些状况下比锁更加方便。若是一个字段被声明成volatile,java线程内存模型确保全部线程看到这个变量的值是一致的。java
处理器为了提升处理速度,不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其余)后再进行操做,但操做完以后不知道什么时候会写到内存,若是对声明了Volatile变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里 用一个图简单的说明上面的过程web
图画的通常般,简单说一下算法
咱们有两个线程, 线程B修改一个共享变量tag, 线程A一直循环干模式, 当发现 tag 设置为了 true 时, 则结束编程
private volatile boolean tag = false; @Test public void testVolatile() throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { int i = 0; System.out.println("in A-------"); while (!tag) { System.out.print((i++) + ","); } System.out.println("\nout A-------"); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { System.out.println("in B---------"); tag = true; System.out.println("out B--------"); } }); threadA.start(); Thread.sleep(1); threadB.start();; }
输出为:数组
in A------- 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,in B--------- 96, out A------- out B--------
从上面的输出能够看出,当进入线程B以后,将 tag设置为true, 对线程A而言,它很迅速的感知到了这个参数的变化, 并终止了循环; 若是将tag前面的volatile
关键字干掉,下面是输出,从最终的结果来看好像并无什么区别,那这个东西到底有什么用,该怎么用?缓存
输出结果安全
in A------- 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,in B--------- 135, out A------- out B--------
一篇参考连接: http://blog.csdn.net/feier7501/article/details/20001083 (说明这篇博文中的case,本机jdk8并无复现....., 因此这是一个失败的case)多线程
再看一个case,并发
public class TestVolatile { int a = 1; int b = 2; public void change(){ a = 3; Thread.sleep(10); // 人肉加长这个赋值的时间 b = a; } public void print(){ System.out.println("b="+b+";a="+a); } public static void main(String[] args) { while (true){ final TestVolatile test = new TestVolatile(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); test.change(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
从上面的代码来看,正常来说,输出1,2; 或者 3, 3, 而实际输出却并非这样
...... b=2;a=1 b=2;a=1 b=3;a=3 b=3;a=3 b=3;a=1 <--------------------- 看这里 b=3;a=3 b=2;a=1 b=3;a=3 b=3;a=3 ......
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用。
synchronized
同步代码块 or 同步方法, 加锁, 简单来说,当一块被这个关键词修饰时,那么这块在统一时刻,只能有一个线程进行访问
一般来说,有三种使用方法,用来修饰成员方法, 静态方法, 和代码快,下面分别来写个测试case
public synchronized static void staticFunc() { System.out.println(Thread.currentThread().getName() + " in 1--->"); try { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print"); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 1--->"); } public synchronized static void staticFunc2() { System.out.println(Thread.currentThread().getName() + " in 2--->"); try { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print"); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 2--->"); } public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { SynchronizedTest.staticFunc(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { SynchronizedTest.staticFunc2(); } }); thread1.start(); thread2.start(); }
输出以下, 两个同步修饰的静态方法, 第一个线程使用其中的方法时,第二个线程即使调用第二个静态方法,依然会被阻塞
Thread-0 in 1---> Thread-0-->synch staticFunc print Thread-0 out 1---> Thread-1 in 2---> Thread-1-->synch staticFunc2 print Thread-1 out 2--->
将上面的 synchronized 修饰去掉, 看下输出以下,也就是说,二者的调用是能够并行的
Thread-1 in 2---> Thread-0 in 1---> Thread-1-->synch staticFunc2 print Thread-0-->synch staticFunc print Thread-1 out 2---> Thread-0 out 1--->
在上面的例子中,稍稍改动便可
public class SynchronizedTest { public synchronized void staticFunc() { System.out.println(Thread.currentThread().getName() + " in 1--->"); try { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print"); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 1--->"); } public synchronized void staticFunc2() { System.out.println(Thread.currentThread().getName() + " in 2--->"); try { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print"); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 2--->"); } public static void main(String[] args) { SynchronizedTest synchronizedTest = new SynchronizedTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc2(); } }); thread1.start(); thread2.start(); } }
输出以下:
Thread-0 in 1---> Thread-0-->synch staticFunc print Thread-0 out 1---> Thread-1 in 2---> Thread-1-->synch staticFunc2 print Thread-1 out 2--->
成员方法和静态方法的修饰区别是什么 ?对上面的代码,作一个简单的修改, Thread1调用对象1的方法1, Thread3 调用对象2的方法1
public static void main(String[] args) { SynchronizedTest synchronizedTest = new SynchronizedTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc2(); } }); SynchronizedTest synchronizedTest2 = new SynchronizedTest(); Thread thread3 = new Thread(new Runnable() { @Override public void run() { synchronizedTest2.staticFunc2(); } }); thread1.start(); thread2.start(); thread3.start(); }
输出以下, 其中线程0 和线程1 保证有序, 可是与线程2就没有什么关系了;即这个锁是针对对象的,这个也很容易理解,毕竟对象都不一样了,对象的成员方法固然是相对独立的
Thread-0 in 1---> Thread-0-->synch staticFunc print Thread-2 in 2---> Thread-2-->synch staticFunc2 print Thread-0 out 1---> Thread-2 out 2---> Thread-1 in 2---> Thread-1-->synch staticFunc2 print Thread-1 out 2--->
同步代码块的使用,就是将一块代码用大括号圈起来, 外面用 synchronized()
进行修饰,括号里面就表示要加锁的东西
public class SynchronizedTest { public void staticFunc() { System.out.println(Thread.currentThread().getName() + " in 1--->"); try { synchronized (this) { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print"); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 1--->"); } public void staticFunc2() { System.out.println(Thread.currentThread().getName() + " in 2--->"); try { synchronized (this) { System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print"); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " out 2--->"); } public static void main(String[] args) { SynchronizedTest synchronizedTest = new SynchronizedTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { synchronizedTest.staticFunc2(); } }); thread1.start(); thread2.start(); } }
输出以下, 对这个说明一点, 若是在静态方法中, 使用了同步代码块, 那么括号里面的能够写什么 ? xx.class
便可
Thread-0 in 1---> Thread-1 in 2---> Thread-0-->synch staticFunc print Thread-0 out 1---> Thread-1-->synch staticFunc2 print Thread-1 out 2--->
源码以下
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
在加锁的代码块, 多了一个 monitorenter
, monitorexit
每一个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的全部权,过程以下:
执行monitorexit的线程必须是objectref所对应的monitor的全部者。
谈到
synchronized
就不可避免的要说到锁这个东西,基本上在网上能够搜索到一大批的关于偏向锁,轻量锁,重量锁的讲解文档,对这个东西基本上我也不太理解,多看几篇博文以后,简单的记录一下
先抛一个结论: 轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能
获取过程
释放过程
“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程以前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁
简单来说,单线程时,使用偏向锁,若是这个时候,又来了一个线程访问这个代码块,那么就要升级为轻量锁,若是这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了。下面更多的显示了这些变更时,标记位的随之改变

单例模式,懒加载的方式,就是一个典型的利用了 synchronized
的案例
public class SingleClz { private static final SingleClz instance; private SingleClz() {} public static SingleClz getINstance() { if(instance == null) { synchronized(SingleClz.class) { if(instance == null) { instance = new SingleClz(); } } } return instance; } }
线程本地变量,每一个线程保存变量的副本,对副本的改动,对其余的线程而言是透明的(即隔离的)
先来瞅一下,这个东西通常的使用姿式。一般要获取线程变量, 直接调用 ParamsHolder.get()
public class ParamsHolder { private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>(); @ToString @Getter @Setter public static class Params { private String mk; } public static void setParams(Params params) { PARAMS_INFO.set(params); } public static void clear() { PARAMS_INFO.remove(); } public static Params get() { return PARAMS_INFO.get(); } public static void main(String[] args) { Thread child = new Thread(new Runnable() { @Override public void run() { System.out.println("child thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("thread")); System.out.println("child thread final: " + ParamsHolder.get()); } }); child.start(); System.out.println("main thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("main")); System.out.println("main thread final: " + ParamsHolder.get()); } }
输出结果
child thread initial: null main thread initial: null child thread final: ParamsHolder.Params(mk=thread) main thread final: ParamsHolder.Params(mk=main)
直接看源码中的两个方法, get/set, 看下究竟是如何实现线程变量的
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
先看set方法, 逻辑是获取当前线程对象, 获取到线程对象中的 threadLocals
属性, 这个属性的解释以下,简单来说, 这个里面的变量都是线程独享的,彻底由线程本身hold住
ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
获取的话主要是从 ThreadLocalMap
中,将存进去的参数捞出来,如今须要了解的就是这个对象的内部构造了, 里面的有个table对象,维护了一个Entry的数组table
, Entry
的key为ThreadLocal
对象, value为具体的值。
聚焦在 int i = key.threadLocalHashCode & (table.length - 1);
这一行,这个就是获取Entry对象在table
中索引值的主要逻辑,主要利用当前线程的hashCode值,假设出现两个不一样的线程,这个code值同样,会如何?下面的getEntry()
逻辑中对key值进行了判断是否为当前线程
//ThreadLocalMap.java static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
针对上面的逻辑,有两个点有必要继续研究下, hashCode
的计算方式, 为何要和数组的长度进行与计算
做为ThreadLocal实例的变量只有 threadLocalHashCode 这一个,
nextHashCode
和HASH_INCREMENT
是ThreadLocal类的静态变量,实际上HASH_INCREMENT
是一个常量,表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量,而nextHashCode 的表示了即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值
全部ThreadLocal对象共享一个AtomicInteger对象nextHashCode用于计算hashcode,一个新对象产生时它的hashcode就肯定了,算法是从0开始,以HASH_INCREMENT = 0x61c88647为间隔递增,这是ThreadLocal惟一须要同步的地方。根据hashcode定位桶的算法是将其与数组长度-1进行与操做
ThreadLocalMap的初始长度为16,每次扩容都增加为原来的2倍,即它的长度始终是2的n次方,上述算法中使用0x61c88647可让hash的结果在2的n次方内尽量均匀分布,减小冲突的几率
ThreadLocal
的注意事项这里主要的一个问题是线程复用时, 若是不清楚掉ThreadLocal 中的值,就会有可怕的事情发生, 先简单的演示一下
private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(0); } }; static class Task implements Runnable { @Override public void run() { AtomicInteger s = threadLocal.get(); int initial = s.getAndIncrement(); // 指望初始为0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); }
输出结果
0 0 1
说好的线程变量,这里竟然没有按照咱们预期的来玩,主要缘由就是线程复用了,而线程中的局部变量没有清零,致使下一个使用这个线程的时候,这些局部变量也带过来,致使没有按照咱们的预期使用
这个最可能致使的一个超级严重的问题,就是web应用中的用户串掉的问题,若是咱们将每一个用户的信息保存在 ThreadLocal
中, 若是出现线程复用了,那么问题就会致使明明是张三用户,结果登陆显示的是李四的账号,这下就真的呵呵了
所以,强烈推荐,对于线程变量,一但不用了,就显示的调用 remove()
方法进行清楚
SimpleDataFormate
是一个非线程安全的类,可使用 ThreadLocal 完成的线程安全的使用
public class ThreadLocalDateFormat { static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String date2String(Date date) { return sdf.get().format(date); } public static Date string2Date(String str) throws ParseException { return sdf.get().parse(str); } }