Java中一些多线程相关的基础知识(锁)

参考文献:
1. Java:CAS(乐观锁)
2. volatile和synchronized的区别
3. Java并发问题–乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
4. Java可重入锁详解
5. JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁
6. wait()和sleep()的区别
7. sleep和wait的区别
8. Android 面试必备 - 线程
9.公平锁与非公平锁的区别
10.AQS原理详解

1. CAS原理:

利用了现代处理器都支持的CAS指令,循环这个指令,直到成功为止
在这里插入图片描述
CAS问题

  • ABA问题 --》解决办法:增加一个版本戳,AtomicMarkableReference, AtomicStampedReference
    AtomicMarkableReference:只关心变量有没有被动过
    AtomicStampedReference:不仅关心变量有没有被动过,还关心被动过几次
  • 开销问题–》采用加锁逻辑
  • 只能保证一个共享变量的原子操作–》解决办法:可以将多个共享变量打包到一个对象中,再用AtomicReference来实现,一次性修改这个对象

2. synchronized修饰普通方法和静态方法的区别?什么是可见性

答:synchronized锁对象,修饰普通方法是锁对象实例,修饰静态方法锁的是唯一的class对象
Synchronized加锁是锁的具体对象
可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看的到修改的值。volatile和加锁都可以解决可见性问题。

3. 锁分哪几类?

在这里插入图片描述
Synchronized是非公平锁、可重入锁、排他锁、重量级锁(jdk1.5以前),读写锁中的读锁是共享锁,读写锁中的写锁是排他锁。
Volatile不是锁

锁的状态
  一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

偏向锁(在线程拿锁的过程中,锁的获得者总是偏向于第一个拿锁/访问锁的线程)
  引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的 CAS 操作。
  偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些 CAS 操作(比如等待队列的一些 CAS 操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM 会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
  偏向锁获取过程:
  步骤 1、 访问 Mark Word 中偏向锁的标识是否设置成 1,锁标志位是否为01,确认为可偏向状态。
  步骤 2、 如果为可偏向状态,则测试线程 ID 是否指向当前线程,如果是,进入步骤 5,否则进入步骤 3。
  步骤 3、 如果线程 ID 并未指向当前线程,则通过 CAS 操作竞争锁。如果竞争成功,则将 Mark Word 中线程 ID 设置为当前线程 ID,然后执行 5;如果竞争失败,执行 4。
  步骤 4、 如果 CAS 获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致 stop theword)
  步骤 5、 执行同步代码。

偏向锁的释放:
  偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁的适用场景
  始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致 stop the word 操作;
  在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致 stw,导致性能下降,这种情况下应当禁用。
  jvm 开启/关闭偏向锁
  开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁
  轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

轻量级锁的加锁过程:
  在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝,官方称之为 Displaced Mark Word。
  拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 LockRecord 的指针,并将 Lock record 里的 owner 指针指向 object mark word。如果更新成功,则执行步骤 4,否则执行步骤 5。
  如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
  如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

在这里插入图片描述

4. ReentrantLock的实现原理

  线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
底层则是利用了JUC中的AQS来实现的。

5.AQS原理(AbstractQueuedSynchronizer)

  AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
在这里插入图片描述

  AQS是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int state成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。
  AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryRelease等等,这些方法做的主要工作其实就是对state成员变量做相关的修改、增加或删除操作,以维持记录同步状态。
  这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
  在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。

6. Synchronized的原理以及与ReentrantLock的区别

在这里插入图片描述
synchronized 的实现原理
  Synchronized 在 JVM 里的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter 和 MonitorExit 指令来实现。
  对同步块,MonitorEnter 指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁,而monitorExit 指令则插入在方法结束处和异常处,JVM 保证每个MonitorEnter 必须有对应的 MonitorExit
  对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter 和 monitorexit 来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED 标示符
  JVM 就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。

Synchronized与ReentrantLock的区别:

  1. Synchronized是内置锁,而ReentrantLock是显示锁。
  2. Synchronized是关键字,ReentrantLock是对象
  3. ReentrantLock提供了Synchronized关键字所没有的一些相关功能,比如拿锁的过程可中断,ReentrantLock可以实现尝试拿锁
  4. Synchronized是非公平锁,ReentrantLock即提供了非公平锁也提供了公平锁

