java 多线程 面试题整理(更新......)

2年前 (2022) 程序员胖胖胖虎阿
256 0 0

一、基础知识​​​​​​1、什么是线程和进程?什么是进程?进程的特点:什么是线程?区别与联系?2、什么是并行与并发?3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的关系6、什么是Callable和Future?7、什么是线程的上下文切换?8、Thread类中的start()和run()方法有什么区别?9、Java中interrupted和isInterruptedd方法的区别?10、为何stop()和suspend()方法不推荐使用10、如何停止一个正在运行的线程?(重要)i:捕捉打断标记并且直接returnii:捕捉打断标记,并且抛出异常终止程序iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记11、sleep和yield的区别?状态的区别:调度的区别:12、sleep,yield为什么是静态方法(重要)13、有三个线程T1,T2,T3,如何保证顺序执行?14、在 java中守护线程和本地线程区别15、sleep和wait的区别?16、线程创建到结束的几种状态?17、对线程优先级的理解?18、什么是后台线程?19、sleep,yiled,wait,join 对比21、Thread.sleep(0)有什么作用?二、锁知识20、什么是线程安全?21、什么是竞态条件?22、什么是临界区?22、什么是不可变对象,它对写并发应用有什么帮助?(重要)23、synchronized关键字最主要的三种使用方式1、修饰实例方法2、修饰静态方法3、修饰代码块24、讲讲你对synchronized的认识?1、无锁状态2、偏向锁状态偏向锁锁撤销:偏向锁存在的意义:偏向锁撤销的情况批量重偏向:批量撤销偏向锁3、轻量级锁锁重入:轻量级锁CAS4、自旋锁25、什么是重量级锁?为什么消耗很大?26、自旋锁的优缺点27、线程同步和互斥有几种实现方法,都是什么?(重要)28、wait的基本使用方法29、wait的相关问题(1)notify()和notifyAll()有什么区别?(2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)(3)为什么wait和notify方法要在同步块中调用?(4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?(5) join方法实现原理(6)如何实现线程间通信31、park和unpark(1)基本使用(2)先调用park再调用unpark(3)先调用unpark再调用park32、park和wait的区别33、park,wait,sleep,yield,join方法的区别34、什么是死锁,死锁发生的条件(重要)死锁的定义:死锁的四个条件:(重要,记忆)怎么预防死锁问题?怎么避免死锁问题怎么检测和解除死锁35、什么是活锁避免活锁的方法活锁与死锁的区别?36、什么是饥饿?37、什么是可重入锁?38、Reentrantlock39、ReentrantLock和Synchronized的相同点和区别(重要)40、ReentrantLock的实现原理41、lock、tryLock和lockInterruptibly的差別42Condition和Object类锁方法区别43、公平锁与非公平锁三、无锁机制(CAS,原子类)44、什么是java内存模型?45、什么是volitile?作用是什么46、什么是原子性?47、有序性48、volatile怎么保证可见性和有序性的?可见性有序性50、单例模式的双检锁是什么?51、synchronized 和 volatile 的区别是什么?为啥synchronized无法禁止指令重排,但可以保证有序性?30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)52、CAS53、synchronized与CAS的区别(重要)54、CAS的缺点(重要)1) CPU开销过大2) 不能保证多个变量的原子性如何解决CAS只能保证一个变量的原子性操作问题?3)ABA问题49、volatile 变量和 atomic 变量有什么不同?50、什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?

一、基础知识

1、什么是线程和进程?

什么是进程?

程序由指令和数据组成,但是这些指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令管理内存管理IO的。

进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程。比如.exe文件运行,进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程

总结:进程是把指令加载给CPU,数据加载到内存并执行的程序实例

进程的特点:

1、每个进程可以包括多个线程

2、每个进程都有自己独立的内存空间,而其内部的线程可以共享这些内存空间,进程上下文切换的开销比较大,不同进程之间不共享内存

什么是线程?

线程是进程的一个子集,一个线程就是一个指令流的执行,线程按照一定的顺序把这些指令流交给CPU执行,就是线程的执行

区别与联系?

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。

不同的进程使用不同的内存空间,而线程共享同一进程的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

线程作为操作系统能够进行运算调度的最小单位,进程作为资源分配的最小单位。

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2、什么是并行与并发?

并发:操作系统的任务调度器调度多个线程轮流使用某个CPU的操作(CPU的时间片为15ms),这个过程中会发生线程的上下文切换

1583408729416

并行:对于多核CPU来讲,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

1583408812725

一般来说对于单核CPU的机器,线程执行是并发的,对于多核CPU来讲,线程执行是既有并行也有并发的

3、什么是同步执行和异步执行

以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步

也就是说一个程序需要运行完了有结果了才能进行下一个线程,这样这个程序就会堵塞其他的程序,这就是同步,异步就是这个程序在运行的时候我仍然可以不管他运行别的程序

多线程可以将同步程序变为异步的,从而增加系统资源的利用率

比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。

4、Java中实现多线程有几种方法?(较难)

Thread的构造方法参数可以传入Runnable接口和FutureTask对象

Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

(1)继承Thread类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

public class MyThread extends Thread {
    public void run() {
        System.out.println("MyThread.run()");
    }
}
MyThread myThread1 = new MyThread();
myThread1.start();

(2)实现runable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程

public class MyThread extends OtherClass implements Runnable {
    public void run() {
        System.out.println("MyThread.run()");
    }
}
​
​

启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:

 MyThread myThread = new MyThread(); 
 Thread thread = new Thread(myThread); 
 thread.start(); 

//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用 target.run() public void run() { if (target != null) { target.run(); } }

(3)实现Callable接口(创建FutureTask(Callable)对象)

