锁、高并发、多线程

高并发

并发编程的三要素是什么(线程的安全性问题体现在哪)?

原子性:一个或多个操作要么全部执行成功,要么全部执行失败。

可见性:一个线程对共享变量的修改,另一个线程能够立刻看到(synchronized,volatile)。

有序性:程序执行的顺序按照代码的先后顺序执行。(有序性不代表禁止指令重排)。

什么是JAVA内存模型?

首先,JAVA内存模型是指JMM,而不是指内存结构,内存结构是在物理上的区域划分,而JMM则是抽象概念上的划分。

JMM(内存模型)主要包括两块:主内存+工作内存

主内存:多个线程间通信的共享内存称之为主内存,即,数据是多个线程工共享的,在物理内存结构上通常对应“堆”中的线程共享数据。

工作内存:多个线程各自对应自己的本地内存,即,数据只属于该线程自己的,在物理内存结构上通常对应“本地方法栈”中的线程私有数据。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写 主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来实现。

volatile 关键字的作用

Java 提供了 volatile 关键字来保证可见性禁止指令重排(一定有序)。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

Volatile是怎么保证可见性的?

对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改。

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据。

小结:lock前缀指令 + MESI缓存一致性协议。

Volatile能保证强一致性吗?

不能。可见性可以认为是最弱的“一致性”(弱一致),只保证用户见到的数据是一致的,但不保证任意时刻,存储的数据都是一致的(强一致)。它只能保证线程过来读取数据时,能获取到当前的最新数据。

什么是MESI缓存一致性协议?

M(修改, Modified): 本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此cache只有本地一个拷贝(专有)。

E(专有, Exclusive): 缓存行内容和内存中的一样, 而且其它处理器都没有这行数据。

S(共享, Shared): 缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝。

I(无效, Invalid): 缓存行失效, 不能使用。

过程:

Core0修改v后,发送一个信号,将Core1缓存的v标记为失效,并将修改值写回内存。

Core0可能会多次修改v,每次修改都只发送一个信号(发信号时会锁住缓存间的总线),Core1缓存的v保持着失效标记。

Core1使用v前,发现缓存中的v已经失效了,得知v已经被修改了,于是重新从其他缓存或内存中加载v。

Volatile是怎么做到禁止指令重排的?

对于volatile修改变量的读写操作,都会加入内存屏障。

每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。

每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排。


synchronized为什么又叫内置锁?

synchronized有多个叫法:内置锁、隐式锁、同步锁、对象锁等。

synchronized是内置于JDK中的,底层实现是native,由C/C++语言实现;同时,加锁、解锁都是JDK自动完成,不需要用户显示控制,非常方便。

 Native关键字的作用是什么?

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。
这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。
java是跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而java要实现对底层的控制,就需要一些其他语言的帮助,这个就是native的作用了。

synchronized 和 volatile 的区别是什么?

synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。

volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别:

volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

同步方法和同步块,哪个是更好的选择?

同步块。

一条原则:同步范围越小越好。

Synchronized和监视器(monitor)有什么关系为什么Synchronized可以使用任意对象?

首先,每个对象都可以被认为是一个“监视器monitor”,这个监视器由三部分组成:独占锁、入口队列,等待队列。

注意:一个对象只能有一个独占锁,但是任意线程都可以拥有这个独占锁(说白了,锁占锁就是一个标记)。

Synchronized需要获取对象锁,实际上就是获取的是对象中的独占锁,通过这个标记来判断是否已有线程进入占用(所以synchronized无论使用什么对象都可以,每个对象在堆中都有独占锁)。

而入口队列中放的则是要竞争锁资源的其他线程,如果线程使用了wait方法,则进入对象的等待列队中。


Synchronized的作用是什么?

synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行,即保证线程的串行化。

synchronized 可以保证可见性、原子性、有序性三大特性。

Syncrhronized怎么保证可见性?

JMM中使用happens-before语义(即遵循happens-before关系,由JMM定义的规则):

1)线程解锁前,必须把共享变量的最新值刷新到主内存中。

