Java并发之 volatile & synchronized & ThreadLocal 讲解

Java 之 volatile & synchronized & ThreadLocal 讲解

在并发编程中,基本上离不开这三个东西,如何实现多线程之间的数据共享,能够用 volatile; 每一个线程维护本身的变量,则采用 ThreadLocal; 为了保证方法or代码块的线程安全,就该 synchronized 上场。这里将主要说明下这三个能够怎么用,以及内部的实现细节html

1. volatile

java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保经过排他锁单独得到这个变量。Java语言提供了volatile,在某些状况下比锁更加方便。若是一个字段被声明成volatile,java线程内存模型确保全部线程看到这个变量的值是一致的。java

实现原理

处理器为了提升处理速度,不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其余)后再进行操做,但操做完以后不知道什么时候会写到内存,若是对声明了Volatile变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里 用一个图简单的说明上面的过程web

图解

图画的通常般,简单说一下算法

  1. cpu与内部缓存进行交互
  2. volatile生命的变量,操做完以后写入内存(data -> data' 同时写入内存)
  3. 其余cpu缓存嗅探总线变更,并设置本身的data无效,使用时,从内存中获取

输入图片说明

测试case

咱们有两个线程, 线程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代码块中,或者为常量时,没必要使用。

2. 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的全部权,过程以下:

  1. 若是monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的全部者。
  2. 若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加1.
  3. 若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的全部权

执行monitorexit的线程必须是objectref所对应的monitor的全部者。

  1. 指令执行时,monitor的进入数减1
  2. 若是减1后进入数为0,那线程退出monitor,再也不是这个monitor的全部者
  3. 其余被这个monitor阻塞的线程能够尝试去获取这个 monitor 的全部权

谈到 synchronized 就不可避免的要说到锁这个东西,基本上在网上能够搜索到一大批的关于偏向锁,轻量锁,重量锁的讲解文档,对这个东西基本上我也不太理解,多看几篇博文以后,简单的记录一下

先抛一个结论: 轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能

1. 偏向锁

获取过程

  • 判断是否为可偏向状态
  • 是,则判断线程ID是否指向当前线程
    • 是,即表示这个偏向锁就是这个线程持有, 直接执行代码块
    • 否,经过CAS操做竞争锁
      • 竞争成功, 则设置线程ID为当前线程, 并执行代码块;
      • 竞争失败,说明多线程竞争啦,问题严重了,当偏向锁到达安全点时,将偏向锁升级为轻量锁

释放过程

  • 当偏向锁遇到其余线程尝试竞争时,持有偏向锁的线程会释放,并升级为轻量锁
  • 到达安全点, 暂停拥有偏向锁的线程,判断锁对象是否处于被锁的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

2. 轻量锁

“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程以前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁

轻量锁

3. 转换

简单来说,单线程时,使用偏向锁,若是这个时候,又来了一个线程访问这个代码块,那么就要升级为轻量锁,若是这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了。下面更多的显示了这些变更时,标记位的随之改变

输入图片说明

4. 经典案例

单例模式,懒加载的方式,就是一个典型的利用了 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;
    }
}

ThreadLocal

线程本地变量,每一个线程保存变量的副本,对副本的改动,对其余的线程而言是透明的(即隔离的)

1. 使用姿式一览

先来瞅一下,这个东西通常的使用姿式。一般要获取线程变量, 直接调用 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)

2. 实现原理探究

直接看源码中的两个方法, 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的数组tableEntry的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 这一个,nextHashCodeHASH_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次方内尽量均匀分布,减小冲突的几率

3. 线程池中使用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()方法进行清楚

4. 经典case

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);
    }
}

参考文档:

  1. 聊聊并发(一)深刻分析Volatile的实现原理
  2. Java 并发编程:volatile的使用及其原理
  3. Synchronized及其实现原理
  4. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
  5. 聊聊并发(二)Java SE1.6中的Synchronized
  6. 理解ThreadLocal / 计算机程序的思惟逻辑
  7. 【ThreadLocal】深刻JDK源码之ThreadLocal类
相关文章
相关标签/搜索