1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class SomeCallable<V> extends OtherClass implements Callable<V> {
​
    @Override
    public V call() throws Exception {
        // TODO Auto-generated method stub
        return null;
    }
​
}
Callable<V> oneCallable = new SomeCallable<V>();   
//由Callable<Integer>创建一个FutureTask<Integer>对象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
  //由FutureTask<Integer>创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   
//至此,一个线程就创建完成了。

5、Future接口,Callable接口,FutureTask实现类的关系

Callable接口中就一个抽象方法call(),有返回值

Future接口中定义了关于线程状态的方法,比如打断线程执行的cancel方法,判断该线程是否被取消的isCancelled()方法,返回线程是否执行完的isDone方法,以及重要的get方法获取返回值

FutureTask实现类实现了Future接口,并且有构造函数,参数是传入一个Callable接口, 

以此获得返回值

其中Future接口的get方法是阻塞方法,没有得到get的值会阻塞主线程

package TestFutureTask;
​
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
​
public class TestMain {
    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
            System.out.println("futureTask开始了");
            Thread.sleep(10000);
            return 100;
        }
        );
        Thread thread = new Thread(futureTask, "thread1");
        thread.start();
        try {
            System.out.println(futureTask.get());//会阻塞主线程使得主线程不能立刻输出语句
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("主线程运行!");
    }
}

6、什么是Callable和Future?

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返 回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执 行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到 异步执行任务的返回值。可以认为是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable用于产生结果,Future 用于获取结果

Futuretask类通过传入一个Callable接口创建一个有返回值的线程任务,并且其实现了Future接口,可以通过其get方法拿到这个结果

7、什么是线程的上下文切换?

多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

可能有以下原因:

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并发的概念)

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态

8、Thread类中的start()和run()方法有什么区别?

start方法是线程从就绪变为启动状态的方法,而run方法是线程启动之后需要执行的代码,如果直接调用run方法,相当于使用thread对象调用它的一个普通方法而已,调用者是线程对象,并且是在主线程中执行的。

而start方法可以使得线程启动,之后再调用run方法便是在该线程中执行

9、Java中interrupted和isInterruptedd方法的区别?

一个清除一个不清除中断标记

interrupted() 不仅返回当前Thread的中断状态,而且会清除当前Thread的中断状态**。所以如果当前Thread.interrupted()返回中断true,紧接着再call一次interrupted() 会返回“非中断false”,因为中断状态在第一次call的时候清除了。(源码中进行了操作)静态方法

isInterrupted() 也会返回当前Thread的中断状态,但是不会主动清除当前Thread的中断状态

10、为何stop()和suspend()方法不推荐使用

用Thread.stop()方法来终止线程将会释放该线程对象已经锁定的所有监视器。如果以前受这些监视器保护的任何对象都处于不连贯状态,那么损坏的对象对其他线程可见,这有可能导致不安全的操作。

suspend()方法 该方法已经遭到反对,因为它具有固有的死锁倾向。调用suspend()方法的时候,目标线程会停下来,并且不会释放锁资源,在目标线程重新开始以前,其他线程都不能访问该资源。除非被挂起的线程恢复运行。对任何其他线程来说,如果想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。

10、如何停止一个正在运行的线程?(重要)

(1)使用stop()来停止线程:stop()方法让线程立即停止运行, 这种暴力停止可能会破坏线程业务的原子性,不推荐使用

(2)使用interrupt产生打断标志位来停止线程

i:捕捉打断标记并且直接return

由于run方法是一个void方法,可以在线程运行的时候用interrupt方法进行打断,此时产生一个打断标记位,捕捉到该标记位之后便可以优雅地结束该线程(可以直接return,也可以进行一些操作后return;)

