Live2d Test Env

互联网大厂高频重点面试题

volatile

volatile是Java虚拟机提供的轻量级的同步机制

volatile提供的轻量级的同步机制

1.1保证可见性

1.2不保证原子性

1.3禁止指令重排

1.4JMM(Java Memory Model)

Java内存模型,简称JMM ,其本身是一种抽象的概念并不是真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中的各个变量(包括实例字段,静态字段和构成数组的元素)的访问方式。

JMM关于同步的规定:

1.线程加锁前,必须把共享的值刷新回主内存

2.线程加锁前,必须读取内存的最新值到自己的工作空间

3.加琐解锁是同一把锁

 

由于JVM运行程序的实体是线程,而每个线程创建时jvm都会为其创建一个工作内存(有些地方称为栈空间)工作内存是每个线程的私有数据区域,
而java内存模型中规定所有的变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,
首先要将变量拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后将变量写回主内存,不能直接操作内存中的变量,
各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

 

1.4.1可见性

通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,
但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.

1.4.2原子性

number++在多线程下是非线程安全的,如何不加synchronized解决?

 

VolatileDemo代码演示可见性+原子性代码

 

有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3中单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.处理器在进行重新排序是必须要考虑指令之间的数据依赖性多线程环境中线程交替执行,
由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

内存屏障(Memory Barrier)

利用该特征实现volatile的内存可见性
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier 则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据上的最新版本

重排1

public void mySort(){   
    int x=11;//语句1    
    int y=12;//语句2    
    x=x+5;//语句3    
    y=x*x;//语句4
}
123421341324
问题:请问语句4 可以重排后变成第一条码?存在数据的依赖性 没办法排到第一个

重排2

int a ,b ,x,y=0;线程1线程2x=a;y=b;b=1;a=2;x=0 y=0 如果编译器对这段代码进行执行重排优化后,可能出现下列情况:线程1线程2b=1;a=2;x=a;y=b;x=2 y=1 这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确定的. 

禁止指令重排小总结(了解)

工作内存与内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他的线程可见

对指令重排序导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排序优化

 DCL(双端检测机制)

DCL(双端检测机制)不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排,原因在某一线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
instance = new SingletonDemo();可分为以下3步完成(伪代码)

//1.分配对象内存空间
memory = allocate();
//2.初始化对象
instance(memory);
//3.设置instance指向刚分配的内存地址,此时 instance != null
instance = memory;

步骤2和3不存在数据依赖关系,而且无论重排前还是重排后的程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的

//1.分配对象内存空间
memory = allocate();
//3.设置instance指向刚分配的内存地址,此时 instance != null,但是对象还没有初始化完成!
instance = memory;
//2.初始化对象
instance(memory);

但是指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例并未初始化完成,也就造成线程安全问题。

你在哪些地方用到过volatile?

3.1 单例模式DCL代码

public class SingletonDemo {    
    private static volatile SingletonDemo instance=null;    
    private SingletonDemo(){                    
    System.out.println(Thread.currentThread().getName()+"\t 构造方法");    
}   
 /**     * 双重检测机制     * @return     */ 
public static SingletonDemo getInstance(){       
     if(instance==null){            
              synchronized (SingletonDemo.class){                
                          if(instance==null){                    
                    instance=new SingletonDemo();                
           }            
       }        
    }        
    return instance;    
}    
public static void main(String[] args) {        
    for (int i = 1; i <=10; i++) {            
        new Thread(() ->{                
            SingletonDemo.getInstance();            
        },String.valueOf(i)).start();        
    }   
 }}     
View Code

3.2代理模式volatile分析

DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排  原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.instance=new SingletonDem(); 可以分为以下步骤(伪代码) memory=allocate();//1.分配对象内存空间instance(memory);//2.初始化对象instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null  步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.memory=allocate();//1.分配对象内存空间instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.instance(memory);//2.初始化对象但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.   
View Code

CAS比较并交换

1.比较并交换

/** * Description * * @author veliger@163.com * @version 1.0 * @date 2019-04-12 9:57 * 1.什么是CAS ? ===> compareAndSet *  比较并交换 **/

