并发编程

一.并发基础

1.什么是并行和并发?

并行,表示两个线程同时(同一时间)做事情。
并发,表示一会做这个事情,一会做另一个事情,存在着调度。

单核 CPU 不可能存在并行(微观上)。

2.什么是活锁?

假设有两个线程 1、2,它们都需要资源 A/B,假设 1 号线程占有了 A 资源,2 号线程占有了 B 资源;由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1 号线程释放了 A 资源占有锁,2 号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。

3.单线程创建方式

单线程创建方式比较简单,一般只有两种方式:继承 Thread 类和实现 Runnable 接口;这两种方式比较常用就不在 Demo 了,但是对于新手需要注意的问题有:

  • 不管是继承 Thread 类还是实现 Runable 接口,业务逻辑写在 run 方法里面,线程启动的时候是执行 start()方法;
  • 开启新的线程,不影响主线程的代码执行顺序也不会阻塞主线程的执行;
  • 新的线程和主线程的代码执行顺序是不能够保证先后的;
  • 对于多线程程序,从微观上来讲某一时刻只有一个线程在工作,多线程目的是让 CPU 忙起来;
  • 通过查看 Thread 的源码可以看到,Thread 类是实现了 Runnable 接口的,所以这两种本质上来讲是一个;

4.终止线程运行的情况?

线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了 yield()方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep()方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

5.如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

6.上下文切换如何检测?

监测:

使用 Lmbench3 可以测量上下文切换的时长;

使用 vmstat 可以测量上下文切换的次数

排查步骤:

1.查询 Java 程序的进程 ID,使用 jstack 命令 dump 出线程文件

sudo -u admin/opt/ifeve/java/bin/jstack31177>/home/t engfei.fangtf/dump17

2.统计线程状态

[tengfei.fangtf@ifeve~]$grep java.lang.ThreadState dump17|awk '{print $2$3$4$5}
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobiectmonitor)
3 WAITING(parking)

3.查看 WAITING 状态的线程在干什么?

