《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)

发布时间:2024年01月24日


该篇博客接着《面试专题-----经典高频面试题收集三》,如需了解之前的blog可前往主页面试专栏查阅

第六章(并发编程进阶)

1.并发编程的三要素分别解释一下,举个简单的例子

原?性:

?个不可再被分割的颗粒,原?性指的是?个或多个操作要么全部执?成功要么全部执?失败,期间不能被中断,也不存在上下?切换,线程切换会带来原?性的问题

int num = 1; // 原?操作

num++; // ?原?操作,从主内存读取num到线程?作内存,进? +1,再把num写到主内存, 除??原?类,即java.util.concurrent.atomic?的原?变量类

解决办法:可以?synchronized 或 Lock(?如ReentrantLock) 来把这个多步操作“变成”原?操作,但是volatile,前?有说到不能修饰有依赖值的情况

有序性:

程序执?的顺序按照代码的先后顺序执?,因为处理器可能会对指令进?重排序 JVM在编译java代码或者CPU执?JVM字节码时,对现有的指令进?重新排序,主要?的是优化运?效率(不改变程序结果的前提)

可?性:

?个线程A对共享变量的修改,另?个线程B能够?刻看到

// 线程 A 执?

int num = 0;

// 线程 A 执?

num++;

// 线程 B 执?

System.out.print(“num的值:” + num);

线程A执? i++ 后再执?线程 B,线程 B可能有2个结果,可能是0和1。 因为 i++ 在线程A中执?运算,并没有?刻更新到主内存当中,?线程B就去主内存当中读取并打 印,此时打印的就是0;也可能线程A执?完成更新到主内存了,线程B的值是1。

所以需要保证线程的可?性 synchronized、lock和volatile能够保证线程可?性

2.说下你知道的调度算法,比如进程间的调度

先来先服务调度算法: 按照作业/进程到达的先后顺序进?调度,即:优先考虑在系统中等待时间最?的作业,排在?进程后的短进程的等待时间?,不利于短作业/进程

短作业优先调度算法: 短进程/作业(要求服务时间最短)在实际情况中占有很??例,为了使得它们优先执?,对?作业不友好

?响应?优先调度算法: 在每次调度时,先计算各个作业的优先权:优先权=响应?=(等待时间+要求服务时间)/ 要求服务时间, 因为等待时间与服务时间之和就是系统对该作业的响应时间,所以优先权=响应?=响应 时间/要求服务时间,选择优先权?的进?服务需要计算优先权信息,增加了系统的开销

时间?轮转调度算法: 轮流的为各个进程服务,让每个进程在?定时间间隔内都可以得到响应 由于?频率的进程切换,会增加了开销,且不区分任务的紧急程度

优先级调度算法: 根据任务的紧急程度进?调度,?优先级的先处理,低优先级的慢处理 如果?优先级任务很多且持续产?,那低优先级的就可能很慢才被处理

3.常见的线程间的调度算法是怎么样的,java是哪种

线程调度是指系统为线程分配CPU使?权的过程,主要分两种:

协同式线程调度(分时调度模式):线程执?时间由线程本身来控制,线程把??的?作执?完之后,要主动通知系统切换到另外?个线程上。最?好处是实现简单,且切换操作对线程??是可知的,没啥线程同步问题。坏处是线程执?时间不可控制,如果?个线程有问题,可能?直阻塞在那?

抢占式线程调度:每个线程将由系统来分配执?时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执?时间,但?法获取执?时间)。线程执?时间系统可控,也不会有?个线程导致整个进程阻塞

Java线程调度就是抢占式调度,优先让可运?池中优先级?的线程占?CPU,如果可运?池中的线程优先级相同,那就随机选择?个线程 所以我们如果希望某些线程多分配?些时间,给?些线程少分配?些时间,可以通过设置线程优先级来完成。

JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运?时,VM?般会运?最?优先级的线 程(Thread.MIN_PRIORITY?Thread.MAX_PRIORITY)

在两线程同时处于就绪runnable状态时,优先级越?的线程越容易被系统选择执?。但是优先级并不是100%可以获得,只不过是机会更??已。

有?会说,wait,notify不就是线程本身控制吗? 其实不是,wait是可以让出执?时间,notify后?法获取执?时间,随机等待队列??获取?已

4.日常开发中用过java中的哪些锁?分别解释下

悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,?如synchronized