public class CASDemo {
  public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(5);
    System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current"+atomicInteger.get());
    System.out.println(atomicInteger.compareAndSet(5, 2014)+"\t current"+atomicInteger.get());
}}

 

2.CAS底层原理?如果知道,谈谈你对UnSafe的理解

atomicInteger.getAndIncrement();

atomicInteger.getAndIncrement()方法的源代码:
/** * Atomically increments by one the current value. * * @return the previous value */
public final int getAndIncrement() {   
 return unsafe.getAndAddInt(this, valueOffset, 1);
}
印出来一个问题:UnSafe类是什么?

UnSafe

1.UnSafe 是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法.
注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
2.变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的 

 

 

3.变量value和volatile修饰,保证了多线程之间的可见性. 

3.CAS是什么

unSafe.getAndIncrement

var1 AtomicInteger对象本身.var2 该对象值的引用地址var4 需要变动的数值var5 是用过var1 var2找出内存中绅士的值用该对象当前的值与var5比较如果相同,更新var5的值并且返回true如果不同,继续取值然后比较,直到更新完成  
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):
1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.
2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.
3.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.
4.这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.
5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.

 

底层汇编

 

 

 

 

简单版小总结

CAS (CompareAndSwap)比较当前工作内存中的值和内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止

CAS应用

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B ,当且仅当A和内存值V相同时,将内存值V修改为B,否则什么都不做。

4.CAS缺点

循环时间长开销很大

只能保证一个共享变量的原子性

引出来ABA问题

原子类

AtomicInteger的ABA问题谈谈?原子更新引用知道吗

ABA问题的产生

  

原子引用

AtomicReferenceDemo

@Getter
@Setter
@AllArgsConstructor
@ToStringclass User{
private String name;
private int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zs = new User("zs", 22);
User ls = new User("ls", 22);
AtomicReference<User> userAtomicReference = new AtomicReference<>();
userAtomicReference.set(zs);
System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
}
}

时间戳原子引用

AtomicStampedReference
/** * Description: ABA问题的解决 * * 
@author veliger
@163.com * 
@date 2019-04-12 21:30 **/
public class ABADemo {    
private static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);    
private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);    
public static void main(String[] args) {        
System.out.println("===以下是ABA问题的产生===");        
new Thread(()->{            
atomicReference.compareAndSet(100,101);            atomicReference.compareAndSet(101,100);       
 },"t1").start();        
new Thread(()->{            //先暂停1秒 保证完成ABA   
         try { TimeUnit.SECONDS.sleep(1); 
} catch (InterruptedException e) {
 e.printStackTrace(); 
}            
System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());        },"t2").start();       
 try { 
TimeUnit.SECONDS.sleep(2); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
}        
System.out.println("===以下是ABA问题的解决===");       
 new Thread(()->{           
 int stamp = stampedReference.getStamp();            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());            //暂停1秒钟t3线程           
 try { 
TimeUnit.SECONDS.sleep(1);
 } catch (InterruptedException e) { 
e.printStackTrace();
 }            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);            System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);            System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());       
 },"t3").start();        
new Thread(()->{           
 int stamp = stampedReference.getStamp();            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());            //保证线程3完成1次ABA           
 try { 
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }            boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);            System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp());            System.out.println("最新的值\t"+stampedReference.getReference());        },"t4").start();    } 
ABADemo

ArrayList线程非安全

我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案

package collection;

import org.junit.Test;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/16/19 8:24 PM
 * @Version 1.0
 * @Description:
 **/

public class ContainerNotSafeDemo {

    public static void main(String[] args) {

        List<String> strings = Arrays.asList("a", "b", "c");

        strings.forEach(System.out::println);

//        ArrayList<Object> objects = new ArrayList<>();
//        List<Object> objects = new Vector<Object>();
//        List<String> objects = Collections.synchronizedList(new ArrayList<>());
        List<String> objects = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                objects.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(objects);
            },String.valueOf(i)).start();
        }
    }

    @Test
    public void demo1() {

        ArrayList integers = new ArrayList<>();

        for (int i = 0; i <5; i++) {
            new Thread(()->{
                    integers.add(UUID.randomUUID().toString().substring(0,5));
                    System.out.println(integers);
            },"Thread"+i).start();
        }

        try { TimeUnit.SECONDS.sleep(2); }catch (Exception e){ e.printStackTrace(); }
        System.out.println(integers);
    }
}
View Code

 

