一、Java如何开启线程,怎么保证线程安全?

image.png

二、volatile和synchronized有什么区别?volatile能不能保证线程安全?DCL(double check lock)单例为什么要加volatile?

synchronized是Java的jvm底层提供的锁的机制。
image.png

三、谈谈你对AQS的理解?AQS如何实现可重入锁?

https://www.bilibili.com/video/BV1bA411P7yr?p=41
1、是一个Java线程同步的框架,是JDK中很多锁工具核心实现框架。

2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,二state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不同的意义。

3、在可重入锁这个场景下,state就是用来表示加锁的次数。0表示无锁,每加一次锁,state就加1.释放锁state就减1。

四、有A,B,C三个线程,如何保证三个线程同时执行?如何在并发的情况下保证三个线程依次执行?如何保证三个线程有序交错执行?

考查并发工具。对三个并发工具的使用

1、CountDownLatch
相当于跑步排队,线程都在起跑线排队。保证同时触发
2、CylicBarrier
做长途客车,保证利润,等到线程都坐满了,再出发
3、Semaphore
在给线程排队的时候,给某些线程定义权重。
https://www.bilibili.com/video/BV1bA411P7yr?p=42

保证三个线程同时执行:CountDownLatch
保证三个线程依次执行:

image.png

三个线程交错执行:
Semaphore信号量。

五、如何对一个字符串进行快速排序?

考查点:对ForkJoin框架的理解
https://www.bilibili.com/video/BV1bA411P7yr?p=43

六、Java线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

https://www.bilibili.com/video/BV1bA411P7yr?p=47

七、并发的三大特性

保证线程安全需要同时保证下面的三大特性。

  • 原子性
  • 可见性
    当多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他的线程能够感知到修改的值。
  • 有序性
    虚拟机在进行代码编译时,对于那些改变了顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之哦户,虽然对变量没有造成影响,但有可能出现线程安全的问题。

https://www.bilibili.com/video/BV1bA411P7yr?p=48
i++不能保证线程安全,使用java类AutomicInteger保证线程安全【通过volatile关键字,保证可见性和有序性,并不能保证原子性。CAS,保证原子性】

八、对线程安全的理解

单线程环境下,执行结果都是一样的,线程安全,但是在多线程场景下,执行结果不一样,那么就是线程不安全的。
https://www.bilibili.com/video/BV1bA411P7yr?p=49
image.png

九、Java死锁如何避免

https://www.bilibili.com/video/BV1bA411P7yr?p=51
造成死锁的原因;
1、一个资源每次只能被一个线程使用。
2、一个线程在阻塞等待某个资源时候,不释放已占资源。
3、一个线程已经获得的资源,在未使用完之前,不能被强行剥夺。
4、若干线程形成头尾相接的循环等待资源关系。
这是造成死锁必须要达到4个条件,如果要避免死锁,只需要不满足其中一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
要注意加锁的顺序,保证每个线程按同样的顺序进行加锁。
要注意加锁的时限,可以针对所设置一个超时时间。
要注意死锁的检查,这是一种预防的机制,确保在第一时间发现死锁并进行解决。

10、如果提交任务时候,线程池队列已满,这时会发生什么?

1、如果使用的是无界队列,那么可以继续提交任务,是没有关系的。
2、如果使用的是有界队列,提交任务时,如果队列满了,如果核心线程数没有达到上限,那么则增加线程,如果线程数已经达到了最大值,则使用拒绝策略进行拒绝。

11、volatile关键字,是如何保证可见性,有序性的。

1、对于加了volatile关键字的成员变量,在对这个变量进行修改时候,会直接将CPU高级缓存中的数据写回到主内存中,对这个变量的读取也会直接从主内存中读取,从而保证可见性。
2、在对volatile修饰的成员变量进行读写时候,会插入内存屏障,而内存屏障可以达到禁止指令重排序的效果,从而保证有序性。

11.1、如何理解volatile关键字

https://www.bilibili.com/video/BV1zA411c7re?p=10
image.png
什么是可见性:
Java并发编程之可见性问题