乐观锁:每次去拿数据的时候都认为别?不会修改,更新的时候会判断是别?是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,?如CAS是乐观锁,但严格来说并不是锁,通过原?性来保证数据的同步,?如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响

?结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会?悲观锁多

公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果?个线程组?,能保证每个线程都能拿到锁 ?如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)

?公平锁:获取锁的?式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,?直拿不到锁,?如synchronized、ReentrantLock

?结:?公平锁性能?于公平锁,更能重复利?CPU的时间

可重?锁:也叫递归锁,在外层使?锁之后,在内层仍然可以使?,并且不发?死锁

不可重?锁:若当前线程执?某个?法已经获取了该锁,那么在?法中尝试再次获取锁时,就会获取不到被阻塞

?结:可重?锁能?定程度的避免死锁 synchronized、ReentrantLock 重?锁

?旋锁:?个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有?个执?单元获得锁.

?结:不会发?线程状态的切换,?直处于?户态,减少了线程上下?切换的消耗,缺点是循环会消耗CPU

常?的?旋锁:TicketLock,CLHLock,MSCLock

共享锁:也叫S锁/读锁,能查看但?法修改和删除的?种数据锁,加锁后其它?户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,?于资源数据共享

互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每?次只能被?个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例?:如果线程A对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据?能修改数据

死锁:两个或两个以上的线程在执?过程中,由于竞争资源或者由于彼此通信?造成的?种阻塞的现象,若?外?作?,它们都将?法让程序进?下去

下?三种是Jvm为了提?锁的获取与释放效率?做的优化 针对Synchronized的锁升级(1.6),锁的状态是通过 对象监视器在对象头中的字段来表明,是不可逆的过程

偏向锁:?段同步代码?直被?个线程所访问,那么该线程会?动获取锁,获取锁的代价更低

轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过?旋的形式尝试获取锁,但不会阻塞,且性能会?点

重量级锁:当锁为轻量级锁的时候,其他线程虽然是?旋,但?旋不会?直循环下去,当?旋?定次数的时 候且还没有获取到锁,就会进?阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进?阻塞,性能也会降低

分段锁、?锁、表锁

5.写一个多线程死锁的例子

线程在获得了锁A并且没有释放的情况下去申请锁B,这时另?个线程已经获得了锁B,在释放锁B之前?要先获得锁A,因此闭环发?,陷?死锁循环

public class DeadLockDemo {  

  private static String locka = "locka";  

  private static String lockb = "lockb";  

  

  public void methodA(){  

?    synchronized (locka){  

?      System.out.println("我是A?法中获得了锁A "+Thread.currentThread().getName() );  

?      //让出CPU执?权,不释放锁  

?      try {  

?        Thread.sleep(2000);  

?      } catch (InterruptedException e) {  

?        e.printStackTrace();  

?      }  

?      synchronized(lockb){  

?        System.out.println("我是A?法中获得了锁B "+Thread.currentThread().getName() );  

?      }  

?    }  

  }  

  

  public void methodB(){  

?    synchronized (lockb){  

?      System.out.println("我是B?法中获得了锁B "+Thread.currentThread().getName()); 

?      //让出CPU执?权,不释放锁  

?      try {  

?        Thread.sleep(2000);  

?      } catch (InterruptedException e) {  

?        e.printStackTrace();  

?      }  

?      synchronized(locka){  

?        System.out.println("我是B?法中获得了锁A "+Thread.currentThread().getName() );  

?      }  

?    }  

  }  

  

  public static void main(String [] args){  

?    System.out.println("主线程运?开始运 ?:"+Thread.currentThread().getName());  

  DeadLockDemo deadLockDemo = new DeadLockDemo();  

  new Thread(()->{  deadLockDemo.methodA();  }).start();  

  new Thread(()->{  deadLockDemo.methodB();  }).start();  

  System.out.println("主线程运?结束:"+Thread.currentThread().getName());  

  } 

}

死锁的4个必要条件

互斥条件:资源不能共享,只能由?个线程使?

请求与保持条件:线程已经获得?些资源,但因请求其他资源发?阻塞,对已经获得的资源保持不释放

不可抢占:有些资源是不可抢占的,当某个线程获得这个资源后,系统不能强?回收,只能由线程使?完??释放

循环等待条件:多个线程形成环形链,每个都占?对?申请的下个资源

只要发?死锁,上?的条件都成?;只要?个不满?,就不会发?死锁