公平锁和非公平锁

公平锁    是指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到非公平锁    是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象
公平锁/非公平锁  并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁 
Java ReentrantLock而言,通过构造哈数指定该锁是否是公平锁 默认是非公平锁 非公平锁的优点在于吞吐量必公平锁大.  对于synchronized而言 也是一种非公平锁.

 

 

 

可重入锁(又名递归锁)

可重入锁(递归锁):指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁,也即是说:线程可以进入任何一个它已经拥有锁同步着的代码块

 

ReentrantLock/synchronized就是一个典型的可重入锁      可重入锁最大的作用就是避免死锁

ReenterLockDemo

package lock;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/16/19 10:42 PM
 * @Version 1.0
 * @Description:
 * 可重入锁(也叫做递归锁)
 * 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
 * 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
 * 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
 **/
class Phone {
    public synchronized void sendSms() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tsendSms");
        sendEmail();
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tsendEmail");
    }
}

public class ReenterLockDemo {
    /**
     * t1 sendSms
     * t1 sendEmail
     * t2 sendSms
     * t2 sendEmail
     * @param args
     */
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                phone.sendSms();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}
ReenterLockDemo
package collection;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/16/19 10:55 PM
 * @Version 1.0
 * @Description: 可重入锁(也叫做递归锁) 
 * 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码 
 * 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁 
 * 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块 
 **/

class Phone implements Runnable {
    private Lock lock = new ReentrantLock();
    @Override    public void run() {
        get();
    }
    private void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tget");            set();
        } finally {
            lock.unlock();
        }
    }
    private void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\tset");
        } finally {
            lock.unlock();
        }
    }}
public class ReenterLockDemo {
    /**     
     * Thread-0 get      
     * Thread-0 set      
     * Thread-1 get      
     * Thread-1 set     
     * @param args    
     * */
    public static void main(String[] args) {
        Phone phone = new Phone();
        Thread t3 = new Thread(phone);
        Thread t4 = new Thread(phone);
        t3.start();
        t4.start();
    }} 
ReenterLockDemo

 

自旋锁

自旋锁(spinlock):是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗cpu

 

SpinLockDemo

package lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/17/19 8:53 AM
 * @Version 1.0
 * @Description:
 **/

public class SpinLockDemo {

    //原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in AvA");

        while (!atomicReference.compareAndSet(null,thread)){

        }

    }

    public void myUnlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock");
    }

    public static void main(String[] args) {


        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(()->{
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep(5); }catch (Exception e){ e.printStackTrace(); }
            spinLockDemo.myUnlock();
        },"AA").start();

        try { TimeUnit.SECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }

        new Thread(()->{
            spinLockDemo.myLock();
            spinLockDemo.myUnlock();
        },"BB").start();

    }

}
SpinLockDemo

独占锁(写)/共享锁(读)/互斥锁

独占锁:指该锁一次只能被一个线程所持有。对于Reentrant和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有
对于ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常有效的,读写,写读,写写的过程是互斥的

 

可重入读写锁

