Java并发编程,终极篇持续更新

  1. java并发

目录

java并发关键字

Volatile

Synchronized

java内存模型

1.CAS

2.讲一下Java内存模型吧?

java线程及通信

2.一个线程连着调用 start 两次会出现什么情况?

3.wait 方法能不能被重写,wait 能不能被中断;

4.线程与进程的区别是什么?线程之间如何进行通信?进程之间如何进行通信?(当时进程如何通信我也没有答出来)

java中的锁

1、什么是线程安全,如何保证线程安全

2、重入锁的概念,重入锁为什么可以防止死锁

3、如何检查死锁,怎么预防死锁通过( jConsole 检查死锁) 

4、 ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理

java并发工具类

1.Java 8并法包下常见的并发类

2.CountDownLatch&CyclicBarrier

3.AQS

java线程池

1.Java中常见的线程池有哪几种?分别适用什么场景?你的项目中用的都是哪一种线程池?

2.声明一个线程池需要哪几个参数(说出线程池拒绝策略和阻塞队列会加分)?

3.线程池添加线程的逻辑是什么(主要回答核心线程数、最大线程数和阻塞队列之间的关系,最好是说出常见的各种线程池之间的区别)?   

4.java并发项目问题


java并发关键字

Volatile

1.Volatile作用是什么?是如何实现的?什么是缓存一致性协议?缓存一致性协议的作用是什么?为什么要用缓存一致性协议?

Volatile作用是:1防止指令重排序,内存屏障StoreLoad LoadLoad 2可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。(1)Lock前缀指令会引起处理器缓存回写到内存,2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

由于IO速度比cpu执行速度慢得多,于是引入了高速缓存

       引入高速缓存带了一个新的问题:缓存一致性。即如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致

缓存一致性协议:

最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

 

Synchronized

1.Synchronized作用于方法、代码块、静态方法时的区别是什么?Synchronized是如何实现的?Synchronized与ReentrantLock的区别是什么?ReentrantLock是如何实现的?Synchronized与ReentrantLock适用的场景分别是什么?synchronized底层实现?synchronized和Lock区别

Synchronized代码块,方法:它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。静态方法时:作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

Synchronized是如何实现的?

线程去争夺锁过程?

锁代码块(锁对象可指定,可为this、XXX.class、全局变量)

synchronized代码块主要是靠monitorenter和monitorexit这两个原语来实现同步的。当线程进入monitorenter获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行monitorexit释放锁后,其他线程才可以竞争获取锁。

在这里,我们先阐释一下Java虚拟机规范中相关内容:

(1)、monitorenter

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

上述第2点就涉及到了可重入锁,意思就是说当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的synchronized语句块嵌套在一起。在进入时,monitor的进入数+1;退出时就-1,直到为0的时候才可以被其他线程竞争获取。

(2)、monitorexit

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

锁普通方法(锁对象是this,即该类实例本身)

这里并没有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。

 

      在这里,我们将以上两种方法进行一下说明: 

      首先是代码块,当程序运行到monitorenter时,竞争monitor,成功后继续运行后续代码,直到monitorexit才释放monitor;而ACC_SYNCHRONIZED则是通过标志位来提示线程去竞争monitor。也就是说,monitorenter和ACC_SYNCHRONIZED只是起标志作用,并无实质操作。

锁静态方法(锁对象是该类,即XXX.class)

      常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。

线程竞争锁:

JVM中是通过队列来控制线程去竞争锁的。

 

