多线程相关问题-再总结

本文探讨了迭代器的工作原理,进程与线程的区别,以及悲观锁、乐观锁的概念和死锁的成因与避免。重点讲解了ThreadLocal的设计原理和Synchronized与ReentrantLock的对比,涉及内存模型、volatile关键字和CAS操作。最后剖析了线程池和原子类在并发编程中的应用。
摘要由CSDN通过智能技术生成

从迭代器谈谈

  • 可迭代 是Java集合框架下的所有集合类的一种共性,也就是把集合中的所有元素遍历一遍。迭代的过程需要依赖一个迭代器对象,采用一种**游标模式,**去访问容器对象中各个元素,而又不需暴露该对象的内部细节。迭代器就是一个接口Iterator,实现了该接口的类就叫做可迭代类,在迭代器Iteartor接口中,有以下3个方法:hasNext()、next()、remove()。Java中还提供了一个Iterable接口,Iterable接口实现后的功能是‘返回’一个迭代器,像常用的实现了该接口的子接口有:Collection< E>、List< E>、Set< E>等。从Java5.0开始,迭代器可以被foreach循环所替代,但是foreach循环的本质也是使用Iterator进行遍历的。实现Iterable接口允许对象成为Foreach语句的目标。就可以通过foreach语句来遍历你的底层序列。
  • 在使用Iterator的时候禁止对所遍历的容器进行改变其大小结构的操作会抛出ConcurrentModificationException。因为在你迭代之前,迭代器已经被通过集合list.itertor()创建出来了,如果在迭代的过程中,又对原集合进行了改变其容器大小的操作,那么此时迭代器就无法主动同步集合做出的改变,Java会认为你做出这样的操作是线程不安全的就会给出异常。
  • 通过查看源码发现原来检查并抛出异常的是checkForComodification()方法。在ArrayList中modCount是当前集合的版本号,每次修改(增、删)集合都会加1;expectedModCount是当前迭代器的版本号,在迭代器实例化时初始化为modCount。我们再到用next方法遍历下一个元素之前,checkForComodification()方法中就是在验证modCount的值和expectedModCount的值是否相等,不相等就抛出异常。这个也称为“快速失败”机制对应有一个“安全失败”机制。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。
  • 安全-失败:在遍历时不是在集合内容上访问,而是先复制原有集合内容,在拷贝的集合上进行遍历,所以在遍历过程中对集合进行更新,不会被迭代器检测到,不会抛出ConcurrentModificationException。优点:避免了这个异常而且支持并发访问。缺点:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的,即:不能立即获取更新的数据。

什么是进程和线程

进程

首先进程是一个具有独立功能,可以独立运行的一个程序,我们打开win下的任务管理器查看.exe文件,那些就是一个进程。有时我们也会发现一个程序可能有多个进行在运行,这种就是一种多进程程序。

操作系统为了使参与并发执行的每个程序都能独立运行,专门配置了一个数据结构进程控制块process control block,pcb:系统利用pcb来描述进程的基本情况和活动的过程,进而控制进程和管理进程,一直在循环不重复的记录当前进程的数据。进程实体由程序段、相关数据段、pcb三部分构成,也就是进程,其实创建一个进程和撤销一个进程实际上是对pcb的创建和撤销。

进程具有以下特征:

  • 动态性:由创建而产生、调度而执行、撤销而消亡
  • 并发性:指的是多个进程在内存中可以同时执行,这也是pcb的作用
  • 独立性:进程是独立的运行,独立的获取资源(包括程序正文、内存磁盘地址、io设备、已打开文件等)、独立的接受调度的单位(pcb可以感知当前线程的存在,也可以根据其他进程的pcb进行进程调度)
  • 异步性和结构性。
  • 运行一个程序就会至少产生一个进程,每个进程都有自己的内存空间和系统资源,进程可以对系统资源进行管理,如果要求资源管理严格的话,可以使用多进程的方式,否则使用单进程
  • 每个java进程对应一个JVM实例,多个线程共享JVM里的堆

线程

  • 进程是操作系统资源分配的基本单位,而线程是处理器进行任务调度和执行的基本单位,是一种轻型进程,相应的系统也为进程分配了一个TCB线程控制块(包括线程标识符、运行状态、优先级、线程专有存储区、堆栈指针等)
  • 是进程中的单个顺序控制流,是一条执行路径,分别去执行一个任务,一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序,多线程可以提高程序的执行效率,帮助进程更多概率的抢到CPU的执行权
  • 每个java线程共享堆区和方法区,每个线程有自己的程序计数器、虚拟机栈、和本地方法栈,可以理解为线程为轻量级的进程。
  • 线程是进程中的单个顺序控制流,是一条执行路径,一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序,多线程可以提高程序的执行效率,帮助进程更多概率的抢到CPU的执行权。每个java线程共享堆区和方法区,每个线程有自己的程序计数器、虚拟机栈、和本地方法栈,可以理解为线程为轻量级的进程。

悲观锁和乐观锁

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