static class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 500000; i++) {
            if (this.isInterrupted()) {
                System.out.println("线程终止, 停止for循环.");
                return;
            }
            System.out.println("i=" + (i + 1));
        }
    }
}
​
public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start();
​
    try {
        Thread.sleep(200);
        thread.interrupt();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
​

ii:捕捉打断标记,并且抛出异常终止程序

捕捉到标记位之后,扔出异常来停止该线程

static class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("线程终止, 停止for循环.");
                    throw new InterruptedException();
                }
                System.out.println("i=" + (i + 1));
            }
        } catch (InterruptedException e) {
            System.out.println("MyThread抛出InterruptedException.");
            e.printStackTrace();
        }
    }
}
​
public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start();
​
    try {
        Thread.sleep(200);
        thread.interrupt();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
​

需要使用throw new Exception来打断

iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记

当线程处于正常状态的时候,打断会产生打断的标记位,但是在线程处于sleep,join,wait,park等状态时,被打断将不会产生标记位,我们可以使用trycatch块来处理该情况,当程序被打断时,在程序catch并处理打断异常时候可以自己添加打断标记,从而设置打断标记。(两阶段终止模式)

@Slf4j
public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        TwoParseTermination twoParseTermination = new TwoParseTermination();
        twoParseTermination.start();
        Thread.sleep(3000);  // 让监控线程执行一会儿
        twoParseTermination.stop(); // 停止监控线程
    }
}
​
​
@Slf4j
class TwoParseTermination{
    Thread thread ;
    public void start(){
        thread = new Thread(()->{
            while(true){
                if (Thread.currentThread().isInterrupted()){
                    log.debug("线程结束。。正在料理后事中");
                    break;
                }
                try {
                    Thread.sleep(500);
                    log.debug("正在执行监控的功能");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
    public void stop(){
        thread.interrupt();
    }
}

注:若程序是while循环,那么在捕捉到打断标记时,也可以用break结束循环从而结束线程

11、sleep和yield的区别?

状态的区别:

调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

调度的区别:

调用sleep之后,该线程将进入阻塞状态,分不到CPU的时间片

调用yield之后,该线程会让出CPU的使用权,但是任务调度器仍然可能分配给该线程时间片,从宏观上只是该线程被分配CPu的概率变低了

12、sleep,yield为什么是静态方法(重要)

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。其他线程上调用这些方法是没有意义的。也就是说只有本线程才能执行休眠操作,如果sleep是成员方法,其他线程可以获得该线程的实例化对象,从而让此线程强制休眠(释放CPU的资源),这样会带来不可预估的后果。

分析:wait,join为什么是成员方法

join可以在其他线程中调用,因为其本身设计的意义就是其他线程等待该线程完成

wait是本线程获取锁之后,锁对象调用的wait方法,实际上还是在本线程中使用

sleep,yield不可以被其他线程调用!只能被自身线程调用,也是就是必须是自愿发生才可以!

13、有三个线程T1,T2,T3,如何保证顺序执行?

确保一个线程启动之后等待他执行完再进行下一个

  1. 1.          t1.start();
    2. •        t1.join();  
    3. •        t2.start();
    4. •        t2.join();
    5. •        t3.start();
    6. •        t3.join();

2、现在可以用wait-notify实现线程间通信而达到顺序执行的目的

14、在 java中守护线程和本地线程区别

java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool);true则把该线程设置为守护线程,默认用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

守护线程的特点是,如果一个进程中的其他用户线程全部运行完毕,那么这时守护线程也会自动结束,比如垃圾回收线程

比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了

15、sleep和wait的区别?

sleep是Thread类的静态方法,在线程使用sleep方法之后会让出CPU的资源,但是不会释放锁资源

wait方法是Object的方法,只能在同步代码块中被调用,某个线程使用锁对象的wait方法,会释放掉该线程的锁资源(同时还有CPU使用权),让其他线程去竞争

16、线程创建到结束的几种状态?

img

1、线程刚被创建的时候是初始化状态New,这时候没有被分配CPU资源

2、采用start方法之后,线程运行状态即RUNNABLE状态,这时可以被分配时间片资源进入RUNNING(RUNNING状态是包含在RUNABLE中的),也可以因为上下文切换暂时分配不到时间片资源

3、当线程处于RUNABLE状态时,通过调用wait,join,park等方法会进入到WAITING状态,并且通过对应的唤醒操作,notify和unpark等操作(还得竞争锁成功)可以让线程从WAITING回到RUNABLE状态,join可以通过线程执行完,主线程便会变为RUNABLE

join的底层原理是把thread对象看为一个对象锁,所以是主线程会进行wait,因此主线程会释放锁(thread锁,比较特殊)

是主线程waiting了,而且锁是thread

4、当线程处于RUNABLE状态时,通过调用wait(n),join(n),park(n),sleep(n)等方法会进入到TIMED_WAITING状态,可以通过等待时间结束(sleep),notify等操作回到原状态(竞争锁成功才能回到原状态)

5、当某个线程与其他线程竞争同一把锁失败会进入BLOCKED状态,处于WAITING的线程被唤醒竞争锁失败也会进入BLOCKED状态,竞争锁成功可以回到RUNNABLE状态

6、线程执行完毕会进入TERMINATED状态

17、对线程优先级的理解?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OSdependent)。可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级

18、什么是后台线程?

就是守护线程,也可以叫做精灵线程

19、sleep,yiled,wait,join 对比

关于join的原理和这几个方法的对比:看这里

补充:

  1. sleep,join,yield,interrupted是Thread类中的方法

  2. wait/notify是object中的方法

sleep 不释放锁、释放cpu join 释放锁(主线程)、抢占cpu(被调用的线程) yiled 不释放锁、释放cpu wait 释放锁、释放cpu

sleep和yield都不会释放锁,但是会释放该线程占用的CPU资源

对于main{

thread.join();

}

会使得主线程释放锁(相当于把thread作为锁对象进行wait),thread线程会占用CPUz资源

21、Thread.sleep(0)有什么作用?

触发操作系统立刻重新进行一次CPU竞争,竞争的结果可能是当前线程仍然获得CPU控制权,也可能是别的线程获得CPU控制权。

二、锁知识

20、什么是线程安全?

线程安全概念:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。

21、什么是竞态条件?

计算的正确性取决于多个线程的交替执行顺序时,就会发生竞态条件。

多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

竞态条件不是某种条件,而是一种问题结果

比如对全局变量的读写操作,A线程读取变量还未执行操作时候发生了上下文切换,另一个线程读取变量(由于A并未对变量操作,所以读取的还是原来的变量)并进行了操作,之后切换到A进行了操作,这时相当于只做了A的操作,B的操作被覆盖了

22、什么是临界区?

一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区

22、什么是不可变对象,它对写并发应用有什么帮助?(重要)

答: 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(MutableObjects)。不可变对象的类即为不可变类(Immutable Class)。

Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。

不可变对象永远是线程安全的。

只有满足如下状态,一个对象才是不可变的;它的状态不能在创建后再被修改;所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的逸出)。 ———————————————— 版权声明:本文为CSDN博主「Java小叮当」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:【2021最新版】Java多线程&并发面试题总结(108道题含答案解析)_程序媛小琬的博客-CSDN博客_java多线程面试题2021

23、synchronized关键字最主要的三种使用方式

1、修饰实例方法

 class Test{
        public synchronized void test() {
​
        }
    }
    //等价于
    class Test{
        public void test() {
            synchronized(this) {
​
            }
        }
    }

使用方法:

Test test = new Test();
test.test();

synchronized加在实例方法上,需要创建该实例方法所属类的对象,某该线程使用对象引用该方法时候,就会给该线程加上此对象锁。

  • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

2、修饰静态方法

class Test{
        public synchronized static void test() {
        }
    }
   // 等价于
    class Test{
        public static void test() {
            synchronized(Test.class) {
​
            }
        }
    }

使用方法:

Test.test()

修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

package TestSynchorized;
​
public class Lock {
    public  synchronized static  void test1(){
        while (true){
            System.out.println("静态方法锁");
        }
    }
    public synchronized void test2() {
            while (true){
                System.out.println("-------------------------------------实例方法锁2");
            }
    }
        public synchronized void test3(){
            for (int i = 0; i < 10; i++) {
                System.out.println("实例方法锁3");
            }
    }
​
​
    public static void main(String[] args) {
        Lock lock = new Lock();
        new Thread(()->{
            Lock.test1();
        }).start();
        new Thread(()->{
            lock.test2();
        }).start();
        new Thread(()->{
            lock.test3();
        }).start();
    }
}
​

访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。两个线程占用的所对象不同,当然不会发生互斥。

但是test2和test3的同步代码块由于使用的是一把锁,所以这两个线程会发生互斥,不解决互斥的方法是,创建两个对象,分别用这两个对象调用test2和test3这样就不会使用同一把锁了

3、修饰代码块

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

Thread t1 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
             synchronized (room) {
             sout("锁住代码块")
            }
        }
    }, "t1");