"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Objectwait()[0x0000000052423000]
java.lang.Thread.State: WAITING(on object monitor) at java.lang.Objectwait(Native Method)
-waiting on <0x00000007969b2280>(a orapachetomcat.utinetAprEndpoint$Worker at java.lang.Objectwait(Objectjava:485)
at org.apache.tomcat.util.netAprEndpoint$Workerawait(AprEndpointjava:1464
-locked <0x00000007969b2280>(a org.apache.tomcatutilnetAprEndpoint$Worker) at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpointjava:1489) at java.lang.Thread.run(Threadjava:662)

4.减少 JBOSS 工作线程数

<maxThreads="250" maxHttpHeaderSize="8192" emptySessionPath="false"minSpareThreads="40" max SpareThreads="75" maxPostSize="512000"protocol="HTTP/1.1" enableLookups="false"redirectPort="8443" acceptCour nt="200" bufferSize="16384"
connectionTimeout="15000"disableUploadTimeout="fal se" useBodyEncodingForURI= "true">

7.什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

如上图例子:线程 A 己经持有了资源 2,它同时还想申请资源 1,线程 B 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

8.写出死锁代码?

死锁的产生必须具备以下四个条件。

  • 互斥条件:一个资源同时只能有一个线程占有.其他线程只能等待.
  • 请求并持有条件:当前线程已经获取到一个资源,又获取其他资源,其他资源被别的线程占有,当前线程等待,但是不释放持有资源.
  • 不可剥夺条件:占有资源期间,不能被其他线程剥夺,只能自己释放.
  • 环路等待条件:等待资源形成环形链.a 被 A 占有,b 被 B 占有,A 想获取 b,B 想获取 a
public class DeadLockDemo {private static String A = "A";private static String B = "B";public static void main(String[] args){new DeadLockDemo().deadLock();}private void deadLock(){Thread t1 = new Thread(new Runnable(){@Overridepublic void run(){//线程1获取A的锁synchronized (A){try {Thread.currentThread().sleep(2000);} catch (InterruptedException e){e.printStackTrace();}synchronized (B){System.out.println("1");}}}});Thread t2 = new Thread(new Runnable(){@Overridepublic void run(){//线程2获取B的锁synchronized (B){//A对象已经被线程1持有synchronized (A){System.out.println("2");}}}});t1.start();t2.start();}
}

9.如何避免死锁呢?

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有条件和环路等待条件是可以被破坏的。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 locktryLock(timeou t)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

如上题代码中,在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,…, n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n.

public class DeadLockRelessDemo {private static String A = "A";private static String B = "B";public static void main(String[] args){new DeadLockRelessDemo().deadLock();}private void deadLock(){Thread t1 = new Thread(new Runnable(){@Overridepublic void run(){//线程1获取A的锁synchronized (A){try {Thread.currentThread().sleep(2000);} catch (InterruptedException e){e.printStackTrace();}synchronized (B){System.out.println("1");}}}});Thread t2 = new Thread(new Runnable(){@Overridepublic void run(){//线程2也获取A的锁synchronized (A){synchronized (B){System.out.println("2");}}}});t1.start();t2.start();}
}

10.线程,进程,协程的区别?

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位.

线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。在 Java 中,当我们启动 main 函数时其实就启动了一个 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

进程:静态分配内存资源的最小单位

线程:动态执行任务的最小单位

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。协程也叫纤程.

线程是在内核态调用的,协程在用户态调用,避免了上下文切换

11.线程的状态?

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用 start 方法
RUNNABLE 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME WAITING 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕
  • NEW :创建了线程对象但尚未调用 start()方法时的状态。
  • RUNNABLE:线程对象调用 start()方法后,线程处于可运行状态,此时线程等待获取 CPU 执行权。
  • BLOCKED:线程等待获取锁时的状态。
  • WAITING:线程处于等待状态,处于该状态标识当前线程需要等待其他线程做出一些特定的操作唤醒自己。
  • TIME_WAITING:超时等待状态,与 WAITING 不同,在等待指定的时间后会自行返回。
  • TERMINATED:终止状态,表示当前线程已执行完毕。

12.线程状态变迁?

  • RUNNABLE 有 2 种状态,一种是 running,调用 yield 变为 ready,一种是 ready,调用 run 为 running
  • New 状态为初始状态,New Thread 的时候是 new 这种状态,调用 start 方法后,变为 runnable
  • Terminated 任务执行完成后线程的状态为终止
  • runnable 状态到 waiting 状态
    • wait()
    • join() 不需要主动唤醒
    • park()
  • waiting 状态到 runnable 状态
    • 唤醒
    • notify()
    • notifyAll()
    • unpark(thread)
  • runnable 状态到 timed_waiting 状态
    • sleep(long)
    • wait(long)
    • join(long) 不需要主动唤醒
    • parkNanos()
    • parkUntil()
  • timed_waiting 状态到 runnable 状态
    • notify()
    • notifyAll()
    • unpark(thread)
  • blocked 到 runnable
    • 获取到锁
  • runnable 到 blocked
    • synchronized 方法
    • synchronized 块

13.CPU 术语定义?

术语 英文单词 描述
内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作的顺序限制
缓冲行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1L2.L3 的或所有)
缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取
写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中
写缺失 write misses the cache 一个有效的缓存行被写人到不存在的内存区域

14.java 内存模型?

Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。

JMM 关于同步的规定:

  1. 线程解锁前,必须把共亨变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

15.什么是 Daemon 线程?

Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。注意 Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置。Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 fnally 块并不一定会执行.

public class Daemon{public static void main(String[] args){Thread thread=new Thread(new DaemonRunner(),"DaemonRunner");thread.setDaemon(true);thread.start();}static class DaemonRunner implements Runnable{@Overridepublic void run() {try{SleepUtilssecond(10);} finally{System.out.println("DaemonThread finally run.");}}}
}

16.说说三个中断方法?

说说 interrupt(),interrupted(),isInterrupted()的区别

  • void interrupt()方法:中断线程,例如,当线程 A 运行时,线程 B 可以调用钱程 A 的 interrupt()方法来设置线程 A 的中断标志为 true 并立即返回.仅仅是设置标志,线程 A 实际并没有被中断,它会继续往下执行.如果线程 A 因为调用了 wait 系列函数、join 方法或者 sleep 方法而被阻塞挂起,这时候若线程 B 调用线程 A 的 interrupt()方法,线程 A 会在调用这些方法的地方抛出 InterruptedException 异常而返回。
  • boolean isinterrupted()方法:检测当前线程是否被中断,如果是返回 true,否则返回 false.
  • boolean interrupted()方法:检测当前线程是否被中断,如果是返回 true,否则返回 false.与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是 static 方法,可以通过 Thread 类直接调用.另外从下面的代码可以知道,在 interrupted()内部是获取当前调用线程的中断标志而不是调用 interrupt 方法的实例对象的中断标志。
  • Java 中断机制是一种协作机制,中断只是给线程打一个中断标记,具体如何操作还要看线程自己 interrupt()函数作用仅仅是为线程打一个中断标记。interrupted()与 isInterrupted()函数,都是返回线程的中断状态,但是 interrupted()被 static 修饰,返回当前线程的中断状态,并且会清除线程的中断标记;而 isInterrupted()未 static 修饰,被 Thread 对象调用,它不会清除线程的中断标记。

从代码中可以看出,interrupted 方法调用的 currentThread()的 native 方法,而 isInterrupted 方法调用的实例对象的 native 方法.Native 方法传参:true 代表清除中断标志,false 代表不清除中断标志

public void interrupt(){if (this != Thread.currentThread())checkAccess();synchronized (blockerLock){Interruptible b = blocker;if (b != null){interrupt0(); //Just to set the interrupt flagb.interrupt(this);return;}}interrupted();
}public static boolean interrupted(){return currentThread().isInterrupted(true);
}public boolean isInterrupted(){return isInterrupted(false);
}private native boolean isInterrupted(boolean ClearInterrupted);

17.说说 join 方法和 yeild 方法?

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理.Thread 类中有一个 join 方法就可以做这个事情,join 方法是 Thread 类直接提供的.join 是无参且返回值为 void 的方法。

Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用.我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了 Thread 类的静态方法 yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。

join 方法可中断响应,释放 CPU,释放的时当前调用 join 方法对象的锁.join 方法会被隐式唤醒

18.说说 sleep 和 yeild 和 wait 区别?

sleep 方法与 yeild 方法的区别在于,当线程调用 sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。不会释放资源锁,只是让出 CPU 的时间片.线程会阻塞.

而调用 yield 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

wait 方法会阻塞,会释放锁,会让出 CPU 的使用权,且不会参与锁竞争,除非被唤醒后才参与竞争.

sleep 方法不释放锁,释放 cpu,可响应中断.先清除中断标志,再打印异常

19.说说 notify 和 wait 方法?

一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait()方法后被挂起的线程。此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回(调用 wait 方法后,会释放当前共享对象的锁,如果不释放会造成死锁),也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会立即获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出 IllegaMonitorStateException 异常。

Thread 类的方法:sleep(),yield()
Object 的方法:wait()和 notify()、notifyAll()

20.说说对象的监视器锁?

一个线程如何才能获取到对象的监视器锁呢

  • 执行 synchronized 同步代码块时,使用该共享变量作为参数。
synchronized (MonitorTest.class){//do something
}
  • 调用该共享变量的方法,并且该方法使用了 synchronized 修饰。
synchronized void add(int a){//do something
}

9.线程阻塞和唤醒方法对比?

wait()、notify()、notifyAll() 这三个方法是 Object 超类中的方法.

await()、signal()、signalAll() 这三个方法是 Lock 的 Condition 中的方法.

21.程序计数器为何线程私有?

为何要将程序计数器设置为线程私有的

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址.线程是占用 CPU 执行的基本单位,而 CPU 一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 CPU 时间片用完后,要让出 CPU,等下次轮到自己的时候再执行.那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出 CPU 时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行.另外需要注意的是,如果执行的是 native 方法,那么 pc 计数器记录的是 undefined 地址,只有执行的是 Java 代码时 pc 计数器记录的才是下一条指令的地址。

22.jvm 内存划分?

局部变量,对象实例,jvm 加载的类,常量及静态变量都存储在内存的什么部位?是线程私有的吗

  • 每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。
  • 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用 new 操作创建的对象实例。
  • 方法区则用来存放虚拟机加载的类、常量及静态变量等信息,也是线程共享的。

23.继承 Thread 类的优劣?

使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码, Runable 则没有这个限制。

public class DemoTest extends Thread {// private int tickets = 20;private volatile int tickets = 20;@Overridepublic void run(){synchronized (this){while (tickets > 0){System.out.println(Thread.currentThread().getName()+"卖出一张票"+ tickets);tickets--;}}}public static void main(String[] args){//实际上一共卖出了80张票,每个线程都有自己的私有的非共享数据。都认为自己有20张票DemoTest test4 = new DemoTest();DemoTest test5 = new DemoTest();DemoTest test6 = new DemoTest();DemoTest test7 = new DemoTest();test4.setName("一号窗口:");test5.setName("二号窗口:");test6.setName("三号窗口:");test7.setName("四号窗口:");test4.start();test5.start();test6.start();test7.start();}
}

24.直接调用 wait 方法?

IllegalMonitorStateException 出现的原因

如果调用 wait()方法的线程没有先获取该对象的监视器锁,则调用 wait 方法时调用线程会抛出 IllegalMonitorState Exception 异常。Object.notify(), Object.notifyAll(), Object.wait(), Object.wait(long), Object.wait(long, int)都会存在这个问题

public class ExceptionTest {public static void main(String[] args){Object object = new Object();try {object.wait();} catch (InterruptedException e){e.printStackTrace();}}
}

25.什么是虚假唤醒?

什么是虚假唤醒?如何避免虚假唤醒?

在一个线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这个线程仍然可以从挂起状态变为可以运行状态(也就是被唤醒),这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个 while 循环中调用 wait()方法进行防范.退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj){//do somethingwhile (条件不满足){obj.wait();}
}

While 在这里是防止虚假唤醒的关键,试想下,一旦发生虚假唤醒,线程会根据 while 添加再次进行判断,一旦条件不满足,会立即再次将线程挂起.

26.说说 ThreadLocal?

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本.当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

ThreadLocalMap的Entry中的key使用的是对ThreadLocal对象的弱引用,这在避免内存泄漏方面是一个进步,因为如果是强引用,即使其他地方没有对ThreadLocal对象的引用,ThreadLocalMap中的ThreadLocal对象还是不会被回收,而如果是弱引用则ThreadLocal引用是会被回收掉的。

但是对应的value还是不能被回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项,虽然ThreadLocalMap提供了set、get和remove方法,可以在一些时机下对这些Entry项进行清理,但是这是不及时的,也不是每次都会执行,所以在一些情况下还是会发生内存漏,因此在使用完毕后及时调用remove方法才是解决内存泄漏问题的王道。

创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。

27.ThreadLocal 的原理?

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap.在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们.其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面.也就是说,ThreadLocal 类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用.如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。另外,Thread 里面的 threadLocals 为何被设计为 map 结构?很明显是因为每个线程可以关联多个 ThreadLocal 变量。

在 Thread 类中有以下变量

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.*/ThreadLocal.ThreadLocalMap threadLocals = null;/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
* Inheritable 可继承的
*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • ThreadLocal 中在 set 操作时,key 为当前 ThreadLocal 对象。
  • ThreadLocal 会为每个线程都创建一个 ThreadLocalMap,对应程序中的 t.threadLocals = new ThreadLocalMap(this, firstValue),ThreadLocalMap 为当前线程的属性。
  • 通过对每个线程创建一个 ThreadLocalMap 实现本地副本。当取值时,实际上就是通过 key 在 map 中取值,当然此时的 key 为 ThreadLocal 对象,而 map 为每个线程独有的 map,从而实现变量的互不干扰。

28.ThreadLocal 中 set 方法?

public class ThreadLocalTest {public static void main(String[] args){ThreadLocal<String> t1 = new ThreadLocal<>();t1.set("1");t1.set("2");System.out.println(t1.get());}
}
//输出结果 2

看下 set 方法的源代码:先获取当前线程 t,然后以 t 为 key 获取当前 ThreadLocalMap.如果 Map 存在则设置,注意设置的 key 为 this,this 代表当前对象,key 不变,所以 value 会被覆盖.如果 map 不存在则进行 createMap。

public void set(T value){Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null){map.set(this, value);} else {createMap(t, value);}
}void createMap(Thread t, T firstValue){t.threadLocals = new ThreadLocalMap(this, firstValue);
}

29.ThreadLocal 继承?

public class ThreadLocalTest2 {public static void main(String[] args){ThreadLocal<String> t1 = new ThreadLocal<>();t1.set("1");new Thread(new Runnable(){@Overridepublic void run(){System.out.println("我是子线程,t1:"+ t1.get());}}).start();System.out.println("我是主线程,t1:"+ t1.get());}
}
//我是主线程,t1:1
//我是子线程,t1:null

也就是说,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。根据之前题目的线程私有的介绍,这应该是正常现象,因为在子线程 thread 里面调用 get 方法时当前线程为 thread 线程,而这里调用 set 方法设置线程变量的是 main 线程,两者是不同的线程,自然子线程访问时返回 null.

30.子线程访问主线程变量?

有没有办法让子线程访问到主线程的 ThreadLocal 变量

InheritableThreadLocal 继承自 ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量.下面看一下 InheritableThreadLocal 的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue){return parentValue;}ThreadLocalMap getMap(Thread t){return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue){t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

这是 InheritableThreadLocal 的全部代码,他继承了 ThreadLocal,并复写了三个方法.

  • 一个是 getMap,获取一个新的 map,
  • 一个是 ceateMap,创建一个新的 ThreadLocalMap,并赋值给 inheritableThreadLocals
  • 一个是 childValue,返回父线程的值

31.InheritableThreadLocal?

InheritableThreadLocal 是如何让子线程可以访问在父线程中设置的本地变量的

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue){return parentValue;}ThreadLocalMap getMap(Thread t){return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue){t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

从代码上看,getMap 和 createMap 没什么稀奇的,无非是创建和获取.这不是原理所在。

除了 getMap 和 createMap,只能来看看 childValue 这个方法了.我们看到代码逻辑是 return parentValue;

为了说清楚 childValue 这个方法,我们得先看 ThreadLocalMap 构造方法:从代码可以看出,初始化的时候进行了判断,如果父类的 inheritableThreadLocals 不为空,则进行 createInheriteMap 方法创建,继续点进去看

private ThreadLocalMap(ThreadLocalMap parentMap){Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++){Entry e = parentTable[j];if (e != null){@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null){//重点此处调用了 childValue,返回 parent的value,//在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode &(len -1);while (table[h]!= null)h = nextIndex(h, len);table[h]= c;size++;}}}
}

总结:InheritableThreadLocal 类通过重写代码.getMap 和 createMap 让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量.当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

public class InheritableThreadLocalTest {public static void main(String[] args){InheritableThreadLocal<String> t1 = new InheritableThreadLocal<>();t1.set("1");new Thread(new Runnable(){@Overridepublic void run(){System.out.println("我是子线程,t1:"+ t1.get());}}).start();System.out.println("我是主线程,t1:"+ t1.get());}
}
//我是主线程,t1:1
//我是子线程,t1:1

说说 InheritableThreadLocal 的使用场景?

情况还是蛮多的,比如子线程需要使用存放在 ThreadLocal 变量中的用户登录信息,再比如一些中间件需要把统一的 id 追踪的整个调用链路记录下来.其实子线程使用父线程中的 ThreadLocal 方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个 map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下 InheritableThreadLocal 就显得比较有用。

32.?this 和 Thread.currentThread()区别?

  • Thread.currentThread 表示当前代码段正在被哪个线程调用的相关信息。
  • this 表示的是当前对象,与 Thread.currentThread 有很大的区别

33.CPU 用户态和内核态?

CPU 的两种工作状态:内核态(管态)和用户态(目态)。

内核态

  • 系统中既有操作系统的程序,也有普通用户程序.为了安全性和稳定性,操作系统的程序不能随便访问,这就是内核态.即需要执行操作系统的程序就必须转换到内核态才能执行!

  • 内核态可以使用计算机所有的硬件资源!

用户态

不能直接使用系统资源,也不能改变 CPU 的工作状态,并且只能访问这个用户程序自己的存储空间!当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为 3 级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态.Ring3 状态不能访问 Ring0 的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为 0 级.执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈.用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态.如果要执行文件操作、网络数据发送等操作必须通过 write、send 等系统调用,这些系统调用会调用内核的代码.进程会切换到 Ring0,然后进入内核地址空间去执行内核代码来完成相应的操作.内核态的进程执行完后又会切换到 Ring3,回到用户态.这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用.这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。

34.用户态/内核态切换条件?

当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。

用户态切换到内核态的 3 种方式

(1)系统调用

这是用户态进程主动要求切换到内核态的一种方式.用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作.例如 fork()就是执行了一个创建新进程的系统调用.系统调用的机制是使用了操作系统为用户特别开放的一个中断来实现.

(2)异常

当 cpu 在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。

(3)外围设备的中断

当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行,如果前面执行的指令是用户态下的程序,那么转换的过程自然就会是由用户态到内核态的切换.如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。

这三种方式是系统在运行时由用户态切换到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的.从触发方式上看,切换方式都不一样,但从最终实际完成由用户态到内核态的切换操作来看,步骤又是一样的,都相当于执行了一个中断响应的过程.系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本一致。

35.用户态切换内核态步骤?

  1. 从当前进程的描述符中提取其内核栈的 ss0 及 esp0 信息。
  2. 使用 ss0 和 esp0 指向的内核栈将当前进程的 cs,eip,eflags,ss,esp 信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  3. 将先前由中断向量检索得到的中断处理程序的 cs,eip 信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

36.伪共享内存顺序冲突?

什么是伪共享内存顺序冲突?如何避免?

由于存放到 CPU 缓存行的是内存块而不是单个变量,所以可能会把多个变量存放到同一个缓存行中,当多个线程同时修改这个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,此时有两个线程同时修改同一个缓存行下的两个不同的变量,这就是伪共享,也称内存顺序冲突。当出现伪共享时,CPU 必须清空流水线,会造成 CPU 比较大的开销。

如何避免:JDK1.8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,例如如下代码:

public final static class FilledLong{public volatile long value=0L;public long pl,p2,p3,p4,p5,p6;
}

假如缓存行为 64 字节,那么我们在 FilledLong 类里填充了 6 个 long 类型的变量,一个 long 类型变量占用 8 字节,加上自己的 value 变量占用的 8 个字节,总共 56 字节.另外,这里 FilledLong 是一个类对象,而类对象的字节码的对象头占用 8 字节,所以一个 FilledLong 对象实际会占用 64 字节的内存,这正好可以放入同一个缓存行。

JDK 提供了 sun.misc Contended 注解,用来解决伪共享问题.将上面代码修改为如下。

@sun.misc.Contendedpublic final static class FilledLong{public volatile longvalue=0L;}

特别注意
在默认情况下,@Contended 注解只用于 Java 核心类,比如 rt 包下的类。
如果用户类路径下的类需要使用这个注解,需要添加 JVM 参数:- XX:-RestrictContended.填充的宽度默认为 128,要自定义填充宽度则可以通过参数-XX:ContendedPaddingWidth 参数进行设置。

37.对象的内存布局?

HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash 码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java 对象头一般占有 2 个机器码(在 32 位虚拟机中,1 个机器码等于 4 字节,也就是 32bit 在 64 位虚拟机中,1 个机器码是 8 个字节,也就是 64bit),但是 如果对象是数组类型,则需要 3 个机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

38.缓存一致性协议?

CPU 多级缓存,切换后如何保证一致性?

一种处理一致性问题的办法是使用 Bus Locking(总线锁)。当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。这个时候,所有 CPU 收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的 CPU 就去内存中获取最新数据更新。但是用锁的方式总是避不开性能问题。总线锁总是会导致 CPU 的性能下降。所以出现另外一种维护 CPU 缓存一致性的方式,MESI。

MESI 是保持一致性的协议。它的方法是在 CPU 缓存中保存一个标记位,这个标记位有四种状态:

  • M: Modify,修改缓存,当前 CPU 的缓存已经被修改了,即与内存中数据已经不一致了
  • E: Exclusive,独占缓存,当前 CPU 的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据
  • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段
  • I: Invalid,实效缓存,这个说明 CPU 中的缓存已经不能使用了

CPU 的读取遵循下面几点:

  • 如果缓存状态是 I,那么就从内存中读取,否则就从缓存中直接读取。
  • 如果缓存处于 M 或 E 的 CPU 读取到其他 CPU 有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为 S。
  • 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为 M。

39.线程优先级?

优先级是针对 CPU 的时间片的长短,不是先后.

prio=5 和 os_prio=0
默认优先级为 5,os_prio 是 jvm 相对于 os 的优先级,优先级的数值在 1~10,越大优先级越高
setPriority 这个方法,他是 jvm 提供的一个方法,并且能够调用本地方法 setPriority().我们发现优先级貌似没有起作用,为什么?

1.我们现在的计算机都是多核的,t1,t2 会让哪个 cpu 处理不好说。由不同的 cpu 同时提供资源执行。

2.优先级不代表先后顺序。哪怕你的优先级低,也是有可能先拿到我们的 cpu 时间片的,只不过这个时间片比高优先级的线程的时间片短。优先级针对的是 cpu 时间片的长短问题。

3.目前工作中,实际项目里,不必要使用 setPriority 方法。我们现在都是用 hystrix,sential 也好,一些开源的信号量控制工具,都能够实现线程资源的合理调度。这个 setPriority 方法,很难控制。实际的运行环境太复杂。

40.守护线程

  • 支持型线程
  • finally 里面的代码不一定会执行
public final void setDaemon(boolean on) {checkAccess();if (isAlive()) {//告诉我们,必须要先设置线程是否为守护线程,然后再调用start方法。如果你先调用start,后设置守护线程,则会抛出异常throw new IllegalThreadStateException();}daemon = on;
}

二.底层原理

1.说说重排序的分类?

在执行程序时,为了提高性能,编译器(jvm 里的)和处理器(操作系统级别的)常常会对指令做重排序.重排序分 3 种类型。

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行.如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序.这些重排序可能会导致多线程程序出现内存可见性问题.对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止).

对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers, Intel 称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

在单线程程序中,对存在数据依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果,因此必须要通过一定的同步手段加以控制。

2.说说重排序?

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

jmm 在实现上在不改变结果的前提下,编译器和处理器可以进行优化性的重排序.

3.volatile 实现原则

volatile 实现原则有 2 条:

  • lock 前缀指令会使处理器缓存写回内存.
  • 一个处理器的缓存写回内存,会导致其他处理器的缓存失效.

4.volatile 的实现原理

为了实现 volatile 内存语义,JMM 会分别禁止如下两种类型的重排序类型:

从图中可看出:

  1. volatile 写写禁止重排序

  2. volatile 读写,读读禁止重排序; volatile 读和普通写禁止重排序

  3. volatile 写读,volatile 写写禁止重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序.对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能.为此,JMM 采取保守策略.下面是基于保守策略的 JMM 内存屏障插入策略。

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

public class Juc_book_fang_03_VolatileBarrierExample {int a;volatile int v1 = 1;volatile int v2 = 2;void readAndWrite(){int i = v1; //第一个volatile读int j = v2; //第二个volatile读a = i + j; //普通写v1 = i +1; //第一个volatile写v2 = j *2; //第二个 volatile写
//其他方法
}
}

屏障说明:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W5bx43zb-1660370613955)(https://s2.loli.net/2022/04/25/oqQwF4R2eWAjlv7.png)]

5.说说 volatile 的内存语义?

从内存语义的角度来说,volatile 的写一读与锁的释放-获取有相同的内存效果:

volatile 写和锁的释放有相同的内存语义;volatile 读与锁的获取有相同的内存语义。

volatile 写的内存语义如下。

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile 读的内存语义如下。

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

6.volatile 的特性?

简而言之,volatile 变量自身具有下列特性。

可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。JMM 模型,主存的本地内存数据一致性.添加了 Lock 指令.

原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性。

由于 volatile 仅仅保证对单个 volatile 变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比 volatile 更强大;在可伸缩性和执行性能上,volatile 更有优势。如果读者想在程序中用 volatile 代替锁,请一定谨慎.

7.volatile 是如何保证可见性的?

如: volatile instance = new instance(); 示例代码中, instance 被 volatile 修饰。
上边的 new 操作,转化成汇编代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8SseSMqs-1660370613960)(https://s2.loli.net/2022/06/11/CStpM3EqlLR5zIF.png)]
有 volatile 变量修饰的共享变量进行写操作的时候会多出第二行 Lock 汇编代码, Lock 前缀的指令在多核处理器下会引发了两件事情: 1)将当前处理器缓存行的数据写回到系统内存.(volatile 写的内存语义) 2)这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效.(volatile 读的内存语义)
如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里(volatile 读的内存语义)。

8.Synchonized 三种使用

java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

  • 对于普通同步方法,锁是当前实例对象。ACC_SYNCHRONIZED
  • 对于静态同步方法,锁是当前类的 Class 对象。ACC_SYNCHRONIZED 和 ACC_STATIC
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象。monitorenter 和 monitorexit,其中 monitorexit 至少有 2 个出口,一个正常出口,一个异常出口

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁

当出现读少写多时,volatile 并不合适,提高吞吐量还得靠重量级锁.

9.Synchonized 的实现原理

在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 MutexLock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,主要是锁升级的过程. owner 表示 Monitor 锁的持有者,而且同一个时刻只能有一个 owner.

JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。

代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的。

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。

任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态.线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

修饰方法

public class SynchonizedTest1 {private static int a = 0;public  synchronized void add(){a++;}
}

可以看到在 add 方法的 flags 里面多了一个 ACC_SYNCHRONIZED 标志,这标志用来告诉 JVM 这是一个同步方法

┌─[qinyingjie@qinyingjiedeMacBook-Pro]-[~/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞]-[Thu Apr 14,18:51]
└─[$]<git:(master*)> javap -v SynchonizedTest1.class
Classfile /Users/qinyingjie/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞/SynchonizedTest1.classLast modified 2022-4-14; size 486 bytesMD5 checksum 1a0bdb0e66832a2980bb5b8c0a58eff7Compiled from "SynchonizedTest1.java"
public class com.xiaofei.antjuc.方腾飞.SynchonizedTest1minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18// java/lang/Object."<init>":()V
#2 = Fieldref #3.#19// com/xiaofei/antjuc/方腾飞/SynchonizedTest1.a:I
#3 = Class #20// com/xiaofei/antjuc/方腾飞/SynchonizedTest1
#4 = Class #21// java/lang/Object
#5 = Utf8  a
#6 = Utf8  I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8  Code
#10 = Utf8  LineNumberTable
#11 = Utf8  LocalVariableTable
#12 = Utf8  this
#13 = Utf8  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;
#14 = Utf8  add
#15 = Utf8 <clinit>
#16 = Utf8  SourceFile
#17 = Utf8  SynchonizedTest1.java
#18 = NameAndType #7:#8 //"<init>":()V
#19 = NameAndType #5:#6 // a:I
#20 = Utf8  com/xiaofei/antjuc/方腾飞/SynchonizedTest1
#21 = Utf8  java/lang/Object
{public com.xiaofei.antjuc.方腾飞.SynchonizedTest1();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: returnLineNumberTable:line 3: 0LocalVariableTable:Start  Length  Slot  Name  Signature
0 5 0  this  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;public synchronized void add();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=1
0: getstatic #2 // Field a:I
3: iconst_1
4: iadd
5: putstatic #2 // Field a:I
8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start  Length  Slot  Name  Signature
0 9 0  this  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;static {};descriptor: ()Vflags: ACC_STATICCode:stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #2 // Field a:I
4: returnLineNumberTable:line 4: 0
}
SourceFile: "SynchonizedTest1.java"

修饰类

有两个 monitorexit 呢?

第一个:正常退出

第二个:异常退出

public class SynchonizedTest2 {private static int a = 0;public void add(){synchronized (SynchonizedTest2.class){a++;
}
}
}
方腾飞|master⚡⇒ jjavap -v SynchonizedTest2
警告: 二进制文件SynchonizedTest2包含com.xiaofei.antjuc.方腾飞.SynchonizedTest2
Classfile /Users/qinyingjie/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞/SynchonizedTest2.classLast modified 2022-4-14; size 599 bytesMD5 checksum e4ad4e62082f26cefee3bb1715e94295Compiled from "SynchonizedTest2.java"
public class com.xiaofei.antjuc.方腾飞.SynchonizedTest2minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#22// java/lang/Object."<init>":()V
#2 = Class #23// com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#3 = Fieldref #2.#24// com/xiaofei/antjuc/方腾飞/SynchonizedTest2.a:I
#4 = Class #25// java/lang/Object
#5 = Utf8  a
#6 = Utf8  I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8  Code
#10 = Utf8  LineNumberTable
#11 = Utf8  LocalVariableTable
#12 = Utf8  this
#13 = Utf8  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;
#14 = Utf8  add
#15 = Utf8  StackMapTable
#16 = Class #23// com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#17 = Class #25// java/lang/Object
#18 = Class #26// java/lang/Throwable
#19 = Utf8 <clinit>
#20 = Utf8  SourceFile
#21 = Utf8  SynchonizedTest2.java
#22 = NameAndType #7:#8 //"<init>":()V
#23 = Utf8  com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#24 = NameAndType #5:#6 // a:I
#25 = Utf8  java/lang/Object
#26 = Utf8  java/lang/Throwable
{public com.xiaofei.antjuc.方腾飞.SynchonizedTest2();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: returnLineNumberTable:line 3: 0LocalVariableTable:Start  Length  Slot  Name  Signature
0 5 0  this  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;public void add();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=1
0: ldc #2 // class com/xiaofei/antjuc/方腾飞/SynchonizedTest2
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field a:I
8: iconst_1
9: iadd
10: putstatic #3 // Field a:I
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: returnException table:
from  to  target type
5 15 18  any
18 21 18  anyLineNumberTable:line 7: 0line 8: 5line 9: 13line 10: 23LocalVariableTable:Start  Length  Slot  Name  Signature
0 24 0  this  Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 18locals = [ class com/xiaofei/antjuc/方腾飞/SynchonizedTest2, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4static {};descriptor: ()Vflags: ACC_STATICCode:stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #3 // Field a:I
4: returnLineNumberTable:line 4: 0
}
SourceFile: "SynchonizedTest2.java"

10.Synchonized 锁信息在对象中的存储位置?

synchronized 用的锁是存在 Java 对象头(Mark Word)里的。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit.数组的 1 字宽存储数组的长度信息.

无锁状态下 32 位 JVM 的 Mark Word 的默认存储结构如下:

有锁状态的 Mark Word 的信息变化如下,并从下图中能够看到锁的信息的确是放到 Mark Word 中的,并且不同的锁类型, Mark Word 中的信息会有变化。

11.synchronized 中的同步队列与等待队列?

同步队列:排队取锁的线程所在的队列
等待队列:调用 wait 方法后,线程会从同步队列转移到等待队列
synchronized 中同步队列有两个_cxq 与 EntryList,基于不同的 QMode 来调整线程的出队策略
_cxq(竞争队列):抢锁失败后,线程会进入此队列,此队列大部分情况时单向链表,入队策略是后来者当头
EntryList:默认情况下,线程被唤醒时,会从等待队列转移到此队列,此队列是一个双向链表
WaitSet:等待队列,调用 wait 方法后,线程会进入此队列

12.ObjectMonitor 的属性

  • header : 重量级锁保存 markword 的地方
  • own: 指向我们持有锁的线程;对象的 markword 里边也保存了指向 monitor 的指针;
  • _cxq 队列: 竞争队列。 A 线程持有锁没有释放; B 和 C 线程同时过来争抢锁,都被 block 了,此时会将 B 和 C 线程加入到 该队列。
  • EntryList 队列:同步队列。A 线程释放锁,B 和 C 线程中会选定一个继承者(可以去争抢锁的这个线程),另外一个线程会被放入我们的 EntryList 队列里边。
  • waitset:等待队列。Object wait 的线程。
  • A 线程持有锁,BC 线程过来竞争失败,进入 cxq – 下轮竞争会把 cxq 里的线程移动到 EntrylIst 中。假设 B 线程竞争到了锁,然后 B 线程调用了 Object.Wait 方法,这时候 B 线程进入 waitset,并释放锁。C 线程拿到了锁,然后唤醒 B 线程。B 线程会从 waitset 里边出来,直接竞争锁。如果竞争失败进入 cxq,继续轮回,如果竞争成功,ok 了。

13.描述下锁分类和锁升级?

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:

锁的类型

  • non-biasable 无锁且不可偏向
  • biasable 无锁可偏向
  • biased 偏向锁
  • thin lock 轻量级锁
  • fat lock 重量级锁

锁的标志

  • 无锁状态 01 偏向标志位 0
  • 偏向锁状态 01 偏向标志位 1
  • 轻量级锁状态 00
  • 重量级锁状态 10
  • GC 11

这几个状态会随着竞争情况逐渐升级.锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁.这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

14.偏向锁的原理?

为了在只有一个线程执行同步块时提高性能,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1 (表示当前是偏向锁):

  • 如果没有设置,则使用 CAS 竞争锁;
  • 如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

当设置了 HashCode 后,不能设置偏向锁,只能升级为轻量级锁,因为 HashCode 生成后,没办法把 HashCode 占位改为偏向线程的 id,这里 HashCode 会存在显式调用(直接调用 HashCode 方法)和隐式调用(比如 HashMap 的 put 方法).

15.批量重偏向和批量撤销?

真正的锁升级,是依赖于 class 的,而并不是依赖于某一个 new 出来的对象(偏向锁升级为轻量级锁)。

真正的锁升级,是依赖于当前 new 出来的对象的(轻量级锁升级为重量级锁)

轻量级锁升级为重量级锁:这个时候,只要我们的线程发生了竞争,并且 CAS 替换失败,就会发起锁膨胀,升级为重量级锁(针对的是一个对象实例)。

rebias & revoke
bulk rebias(批量重偏向):如果已经偏向 t1 线程的对象,在 t2 线程申请锁时撤销偏向后升级为轻量级锁的对象数量达到一定值(20),后续的申请会批量重偏向到 t2 线程;
bulk revoke(批量撤销):在单位时间(25s)内某种 Class 的对象撤销偏向的次数达到一定值(40),JVM 认定该 Class 竞争激烈,撤销所有关联对象的偏向锁,且新实例也是不可偏向的;并且有第三条线程 C 加入了,这个时候会触发批量撤销。JVM 会标记该对象不能使用偏向锁,以后新创建的对象,直接以轻量级锁开始。这个时候,才是真正的完成了锁升级。

-XX:+UseBiasedLocking 启用偏向锁,默认启用
-XX:+PrintFlagsFinal 打印JVM所有参数
-XX:BiasedLockingStartupDelay=4000 偏向锁启用延迟时间,默认4秒
-XX:BiasedLockingBulkRebiasThreshold=20 批量重偏向阈值,默认20
-XX:BiasedLockingBulkRevokeThreshold=40 批量撤销阈值,默认40
-XX:BiasedLockingDecayTime=25000

16.偏向锁撤销原理?

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

如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WgveuPnY-1660370613961)(https://s2.loli.net/2022/04/15/l5Hoa2tPcXEDOZ8.png)]

17.JVM 偏向锁可以撤销吗?

偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动 4 秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

18.轻量级锁加锁和解锁的过程?

轻量级锁是为了在线程近乎交替执行同步块时提高性能。

轻量级锁加锁

  • 线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录空间中,官方称为 Displaced Mark Word;
  • 然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
  • 如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁

  • 会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生.如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

19.Mark Word 说明

Mark Word 64 位,格式如下:

java 对象头长度

具体分析:

如果是无锁状态,则分布为 25 位 unused,31 位 hashcode,1 位 unused,4 位 age,偏向标志位,锁的标志位

如果是偏向锁状态,54 位 thread 的 id,2 位 epoch,1 位 unused,4 位 age,偏向标志位,锁的标志位

如果是轻量级锁,62 位的 lock_record,锁的标志位

如果是重量级锁,62 位的 monitor 指针,锁的标志位,owner 指向持有锁的线程

synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个字宽(W/ord)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit,如表 2-2 所示。

20.描述下偏向锁到轻量级锁再到重量级锁的过程?

偏向锁:
1.A 线程获取偏向锁,并且 A 线程死亡退出。B 线程争抢偏向锁,会直接升级当前对象的锁为轻量级锁。这只是针对我们争抢了一次。
2.A 线程获取偏向锁,并且 A 线程没有释放偏向锁,还在 sync 的代码块里边。B 线程此时过来争抢偏向锁,会直接升级为重量级锁。
3.A 线程获取偏向锁,并且 A 线程释放了锁,但是 A 线程并没有死亡还在活跃状态。B 线程过来争抢,会直接升级为轻量级锁。综上所述,当我们尝试第一次竞争偏向锁时,如果 A 线程已经死亡,升级为轻量级锁;如果 A 线程未死亡,并且未释放锁,直接升级为重量级锁;如果 A 线程未死亡,并且已经释放了锁,直接升级为轻量级锁。
4.A 线程获取偏向锁,并且 A 线程没有释放偏向锁,还在 sync 的代码块里边。B 线程多次争抢锁,会在加锁过程中采用重量级锁;但是,一旦锁被释放,当前对象还是会以轻量级锁的初始状态执行。
5.A 线程获取偏向锁,并且 A 线程释放了锁,但是 A 线程并没有死亡还在活跃状态。B 线程过来争抢。部分争抢会升级为轻量级锁;部分争抢会依旧保持偏向锁。

偏向锁到轻量级锁:

线程 1 作为持有者,线程 2 作为竞争者出现了,线程 2 由于 cas 替换偏向锁中的线程 id 失败,发起了撤销偏向锁的动作.此时线程 1 还存活,暂停了线程 1 的线程,此时线程 1 的栈中的锁记录会被执行遍历,将对象头中的锁的是否是偏向锁位置改成 0,并将锁标志位从 01(偏向锁)改成 00(轻量级锁),升级为轻量级锁。

轻量级锁到重量级锁:

线程 1 为锁的持有者,线程 2 为竞争者.线程 2 尝试 CAS 操作将轻量级锁的指针指向自己栈中的锁记录失败后。发起了升级锁的动作。线程 2 会将 Mark Word 中的锁指针升级为重量级锁指针。自己处于阻塞状态,因为此时线程 1 还没释放锁。当线程 1 执行完同步体后,尝试 CAS 操作将 Displaced Mark Word 替换回到对象头时,此时肯定会失败,因为 mark word 中已经不是原来的轻量级指针了,而是线程 2 的重量级指针.那么此时线程 1 很无奈,只能释放锁,并唤醒其他线程进行锁竞争。此时线程 2 被唤醒了,获取了重量级锁。

轻量级锁---重量级锁:
释放锁(前四步)并唤醒等待线程
1.线程1初始化monitor对象;
2.将状态设置为膨胀中(inflating);
3.将monitor里边的header属性,set称为对象的markword;(将自己lockrecord里边的存放的markword的hashcode,分代年龄,是否为偏向锁set到objectmonitor对象的header属性里)
4.设置对象头为重量级锁状态(标记为改为00);然后将前30位指向monitor对象;(真正的锁升级是由线程1操控的)
5.唤醒线程2;
6.线程2开始争抢重量级锁。

21.比较三种锁的优缺点及使用场景?

其实偏向锁,本就为一个线程的同步访问的场景.在出现线程竞争非常小的环境下,适合偏向锁。轻量级锁自旋获取线程,如果同步块执行很快,能减少线程自旋时间,采用轻量级锁很适合。重量级锁就不用多说了,synchronized 就是经典的重量级锁。使用 synchronized 不一定会升级为重量级锁,得看条件.

偏向锁:

自始至终,对这把锁都不存在竞争,只需要做个标记,这就是偏向锁,每个对象都是一个内置锁(内置锁是可重入锁),一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有线程来访问并尝试获取锁的时候,它就会把这个线程记录下来,以后如果获取锁的线程正式偏向锁的拥有者,就可以直接获得锁,偏向锁性能最好。

轻量级锁:

轻量级锁是指原来是偏向锁的时候,这时被另外一个线程访问,存在锁竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

重量级锁:

重量级锁是互斥锁,主要是利用操作系统的同步机制实现的,当多个线程直接有并发访问的时候,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就升级为重量级锁,重量级锁会使得其他拿不到锁的线程陷入阻塞状态,重量级锁的开销相对较大。

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

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

22.为什么要引入轻量级锁?

解答这个问题,先要自问一句,不引入轻量级锁,直接用重量级锁有什么坏处。我们知道重量级锁,如果线程竞争锁失败,会直接进入阻塞(Blocked)状态,阻塞线程需要 CPU 从用户态转到内核态,代价较大,假设一个线程刚刚阻塞不久这个锁就被释放了,这个线程被唤醒后,还需要从内核态切换到用户态,一来一回就两次状态切换,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋的等待锁释放。如果自旋的等待锁的释放,正好是我们的轻量级锁的特性,那么为什么引入轻量级锁就明白了。

23.什么是适应性自旋?和普通自旋的区别?

JDK 1.5 的自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。自旋次数可以设定,通过-XX:PreBlockSpin=10 自行设置自旋次数,此处举例说明设置为 10 次。

在 JDK 1.6 引入了适应性自旋锁,XX:PreBlockSpin 参数也就没有用了.适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如下三点优化非常突出。

  1. 如果平均负载小于 CPU 则一直自旋

  2. 如果有超过(CPU/2)个线程正在自旋,则后来线程直接阻塞(升级为重量级锁)

  3. 如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞(升级为重量级锁)

24.JVM 如何开启轻量级锁?

JDK 1.5 使用- XX:+UseSpinning 手动开启。

JDK1.6 及后续版本默认开启轻量级锁。

25.对于同步方法,处理器如何实现原子操作?

处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

总线锁定:

如果多个处理器同时对非同步共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致.原因可能是多个处理器同时从各自的缓存中读取变量 i,分别进行加 1 操作,然后分别写入系统内存中。

对于同步方法操作 i++时,部分处理器使用总线锁就是来解决这个问题的.所谓总线锁就是使用处理器提供的一个 LOCK#信号(参见 93 题的 Lock 汇编指令),当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存,只不过总线锁定开销很大。

缓存锁定:

所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言 LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效.

26.缓存锁定性能优于总线锁定,为什么不淘汰总线锁定?

有两种情况下处理器不会使用缓存锁定。

第一种情况是:当操作的数据不能被缓存在处理器内部(比如外部磁盘数据),或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。

第二种情况是:有些处理器不支持缓存锁定.对于 Intel 486 和 Pentium 处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

27.什么是原子操作?说说 i++操作

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。

i++是读改写系列操作,操作中包括如下三个:

  • 读操作:读 i 的当前值;
  • 改操作:在 i 的当前值上做+1 操作;
  • 写:将修改后的值写回内存。

28.java 如何保证原子操作的?

在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作

CAS:从 Java 1.5 开始,JDK 的并发包里提供了一些类来支持原子操作,如 AtomicBoolean (用原子方式更新 boolean 值)、 AtomicInteger (用原子方式更新 int 值)和 AtomicLong (用原子方式更新的 long 值),其中就是依靠 CAS 操作来完成的。

锁:如 synchronized 以及 Lock 锁,线程获取对象锁之后,会完成系列操作后释放锁,运行期间,其他线程会处于阻塞状态,因此是原子性的操作.

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域.JVM 内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁.有意思的是除了偏向锁,JVM 实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环 CAS 的方式来获取锁,当它退出同步块的时候使用循环 CAS 释放锁

29.锁的获取和释放内存语义?

对比锁释放-获取的内存语义与 volatile 写一读的内存语义可以看出:

锁释放与 volatile 写有相同的内存语义;

锁获取与 volatile 读有相同的内存语义。

下面对锁释放和锁获取的内存语义做个总结。

  • 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。

  • 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变变量所做修改的)消息。

  • 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

从对 ReentrantLock 的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式。

1)利用 volatile 变量的写-读所具有的内存语义。

2)利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。

30.CAS 操作的原理?

JDK 文档对该方法的说明如下:如果当前状态值(内存值)等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义

所谓的 CAS,其实是个简称,全称是 Compare And Swap,对比之后交换数据.内存值–预期值–新值

//原子类Atomic中的cas
public final boolean compareAndSet(boolean expect, boolean update){int e = expect ? 1 : 0;int u = update ? 1 : 0;return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
//底层实现是用的Unsafe的cas,包含3个方法
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面的方法,有几个重要的参数:

(1)this: Unsafe 对象本身,需要通过这个类来获取 value 的内存偏移地址。

(2)valueOffset:value 变量的内存偏移地址。

(3)expect:期望更新的值。

(4)update:要更新的最新值。

其中步骤(1)和步骤(2)是根据内存偏移地址获取当前的值 value,expect 值是未修改之前的 value 值.如果修改时通过内存偏移地址获取到的 value 与 except 的 value 值一样,则进行更新,否则不更新,再次从新获取 value 值,循环下去,直到成功为止.

//CAS-c++源码:
inline jint Atomic::cmpxchg (jint exchange value, volatile jint*dest,jint compare value){// alternative for InterlockedCompareExchangeint mp=os::isMP();//是否为多核心处理器_asm {mov edx, dest //要修改的地址mov ecx, exchange_value //新值值mov eax,compare_value //期待值LOCK_IF_MP(mp) //如果是多处理器,在下面指令前加上LOCK前缀cmpxchg dword ptr [edx],ecx//[edx]与eax对比,相同则[edx]=ecx,否则不操作}
}

这里看到有一个 LOCK_IF_MP,作用是如果是多处理器,在指令前加上 LOCK 前缀,因为在单处理器中,是不会存在缓存不一致的问题的,所有线程都在一个 CPU 上跑,使用同一个缓存区,也就不存在本地内存与主内存不一致的问题,不会造成可见性问题.然而在多核处理器中,共享内存需要从写缓存中刷新到主内存中去,并遵循缓存一致性协议通知其他处理器更新缓存.Lock 在这里的作用:

  • 在 cmpxchg 执行期间,锁住内存地址[edx],其他处理器不能访问该内存,保证原子性.即使是在 32 位机器上修改 64 位的内存也可以保证原子性。
  • 将本处理器上写缓存全部强制写回主存中去,保证每个线程的本地内存与主存一致。
  • 禁止 cmpxchg 与前后任何指令重排序,防止指令重排序。

31.CAS 存在的问题,ABA 问题,如何解决的?

主要有 3 个问题

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

ABA 问题.因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了.ABA 问题的解决思路就是使用版本号.在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→ B→A 就会变成 1A→2B→3A。

解决一:从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题.这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

解决二:使用 AtomicMarkableReference 可以通过 Boolean 类型进行判断

32.CAS 循环时间太长,会有什么问题?

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。

使用 CAS 自旋,需要考虑业务场景是否是多任务快速处理的场景,如果单个任务处理够快且任务量大,使用 CAS 会带来很好地效果.轻量级锁的设计原理底层就是使用了 CAS 的操作原理。

33.final 域的内存语义?

对于 final 域,编译器和处理器要遵守两个重排序规则。

  • 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含 fnal 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
  • 简单说,普通变量可能会重排序,final 变量在构造函数返回之前不会被其他引用提前访问
public class Juc_book_fang_12_FinalExample {int i; // 普通变量final int j; // final变量static Juc_book_fang_12_FinalExample obj;public Juc_book_fang_12_FinalExample() { // 构造函数i = 1; // 写普通域j = 2; // 写final域}public static void writer() { // 写线程A执行obj = new Juc_book_fang_12_FinalExample();}public static void reader() { // 读线程B执行Juc_book_fang_12_FinalExample object = obj;// 读对象引用int a = object.i; // 读普通域int b = object.j; // 读final域}
}

写 final 规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NhOPLR2L-1660370613967)(https://s2.loli.net/2022/06/09/FTj5Gh6nbB8DAVY.png)]

读 final 重排序规则

34.内存屏障的种类以及说明?

StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他 3 个屏障的效果.现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持).执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush).

35.说说对于 happens-before 的理解?

与程序员密切相关的 happens-before 规则如下。

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。

  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  • 传递性:如果 Ahappens-beforeB,且 Bhappens-beforeC,那么 Ahappens-beforeC

注意两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the frst is visible to and ordered before the second)。happens-before 的定义很微妙,后文会具体说明 happens-before 为什么要这么定义。

36.什么是 as-if-serial 语义?

不管怎么重排序,单线程执行结果不变

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

37.双重检查锁的单例模式

public class Juc_book_fang_11_Dcl {private static volatile Person instance;public static Person getInstance(){if (instance == null){//步骤一synchronized (Juc_book_fang_11_Dcl.class){//步骤二if (instance == null){//步骤三instance = new Person();//步骤四}return instance;}}return instance;}
}

看着图中的注释,假设线程 A 执行 getInstance 方法

步骤一: instance 为 null,则进入 if 判断;

步骤二:获取 synchronized 锁,成功,进入同步代码块;

步骤三:继续判断 instance,为 null 则进入 if 判断;

步骤四: instance = new Instance().看似是一句代码,其实是三句代码。

memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory);//2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址

上面 2 和 3 两者没有依赖关系,设置 instance 指向刚分配的内存地址和初始化对象会存在重排序.

使用 volatile 并不会解决 2 和 3 的重排序问题,因为 2 和 3 都在一个 new 指令里面,内存屏障是针对指令级别的重排序,双重检查锁 volatile 禁止重排序的原理,new 指令是单一指令,也就是前面加 StoreStore 屏障,后面加 StoreLoad 屏障,后面的线程必不会读到 instance 为 null

有 2 种解决方案:

  • 使用 volatile 禁止重排序,原理还是其他线程不可见

  • 允许 2 和 3 重排序,但是不允许其他线程可见

    • 基于类初始化
    • CLASS 对象的初始化锁只能有一个线程访问,对其他线程不可见

基于类的初始化

public class InstanceFactory {private static class InstanceHolder {public static Instance instance=new Instance();}public static Instance getinstance() {return InstanceHolder.instance; // 这里将导致InstanceHolder类被初始化}
}

java 中,一个类或接口类型 T 将被立即初始化的情况如下

  1. T 是一个类,而且一个 T 类型的实例被创建。

  2. T 是一个类,且 T 中声明的一个静态方法被调用。

  3. T 中声明的一个静态字段被赋值。

  4. T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段。

  5. T 是一个顶级类(TopLevelClass,见 Java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。

    在示例代码中,首次执行 getlnstance)方法的线程将导致 InstanceHolder 类被初始化(符合情况 4)。

38.fail-fast 和 fail-safe 对比?

对比项 fail-fast fail-safe
Throw ConcurrentModification Exception 不会
Clone 不会
Memory Overhead 不会
Examples HashMap Vector ArrayList HashSet CopyOnWriteArrayList

fail-safe 也是得具体情况具体分析的。

  1. 如果是 CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就属于 复制原来的集合,然后在复制出来的集合上进行操作 的情况 ,所以是不会抛出这个 ConcurrentModificationException 的 。
  2. 如果是这个 concurrentHashMap 的,就比较硬核了~

    【檀越剑指大厂--并发编程】并发编程总结相关推荐

    1. 【檀越剑指大厂—SpringCloudAlibaba】SpringCloudAlibaba高阶

      一.Nacos 1.什么是 nacos? Nacos 的全称是 Dynamic Naming and Configuration Service,Na 为 naming/nameServer 即注册中 ...

    2. 【檀越剑指大厂--mysql】mysql高阶篇

      文章目录 一.Mysql 基础 1.数据库与实例? 2.mysql 的配置文件 3.mysql 体系结构 4.innodb 的特点? 5.innodb 和 myisam 的区别 6.其他存储引擎? 7 ...

    3. 【檀越剑指大厂--redis】redis高阶篇

      一.数据结构与对象 1.什么是 SDS? Redis 没有直接使用 C 语言传统的字符吕表示 (以空字符结尾的字符数组,以下简称 C 字符串),而是自己构建了 一种名为简单动态字符串(simple d ...

    4. 【檀越剑指大厂—SpringMVC】SpringMVC篇

      一.基础概念 1.什么是 MVC MVC 是模型(Model).视图(View).控制器(Controller)的简写,是一种软件设计规范. 是将业务逻辑.数据.显示分离的方法来组织代码. MVC 主 ...

    5. 【檀越剑指大厂—SpringCloudNetflix】SpringCloudNetflix高阶篇

      一.基础概念 1.架构演进 在系统架构与设计的实践中,从宏观上可以总结为三个阶段: 单体架构 :就是把所有的功能.模块都集中到一个项目中,部署在一台服务器上,从而对外提供服务(单体架构.单体服务.单体 ...

    6. 【檀越剑指大厂—kafka】kafka高阶篇

      一.认识 kafka 1.kafka 的定义? Kafka 传统定义:Kafka 是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域.发布/订阅:消 ...

    7. 【檀越剑指大厂--linux】Linux汇总

      一.系统命令 1.操作系统 uname -a 2.主机名 #查看主机名 hostname#查看当前linux的版本 more /etc/RedHat-releasecat /etc/redhat-re ...

    8. 【檀越剑指大厂--网络安全】网络安全学习

    9. 【檀越剑指大厂--ElasticSearch】ElasticSearch进阶

    最新文章

    1. golang []byte和string相互转换
    2. python list遍历 间隔_Python 列表(List) 的三种遍历(序号和值)方法
    3. ProgressBar--进度条
    4. javascript 中面向对象实现 如何继承
    5. C++---------之--------【虚析构函数】
    6. 学习SQL:SQL Server日期和时间函数
    7. python中的random模块_Python中的random模块
    8. FreeBSD portsnap方法更新ports
    9. 互联网项目架构经验分享
    10. 数据分析笔试题(网易,阿里,京东...)
    11. linux工作周报范文300字,工作周报范文300字
    12. scrollTop以及页面回到顶部
    13. 保存为UTF8的1byte文字,2byte文字,3byte文字,4byte文字大全
    14. VS Code:推荐插件 - HTML格式化(包括JS、CSS)
    15. 两个瓶子水怎样一样多_大班科学领域数学活动 | 一样多的水(容积守恒)
    16. STL容器的底层数据结构
    17. 编写一个程序,计算学生的总分和平均成绩(一)
    18. css flex所有属性总结
    19. TensorFlow实现非线性回归
    20. JAVA冰箱评测开题报告_基于CFD的风冷冰箱后风道数值计算和优化设计开题报告...

    热门文章

    1. 怎么判定一个词是不是刷的百度指数的
    2. ValueError: There are no more samples after a first-order seasonal differencing.
    3. POSTMAN调用webservice
    4. C/C++的指针与数组
    5. 电子信息、电气、计算机类专业中外合作硕士项目一览
    6. TSNE高维数据降维可视化工具 + python实现
    7. 微机原理-I/O并行接口和并行接口芯片8255A
    8. 【python起床服务】播放起床铃+给老板发邮件辞职
    9. 2a计算机专业学校排名,计算机专业(研究生)排名
    10. 经典网页设计:20个新鲜出炉的 HTML5 网站