/** * 资源类 */class MyCaChe {    /**     * 保证可见性     */    
private volatile Map<String, Object> map = new HashMap<>();    
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();    
/**     * 写     *     * @param key     * @param value     */    public void put(String key, Object value) {        reentrantReadWriteLock.writeLock().lock();        
try {            
System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);            //模拟网络延时            
try {                
TimeUnit.MICROSECONDS.sleep(300);            
} catch (InterruptedException e) {                
e.printStackTrace();            
}            
map.put(key, value);            System.out.println(Thread.currentThread().getName() + "\t正在完成");        } finally {            
reentrantReadWriteLock.writeLock().unlock();        
}    }    /**     * 读     *     * @param key     */    
public void get(String key) {        reentrantReadWriteLock.readLock().lock();        
try {            
System.out.println(Thread.currentThread().getName() + "\t正在读取");            //模拟网络延时            
try {                
TimeUnit.MICROSECONDS.sleep(300);            
} catch (InterruptedException e) {                
e.printStackTrace();            
}            
Object result = map.get(key);            System.out.println(Thread.currentThread().getName() + "\t正在完成" + result);        
} finally {            
reentrantReadWriteLock.readLock().unlock();        
}    
}    
public void clearCaChe() {        
map.clear();    
}}/** * Description: * 多个线程同时操作 一个资源类没有任何问题 所以为了满足并发量 * 读取共享资源应该可以同时进行 * 但是 * 如果有一个线程想去写共享资源来  就不应该有其他线程可以对资源进行读或写 * <p> * 小总结: * 读 读能共存 * 读 写不能共存 * 写 写不能共存 * 写操作 原子+独占 整个过程必须是一个完成的统一整体 中间不允许被分割 被打断 * * @author veliger@163.com * @date 2019-04-13 0:45 **/
public class ReadWriteLockDemo {    
public static void main(String[] args) {        
MyCaChe myCaChe = new MyCaChe();       
 for (int i = 1; i <= 5; i++) {           
 final int temp = i;            
new Thread(() -> {                
myCaChe.put(temp + "", temp);            
}, String.valueOf(i)).start();        
}        
for (int i = 1; i <= 5; i++) {           
 int finalI = i;            new Thread(() -> {                myCaChe.get(finalI + "");            }, String.valueOf(i)).start();        }    }} 
ReadWriteLockDemo

CountDownLatch/CyclicBarrier/Semaphore

CountDownLatch

让一些线程阻塞直到另外一些完成后才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞.其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行

CountDownLatchDemo

package lock;

import java.util.concurrent.CountDownLatch;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/17/19 10:52 AM
 * @Version 1.0
 * @Description:
 **/

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i < 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t国,被灭");
                countDownLatch.countDown();
            },CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t 秦国一统天下!");
    }




    public static void closeDoor(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t 上完自习,离开教室");
                countDownLatch.countDown();
                },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t 班长最后关门走人!");
    }
}
CountDownLatchDemo
package lock;

public enum CountryEnum {

    ONE(1,"齐"),TWO(2,"楚"),THREE(3,"燕"),FOUR(4,"赵"),FIVE(5,"魏"),SIX(6,"韩");

    private Integer retCode;

    private String retMessage;

    public Integer getRetCode() {
        return retCode;
    }

    public String getRetMessage() {
        return retMessage;
    }

    CountryEnum() {
    }

    CountryEnum(Integer retCode, String retMessage) {
        this.retCode = retCode;
        this.retMessage = retMessage;
    }

    public static CountryEnum forEach_CountryEnum(int index){

        CountryEnum[] values = CountryEnum.values();

        for (CountryEnum element:values) {
            if (index == element.getRetCode()){
                return element;
            }
        }

        return null;
    }
}
CountryEnum

CyclicBarrier

CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法.

 

CyclicBarrierDemo

集齐7颗龙珠就能召唤神龙

package lock;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/17/19 11:45 AM
 * @Version 1.0
 * @Description:
 **/

public class CyclicBarrierDemo {


    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤神龙"); });

        for (int i = 0; i < 7; i++) {
            final int tmp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t收集到第:"+tmp+"颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }


}
CyclicBarrierDemo

 

Semaphore

信号量的主要用户两个目的,一个是用于多喝共享资源的相互排斥使用,另一个用于并发资源数的控制.
package lock;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/17/19 11:59 AM
 * @Version 1.0
 * @Description:
 **/

public class SemaphoreDemo {

    public static void main(String[] args) {

        Semaphore semaphore = new Semaphore(3);//三个停车位

        for (int i = 0; i < 6; i++) {//6部车
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t抢到车位");
                    try { TimeUnit.SECONDS.sleep(3); }catch (Exception e){ e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName()+"\t停车三秒后离开车位");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {

                }
            },String.valueOf(i)).start();
        }
    }

}
SemaphoreDemo

 

阻塞队列

队列+阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如图所示:\                                                
线程1往阻塞队列中添加元素二线程2从队列中移除元素

当阻塞队列是空时,从队列中获取元素的操作将会被阻塞.
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞.同样试图往已满的阻塞队列中添加新圆度的线程同样也会被阻塞,