●上面的例子怎么解决死锁,优化下代码

○调整申请锁的范围

○调整申请锁的顺序


public class DeadLockDemo {  

  private static String locka = "locka";  

  private static String lockb = "lockb";  

  

  public void methodA(){  

?    synchronized (locka){  

?      System.out.println("我是A?法中获得了锁A "+Thread.currentThread().getName() );  

?      //让出CPU执?权,不释放锁  

?      try {  

?        Thread.sleep(2000);  

?      } catch (InterruptedException e) {  

?        e.printStackTrace();  

?      }

?    } 

?    synchronized(lockb){  

?      System.out.println("我是A?法中获得了锁B "+Thread.currentThread().getName() );  

?    }  

  }  

  

  public void methodB(){  

?    synchronized (lockb){  

?      System.out.println("我是B?法中获得了锁B "+Thread.currentThread().getName()); 

?      //让出CPU执?权,不释放锁  

?      try {  

?        Thread.sleep(2000);  

?      } catch (InterruptedException e) {  

?        e.printStackTrace();  

?      }  

?    }  

?    synchronized(locka){  

?      System.out.println("我是B?法中获得了锁A "+Thread.currentThread().getName() );  

?    } 

  }  

  

  public static void main(String [] args){  

?    System.out.println("主线程运?开始运 ?:"+Thread.currentThread().getName());  

  DeadLockDemo deadLockDemo = new DeadLockDemo();  

  new Thread(()->{  deadLockDemo.methodA();  }).start();  

  new Thread(()->{  deadLockDemo.methodB();  }).start();  

  System.out.println("主线程运?结束:"+Thread.currentThread().getName());  

  } 

}

6.设计一个简单的不可重入锁

//不可重?锁:若当前线程执?某个?法已经获取了该锁,那么在?法中尝试再次获取锁时,就会获取 不到被阻塞

public class UnreentrantLock {  

  private boolean isLocked = false;  

  

  public synchronized void lock() throws InterruptedException {        

?     System.out.println("进?lock加锁 "+Thread.currentThread().getName());  

?     //判断是否已经被锁,如果被锁则当前请求的线程进?等待  

?     while (isLocked){  

?       System.out.println("进?wait等待 "+Thread.currentThread().getName());  

?       wait();  

?     }  

?     //进?加锁  

?     isLocked = true;  

   }  

   

   public synchronized void unlock(){ 

   System.out.println("进?unlock解锁 "+Thread.currentThread().getName()); 

   isLocked = false;  

   //唤醒对象锁池??的?个线程  

   notify(); 

   } 

} 

 

public class Main {  

  private UnreentrantLock unreentrantLock = new UnreentrantLock();  

  //加锁建议在try??,解锁建议在finally  

  public void methodA(){  

?    try {  

?      unreentrantLock.lock();  

?      System.out.println("methodA?法被调?"); 

?      methodB();  

?    } catch (InterruptedException e){  

?      e.fillInStackTrace();  

?    } finally {  

?      unreentrantLock.unlock();  

?    }  

  } 

  

  public void methodB(){  

?    try {

?      unreentrantLock.lock();  

?      System.out.println("methodB?法被调?");  

?    } catch (InterruptedException e){  

?      e.fillInStackTrace();  

?    } finally {  

?      unreentrantLock.unlock();  

?    }  

  }  

  

  public static void main(String [] args){  

  //演示的是同个线程  

  new Main().methodA();  

  } 

} 

//同?个线程,重复获取锁失败,形成死锁,这个就是不可重?锁

7.设计一个简单的重入锁

//可重入锁:也叫递归锁,在外层使?锁之后,在内层仍然可以使?,并且不发?死锁

public class ReentrantLock {  

  private boolean isLocked = false;  

  //?于记录是不是重?的线程  

  private Thread lockedOwner = null;  

  //累计加锁次数,加锁?次累加1,解锁?次减少1  

  private int lockedCount = 0;  
  public synchronized void lock() throws InterruptedException {

?     System.out.println("进?lock加锁 "+Thread.currentThread().getName());  

?     Thread thread = Thread.currentThread();  

?     //判断是否是同个线程获取锁, 引?地址的?较  

?     while (isLocked && lockedOwner != thread ){ 

?        System.out.println("进?wait等待 "+Thread.currentThread().getName());  

?        System.out.println("当前锁状态 isLocked = "+isLocked);  

?        System.out.println("当前count数量 lockedCount = "+lockedCount);  

?        wait(); 

?      }  

?      //进?加锁  

?      isLocked = true;  

?      lockedOwner = thread;  

?      lockedCount++;  

  } 

