前言
线程平安问题,在做高并发的零碎的时候,是程序员常常须要思考的中央。怎么无效的避免线程平安问题,保证数据的准确性?怎么正当的最大化的利用系统资源等,这些问题都须要充沛的了解并运行线程。当然对于多线程的问题在面试的时候也是呈现频率比拟高的。上面就来学习一下吧!
线程
先来看看什么是过程和线程?
过程是资源(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;若不同,则阐明,该数据产生了并发,被别的线程应用了,进行递归操作,再次执行递归办法,直到胜利更新数据为止。