12、sychronized和ReentranctLock的区别?

https://www.bilibili.com/video/BV1bA411P7yr?p=55
1、sychronized是关键字,ReentranctLock是一个类。
2、sychronized会自动加锁和释放锁,ReentranctLock需要程序员自己手动加锁和释放锁。
3、sychronized底层是基于jvm的锁,ReentranctLock是一个API层面的锁。
4、sychronized是非公平锁,可以选择公平锁或非公平锁。
5、sychronized锁的是对象,锁信息保存在对象头中,ReentranctLock锁的是线程,通过代码中int类型的state标识来标识锁的状态。
6、底层有一个锁升级的过程,

13、简述线程池的原理,FixedThreadPool用的阻塞队列是什么?

https://www.bilibili.com/video/BV1bA411P7yr?p=56
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。

14、如何理解volatile关键字?

https://www.bilibili.com/video/BV1bA411P7yr?p=57
在并发领域中,存在三大特性:原子性,可见性,有序性。
volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。

15、对守护线程的理解?

https://www.bilibili.com/video/BV1bA411P7yr?p=58
守护线程:
为所有非守护线程提供服务的线程;任何一个守护线程都是珍格格JVM中所有非守护线程的保姆;

守护线程类似于整个进程一个默默无闻的小弟,它的生死无关重要,他却依赖整个进程而运行;哪天那个线程结束了,没有要执行的了,程序就结束了,理都不理守护线程,就把它中断了。

注意:
由于守护线程的终止是自身无法控制的,因此千万不要把io、file等重要操作逻辑分配给它;因为它不靠谱;

守护线程的作用:
举例:GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何

16、为什么使用线程池,解释下线程池参数?

https://www.bilibili.com/video/BV1bA411P7yr?p=59

17、线程池的处理流程:

https://www.bilibili.com/video/BV1bA411P7yr?p=60

image.png

18、线程池中的线程复用原理

线程池将线程和任务进行解耦、线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断的获取新任务来执行,其核心原理就是线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()方法来创建线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法去执行,通过这种方式只使用固定的线程就将所有的任务的run方法串联起来。

19、线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程

https://www.bilibili.com/video/BV1bA411P7yr?p=63
1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用贵族赛队列的take方法挂起,从而维持核心线程的存活,不至于一直占用CPU资源。

2、在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体的效率。

20、线程的生命周期及状态

