关于thread:4种解决线程安全问题的方式

11次阅读

共计 5476 个字符,预计需要花费 14 分钟才能阅读完成。

前言

线程平安问题,在做高并发的零碎的时候,是程序员常常须要思考的中央。怎么无效的避免线程平安问题,保证数据的准确性?怎么正当的最大化的利用系统资源等,这些问题都须要充沛的了解并运行线程。当然对于多线程的问题在面试的时候也是呈现频率比拟高的。上面就来学习一下吧!

线程

先来看看什么是过程和线程?

过程是资源(CPU、内存等)调配的根本单位,它是程序执行时的一个实例。程序运行时零碎就会创立一个过程,并为它分配资源,而后把该过程放入过程就绪队列,过程调度器选中它的时候就会为它调配 CPU 工夫,程序开始真正运行。就比如说,咱们开发的一个单体我的项目,运行它,就会产生一个过程。

线程是程序执行时的最小单位,它是过程的一个执行流,是 CPU 调度和分派的根本单位,一个过程能够由很多个线程组成,线程间共享过程的所有资源,每个线程有本人的堆栈和局部变量。线程由 CPU 独立调度执行,在多 CPU 环境下就容许多个线程同时运行。同样多线程也能够实现并发操作,每个申请调配一个线程来解决。在这里强调一点就是:计算机中的线程和应用程序中的线程不是同一个概念。

总之一句话形容就是:过程是资源分配的最小单位,线程是程序执行的最小单位。

什么是线程平安

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

线程平安: 当多个线程拜访一个对象时,如果不必思考这些线程在运行时环境下的调度和交替执行,也不须要进行额定的同步,或者在调用方进行任何其余的协调操作,调用这个对象的行为都能够取得正确的后果,那这个对象就是线程平安的。

那什么时候会造成线程平安问题呢?当多个线程同时去拜访一个对象时,就可能会呈现线程平安问题。那么怎么解决呢?请往下看!

解决线程平安

在这里提供 4 种办法来解决线程平安问题,也是最罕用的 4 种办法。前提是我的项目在一个服务器中,如果是分布式我的项目可能就会用到散布锁了,这个就放到前面文章来详谈了。

讲 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;若不同,则阐明,该数据产生了并发,被别的线程应用了,进行递归操作,再次执行递归办法,直到胜利更新数据为止。

正文完
 0