知道其他线程从队列中移除一个或者多个元素或者全清空队列后使队列重新变得空闲起来并后续新增.

 

 

为什么用?有什么好处?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程优惠被自动唤醒为什么需要使用BlockingQueue好处是我们不需要关心什么时候需要阻塞线程,
什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了在concurrent包 发布以前,
在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.

 

BlockingQueue的核心方法

抛出异常当阻塞队列满时,再往队列里面add插入元素会抛IllegalStateException: Queue full当阻塞队列空时,再往队列Remove元素时候回抛出NoSuchElementException特殊值插入方法,成功返回true 失败返回false移除方法,
成功返回元素,队列里面没有就返回null一直阻塞当阻塞队列满时,生产者继续往队列里面put元素,队列会一直阻塞直到put数据or响应中断退出当阻塞队列空时,消费者试图从队列take元素,队列会一直阻塞消费者线程直到队列可用.超时退出当阻塞队列满时,
队列会阻塞生产者线程一定时间,超过后限时后生产者线程就会退出

 

 

架构梳理+种类分析

架构介绍

种类分析

ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.

LinkedBlockingDeque: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.

PriorityBlockingQueue:支持优先级排序的无界阻塞队列.

DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.

SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.

理论:SynchronousQueue没有容量与其他BlcokingQueue不同,SynchronousQueue是一个不存储元素的BlcokingQueue每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然. 
package lock;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/17/19 9:14 PM
 * @Version 1.0
 * @Description:
 **/
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();
        new Thread(() -> {
            try {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }}
SynchronousQueueDemo

LinkedTransferQueue:由链表结构组成的无界阻塞队列.

 

LinkedBlockingDeque:由了解结构组成的双向阻塞队列.

 

用在哪里

生产者消费者模式

传统版

/** * 共享资源类 */
class ShareData {    
private int num = 0;    
private Lock lock = new ReentrantLock();    
private Condition condition = lock.newCondition();    
public void increment() throws Exception {        
lock.lock();        
try {            //判断            
while (num != 0) {                //等待 不生产                
condition.await();            
}            //干活            
num++;            
System.out.println(Thread.currentThread().getName() + "\t" + num);            //通知唤醒            
condition.signalAll();        
} finally {            
lock.unlock();        
}    
}    public void deIncrement() throws Exception {        
lock.lock();        
try {            //判断            
while (num == 0) {                //等待 不生产                
condition.await();            
}            //干活            
num--;            
System.out.println(Thread.currentThread().getName() + "\t" + num);            //通知唤醒            
condition.signalAll();        
} finally {            
lock.unlock();        
}    
}}/** * Description * 一个初始值为0的变量 两个线程交替操作 一个加1 一个减1来5轮 * * @author veliger@163.com * @version 1.0 * @date 2019-04-13 14:01 **/
public class ProdConsumerTraditionDemo {    
public static void main(String[] args) {        
ShareData shareData = new ShareData();        
new Thread(() -> {            for (int i = 1; i <= 5; i++) {                
try {                    
shareData.increment();                
} catch (Exception e) {                    
e.printStackTrace();                
}            
}        
}, "AA").start();        
new Thread(() -> {            
for (int i = 1; i <= 5; i++) {                
try {                    
shareData.deIncrement();                
} catch (Exception e) {                    
e.printStackTrace();                
}            
}        
}, "BB").start();    
}} 
ProdConsumerTraditionDemo

阻塞队列版

class MyResource {    
/**     * 默认开启 进行生产消费的交互     */    
private volatile boolean flag = true;   
 /**     * 默认值是0     */    
private AtomicInteger atomicInteger = new AtomicInteger();    
private BlockingQueue<String> blockingQueue = null;    
public MyResource(BlockingQueue<String> blockingQueue) {        this.blockingQueue = blockingQueue;        System.out.println(blockingQueue.getClass().getName());    
}    
public void myProd() throws Exception {        
String data = null;        
boolean returnValue;        
while (flag) {            
data = atomicInteger.incrementAndGet() + "";            
returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);            
if (returnValue) {                System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "成功");            
} else {                
System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "失败");            
}            
TimeUnit.SECONDS.sleep(1);        
}        
System.out.println(Thread.currentThread().getName() + "\t 停止 表示 flag" + flag);    
}    
public void myConsumer() throws Exception {        
String result = null;        
while (flag) {            
result = blockingQueue.poll(2L, TimeUnit.SECONDS);            if(null==result||"".equalsIgnoreCase(result)){                
flag=false;                System.out.println(Thread.currentThread().getName()+"\t"+"超过2m没有取到 消费退出");                
System.out.println();                
System.out.println();                
return;            
}            
System.out.println(Thread.currentThread().getName() + "消费队列" + result + "成功");        
}    
}    
public void stop() throws Exception{        
flag=false;    
}}/** * Description * volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用 * * @author veliger@163.com * @version 1.0 * @date 2019-04-13 14:02 **/
public class ProdConsumerBlockQueueDemo {    
public static void main(String[] args) throws Exception {        
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));        
new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t生产线程启动");           
 try {                
myResource.myProd();            
} catch (Exception e) {                
e.printStackTrace();            
}        
},"Prod").start();        
new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t消费线程启动");            
try {                
myResource.myConsumer();            
} catch (Exception e) {                
e.printStackTrace();            
}        
},"consumer").start();        
try { TimeUnit.SECONDS.sleep(5); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
}        
System.out.println();        
System.out.println();        
System.out.println();        
System.out.println("时间到,停止活动");        
myResource.stop();    
}} 
ProdConsumerBlockQueueDemo