synchronized 关键字底层原理属于 JVM 层面。

总结:synchronized锁住的同步代码块在执行之前需要获取对应的对象锁

24、讲讲你对synchronized的认识?

java中的对象是由对象头和实例数据组成的,对象头如下:

img

1、无锁状态

当一个对象没有被加锁的时候,是无锁状态,对象头中记录了其hashcode以及无锁标志位001

2、偏向锁状态

0是可偏向状态,1是已偏向状态

当一个对象被某个线程加锁,当进入临界区执行代码的时候,该对象从无锁状态变为偏向锁状态,该线程使用CAS操作将线程ID写入该对象的MarkWord,该对象对象头中记录了该线程的ID以自身的偏向锁标志101

偏向锁锁撤销:

如果有另外一个线程A也对该对象加锁,那么会引起锁撤销流程:

该线程检查该对象的MakWork,如果检查到线程ID不是自己A,也就是偏向别的线程B,就发生了竞争现象

就会执行偏向锁的撤销:

过程:

(1)偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的,stop-the-world),到达全局安全点后,持有偏向锁的线程B也被暂停了。 (2)检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着): (3) 如果线程还存活,则检查线程是否还在执行同步代码块中的代码: (4) 如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。 (5)如果线程未存活,或线程未在执行同步代码块中的代码,将该对象设置为无锁状态,A线程再使用CAS操作使得该对象重新偏向自己

总之:有线程竞争时,判断是不是偏向自己,不是,看看原线程是不是再执行临界区代码,不执行重偏向自己,执行,升级到轻量级锁(重偏向或者升级到重量级锁)

偏向锁存在的意义:

1、由于很多方法的临界区代码都只被一个线程所执行,使用偏向锁可以降低系统的开销(使用轻量级锁和重量级锁开销较大)

2、少了轻量级锁可重入的开销(会检查线程ID是否为自己,如果是的话就不需要重新置换MArkWord了)

偏向锁撤销的情况

锁对象调用hashcode()方法会使得其进入无锁状态

其他线程竞争。。。

批量重偏向:

批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。 批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

