4种解决线程安全问题的方式

前言

线程安全问题,在作高并发的系统的时候,是程序员常常须要考虑的地方。怎么有效的防止线程安全问题,保证数据的准确性?怎么合理的最大化的利用系统资源等,这些问题都须要充分的理解并运行线程。固然关于多线程的问题在面试的时候也是出现频率比较高的。下面就来学习一下吧!java

线程

先来看看什么是进程和线程?程序员

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会建立一个进程,并为它分配资源,而后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。就好比说,咱们开发的一个单体项目,运行它,就会产生一个进程。面试

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程能够由不少个线程组成,线程间共享进程的全部资源,每一个线程有本身的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就容许多个线程同时运行。一样多线程也能够实现并发操做,每一个请求分配一个线程来处理。在这里强调一点就是:计算机中的线程和应用程序中的线程不是同一个概念。数据库

总之一句话描述就是:进程是资源分配的最小单位,线程是程序执行的最小单位。安全

什么是线程安全

什么是线程安全呢?什么样的状况会形成线程安全问题呢?怎么解决线程安全呢?这些问题都是在下文中所要讲述的。服务器

线程安全:当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以得到正确的结果,那这个对象就是线程安全的。多线程

那何时会形成线程安全问题呢?当多个线程同时去访问一个对象时,就可能会出现线程安全问题。那么怎么解决呢?请往下看!并发

解决线程安全

在这里提供4种方法来解决线程安全问题,也是最经常使用的4种方法。前提是项目在一个服务器中,若是是分布式项目可能就会用到分布锁了,这个就放到后面文章来详谈了。jvm

讲4种方法前,仍是先来了解一下悲观锁和乐观锁吧!分布式

悲观锁,顾名思义它是悲观的。讲得通俗点就是,认为本身在使用数据的时候,必定有别的线程来修改数据,所以在获取数据的时候先加锁,确保数据不会被线程修改。形象理解就是总以为有刁民想害朕。

而乐观锁就比较乐观了,认为在使用数据时,不会有别的线程来修改数据,就不会加锁,只是在更新数据的时候去判断以前有没有别的线程来更新了数据。具体用法在下面讲解。