  public synchronized void unlock(){  

?    System.out.println("进?unlock解锁 "+Thread.currentThread().getName());  

?    Thread thread = Thread.currentThread();  

?    //线程A加的锁,只能由线程A解锁,其他线程B不能解锁  

?    if(thread == this.lockedOwner){ 

?      lockedCount--;  

?      if(lockedCount == 0){  

?        isLocked = false;  

?        lockedOwner = null;  

?        //唤醒对象锁池??的?个线程  

?        notify();  

?      }  

?    }  

  } 

} 

public class Main {  

 private ReentrantLock reentrantLock = new ReentrantLock();  

 //加锁建议在try??,解锁建议在finally  

 public void methodA(){  

   try {  

?     reentrantLock.lock();  

?     System.out.println("methodA?法被调?");  

?     methodB();  

   } catch (InterruptedException e){  

?     e.fillInStackTrace();  

   } finally { 

?     reentrantLock.unlock();  

   }  

 }  

 

 public void methodB(){  

   try {  

?     reentrantLock.lock();  

?     System.out.println("methodB?法被调?");  

   } catch (InterruptedException e){  

?     e.fillInStackTrace();  

   } finally {  

?     reentrantLock.unlock();  

   }  

 } 

 

 public static void main(String [] args){  

   for(int i=0 ;i<10;i++){  

?     //演示的是同个线程  

?     new Main().methodA();  

   }  

 } 

}

 

 

 

## 8.对sychronized了解吗,介绍下对它的理解

synchronized是解决线程安全的问题,常?在 同步普通?法、静态?法、代码块 中 ?公平、可重? 

每个对象有?个锁和?个等待队列,锁只能被?个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出?个并唤醒,唤醒哪个线程是不确定的,不保证公平性 

 

两种形式: 

1.?法:?成的字节码?件中会多?个 ACC_SYNCHRONIZED 标志位,当?个线程访问?法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执?线程将先获取monitor,获取成功之后才能执??法体,?法执?完后再释放monitor。在?法执?期间,其他任何线程都?法再获得同?个monitor对象,也叫隐式同步 

 

2.代码块:加了 synchronized 关键字的代码段,?成的字节码?件会多出 monitorenter 和 monitorexit 两条指令,每个monitor维护着?个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当?个线程获执?monitorenter后,该计数器?增1;当同?个线程执?monitorexit指令的时候,计数器再?减1。当计数器为0的时候,monitor将被释放.也叫显式同步 

两种本质上没有区别,底层都是通过monitor来实现同步, 只是?法的同步是?种隐式的?式来实现,?需通过字节码来完成

 

有得到锁的资源进?Block状态,涉及到操作系统?户模式和内核模式的切换,代价?较?

jdk1.6进?了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性 能仍然较低

 

 

 

## 9.了解CAS吗,解释一下

全称是Compare And Swap,即?较再交换,是实现并发应?到的?种技术 

底层通过Unsafe类实现原?性操作。

操作包含三个操作数:内存地址(V)、预期原值(A)和新值 (B)。 

如果内存位置的值(V)与预期原值相(A)匹配,那么处理器会?动将该位置值更新为新值 ,若果在第?轮循环中,a线程获取地址??的值(V)被b线程修改了,那么a线程需要?旋,到下次循环才有可能机会执?。

CAS这个是属于乐观锁,性能较悲观锁有很?的提? AtomicXXX 等原?类底层就是CAS实现,?定程度?synchonized好,因为后者是悲观锁

 

 

 

## 10.CAS会存在什么比较严重的问题

1、?旋时间?CPU利?率增加,CAS??是?个循环判断的过程,如果线程?直没有获取到状态,cpu资源会?直被占? 

2、存在ABA问题

 

 

11.什么是ABA问题,如何避免

如果?个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?

其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从?赋值给V 

给变量加?个版本号即可,在?较的时候不仅要?较当前变量的值 还需要?较当前变量的版本号。 在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引?是否等于预期引 ?,其次检查当前标志是否等于预期标志,如果都相等就会以原?的?式将引?和标志都设置为新值
文章来源:https://blog.csdn.net/weixin_44976692/article/details/135813244
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。