7. Synchronized做了哪些优化

  引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析等技术来减少锁操作的开销。
逃逸分析:如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。
锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
锁粗化:将临近的代码块用同一个锁合并起来。消除无意义的锁获取和释放,可以提高程序运行性能。(减少上下文切换)

8. Synchronized static锁(类锁)与非static锁(对象锁)的区别和范围

  对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
  但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

9.volatile能否保证线程安全?在DCL上的作用是什么?

  不能保证, 在DCL(双重检测锁定)的作用是:volatile是会保证被修饰的变量的可见性和有序性
保证了单例模式下,保证在创建对象的时候的执行顺序一定是
1.分配内存空间
2.实例化对象instance
3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null 了的步骤,从而保证了instance要么为null,要么是已经完全初始化好的对象

volatile 的实现原理
  volatile 关键字修饰的变量会存在一个“lock:”的前缀。
  Lock 前缀,Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU 总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。
  同时该指令会将当前处理器缓存行的数据直接写回到系统内存中,且这个写回内存的操作会使在其他 CPU 里缓存了该地址的数据无效。

10. volatile和synchronize有什么区别?

volatile是最轻量的同步机制。
  volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。
  关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

11. 什么是守护线程?你是如何退出一个线程的?

  Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。

线程的启动与中止

启动线程的方式有:

  1. X extends Thread;,然后 X.start
  2. X implements Runnable;然后交给 Thread 运行

Thread 和 Runnable 的区别
  Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。

  线程的中止: 要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
  暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。因为会导致程序可能工作在不确定状态下。不建议使用的原因主要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、
resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

  安全的中止则是其他线程通过调用某个线程A的interrupt() 方法对其进行中断操作 ,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false
  如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
  不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
  一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,
  二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断

12. sleep、wait、yield的区别,wait的线程如何唤醒它?

  yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。
  所以执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行
  yield()、sleep()被调用后,都不会释放当前线程所持有的锁
  调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
  Wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。
  wait的线程使用notify/notifyAll() 进行唤醒。

13. sleep是可中断的么?

sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。

14. 线程生命周期。

Java中线程的状态分为6种:
  ① 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  ② 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
  线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  ③ 阻塞(BLOCKED):表示线程阻塞于锁。
  ④ 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  ⑤ 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  ⑥ 终止(TERMINATED):表示该线程已经执行完毕。
在这里插入图片描述

15. ThreadLocal是什么?

  ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
  在内部实现上,每个线程内部都有一个ThreadLocalMap,用来保存每个线程所拥有的变量副本。

16. 线程池基本原理

在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。第二:提高响应速度。第三:提高线程的可管理性。
  1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  2)如果运行的线程等于或多于corePoolSize,则将任务加BlockingQueue。
  3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
  4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

17. 有三个线程T1,T2,T3,怎么确保它们按顺序执行?

可以用join方法实现。在T3线程中调用T2.join(),在T2线程中调用T1.join(),在这种情况下,T1没有执行完成T2是没有办法执行完成的,T2没有执行完成T3是没有办法执行完成的

18. synchronized和lock的区别

区别:
1)synchronized是关键字,Lock是接口;
2)synchronized是隐式的加锁,lock是显式的加锁;
3)synchronized可以作用于方法上,lock只能作用于方法块;
4)synchronized底层采用的是objectMonitor,lock采用的AQS;
5)synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;
6)synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;
7)synchronized只支持非公平锁,lock支持非公平锁和公平锁;
8)synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);
9)lock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;

相同点:
1) Lock是一个接口,为了使用一个Lock对象,需要用到;
2) Lock lock = new ReentrantLock();
3)与 synchronized (someObject) 类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了;
4)与synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行;
5)synchronized 是不占用到手不罢休的,会一直试图占用下去
6)与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法;
7)trylock会在指定时间范围内试图占用。 如果时间到了,还占用不成功,就选择放弃;

注意:
因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常;

8)使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法;
9)Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法;

注意: 不是Condition对象的wait,nofity,notifyAll方法,是await,signal,signalAll;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值