1、线程通常有五种状态:创建,就绪,运行,阻塞和死亡。
2、阻塞的情况又分为三种:

  • 等待阻塞:运行的线程执行wait方法,该线程会释放占用的左右的资源,jvm会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或者notifyAll方法才能被唤醒,wait是object类的方法
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则jvm会把该线程放入“锁池”中。
  • 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,jvm会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止超时,或者I/O处理完毕时,线程重新转入就绪状态。sleep时Thread类方法。
  1. 新建状态(new):新创建一个线程对象。
  2. 就绪状态(runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了cpu,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

    20、ReentrantLock中的公平锁和非公平锁的底层实现

    https://www.bilibili.com/video/BV1zA411c7re?p=11

21、ReentrantLock中tryLock()和lock()方法的区别

https://www.bilibili.com/video/BV1zA411c7re?p=12
lock方法返回值void。阻塞加锁,只有拿到锁,才能执行下面的代码
tryLock方法有bool返回值。返回值是布尔值,看程序员怎么在拿到锁和没拿到锁要怎么操作,一切在于程序员的操作了。

  1. boolean result = reentranctLock.tryLock(); //非阻塞
  2. if(result)
  3. ... // 加到锁,执行业务
  4. else
  5. ... // 没加到锁,执行另外的业务。

22、CountDownLatch和Semaphore的区别和底层原理

https://www.bilibili.com/video/BV1zA411c7re?p=13
image.png
闭锁CountDownLatch

23、说一下ThreadLocal

https://www.bilibili.com/video/BV1bA411P7yr?p=38

24、线程之间如何通信

https://www.bilibili.com/video/BV1bA411P7yr?p=39

30、【java并发】sleep、wait、join、yield

https://www.bilibili.com/video/BV1bA411P7yr?p=30
image.png
image.png
image.png

区别:
在java中,sleep()和yield()方法是Thread类中的方法,而wait()是Object类中的方法。也就是说,在java中,所有的类都有wait()方法,而只有继承了Thread类的方法才有sleep()和yield()方法

其中sleep()和wait()的区别

  • sleep()方法用于短暂暂停线程的运行,而wait()方法用于线程间的通信
  • 线程调用sleep()方法后,当前线程不会释放其持有的资源;线程调用wait()方法后,当前线程会释放其持有的资源
  • sleep()是一个静态方法,作用在当前线程上;而wait()是一个实例方法,并且只能由其他线程调用本实例的notify()或者notifyAll()方法后才能被唤醒。且如果线程调用sleep()方法被唤醒后,会立刻进入就绪状态,而调用wait()方法的线程则不会立刻进入,而是先获得同步锁,然后才进入就绪状态。
  • image.png
  • wait()只能在同步环境中被使用,而sleep()没有限制,可以在任何地方使用
  • wait()方法针对的是一个被同步代码块加锁的对象,而sleep()针对的是一个线程进行休眠。

    sleep()和yield()的区别

    与sleep()不同的是,当前线程执行yield()后,也就是告诉CPU,当前线程已经执行的差不多了,线程调度器可以将当前的CPU让给那些比当前线程优先级更高的线程或者优先级和当前线程同级的线程去执行,如果没有这样的线程,那么当前线程继续执行,如果有这样的线程,那么当前线程进入就绪状态。

    sleep()的注意

  • 执行t.sleep()是不会让线程t暂停,而是当前线程暂停,因为sleep()是一个静态方法。

  • 当前一个线程执行完sleep()方法后,不能保证能立马获得CPU资源,而是需要竞争,也就是当前线程会进入就绪状态。
  • 如果其他线程中断了一个正在休眠的线程(调用sleep()方法休眠的线程),那么sleep()会抛出一个Interrupted Exception异常
  • 一个线程执行完sleep()后,会将其本身拥有的cpu资源让给线程调度器
  • sleep()有两个方法,一个是传入一个毫秒数,一个需要传入一个毫秒数和一个纳秒数
  1. public static native void sleep(long millis) throws InterruptedException;
  2. public static void sleep(long millis, int nanos) throws InterruptedException {}
  • yield()和sleep()方法都是作用在当前线程上,因为他们都是静态方法,不是实例方法。 ```java public static native void yield();
  1. <a name="MC5tL"></a>
  2. ### join()的作用
  3. 在多线程中,使用join()方法用于让主线程等待子线程运行完之后,主线程才继续执行。如下面的代码,不加join()方法:
  4. ```java
  5. public static void main(String[] args) throws InterruptedException {
  6. TreeNode root = TreeNode.getBST();
  7. System.out.println(new FDASF().getMinGap(root));
  8. Thread[] tt = new Thread[5];
  9. for (int i = 0; i < 5; i++) {
  10. tt[i] = new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. for (int j = 0; j < 5; j++) {
  14. System.out.println("this is thread ==" + this.getClass().getName() + " j=" + j);
  15. }
  16. }
  17. });
  18. }
  19. for (int i = 0; i < tt.length; i++) {
  20. tt[i].start();
  21. }
  22. }
  23. 运行结果:
  24. this is thread ==Thread-0 j=0
  25. this is thread ==Thread-0 j=1
  26. this is thread ==Thread-2 j=0
  27. this is thread ==Thread-0 j=2
  28. this is thread ==Thread-3 j=0
  29. this is thread ==Thread-3 j=1
  30. this is thread ==Thread-2 j=1
  31. this is thread ==Thread-4 j=0
  32. this is thread ==Thread-1 j=0
  33. this is thread ==Thread-3 j=2
  34. this is thread ==Thread-0 j=3
  35. this is thread ==Thread-3 j=3
  36. this is thread ==Thread-1 j=1
  37. this is thread ==Thread-4 j=1
  38. this is thread ==Thread-2 j=2
  39. this is thread ==Thread-4 j=2
  40. this is thread ==Thread-1 j=2
  41. this is thread ==Thread-1 j=3
  42. this is thread ==Thread-1 j=4
  43. this is thread ==Thread-3 j=4
  44. this is thread ==Thread-0 j=4
  45. this is thread ==Thread-4 j=3
  46. this is thread ==Thread-2 j=3
  47. this is thread ==Thread-4 j=4
  48. this is thread ==Thread-2 j=4