ListA listA = new ArrayList();
    Thread t1 = new Thread(() - {
        for (int i = 0; i  50; i++) {
            A a = new A();
            synchronized (a) {
                listA.add(a);
            }
        }

当撤销的对象个数达到二十个,JVM把其余其他对象都偏向给另一个线程

批量撤销偏向锁

  • 当 撤销偏向锁的阈值超过39以后 ,就会将整个类的对象都改为不可偏向

    因为偏向锁的作用是为了偏向某个线程,然而过多的撤销会让JVM觉得这个类的对象锁不可以再偏向了,所以再new 该对象会将这个对象锁置为不可偏向的

3、轻量级锁

当锁升级到轻量级锁的时候,线程会在栈内存中创建一个锁记录对象,锁记录对象包括线程地址+轻量级锁标记00,以及对象指针

锁对象此时是无锁状态,hashcode+01

1583755737580

经过CAS操作之后锁记录对象中的线程地址与锁对象的对象头互换,并且锁记录对象指向该锁对象

此时锁对象中有线程的地址+00标记位(轻量级锁)

1583755964276

线程与锁对象分别记录对方的信息

锁重入:

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。

比如轻量级锁,在执行某个临界区之前已经加了锁,之后在这段代码中继续加锁,就会发生锁重入

1583756190177

锁重入,线程栈内存会增加一条记录指向锁对象,会把锁记录地址置为null; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)

轻量级锁CAS

轻量级锁CAS是指线程栈内存的锁记录对象地址和锁对象MArkWord互换的过程,这个过程互换成功,说明该线程竞争到了轻量级锁,互换失败的原因有两个:

1、锁膨胀,该线程没有竞争到轻量级锁,进入锁膨胀过程,申请Monitor,并且进入EntryList等待

1583757586447

Monitor的Oewner指向竞争成功的线程。

2、锁重入机制

因此轻量级锁有其他线程竞争时就会进入锁膨胀,竞争成功的线程成为重量级锁的OWner,失败的线程进入EntryList进行等待(BLOCKED状态)

4、自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

主要是在重量级锁的竞争过程

也就是说在线程0获得重量级锁执行同步代码块的时候,线程一不断地用CAS操作去攻击对象锁的MarkWord看其是不是能交换成功,如果在这个不断攻击的过程中线程0执行完了同步代码块,这时候线程1就可以获得重量级锁,就不用进入EntryList等待了,而如果攻击了多次没有效果那么就会自旋失败

特点没有竞争成功,可以不立马进入休眠状态,而是不断地使用CAS操作与对象锁进行MarkWord进行交换,交换成功则自旋成功,自旋一定次数之后就会失败,进入EntryList休眠

1583758113724

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,*多核 CPU 自旋才能发挥优势*

25、什么是重量级锁?为什么消耗很大?

Monitor:

每个对象都有一个监视锁,或者叫管 程,他是为了该对象成为重量级锁对象准备的,此时锁对象的MarkWord指向该Monitor地址。

重量级锁是轻量级锁在出现多线程竞争时膨胀得到的一种锁,其依靠锁对象的Monitor锁实现的,对于竞争到锁的线程,Monitor的Owner便是该线程,对于竞争失败的线程,经历了一定次数的自旋之后便会进入EntryList进行等待,锁对象的MarkWork地址是Monitor的地址。

为什么重量级线程开销很大的?

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

也被成为互斥锁,同步锁,悲观锁

26、自旋锁的优缺点

优点:在线程竞争不激烈或者临界区代码执行耗时不长的时候,自旋可以减少线程阻塞进入等待队列的操作,从而减少了操作系统挂起,唤醒线程的操作,降低系统消耗

缺点:在线程竞争激烈或者临界区代码执行耗时长的时候,会出现自旋失败的情况,空耗CPU的资源

27、线程同步和互斥有几种实现方法,都是什么?(重要)

28、wait的基本使用方法

thread1 --->
synchronized(obj){
   while(condition is not satified){
   obj.wait;
   }
}
thread2--->
sychronized(obj){
while (condition is satified){
obj.notify/notifyAll
}
}

特点:

当线程0获得到了锁, 成为Monitor的Owner, 但是此时它发现自己想要执行synchroized代码块的条件不满足; 此时它就调用obj.wait方法, 进入到Monitor中的WaitSet集合, 此时线程0的状态就变为WAITING。 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别: BLOCKED状态的线程是在竞争锁对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态 WAITING状态的线程是获得了对象的锁,但是自身的原因无法执行synchroized的临界区资源需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态 处于BLOCKED状态的线程会在锁被释放的时候被唤醒(包括Owner线程执行了wait,以及owner执行完临界区代码释放锁) 处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。然后它会进入到EntryList, 重新竞争锁 (此时就将锁升级为重量级锁)

注意:**obj.wait和obj.notify方法必须是拿到该锁的线程执行才可以

总之:BLOCKED的线程是自身竞争不到锁,进入ENtryList等待OWner释放锁并竞争

WAITING的线程是由于自身某些执行的条件不满足,自己进行wait,等待条件满足时,会被其他线程唤醒,进入到ENTryList进行竞争锁

29、wait的相关问题

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

notify和notifyAll都可以同一把锁唤醒处于WAITING状态的线程,并且让它们进入EntryList去竞争锁,但是notify只能随即唤醒一个线程,而notifyALl会唤醒所有的线程

(2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)

Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait,notify,notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象

线程为了进入临界区(也就是同步块内),需要获得锁并等待锁可用,它们并不知道也不需要知道哪些线程持有锁,它们只需要知道当前资源是否被占用,是否可以获得锁,所以锁的持有状态应该由同步监视器来获取,而不是线程本身

如果wait()方法定义在Thread类中,线程正在等待哪个锁就不明显了

(3)为什么wait和notify方法要在同步块中调用?

在同步块中调用的意义是首先获得某个对象锁,如果不在同步块中调用,notify将无法获知唤醒的是哪个锁的等待线程,wait也无法获知等待那个锁

notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)

(4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列(BLOCKINGQUEUE)是一个在队列基础上又支持了两个附加操作的队列。

2个附加操作:

支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程(生产者线程),直到队列不满。

支持阻塞的移除方法:队列空时,队列会阻塞获取元素的线程(消费者线程),直到队列变为非空。

jdk1.5之前使用简单的wait和notify实现生产者消费者模式,之后使用rentreelock的await/singal实现阻塞队列并实现生产者消费者模式

通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。当消费者向空队列中取元素的时候会被阻塞,直到生产者生产了一个元素之后,会通知消费者当前队列可用

(5) join方法实现原理

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
​
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
​
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

底层是通过wait实现的,主线程将thread作为锁对象,并调用wait方法实现阻塞,当阻塞超时,或者线程执行完毕(!isAlive)死亡,主线程便会唤醒,因此这是主线程的阻塞,等待线程执行完毕,释放锁,停止阻塞

(6)如何实现线程间通信

wait/notify机制

对于A线程需要等待某个条件成立在执行,对于B线程可以生产该条件

A可以wait,b可以生产该条件之后,notify A

31、park和unpark

(1)基本使用

thread1----->
{
LockSupport.park();
}

park是LockSupport的一个静态方法,它在某个线程中被调用时,会暂停该线程的执行,并且该线程会进入WAITING状态

LockSupport.unpark(thread1);

unpark也是LockSupport的一个静态方法

会把被暂停的线程重新唤醒

(2)先调用park再调用unpark

每个线程都有自己的一个 Parker 对象(底层,由c代码实现),由三部分组成 _counter, _cond和 _mutex

先调用park,检查cond变量如果是0,则线程进入Parker对象锁的Waiting队列(这也解释了为什么park之后是WAITING对象)

再调用unpark对象,cond变为1,线程获得锁对象,正常运行,cond再变回0

(3)先调用unpark再调用park

先调用unpark,检查cond是0,设置cond为1,再调用park发现cond是1,无需打断线程执行,把cond置为0

unpark调用时会把cond变为1,park调用时会检查cond,为0才打断运行,为1则不打断并且重新置为0

32、park和wait的区别

park是静态方法属于LockSupport,执行层面上是属于线程的,wait是Object的一个方法,执行层面是属于锁对象的

park可以先调用park再调用unpark,但是wait不能先调用notify

park唤醒的线程比较精确,而notify不精确

33、park,wait,sleep,yield,join方法的区别

wait和join底层都是wait,WAITING

sleep是属于Thread的静态方法,TIMED_WAITING

yield属于thread对象,RUNABLE

park,WAITING

34、什么是死锁,死锁发生的条件(重要)

public static void main(String[] args) {
    final Object A = new Object();
    final Object B = new Object();
    
    new Thread(()->{
        synchronized (A) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (B) {
​
            }
        }
    }).start();
​
    new Thread(()->{
        synchronized (B) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (A) {
​
            }
        }
    }).start();
}
​