2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值。

(注意:加锁与解锁需要是同一把锁)

     通过以上两点,可以看到synchronized能够实现可见性。

Synchronized怎么保证原子性?

JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。通过该标志,表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。

在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁,它由编译器自动产生的一个异常处理器来执行。

synchronized可重入的原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁,且不再被阻塞。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

Synchronized怎么保证有序性?

首先,Synchronized保证有序性,但不表示他能禁止指令重排。

有序性是指程序间的依赖顺序和代码顺序一致。

而之所以会有序性问题,是因为硬件层面做了很多优化,比如处理器做强化和指令重排等,这些技术引入会导致有序性问题。

这有序性问题主要出在多线程中,因为单线程中是遵循JMM的as-if-serial语义的,能保证数据间的依赖关系的,比如A依赖于B,B依赖于C,那A的实现之前,必须会先执行C。

as-if-serial语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

但是多线程就可能因为指令重排导致在另一个线程中先执行到了C,多线程程序的语义就被重排序破坏了!

Synchronized同步代码块可以锁住当前线程,这样每个线程单独执行,就可以保证有序性了。

Synchronized中的锁中什么是重量锁(对象锁),自旋锁,自适应自旋锁,轻量锁,偏向锁,锁消除,锁粗化?

自旋锁:

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的,所以引入自旋锁。 

就是等待锁的线程并不进入阻塞状态,而是执行一个无意义的循环。在循环结束后查看锁是否已经被释放,若已经释放则直接进入执行状态。因为长时间无意义循环也会大量浪费系统资源,因此自旋锁适用于间隔时间短的加锁场景。

自适应自旋锁:

自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 

偏向锁:偏向于第一个获得它的线程。当线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花进行CAS加锁和解锁操作。

适用于只有1个线程的情况。无法代替重量锁。

轻量锁:如果有第二线程过来竞争,则从偏向锁升级为轻量锁,线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败。

适用于只有2个线程情况。无法代替重量锁。

重量锁:当有3个及以上的线程竞争时,升级为重量锁,获得锁的执行,没获得锁的阻塞挂起,直到持有锁的线程执行完同步块唤醒它们。

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

锁消除:JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除。比如一个方法中使用变量是属于自己方法中的,那么这个变量是只属于该线程自己的,其他线程抢不走,这时候这个方法中的变量就没必要加锁了。

锁消除的依据是逃逸分析(底层判断该数据是否有被全局引用或者程序指向无法被访问到的地方等)的数据支持。 

锁粗化:锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

多线程中 synchronized 锁升级的原理是什么?
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

锁会自动升级,那会降级吗?

Synchronized锁只会自动升级,不会降级(ReentrantReadWriteLock读写锁可以降级)。

什么是CAS自旋?

CAS: Compare And Swap(比较并转换),即执行一半,发现已被其他线程抢先修改数据,该线程则重新获取最新内存值的过程。

自旋涉及三个值:新值、旧值、内存值(内存位置),线程会先获取内存值,然后复制到变量副本,生成旧值,旧值在一系列操作后生成新值。

若旧值等于内存值,说明没有被线程B抢先执行赋值,则修改内存值为新值;

若旧值不等于内存值,说明内存值已被其它线程修改,则自旋(获取新的内存,再重新操作)。

自旋会存在什么问题?

1、ABA问题:也就是第一个线程刚获得A,就被第二个线程抢走也获得A,并且改成B后又改回A,这时候第一个线程再执行,发现是它要的A就继续执行,这就会有潜藏的问题,比如修改的是金额存一笔跟存两笔就是两个概念了。

2、循环时间开销大:如果资源竞争激烈,CAS自旋概率较大,反而浪费更多CPU,导致效率比Synchronized更低。

3、只能保证一个共享变量的原子操作:CAS对多个共享变量操作的时候,无法保证原子性,只能用锁。

什么是死锁?

A需要B解锁,B需要A解锁,两个都在中间互相等待,却谁也无法满足条件,从而发生阻塞,就是死锁。