悲观锁机制存在以下问题:

  1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会 导致优先级倒置,引起性能风险。
    对此于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止
  • 乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_ condition机制, 实都是提供的乐观锁。在Java中java.til.concurrent.atomic包 下面的原变类就是使用了乐观锁的-种实现方式CAS实现的。

乐观锁( Optimistic Locking)在上文已经说过了,其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

死锁

  • 概念:所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。例如:此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁
  • 产生原因:竞争资源(不可剥夺资源、可剥夺资源)、进程间推进顺序非法
  • 产生条件
    1. 互斥条件:在一段时间内某资源仅为一个线程占用
    2. 请求和保持条件:当进程因请求资源而阻塞时,已获得资源保持不放
    3. 不剥夺条件:进程已获得的资源在未使用前不能剥夺,只能在使用完后自己释放
    4. 环路等待条件:在发生死锁时,必然存在一个(进程-资源)的环形链。即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
  • 预防死锁:(破坏四个必要条件中的一个或多个来预防死锁)
    1. 一次性分配所有资源,这样就不会再有请求了(破坏互斥条件
    2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请求保持条件
    3. 当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件
    4. 系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件
  • 避免死锁:
    1. 系统安全状态法:在进行系统资源分配之前,先计算此资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。
    2. 指定线程获取锁的顺序,并强制线程按照指定顺序获取锁、释放锁,即规范线程间的推进顺序。

ThreadLocal了解吗?是怎样实现的?

ThreadLocal可以实现线程自己专属的本地变量,每个线程持有的是这个变量的一个副本,可以通过set/get方法独立修改和访问这个变量,并且线程之间不会发生冲突,避免了线程安全。

原理:Thread类中有threadLocals变量是ThreadLocalMap类型,调用ThreadLocal的set/get方法其实就是在调用这个Map的set/get方法,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在 ThreadLocal上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值,ThreadLocalMap的key就是ThreadLocal对象,value 就是 ThreadLocal对象调用set方法设置的值。ThreadLocal的map结构是为了让每个线程可以关联多个 ThreadLocal变量而存储多个本地变量值。

当创建一个ThreadLocal并调用set时,实际就是向ThreadLocalMap存入一个Entry节点,Entry节点中存储了ThreadLocal变量和值,其中这个Entry继承了WeakReference是一个弱引用,而 value 是强引用。当ThreadLocal的引用设为空时,ThreadLocal的对象将由于Entry中key是一个弱引用所以就会被GC,这样做的目的就是当防止ThreadLocal内存泄漏,而value则需要我们手动的调用remove方法来进行释放。

Synchronized和ReentrantLock的区别

ReentrantLock(再入锁)

  • 位于java.util.concurrent.locks包
  • 和CountDownLatch、FutureTask、 Semaphore一样基于AQS实现
  • 能够实现比synchronized更灵活和更加细粒度的控制,如控制fairness
  • 调用lock()之后,必须调用unlock()释放锁
  • 性能未必比synchronized高,并且也是可重入的

ReentrantLock公平性的设置

  • ReentrantLock fairLock = new ReentrantLock(true);
  • 参数为true时,倾向于将锁赋予等待时间最久的线程
  • 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)(等待时间最长的一定先获取锁,非公平锁的效率要高一些,因为他把线程挂起和获得执行权的这段时间充分利用起来了,而公平锁则需要让那个等待最久的线程从挂起恢复到真正执行),hasQueuedPredecessors()判断当前节点是否有前驱节点,也就是是否有更早申请锁的线程正在等待。
  • 非公平锁:抢占的顺序不一定,看运气。synchronized是非公平锁

总结

  1. synchronized是关键字 , ReentrantLock是类
  2. ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
  3. ReentrantLock可以获取各种锁的信息
  4. ReentrantLock可以 灵活地实现多路通知
  5. 机制: sync操作Mark Word , lock调用Unsafe类的park()方法
  6. Lock:获取的锁可以被中断、超时获取锁、尝试获取锁、读多写少用读写锁