(1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。

(2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。

(4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。

(5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。

      处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。 

      为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。 

      对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。

      所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。

比较Synchronized与ReentrantLock的区别是什么

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

Synchronized是如何实现的?:JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

java内存模型

1.CAS

CAS是什么意思?为什么要用CAS?CAS问题?

Java的compareAndSet()方法调用简称CAS,如果当前状态值等于预期值,则原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的语义。(非公平锁,乐观锁效率高)我认为是为了效率和简单,同时我个人推测这些原子指令被当下的硬件广泛支持的原因就是为了高效实现类似锁的并发控制

1ABA问题:2循环时间长开销大3只能保证一个共享变量的原子操作

2.讲一下Java内存模型吧?

    Java 内存模型(JMM)规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存。 线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 而 JMM 就作用于工作内存和主存之间数据同步过程。它规定了如何做数据同步以及什么时候做数据同步。

java线程及通信

  1. 多线程了解吧?讲一下多线程

多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

多线程的好处:

可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的劣势:

  1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
  2. 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
  3. 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。

2.一个线程连着调用 start 两次会出现什么情况?

线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。

3.wait 方法能不能被重写,wait 能不能被中断;

wait()、notify()和notifyAll()定义在Object类中,并且为final方法,无法被重写

wait是阻塞方法,线程被中断会抛出InterruptedException异常。当前线程必须拥有对象锁,才能调用对象的wait方法,调用wait方法会使当前线程阻塞,当前线程会进入到对象的等待队列,同时释放对象锁,调用wait的伪代码如下

4.线程与进程的区别是什么?线程之间如何进行通信?进程之间如何进行通信?(当时进程如何通信我也没有答出来)

线程通信

  1. 同步:多个线程通过synchronized关键字实现线程间通信,这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
  2. While轮询的方式

轮询的条件的可见性问题,关于内存可见性问题, volatile关键字的可见性”

线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的 条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环。

 

  1. Wait/notify机制:

thread.join(), object.wait(), object.notify(), CountdownLatch, CyclicBarrier, FutureTask, Callable使用生产消费模式实现线程之间的通信.

进程通信

java中的锁

1、什么是线程安全,如何保证线程安全

多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。

以下几种实现方式:

不可变:

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

互斥同步

synchronized 和 ReentrantLock。

非阻塞同步:互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

CAS和AtomicInteger

无同步方案:threadLocal

2、重入锁的概念,重入锁为什么可以防止死锁

重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。

重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

3、如何检查死锁,怎么预防死锁通过( jConsole 检查死锁) 

Jconsole是JDK自带的图形化界面工具和Jstack检测死锁: Jstack是JDK自带的命令行工具,主要用于线程Dump分析。查看dump文件,然后进行分析

4、 ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理

     ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

实线代表强引用,虚线代表弱引用。

 

每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

  1. 也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在GC 时会被回收。
  2. ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
  3. 总的来说就是,ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。

但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法。

  1. 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
  2. 但是这些被动的预防措施并不能保证不会内存泄漏:

(1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。

(2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。

为什么使用弱引用而不是强引用?OOM是否是弱引用的锅?

 

下面我们分两种情况讨论:

(1)key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

(2)key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

java并发工具类

1.Java 8并法包下常见的并发类

CountDownLatch、CyclicBarrier、Semaphore、Exchanger、AtomicInteger

2.CountDownLatch&CyclicBarrier

1.CountDownLatch的作用是什么?是如何实现的?与CyclicBarrier的区别是什么?CountDownLatch和CyclicBarrier分别适用于哪种场景CyclicBarrier是如何实现的?

CountDownLatch用来控制一个线程等待多个线程。维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。原理:CountDownLatch是自定义AQS同步组件,CountDownLatch实质上就是一个AQS计数器,通过AQS来实现线程的等待与唤醒。,接下来就以自定义同步器Sync、countDown方法和await方法为切入点两种await,一种有时间,一种无时间。

CyclicBarrier用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。和CountdownLatch 相似,都是通过维护计数器来实现的。线程执行await()方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

CyclicBarrier和countDownLatch区别

  1. CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  2. CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重 置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果:?计算发生错误,可以重置计数 器,并让线程重新执行一次。

CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier 阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。

 

Semaphore 原理和Exchanger原理

AtomicInteger 底层实现原理         

3.AQS

AQS是什么(中文名和英文全称最好都记住)?AQS的作用是什么?AQS是如何实现的?有哪些地方用到了AQS(ReentrantLock就是通过调用AQS来实现的)?

https://www.jianshu.com/p/df0d7d6571de

java线程池

1.Java中常见的线程池有哪几种?分别适用什么场景?你的项目中用的都是哪一种线程池?

2.声明一个线程池需要哪几个参数(说出线程池拒绝策略和阻塞队列会加分)?

3.线程池添加线程的逻辑是什么(主要回答核心线程数、最大线程数和阻塞队列之间的关系,最好是说出常见的各种线程池之间的区别)?   

4.java并发项目问题

1.你的项目中有哪些地方用到了多线程?为什么要用多线程?怎么解决多线程同步问题?

 

你可能感兴趣的:(java)