怎么防止死锁?

  1. 不要写嵌套锁,容易死锁;
  2. 尽量少用同步代码块(Synchronized);
  3. 尽量使用ReentrantLock的tryLock方法设置超时时间,超时可以退出,防止死锁;
  4. 尽量降低锁粒度,尽量不要几个功能一把锁;
  5. 尽量使用JUC包;

synchronized 和 ReentrantLock 区别是什么?

synchronized 是和 if、else、for、while 一样的关键字;

ReentrantLock 是类,这是二者的本质区别。

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

相同点:两者都是可重入锁

主要区别如下:

ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;

ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。

二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象;静态同步方法,锁是当前类的class对象;同步方法块,锁是括号里面的对象。

Lock锁是公平锁还是非公平锁?

看情况。使用ReentrantLock锁时,可以通过构造方法确定使用公平锁还是非公平锁。

ReentrantLock默认使用的是非公平锁,减少一定的上下文切换,保证系统更大的吞吐量。

乐观锁是公平锁还是非公平锁?

乐观锁对应的是悲观锁,和是否是公平锁没有必然联系。

死锁与活锁的区别?

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

你了解AQS机制吗?它的核心原理是什么?

AQS(Abstract Queued Synchronizer:抽象队列同步)是一个抽象类,它提供了一个双向队列,可以看成是一个用来实现同步锁及其他涉及到同步功能的核心组件,比如ReentrantLock,ReentrantReadWriteLock,FutureTask等等皆是基于AQS的。

AQS的核心原理是,如果资源空间,就设请求线程为有效的工作线程,并锁定该线程;

如果资源已被占用,AQS就把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。

当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程

AQS定义了两种资源共享方式:

  1. 独占,只有一个线程能执行;ReentrantLock
  2. 共享,多个线程可同时执行:Semphore、CountDownLatch

什么是可重入锁(ReentrantLock)?

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

Synchronized隐式支持重入性,具体见之前的回答。

ReentrantLock怎么实现公平锁和非公平锁?

ReentrantLock支持公平锁和非公平锁两种方式,通过构造方法来决定使用哪个锁方式。

什么是公平锁,什么是非公平锁?

公平锁,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

非公平锁,也是针对获取锁而言的,当多个线程竞争同一个资源时,可能会同一个线程多次抢到资源,而不是按顺序由下个线程获取。

从本质来说,底层都是AQL队列,非公平锁只有第一次线程进入时会进行抢占,如果抢占失败,就会进入队列。

ReetrantReadWriteLock读写锁和RenntrantLock有什么区别?

ReentrantLock有一定的局限性,它的读锁与读锁间也会互斥,但读数据并不会改动数据,没有必要加锁保护,这就降低了程序的性能。

因以上问题,诞生了读写锁,读写锁一种读写分离技术,它的读锁是共享的,写锁是独占的,也就是说,多个线程是可以一起读数据的,只有写数据的时候,才会同步线程。

读写锁ReentrantReadWriteLock有什么特点?

  1. 公平性可以选择:支持非公平(默认)和公平的锁获取,吞吐量非公平优于公平。
  2. 重进入:读锁和写锁都支持线程重进入。
  3. 锁降级:获取写锁,再获取读锁,然后释放写锁,这样写锁就降级为了读锁。(注:Synchronized是不能进行锁降级的,意义不一样)。

多线程

什么是上下文切换?

当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

如何避免线程死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

创建线程有哪几种方式?

创建线程有四种方式:

1、继承 Thread 类;

2、实现 Runnable 接口;

3、实现 Callable接口,创建FutureTask对象(FutureTask 也是Runnable 接口的实现类),与Runnable的区别是有返回值;

4、使用创建线程池

说一下 runnable 和 callable 有什么区别?

相同点:

1、都是接口

2、都可以编写多线程程序

3、都采用Thread.start()启动线程

主要区别:

1、Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

2、Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。

注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程的 run()和 start()有什么区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

start()方法为什么能开启多线程?