讲讲Synchronized

  1. Synchronized首先是一个同步关键字,满足互斥性、可见性。他可以修饰代码块,成员方法、静态方法,被修饰后就相当于加了锁,代码块中的锁对象可以是任意对象,成员方法中的锁默认是this,静态方法中的锁是类对象。在修饰代码块时,有两个指令,monitorenter指向代码块开始的地方,monitorexit指向代码块结束的地方,当一个线程视图获取锁时,会去锁对象头中查看monitor中的锁计数器变量,如果是0那么可以获取锁,获取后修改为1,别的线程是不能访问的,释放时在修改回0。在修改时方法时,底层对方法会加一个ACC_SYNCHRONIZED标识该方法是一个加了锁的方法,线程访问时要获取锁对象。
  2. 早期的synchronized效率低下,因为Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
  3. Synchronized在1.6以后做了很大的优化,比如:锁粗化、锁消除等。锁还有锁升级的一个过程,首先是无锁状态,然后是偏向锁:偏向锁指的是当一个线程获取到锁时(按照经验:锁一般情况总是由那一个线程多次获得),这个锁就会进入了偏向模式,锁对象内部的MarkWord结构也会变成偏向锁结构,当该线程下次在请求锁时,只需要检查MarkWord锁标记位和ThreadId是否相等即可,不用做额外的申请锁的操作。但是当一旦存在多个线程竞争这个锁时,该锁就会升级为轻量级锁,轻量级锁使用的CAS避免使用互斥操作的开销,CAS就是比较和交换(修改共享变量前会查,修改后会查,然后比较两次是否相等,如果不相等那么此修改是不安全的,否则此修改就是安全的可以修改),(按照经验:一个线程持续锁定的时间一般是很短的)所以此时会让线程处于等待但是不让出cpu,也就是“忙循环”、自旋状态。在自旋一定次数时,会升级为重量锁。但是如果锁竞争激烈的话,轻量级锁才会升级为重量级锁,因为长时间的自旋和频繁的CAS操作会带来性能上的开销。锁升级的过程,每个状态全部记录在MarkWord的8个字节(64位)中。
    锁消除:如果一个方法中的对象没有逃逸出方法不会被其他线程访问到,即对外部没有影响时,会优化为无锁。
    锁粗化:jvm探测到一连串的细小操作都使用了同一个对象锁,会将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。例如一个for循环中出现了StringBuffer的方法,就会优化为对for加锁进行粗化。
    平时代码怎么优化: 1. 减少synchronized的范围,减少里面的代码 。 2. 降低synchro的粒度 。 3. 进行读写分离,使用juc包下的容器。

理解CAS

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”通常将 CAS 用于同步的方式是从地址V读取值 A,执行多步计算来获得新值B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

cas的问题

  • ABA:就是在第二次读取时,虽然和第一次读取的值是相等的,但是再此期间可能情况是别的线程已经对该变量进行了修改,只是其他线程经过数次修改后那个值和你第一次读取的值相等。解决办法:给变量在加一个版本号或者boolean值来标记是否被其他线程改动过。
  • 循环时间过长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。解决办法:JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用:
  • 不能保证多个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

线程池

就是提前创建若干线程,然后把这些线程统一管理起来,因为创建和销毁线程是很消耗系统性能的,锁一次性创建若干个线程,可以降低资源消耗,提高程序响应速度,最主要的是提高线程的可管理性。线程池主要核心是接收任务(Runable、Callable接口)、执行任务(submit方法)、返回结果(Future接口)。

执行机制:向线程池提交一个任务时,首先会判断核心线程池是否已满,如果未满就通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。否则通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态、并且队列未满可以加入任务,该任务才会被加入进去。否则在去判断线程池是否已满,如果未满就创建线程,否则最后按照策略处理该线程。

Executor框架提供了对线程池的管理、创建线程池等。ThreadPoolExecutor类中通过构造方法可以直接创建一个指定线程数的线程池,他的构造方法有几个重要的参数(核心线程数、最大线程数、拒绝策略等)

内存模型与volatile

  • 在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型规定了所有的变量都存储在主内存中,但是每个线程会有自己的工作内存,线程对变量的访问不是直接在主存中进行读写,而是拷贝保存在工作内存中。线程对变量的操作都必须在工作内存中进行,不同线程之间无法直接访问对方工作内存中的变量,线程间变量值从传递都要经过主内存完成。
  • 这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
  • 要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。说白了, volatile关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。

volatile

  1. 可以理解为轻量级的synchronized,它在多处理器开发中保证了数据的读的一致性,也就是说当一个线程修改一个volatile修饰的共享变量时,另外一个线程能立即读到这个共享变量的值。(可见性

  2. 如果volatile变量修饰符使用的恰当的话,他的运行成本会大大降低,因为他不会引起上下文的切换和调度,他并不会阻塞线程,也因此他不能保证多个线程对数据进行写操作时的安全性(即不保证原子性)。

  3. 还有一点他禁止了指令重排,“指令重排:指的是jvm在执行语句时,为了执行效率,在不影响执行结果的情况下,可能会对代码执行顺序进行重排,虽然不影响执行结果,但是在多线程下就会出现不安全问题。”例如:创建对象(先加载类的字节码文件,在分配内存空间,然后实例化对象,并让对象指向内存空间),此时如果多线程出现了指令重排,就可能会出现实例化对象没有被实例化的问题,volatile就可以解决这个问题禁止指令重排。在单例模式中,我们可以使用volatile关键字结合Synchronized代码块创建一个线程安全的单例。

可见性原理:volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀指令。
lock前缀的指令在多核处理器下会发生两件事情:1. 将当前处理器缓存行数据写回主内存2. 一个处理器的缓存写回到内存会导致其他处理器的缓存失效

如果对声明了Volatile 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自已缓存的值是不是过期了,当处理器发现自已缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

有序性原理:(禁止指令重排)指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况:
在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作,因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。

原子类

java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。synchronized关键字可以保证可见性和有序性却无法保证原子性。而这个Atomic类的作用就是为了保证原子性。他主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

原子类就是具有原子/原子操作特征的类。juc包中的原子类分为4类(基本类型、数组类型、引用类型、对象的属性修改类型)

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。
  • AtomicStampedReference :原子更新带有版本号的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值