线程池

消息中间件

 Synchronized和Lock区别

1.原始构成
  synchronized是关键字属于JVM层面
  monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只有在同步块或方法中才能调wait/notify等方法monitorexit)
  Lock是具体类(import java.util.concurrent.locks.Lock)是api层面的锁

2.使用方法
  synchronized是不需要用户去手动释放资源,当synchronized代码执行完成后系统会自动让线程释放对锁的占用
  ReentrantLock 则需要用户去手动释放若没有主动释放就有可能导致出现死锁的现象。需要lock()和unlock()方法配合try...catch..finally 语句来使用

3.等待是否可中断
  synchronized不可中断,除非抛出异常或者正常运行完成
  ReentrantLock 可中断,
      1.设置超时方法tryLock(Long timeout,TimeUnit   unit)
      2.lockInterruptibly() 放代码块中,调用interrupt()方法可中断

4.加锁是否公平
  synchronized非公平锁
  ReentrantLock 两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5.锁绑定多个条件Condition
  synchronized没有
  ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个要么唤醒全部线程

 

 

线程池 ThreadPoolExecutor

为什么使用线程池,优势

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果先生超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行. 
他的主要特点为:线程复用:控制最大并发数:管理线程.
第一:降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗.
第二: 提高响应速度.当任务到达时,任务可以不需要等到线程和粗昂就爱你就能立即执行.
第三: 提高线程的可管理性.线程是稀缺资源,如果无限的创阿金,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控.

 

线程池如何使用?

架构实现

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类.  

 

编码实现

了解

Executors.newCachedThreadPool();
java8新出:
Executors.newWorkStealingPool(int);
java8新增,使用目前机器上可以的处理器作为他的并行级别

 

 

重点

Executors.newFixedThreadPool(int)

主要特点如下:1.创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待.2.newFixedThreadPool创建的线程池corePoolSize和MaxmumPoolSize是 相等的,它使用的的LinkedBlockingQueue

执行一个长期的任务,性能好很多

Executors.newSingleThreadExecutor()

主要特点如下:1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行.2.newSingleThreadExecutor将corePoolSize和MaxmumPoolSize都设置为1,它使用的的LinkedBlockingQueue

一个任务一个线程执行的任务场景

Executors.newCachedThreadPool()

 主要特点如下:1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程.2.newCachedThreadPool将corePoolSize设置为0MaxmumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程 

适用:执行很多短期异步的小程序或者负载较轻的服务器

 

ThreadPoolExecutor

 

线程池七大重要参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
View Code

1.corePoolSize:线程池中的常驻核心线程数

1.在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程2.当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中.

2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1

 3.keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止

默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,知道线程中的线程数不大于corepoolSIze,

