并发编程(二)synchronized 和 ThreadLocal

并发编程(二)synchronized 和 ThreadLocal

一、synchronized 关键字

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

双重校验锁实现对象单例(线程安全)

public class Singleton {

   private volatile static Singleton uniqueInstance;

   private Singleton() {
   }
 
   public static Singleton getUniqueInstance() {
      //先判断对象是否已经实例过,没有实例化过才进入加锁代码
       if (uniqueInstance == null) {
           //类对象加锁
           synchronized (Singleton.class) {
               if (uniqueInstance == null) {
                   uniqueInstance = new Singleton();
               }
           }
       }
       return uniqueInstance;
   }
}

synchronized 原理

synchronized 关键字底层原理属于 JVM 层面。

① synchronized 同步语句块的情况

public class SynchronizedDemo {
public void method() {
 synchronized (this) {
  System.out.println("synchronized 代码块");
 }
}
}

查看字节码文件可知:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

② synchronized 修饰方法的的情况

public class SynchronizedDemo2 {
public synchronized void method() {
 System.out.println("synchronized 方法");
}
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

synchronized关键字和volatile关键字比较
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些

  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

二、ThreadLocal

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

/**
* @author chenqi
* @date 2019/7/15 17:38
*/
public class ThreadLocalTest implements Runnable {

   // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
   private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
//    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
//        @Override
//        protected SimpleDateFormat initialValue()
//        {
//            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        }
//    };

   public static void main(String[] args) throws InterruptedException {
       ThreadLocalTest obj = new ThreadLocalTest();
       for (int i = 0; i < 10; i++) {
           Thread t = new Thread(obj, "" + i);
           Thread.sleep(new Random().nextInt(1000));
           t.start();
       }
   }

   @Override
   public void run() {
       System.out.println("线程名: " + Thread.currentThread().getName() + " 默认格式:" + formatter.get().toPattern());
       try {
           Thread.sleep(new Random().nextInt(1000));
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       //formatter pattern is changed here by thread, but it won't reflect to other threads
       formatter.set(new SimpleDateFormat());

       System.out.println("线程名: " + Thread.currentThread().getName() + " 转化后的格式:" + formatter.get().toPattern());
   }
}

/** 输出:
线程名: 0 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 0 转化后的格式:yy-M-d ah:mm
线程名: 1 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 2 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 3 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 1 转化后的格式:yy-M-d ah:mm
线程名: 3 转化后的格式:yy-M-d ah:mm
线程名: 2 转化后的格式:yy-M-d ah:mm
线程名: 4 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 5 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 4 转化后的格式:yy-M-d ah:mm
线程名: 6 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 6 转化后的格式:yy-M-d ah:mm
线程名: 5 转化后的格式:yy-M-d ah:mm
线程名: 7 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 8 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 9 默认格式:yyyy-MM-dd HH:mm:ss
线程名: 8 转化后的格式:yy-M-d ah:mm
线程名: 7 转化后的格式:yy-M-d ah:mm
线程名: 9 转化后的格式:yy-M-d ah:mm
*/

可以看出每个线程都改变了原来的默认值,但是下一个进来的线程得到的还是最开始没有被修改的值。

ThreadLocal原理

Thread类源码:

public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

ThreadLocal类的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);
   }
   ThreadLocalMap getMap(Thread t) {
       return t.threadLocals;
   }

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 是 map结构是为了让每个线程可以关联多个 ThreadLocal变量。这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。

ThreadLocalMapThreadLocal的静态内部类。

发表评论