真正实现开启多线程的是start() 方法中的 start0() 方法。

调用start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE);具体什么时候执行,取决于 CPU ,由 CPU 统一调度;我们又知道 Java 是跨平台的,可以在不同系统上运行,每个系统的 CPU 调度算法不一样,所以就需要做不同的处理,这件事情就只能交给 JVM 来实现了,start0() 方法自然就表标记成了 native。

线程的6种状态是什么?

  1. 新建状态(new):创建线程对象。
  2. 就绪状态(runnable):start方法。
  3. 阻塞状态(blocked):无法获得锁对象(线程没抢到)。
  4. 等待状态(waiting):wait方法。
  5. 计时状态(timed_waiting):sleep方法。
  6. 死亡状态(terminated):全部代码运行完毕。

线程的调度模式是什么?

两分时调度和抢占式式调度。

分时调度:轮流获取CPU使用权。

抢占式调度:优先级高的线程占用CPU。

请说出与线程同步以及线程调度相关的方法。

(1)wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;

(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;

(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

sleep() 和 wait() 有什么区别?

相同点:两者都可以暂停线程的执行。不同点:

sleep方法,不会释放资源(本质是占用线程),如果占具锁资源,则其他线程不可进;wait方法会释放锁资源,即其他线程可进来。

wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

Java 中你怎样唤醒一个阻塞的线程?

首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;

其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。

notify() 和 notifyAll() 有什么区别?

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。

notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁,也就是synchronized对象锁。

Java 线程数过多会造成什么异常?

1、线程的生命周期开销非常高

2、消耗过多的 CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

3、降低稳定性JVM
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

你了解ThreadLocal的原理吗?

threadlocal是一个线程内部的存储类,提供了线程内存储变量的能力,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。

其内部维护了一个ThreadLocalMap,该Map用于存储每一个线程的变量副本。并且key为线程对象,value为对应线程的变量副本。

线程池

Executors类有哪几种常见的线程池

4种:单例线程池、固定大小线程池、可缓存线程池、大小无限线程池。

(1)newSingleThreadExecutor:创建一个单例线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

在 Java 中 Executor 和 Executors 的区别?
Executors 工具类的可以直接创建不同的线程池。
Executor 是个接口。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
ThreadPoolExecutor 是Executor接口的实现类,可以创建自定义线程池。
线程池中 submit() 和 execute() 方法有什么区别?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有。

异常处理:submit()方便Exception处理。

Executors 的弊端是什么?

newFixedThreadPool 和 newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM(内存溢出)。

newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

线程池之ThreadPoolExecutor

你对ThreadPoolExecutor熟悉吗?

ThreaPoolExecutor可以自定义创建线程池,具体参数可以走它的构造函数。

ThreadPoolExecutor的核心参数有哪些?

七个核心参数:

参数一:核心线程数(不能小于0)

参数二:最大线程数(>=核心线程数)

参数三:临时线程最大存活时间(不能小于0)

参数四:时间单位(参数三的单位)

参数五:等待列队(不能为null)

参数六:创建线程工厂(不能为null,一般用默认线程工厂)

参数七:任务的拒绝策略(不能为null)

ThreadPoolExecutor的拒绝策略有哪些?

4种:

1、ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常(默认);

2、ThreadPoolExecutor.DiscardPolicy:丢弃任务,不抛异常(不推荐);

3、ThreadPoolExecutor.DiscardOldestPolicy:丢弃等待最久的线程;

4、ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程(main)运行run方法。

当任务过多时,ThreadPoolExeccutor的执行顺序是怎么样的?

  1. 核心线程满后;
  2. 阻塞队列满后;
  3. 临时线程满后(最大线程数 - 核心线程数 = 临时线程数);
  4. 拒绝策略。

并发工具类

什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

java.util.concurrent.atomic(JUC包下) 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

有使用过什么并发工具类吗?

CountdownLatch和Semaphore。

CountdownLatch有什么作用?

CountDownLatch(倒计时器)是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

Semaphore有什么作用?

Semaphore(信号量/通行令牌)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值