加上join()方法之后:

  1. public static void main(String[] args) throws InterruptedException {
  2. TreeNode root = TreeNode.getBST();
  3. System.out.println(new FDASF().getMinGap(root));
  4. Thread[] tt = new Thread[5];
  5. for (int i = 0; i < 5; i++) {
  6. tt[i] = new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. for (int j = 0; j < 5; j++) {
  10. System.out.println("this is thread ==" + this.getClass().getName() + " j=" + j);
  11. }
  12. }
  13. });
  14. }
  15. for (int i = 0; i < tt.length; i++) {
  16. tt[i].start();
  17. tt[i].join();
  18. }
  19. }
  20. 运行结果:
  21. this is thread ==Thread-0 j=0
  22. this is thread ==Thread-0 j=1
  23. this is thread ==Thread-0 j=2
  24. this is thread ==Thread-0 j=3
  25. this is thread ==Thread-0 j=4
  26. this is thread ==Thread-1 j=0
  27. this is thread ==Thread-1 j=1
  28. this is thread ==Thread-1 j=2
  29. this is thread ==Thread-1 j=3
  30. this is thread ==Thread-1 j=4
  31. this is thread ==Thread-2 j=0
  32. this is thread ==Thread-2 j=1
  33. this is thread ==Thread-2 j=2
  34. this is thread ==Thread-2 j=3
  35. this is thread ==Thread-2 j=4
  36. this is thread ==Thread-3 j=0
  37. this is thread ==Thread-3 j=1
  38. this is thread ==Thread-3 j=2
  39. this is thread ==Thread-3 j=3
  40. this is thread ==Thread-3 j=4
  41. this is thread ==Thread-4 j=0
  42. this is thread ==Thread-4 j=1
  43. this is thread ==Thread-4 j=2
  44. this is thread ==Thread-4 j=3
  45. this is thread ==Thread-4 j=4

从上面的代码以及运行结果可以看出,join()的具体作用了。

31、【java并发】Sychronized的偏向锁、轻量级锁、重量级锁

https://www.bilibili.com/video/BV1bA411P7yr?p=31

  • 偏向锁:
    在锁对象的对象头中记录一下当前获取该锁的线程id,该线程下次如果又来获取该锁就可以直接获取到了。

  • 轻量级锁:

由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁。此时又有另外一个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了跟重
量级锁区分开,轻量级锁的底层是通过自旋来实现的,不会真正的调用操作系统的api,并不会阻塞线程。而自旋的次数过多,是更加消耗CPU,包括我们的
concurrentHashMap,并发工具类,Sychronized底层都会通过自旋锁尝试的去获取锁,但是尝试的次数过多,仍然没有获取到锁的话,就会升级为重量级锁,调用
操作系统的api,阻塞线程。

  • 重量级锁
    轻量级锁自旋次数足够多,还是没能获取到锁,升级为重量级锁,调用操作系统api,阻塞线程。

  • 自旋锁
    自旋锁就是线程在获取的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了,这个过程一直在运行中,相对而言,没有使用太多的操作系统的资源,比较轻量。

https://blog.csdn.net/Kirito_j/article/details/79201213
锁宏观上看:
乐观锁:
认为读多写少,并发可能性不高。更新时候,判断一下期间是否有更新这个数据,采取在写时候读出当前的版本号,然后再加锁的操作。基本上通过CAS操作实现。

悲观锁
认为写多读少,并发可能性高。每次拿到数据都会上锁,java中的悲观锁就是Synchronized