死锁的定义:

所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

比如上述代码,线程1需要获得线程2所持有的锁B才能释放自己所持有的锁A,线程2需要获得线程1所持有的锁A才能释放自身持有的锁B,这样线程12均会一直处于等待状态无法推进,就发生了死锁

死锁的四个条件:(重要,记忆)

互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。

请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。

非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

循环等待条件(Circular wait):系统中若干进程组成环路,改环路中每个进程都在等待相邻进程正占用的资源。

处理死锁问题,可以从预防,避免,检测与恢复三个方面来进行

怎么预防死锁问题?

1〉破坏互斥条件。*即允许进程同时访问某些资源*。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

〈2〉破坏不可剥夺条件。*即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请*。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

比如使用rentreelock的 tryLock方法,当一个线程尝试获得某个锁资源一段时间后,就会放弃对该资源的请求,并且主动释放之前获得的锁,通过这种方式,可以避免死锁

〈3〉破坏请求与保持条件。*可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行*。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:

(1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;

(2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;

(3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

< 4 >破坏循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:

(1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;

(2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

怎么避免死锁问题

该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的。

对于线程申请某个资源,如果同意该请求,会不会导致死锁的发生,如果会发生死锁,则阻塞该线程,直到直到同意该请求后系统状态仍然是安全的,否则,分配给该线程资源

怎么检测和解除死锁

这种方式不需要对线程的资源做任何限定,只需要要求系统发生死锁的时候能够快速的检测并解决即可

检测死锁 首先为每个进程和每个资源指定一个唯一的号码; 然后建立资源分配表和进程等待表。 解除死锁: 当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态; 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

35、什么是活锁

任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。 处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

活锁出现在两个线程 互相改变对方的结束条件,谁也无法结束。

避免活锁的方法

  • 在线程执行时,中途给予 不同的间隔时间, 让某个线程先结束即可。

活锁与死锁的区别?

处于活锁的线程并没有阻塞,状态·也在不停的改变,就是因为其他线程改变其终止条件而无法终止

处于死锁的线程互相锁住了对象所需要的资源,从而导致了死锁线程的阻塞

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

36、什么是饥饿?

饥饿:如果一个线程因为 CPU 时间全部被其他线程抢走而得不到 CPU 运行时间,这种状态被称之为“饥饿”;

二、饥饿原因

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。(比如使用synchronized的时候,一直有大量的线程去竞争同一个锁)

  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。(比如某个线程设置了永远无法完成的条件进入wait状态,那么它就永远不会被唤醒)

  3. 线程在等待一个本身(在其上调用 wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

37、什么是可重入锁?

广义上的可重入锁指的是可重复可递归调用的锁同一线程在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

也就是说自身线程可以重复对一个对象上锁,并且不会出现异常现象

synchronized:

thread---->{
synchronized(obj){
//外部代码块
synchronized(obj){
//内部代码块
}
}
}

Rentreelock

thread--->{
   lock.lock();
   try{
   //代码
      lock.lock();
   }
}

38、Reentrantlock

(1)基本使用方法

Reentrantlock lock = new Reentrantlock();
thread--->{
    lock.lock();
       try{
       //代码
       }finally{
         lock.unlock();
       }
}

创建锁对象,使用lock获取锁,执行完代码之后释放锁

(2)可打断性

lockinterruptly

Reentrantlock lock = new Reentrantlock();
thread--->{
       try{
       lock.lockinterruptly();
       //代码
       }catch(Exception e){
       
       }finally{
         lock.unlock();
       }
}

其获得锁的过程是可以被打断的

main{
   thread.interrupt();
}
  • 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败

  • 处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行

  • 可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法;

可打断的设计目的是为了放置某个线程阻塞等待获取某个锁而导致的死锁现象,当该线程处于阻塞状态等待获得锁的时候,可以用其他线程打断它,放置死锁

(3)锁超时(获取不到锁不会停止运行)

lock.trylock(time)

如果在一定时间内没有获得到锁,那么就放弃该锁,以及已经有的资源,可以用在死锁的预防里

放弃该锁并不是说以后就不竞争锁了,只是当前放弃

注意这里会放弃已有的资源

(4)公平锁

synchronized锁中,在entrylist等待的锁在竞争时不是按照先到先得来获取锁的,所以说synchronized锁时不公平的;ReentranLock锁默认是不公平的,但是可以通过设置实现公平锁。本意是为了解决之前提到的饥饿问题,但是公平锁一般没有必要,会降低并发度,使用trylock也可以实现。

实现方式是在创建reentrantlock的时候参数设置为(true)

(5)条件变量

package ReentrantLock;
​
import lombok.extern.slf4j.Slf4j;
​
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "Await")
public class Await {
    //定义一个非公平Lock
    public static final ReentrantLock lock = new ReentrantLock();
    //定义两个等待变量
    private static Condition waityanRoom = lock.newCondition();
​
    private static Condition waitwaimaiRoom = lock.newCondition();
​
    private static Boolean hasyan = false;
​
    private static Boolean haswaimai = false;
​
    //定义一个执行干活的方法
    public void Dojob() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            //由于不会出现死锁等问题,所以用正常的lock即可
            lock.lock();
            try{
                log.info("烟送来了吗:{}",hasyan);
                while (!hasyan){
                    try {
                        waityanRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //有烟的时候就执行干活
                    log.info("烟来了吗{},t1开始干活吧",hasyan);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            //由于不会出现死锁等问题,所以用正常的lock即可
            lock.lock();
            try{
                log.info("外卖送来了吗:{}",haswaimai);
                while (!haswaimai){
                    try {
                        waitwaimaiRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //有外卖的时候就执行干活
                    log.info("外卖来了吗{},t2开始干活吧",haswaimai);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "t2");
        //剩下的是不需要等待烟或者外卖的线程
        Thread t3 = new Thread(() -> {
            //由于不会出现死锁等问题,所以用正常的lock即可
            lock.lock();
            try{
                log.info("正常员工开始干活了......");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "t3");
​
        Thread t4 = new Thread(() -> {
            lock.lock();
            try {
                //先唤醒再送烟
                waityanRoom.signal();
                hasyan = true;
            } finally {
                lock.unlock();
            }
        }, "t4");
​
        Thread t5 = new Thread(() -> {
            lock.lock();
            try {
                //先唤醒再送烟
                waitwaimaiRoom.signal();
                haswaimai = true;
            } finally {
                lock.unlock();
            }
        }, "t5");
        t3.start();
        t2.start();
        t1.start();
        Thread.sleep(1000);
        t4.start();
        Thread.sleep(1000);
        t5.start();
    }
}
​

使用条件变量可以唤醒指定的线程,而不是像notify/notifyAll随即唤醒或者唤醒所有的线程

Reentrantlock lock;
Condition condition1=lock.newCondition;
Condition condition2=lock.newCondition;
thread1--->{
   lock.lock();
   condition1.await();
}
thread2--->{
   lock.lock();
   condition2.await();
}
main--->{
   lock.lock();
   condition1.signal();
}

39、ReentrantLock和Synchronized的相同点和区别(重要)

(1)相同点:

都是同步锁,互斥锁,都是可重入锁

两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

(2)不同点:

1、synchronized是关键字,是原生语言层面的互斥,需要JVM实现,Reentrantlock是API层面的互斥

2、syn通过JVM加锁和解锁,Reen通过获得对象,并使用lock和unlock方法加锁解锁

3、syn是JVM自动解锁,Reen是必须手动解锁,否则可能出现死锁现象,需要联合try/finally实现

4、Reen在功能上比较丰富,可以进行获取锁中断,获取锁超时,以及设置公平锁等

5、都可以设置条件从而让线程进入等待状态,不过REEn通过设置不同地Condition实现,可以唤醒具体的线程,而synchronized只能通过notify/notifyAll唤醒非具体特定的线程

6、在线程竞争激烈的时候Reen的性能要比sychronized好一些,在基本没有线程竞争锁的时候,syn的性能比较好

reentrantLock基于 AQS 实现。AQS 内部通过对 volatile 的 state 读写以及cas 操作 和在某些条件下让线程进入阻塞状态实现。

性能:偏向锁 > 轻量级锁 > reentrantLock > 重量级锁

为什么高并发时Reen的性能要好些

因为在reen默认是非公平锁,在进行锁的竞争时使用的是队列首位线程CAS的操作去竞争锁,而syn

40、ReentrantLock的实现原理

Reen是通过AQS队列同步器实现的

底层主要由三个组件构成:

由votile修饰的变量state,当前获得锁的线程,以及阻塞排队队列

img

当某一个线程使用lock方法时,通过CAS操作查询变量state是否为0,为0代表当前锁没有被线程占有,因此可以通过CAS操作获取当前锁,否则,进入阻塞队列等待锁的释放,当持有锁的线程执行完时,通过unlock方法修改state,唤醒排队队列的第一个线程进行CAS操作,获取锁

CAS操作:修改state=1,并把当前线程置为自身

Reen默认是非公平锁,比如线程1执行完毕之后,本该到队列中的线程获取锁,这是如果竞争比较激烈,在队列之外出现别的线程进行CAS操作并成功,这时就是不公平的

开启公平锁之后,队列之外的线程想要竞争该锁,首先要判断队列中是否有等待线程,如果有的话,那么该线程需要插入队列并等待队列唤醒

也就是说公平的意思是不允许插队

41、lock、tryLock和lockInterruptibly的差別

lock():若lock被thread A取得,thread B会进入block状态,直到取得lock; tryLock():若当下不能取得lock,thread就会放弃,可以设置一个超时时间参数,等待多久获取不到锁就放弃; lockInterruptibly():跟lock()情況一下,但是thread B可以通过interrupt中断,放弃继续等待锁

42Condition和Object类锁方法区别

  Condition是lock锁里面的类。

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效

  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效

  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

43、公平锁与非公平锁

  公平锁指的是锁的分配机制是公平的,通常是先到先得,RenntrantLock可以在构造函数中定义公平和非公平

  非公平锁,随机、就近原则分配锁的机制,线程过来后会先自旋,尝试直接获取到锁,获取不到再去排队。非公平锁的效率要更高。

三、无锁机制(CAS,原子类)

44、什么是java内存模型?

JMM 即 Java Memory Model,它从java层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在以下几个方面

  1. 原子性 - 保证指令不会受到线程上下文切换的影响

  2. 可见性 - 保证指令不会受 cpu 缓存的影响

  3. 有序性 - 保证指令不会受 cpu 指令并行优化的影响

45、什么是volitile?作用是什么

对于存在于主线程中的变量,线程会将其存储到自己工作内存的高速缓存中,这样线程读到的值可能不是该变量最新的值,volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值线程操作 volatile 变量都是直接操作主存

从而保证了线程对变量的修改都是对其他线程可见的

使用synchronized关键字也有相同的效果!在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁

因此临界区使用的变量也是最新的

不能保证原子性!可以保证有序性

46、什么是原子性?

保证指令不会受到线程上下文切换的影响,使用synchronized可以保证代码的原子性

因为上下文切换的时候,由于互斥锁的原因,其他线程无法执行该临界区代码,所以不会发生错误

Reentrantlock和actomic下的包都可以保证原子性

47、有序性

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会对一些指令的顺序进行重新排序

重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了

int num = 0;
​
// volatile 修饰的变量,可以禁用指令重排 volatile boolean ready = false; 可以防止变量之前的代码被重排序
boolean ready = false; 
// 线程1 执行此方法
public void actor1(I_Result r) {
 if(ready) {
    r.r1 = num + num;
 } 
 else {
    r.r1 = 1;
 }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
 num = 2;
 ready = true;
}
​

48、volatile怎么保证可见性和有序性的?

可见性

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

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

lock前缀指令 + MESI缓存一致性协议

由于只有volatile修饰的变量写的时候才会发送lock指令,所以没有写指令的时候,本地缓存的数据是有效的。

在写的时候把其他线程本地缓存的数据置为无效数据,但是其他线程此时不更改,直到其他线程要读的时候才从主存中读取最新值

有序性

(2)内存屏障:禁止重排序

底层就是插入了XX内存屏障,XX内存屏障,就可以保证指令不会重排

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

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

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

在这里插入图片描述

img

50、单例模式的双检锁是什么?

51、synchronized 和 volatile 的区别是什么?

  • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

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

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

为啥synchronized无法禁止指令重排,但可以保证有序性?

由于syn是互斥锁,加了锁之后,同一时间只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的。

30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

2、java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

CAS缺点:

  1. ABA问题:

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

2、循环时间长开销大:

对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

3、只能保证一个共享变量的原子操作:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

52、CAS

CAS操作的流程:

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

底层通过Unsafe类提供的操作系统原生方法来实现

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

假设有变量i目前的值是10,线程1想要做自增操作

对于线程一来讲,变量的预期值是10,但是如果有其他线程2修改了内存(主存)中i的值为11,这时对于线程1来讲i就是被修改了(预期值与内存值不同),因此不能自增,需要把自身的预期值改为11,再与主存值比较是否相同(也就是判定该值是否被其他线程修改),这个过程称为自旋,直到比较成功,在进行更新(自增)

CAS的自旋:当线程想要对某个变量进行更新操作时,需要先把自己缓存中的期望值与主存中的值进行比较(如果相同,说明没有其他线程对此变量进行修改),如果不相同,把自身缓存中的期望值更改为主存中的值,再次进行比较,这种过程叫做自旋,比较结果相同时候,自旋成功,该线程可以进行更新的操作。

CAS:比较并更新,比较的过程就是自旋的过程,自旋成功才能够进行数值的更新

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:

  1. 使用原子的方式更新基本类型

    • AtomicInteger:整型原子类

    • AtomicLong:长整型原子类

    • AtomicBoolean :布尔型原子类

原子类的包都使用了volatile来修饰变量,并且使用操作系统底层的CAS操作来进行无锁操作,保证方法的原子性

53、synchronized与CAS的区别(重要)

  • 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

  • 对于资源竞争严重的情况,CAS自旋的概率会比较大(比如getAndAddInt方法中的do-while循环),从而浪费更多的CPU资源,效率低于synchronized。

54、CAS的缺点(重要)

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

当线程比较多的时候,对于某个资源竞争激烈,可能会导致一些线程的CAS一直处于自旋状态,从而白白浪费CPU资源

2) 不能保证多个变量的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

如何解决CAS只能保证一个变量的原子性操作问题?

使用引用类型包装需要保证原子性的变量,之后再用AtomicRefernce对该引用类型进行包装

BankCard {
​
    private final String accountName;
    private final int money;
    }
private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

通过AtomicRefernce类中的CAS方法对该引用类型的包装属性进行原子性操作

3)ABA问题

什么是ABA问题?

在线程进行自旋的时候,假设线程1的期望值是A,而此时主存存储的对象也是A,但是此时其他线程通过CAS操作将主存中的数值改成了B,再次又改回了A,这时线程1进行CAS操作,由于主存和线程1的期望值是相同的,这时线程1便认为该值没有被修改,但是实际上该值是经历了其他线程的两次修改的之后的值。

这个过程看起来没有问题,结合实际有问题,比如剩余100有两个线程需要提取100,但是只能一个,假设线程1使用CAS提取了100,剩余0,本来线程2不应该再提取了,但是这时,线程3(老板)给打了100元过来,这时线程2便可以在提取100元,而误认为线程1没有提取到,是自己竞争到了,这与实际情况相悖,也就是说线程2被骗了!

解决方法:使用AtomicStampedReference类,对于每个对象都采用版本号机制,线程对对象进行更新时,也会对其版本号进行更新,而CAS比较时需要比较版本号是否相同。

或者使用锁(syn Reen)

49、volatile 变量和 atomic 变量有什么不同?

volatile修饰的变量可以保证其可见性和有序性,但是不能保证原子性

atomic包下的类比如AtomicInteger等通过定义volatile的变量,并通过CAS操作实现变量读写的原子性

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

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

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

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

Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS

的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境

下避免数据不一致必须的手段。

int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程

有可能会读到之前的值,这就会引发错误。

为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同

步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和

long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需

要使用同步。

java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程

环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当

某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像

自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个

线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,

AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean

来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累

加来反映中间有没有变过)

版权声明:程序员胖胖胖虎阿 发表于 2022年9月4日 上午6:40。
转载请注明:java 多线程 面试题整理(更新......) | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...