如今来看有那4种方法吧!

  • 方法一:使用synchronized关键字,一个表现为原生语法层面的互斥锁,它是一种悲观锁,使用它的时候咱们通常须要一个监听对象 而且监听对象必须是惟一的,一般就是当前类的字节码对象。它是JVM级别的,不会形成死锁的状况。使用synchronized能够拿来修饰类,静态方法,普通方法和代码块。好比:Hashtable类就是使用synchronized来修饰方法的。put方法部分源码:

    public synchronized V put(K key, V value) {
            // Make sure the value is not null
            if (value == null) {
                throw new NullPointerException();
            }

    而ConcurrentHashMap类中就是使用synchronized来锁代码块的。putVal方法部分源码:

    else {
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;

    synchronized关键字底层实现主要是经过monitorenter 与monitorexit计数 ,若是计数器不为0,说明资源被占用,其余线程就不能访问了,可是可重入的除外。说到这,就来说讲什么是可重入的。这里其实就是指的可重入锁:指的是同一线程外层函数得到锁以后,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中全部同步方法不用再次得到锁。避免了频繁的持有释放操做,这样既提高了效率,又避免了死锁。

    其实在使用synchronized时,存在一个锁升级原理。它是指在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,若是一致则能够直接使用此对象,若是不一致,则升级偏向锁为轻量级锁,经过自旋循环必定次数来获取锁,执行必定次数以后,若是尚未正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。锁升级的目的是为了减低了锁带来的性能消耗。在 Java 6 以后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。可能你又会问什么是偏向锁?什么是轻量级锁?什么是重量级锁?这里就简单描述一下吧,可以帮你更好的理解synchronized。

    偏向锁(无锁):大多数状况下锁不只不存在多线程竞争,并且老是由同一线程屡次得到。偏向锁的目的是在某个线程得到锁以后(线程的id会记录在对象的Mark Word中),消除这个线程锁重入(CAS)的开销,看起来让这个线程获得了偏护。

    轻量级锁(CAS):就是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的状况下,经过CAS操做尝试将MarkWord更新为指向LockRecord的指针,减小了使用重量级锁的系统互斥量产生的性能消耗。

    重量级锁:虚拟机使用CAS操做尝试将MarkWord更新为指向LockRecord的指针,若是更新成功表示线程就拥有该对象的锁;若是失败,会检查MarkWord是否指向当前线程的栈帧,若是是,表示当前线程已经拥有这个锁;若是不是,说明这个锁被其余线程抢占,此时膨胀为重量级锁。

  • 方法二:使用Lock接口下的实现类。Lock是juc(java.util.concurrent)包下面的一个接口。经常使用的实现类就是ReentrantLock 类,它其实也是一种悲观锁。一种表现为 API 层面的互斥锁。经过lock() 和 unlock() 方法配合使用。所以也能够说是一种手动锁,使用比较灵活。可是使用这个锁时必定要注意要释放锁,否则就会形成死锁。通常配合try/finally 语句块来完成。好比:

    public class TicketThreadSafe extends Thread{
          private static int num = 5000;
          ReentrantLock lock = new ReentrantLock();
          @Override
          public void run() {
            while(num>0){
                 try {
                   lock.lock();
                   if(num>0){
                     System.out.println(Thread.currentThread().getName()+"你的票号是"+num--);
                   }
                  } catch (Exception e) {
                     e.printStackTrace();
                  }finally {
                     lock.unlock();
                  }
                }
          }
    }

    相比 synchronized,ReentrantLock 增长了一些高级功能,主要有如下 3 项:等待可中断、可实现公平锁,以及锁能够绑定多个条件。

    等待可中断是指:当持有锁的线程长期不释放锁的时候,正在等待的线程能够选择放弃等待,改成处理其余事情,可中断特性对处理执行时间很是长的同步块颇有帮助。

    公平锁是指:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会得到锁。synchronized 中的锁是非公平的,ReentrantLock 默认状况下也是非公平的,但能够经过带布尔值的构造函数要求使用公平锁。

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }

    锁绑定多个条件是指:一个 ReentrantLock 对象能够同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法能够实现一个隐含的条件,若是要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样作,只须要屡次调用 newCondition() 方法便可。

    final ConditionObject newCondition() { //ConditionObject是Condition的实现类
                return new ConditionObject();
        }
  • 方法三:使用线程本地存储ThreadLocal。当多个线程操做同一个变量且互不干扰的场景下,可使用ThreadLocal来解决。它会在每一个线程中对该变量建立一个副本,即每一个线程内部都会有一个该变量,且在线程内部任何地方均可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。在不少状况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。经过set(T value)方法给线程的局部变量设置值;get()获取线程局部变量中的值。当给线程绑定一个 Object 内容后,只要线程不变,就能够随时取出;改变线程,就没法取出内容.。这里提供一个用法示例:

    public class ThreadLocalTest {
          private static int a = 500;
          public static void main(String[] args) {
                new Thread(()->{
                      ThreadLocal<Integer> local = new ThreadLocal<Integer>();
                      while(true){
                            local.set(++a);   //子线程对a的操做不会影响主线程中的a
                            try {
                                  Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                  e.printStackTrace();
                            }
                            System.out.println("子线程:"+local.get());
                      }
                }).start();
                a = 22;
                ThreadLocal<Integer> local = new ThreadLocal<Integer>();
                local.set(a);
                while(true){
                      try {
                            Thread.sleep(1000);
                      } catch (InterruptedException e) {
                            e.printStackTrace();
                      }
                      System.out.println("主线程:"+local.get());
                }
          }
    }

    ThreadLocal线程容器保存变量时,底层实际上是经过ThreadLocalMap来实现的。它是以当前ThreadLocal变量为key ,要存的变量为value。获取的时候就是以当前ThreadLocal变量去找到对应的key,而后获取到对应的值。源码参考以下:

    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; //ThreadLocal.ThreadLocalMap threadLocals = null;Thread类中声明的
        }
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    观察源码就会发现,其实每一个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

    初始时,在Thread里面,threadLocals为空,当经过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,而且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

    而后在当前线程里面,若是要使用副本变量,就能够经过get方法在threadLocals里面查找便可。

  • 方法四:使用乐观锁机制。前面已经讲述了什么是乐观锁。这里就来描述哈在java开发中怎么使用的。

    其实在表设计的时候,咱们一般就须要往表里加一个version字段。每次查询时,查出带有version的数据记录,更新数据时,判断数据库里对应id的记录的version是否和查出的version相同。若相同,则更新数据并把版本号+1;若不一样,则说明,该数据发生了并发,被别的线程使用了,进行递归操做,再次执行递归方法,直到成功更新数据为止。

相关文章
相关标签/搜索