4.unit:keepAliveTime的单位

5.workQueue:任务队列,被提交但尚未被执行的任务.

6.threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可

7.handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝.

 

线程池的底层工作原理

 

 

 

 

线程池参数配置

线程池用过吗?生产上你是如何设置合理参数

 线程池的拒绝策略请你谈谈

是什么

等待队列也已经排满了,再也塞不下新的任务了同时,线程池的max也到达了,无法接续为新任务服务这时我们需要拒绝策略机制合理的处理这个问题.

JDK内置的拒绝策略

AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行

CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交

DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略

 

 

拒绝策略:

AbortPolicy(默认):直接抛出RejectedExecution异常阻止系统正常运行
CallerRunsPoliy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPoliy:抛弃队列中的等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:直接丢弃任务,不给予任何处理也不抛异常。如果允许任务丢失,这是最好的一种方案。

 

 以上内置策略均实现了RejectExecutionHandler接口

 你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?超级大坑

答案是一个都不用,我们生产上只能使用自定义的
参考阿里巴巴java开发手册 
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

你在工作中是如何创建线程池的,是否自定义过线程池使用

 

package ThreadPool;

import java.util.concurrent.*;

/**
 * @ProjectName SummaryOfInterviewTopics
 * @Author 麦奇
 * @Email biaogejiushibiao@outlook.com
 * @Date 8/18/19 11:02 AM
 * @Version 1.0
 * @Description:
 **/

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<Runnable>(3),
                Executors.defaultThreadFactory(),                //默认抛出异常
//                new ThreadPoolExecutor.DiscardPolicy());
new ThreadPoolExecutor.AbortPolicy());
// 回退调用者
// new ThreadPoolExecutor.CallerRunsPolicy()
// 处理不来的不处理
// new ThreadPoolExecutor.DiscardOldestPolicy()
// 模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
        try {
            for (int i = 1; i <= 15; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }        //threadPoolInit();
    }
    private static void threadPoolInit() {
        /**
         * 一池5个处理线程
         */
//ExecutorService threadPool= Executors.newFixedThreadPool(5);
/**         *
 * 一池一线程
 */
//ExecutorService threadPool= Executors.newSingleThreadExecutor();
/**
 *  一池N线程
 */
ExecutorService threadPool = Executors.newCachedThreadPool();
//模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
        try {
            for (int i = 1; i <= 20; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
                try {
                    TimeUnit.MICROSECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
View Code

 

 合理配置线程池你是如何考虑的?

CPU密集型 

System.out.println(Runtime.getRuntime().availableProcessors());查看CPU核数 

 

 

 IO密集型

 

死锁编码及定位分析 

是什么

产生死锁的主要原因

系统资源不足
进程运行推进的顺序不合适
资源分配不当

 

代码

class HoldThread implements Runnable {    
private String lockA;    
private String lockB;    
public HoldThread(String lockA, String lockB) {        
this.lockA = lockA;        
this.lockB = lockB;    
}    
@Override    
public void run() {        
synchronized (lockA) {            System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockA + "尝试获得" + lockB);            
try {                
TimeUnit.SECONDS.sleep(1);           
} catch (InterruptedException e) {                
e.printStackTrace();            
}            
synchronized (lockB) {                System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockB + "尝试获得" + lockA);           
}        
}    
}}/** * Description: * 死锁是指两个或者以上的进程在执行过程中, * 因争夺资源而造成的一种相互等待的现象, * 若无外力干涉那他们都将无法推进下去 * * @author veliger@163.com * @date 2019-04-14 0:05 **/
public class DeadLockDemo {    
public static void main(String[] args) {        
String lockA = "lockA";        
String lockB = "lockB";        
new Thread(new HoldThread(lockA, lockB), "threadAAA").start();        
new Thread(new HoldThread(lockB, lockA), "threadBBB").start();    
}} 
View Code

 

解决

jps命令定位进程编号
jstack找到死锁查看

 

 

Java里面锁请谈谈你的理解能说多少说多少

 JVM+GC

1.JVM体系结构

 

2垃圾收集器

 

 

posted @ 2019-07-30 11:57  麦奇  阅读(1266)  评论(0编辑  收藏  举报