Java并发编程核心面试题小总结

既然是面试就应该具备面试题回答的思维方式,能在短时间内把面试官的问题口语化的完整表达出来。近期结合自己所学的多线程JUC相关知识点,对Java并发编程几个常见问题,尝试着总结写一写。我觉得可以在总—分的思维方式回答的基础上,发散性的补充与问题相关的知识点。
一、Java如何开启线程?怎么保证线程安全?
Java如何开启线程?
1、一谈到线程我首先想到是线程的使用口诀:线程操作资源类,我们开启多线程的主要目的是操作系统中的共享资源。一般来说,开启线程的方式一般情况下主要有4种,分别为:
(1)继承Thread类,重写run方法。
(2)实现Runnable接口,实现run方法。
(3)实现Callable接口,实现call方法。通过FutureTask创建一个线程并可以获取到线程的返回值。
(4)通过线程池开启线程。
2、特点比较:
(1)其中实现Runnable接口开启线程比继承Thread类开启线程的好处是:由于Java语言的规范是单继承多实现的,如果资源类本来就是一个子类,那么他就再难以通过Thread类开启线程,总的来说通过实现Runnable接口开启线程的方法更具扩展性。
(2)实现Callable接口开启线程比实现Runnable接口开启线程的主要优越性在于前者具有返回值。
(3)使用线程池的当时开启线程,准确来说是实现Runnable接口和实现Callable接口开启线程的优化方案,主要优点在于可以实现线程复用、控制最大的并发数、线程管理。
(Callable接口的使用方法和线程池的使用可参考之前发过的一篇博客第四章的1、2节内容,链接如下:JUC并发编程小总结

怎么保证线程安全?
我们可以通过加锁的方法保证线程的安全。(1)通过JVM提供的锁,synchronized关键字。 (2)通过JUC提供的各种Lock。ReentrantLock、ReadWriteLock等。

二、synchronized和Volatile有什么区别?volatile能不能保证线程安全?DCL(DoubleCheck Lock)单例为什么要加volatile?
synchronized和volatile有什么区别?
1、synchronized和volatile都是JVM层面的关键字。前者用来加锁;后者一般用来保证线程的可见性,一般多用于一个线程写,多个线程读的场景。
2、关于volatile有三个性质,(1)可见性;(2)不具备原子性;(3)禁止指令重排。
(1)关于volatile的可见性可以先了解JMM内存模型。共享变量存在于主内存中,线程对变量的操作必须在线程私有的工作内存中进行。因此当线程要对主内存的变量进行修改时,先把变量拷贝到私有的本地内存中,修改完毕后再把最新的变量值更新到主内存。使用volatile的目的是使其他线程及时感知到主内容中的变量发生变化。
《Java并发编程核心面试题小总结》
验证可见性代码如下:

public class VolatileDemo01 { 
	//如果不适用volatile,线程1不能感知到主线程的flag发生了变量,仍出去无线等待状态
    public static volatile   boolean flag = true;
    public static void main(String[] args) throws InterruptedException { 
        new Thread(() -> { 
            while (flag) { 

            }
            System.out.println("线程1结束");

        }, "线程1").start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("把flag设置为false");
        flag = false;
    }
}

(2)volatile不保证原子性。
由JMM模型可以知道每一个线程对共享变量的操作分为三个步骤:拷贝,修改,写回主内存。解析例子如下:

  1. 假设主内存的初始变量a = 0;
  2. A,B线程同时拷贝获得变量a的副本 a = 0;A线程执行+1操作,B线程执行+2操作;那么对于A线程来说a = 1, B线程来说a = 2
  3. 由于A、B线程争抢写回主内存,谁先写,谁统一。假设A线程的a先写回主内存,由于volatile可见性,B线程的a立马改为1,那么B线程的a写回主内存的值由本来的2改为了1,对于主内存的a来说丢失了一次+2操作。
    另外,一个常见的不保证原子性的例子如下,10个线程,把变量a加到2000的操作,代码如下:
public class VolatioeDemo02 { 
    public static void main(String[] args) { 
        Data data = new Data();
        for(int i = 0; i < 10; i++) { 
            new Thread(()-> { 
                for(int j = 0; j < 2000; j++) { 
                    data.addOne();
                }
            }).start();
        }
        System.out.println(data.a);
    }
}
//资源类
public class Data { 
    volatile int a = 0;
    void addOne() { 
        this.a += 1;
    }
}

(3)禁止指令重排
我们很多时候在执行一行代码的时候,底层包括多个指令操作。
例如:Integer i = 8 这一行简单的代码其实可以拆解为三部分,执行顺序如下:

  1. 分配内存,初始值为0
  2. 对象初始化,把对象值初始化为8
  3. 建立指针对应关系,建立引用指向
    《Java并发编程核心面试题小总结》
    正常情况下,系统优化作用会导致指令重排,例如执行上述步骤指令重排后的顺序为:1、3、2。单线程的情况下结果并无差异,但是在多线程的情况下,由于假设线程A执行了1、 3,还没执行2时,线程B就已经对变量i进行了拷贝到工作内存中并进行操作,此时i = 0而不是等于8,导致结果不一致问题。

Volatile能不能保证线程安全?
由于Volatile不能保证原子性,因此不保证线程的安全。

DCL(Double Check Lock)单例为什么要加Volatile?
主要目的是为了禁止指令重排
DCL的代码如下:

public class SingletonDemo01 { 
    private static SingletonDemo01 singletonDemo01;
    private SingletonDemo01 () { }
    public static SingletonDemo01 getIntence() { 
        if (singletonDemo01 == null) { 
            synchronized (SingletonDemo01.class) { 
                if (singletonDemo01 == null) { 
                    singletonDemo01 = new SingletonDemo01();
                }
            }
        }
        return singletonDemo01;
    }
}

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

1、Java的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。
2、锁机制就是根据资源竞争的剧烈程度不断进行锁升级的过程。
我们可以通过jol库来查看锁的内存状态,依赖如下:

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

代码:

public class JOLDemo1 { 

    public static void main(String[] args) { 
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        try { 
            Thread.sleep(5000);
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }

        synchronized (o){ 
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

《Java并发编程核心面试题小总结》
我们可以根据对象头的标志位信息来判断锁的类型:
《Java并发编程核心面试题小总结》
3、锁的升级机制如下图所示:
《Java并发编程核心面试题小总结》

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

1、AQS是一个Java线程的同步框架,是JDK中很多锁的工具的核心实现框架,ReentrantLock的可重入性基于AQS实现。
2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列就是用来给线程排队的,而state就像一个红绿灯,用来控制线程排队或放行,不同的场景具有不同的意义。
3、在可重入锁这个场景下,state就是用来表示加锁的次数,0表示无标识,没加1次锁,state加1,释放锁state就减1。

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

1、实现三个线程同时执行可以使用JUC的信号量工具类CountDownLatch或者CycliBarrier实现,代码如下:

//使用CountDownLatch
public class ThreadSafeDemo { 
    public int count = 0;
    public void add(){ 
        count ++;
    }
    public static void main(String[] args) throws InterruptedException { 
        int size = 3;
        ThreadSafeDemo threadSafeDemo = new ThreadSafeDemo();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < size; i++) { 
            new Thread(()->{ 
                try { 
                    countDownLatch.await();
                    System.out.println(System.currentTimeMillis());
// Thread.sleep(100);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(100);
        countDownLatch.countDown();
    }
}
//使用CycliBarrier
public class ThreadSafeDemo { 
    public int count = 0;
    public void add(){ 
        count ++;
    }
    public static void main(String[] args) throws InterruptedException { 
        int size = 3;
        ThreadSafeDemo threadSafeDemo = new ThreadSafeDemo();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, ()-> { 
            System.out.println("三个线程同时执行了");
            try { 
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) { 
                e.printStackTrace();
            }
        } );
        for (int i = 0; i < size; i++) { 
            new Thread(()->{ 
                try { 
                    cyclicBarrier.await();
                    System.out.println(System.currentTimeMillis());
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                } catch (BrokenBarrierException e) { 
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

2、如何在并发情况下保证三个线程依次执行?
可以利用volatile的可见性实现,也可以用JUC的Condition类实现,代码如下:

//使用volatile实现
public class OrderThread2 { 
    static volatile int ticket=1;
    public static void main(String[] args) { 
        Thread t1 = new Thread(()->{ 
                while(true){ 
                    if(ticket ==1){ 
                        try { 
                            Thread.sleep(100);
                            for (int i = 0; i < 10; i++) { 
                                System.out.println("a"+i);
                            }
                        } catch (InterruptedException e) { 
                            e.printStackTrace();
                        }
                        ticket = 2;
                        return;
                    }
                }
        });

        Thread t2 = new Thread(()->{ 
            while(true){ 
                if(ticket ==2){ 
                    try { 
                        Thread.sleep(100);
                        for (int i = 0; i < 10; i++) { 
                            System.out.println("b"+i);
                        }
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                    ticket = 3;
                    return;
                }
            }
        });
        Thread t3 = new Thread(()->{ 
            while(true){ 
                if(ticket ==3){ 
                    try { 
                        Thread.sleep(100);
                        for (int i = 0; i < 10; i++) { 
                            System.out.println("c"+i);
                        }
                    } catch (InterruptedException e) { 
                        e.printStackTrace();
                    }
                    ticket = 1;
                    return;
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

如何保证三个线程有序交错进行?
可以使用Semapfore实现,也可以使用JUC的Condition实现。使用Semapfore实现的代码如下:

public class OrderThread { 
    //利用信号量来限制
    private static Semaphore s1 = new Semaphore(1);
    private static Semaphore s2 = new Semaphore(1);
    private static Semaphore s3 = new Semaphore(1);

    public static void main(String[] args) { 

        try { 
            //首先调用s2为 acquire状态
            s1.acquire();
            s2.acquire();
// s2.acquire(); 调a用s1或者s2先占有一个
        } catch (InterruptedException e1) { 
            e1.printStackTrace();
        }

        new Thread(()->{ 
            while(true) { 
                try { 
                    s1.acquire();
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                try { 
                    Thread.sleep(500);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                System.out.println("A");
                s2.release();
            }
        }).start();

        new Thread(()->{ 
            while(true) { 
                try { 
                    s2.acquire();
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                try { 
                    Thread.sleep(500);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                System.out.println("B");
                s3.release();
            }
        }).start();

        new Thread(()->{ 
            while(true) { 
                try { 
                    s3.acquire();
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                try { 
                    Thread.sleep(500);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                System.out.println("C");
                s1.release();
            }
        }).start();
    }
}

(使用Condition实现可参考JUC并发编程小总结的第二章第3节)

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

使用Fork/Join框架实现,步骤如下:
1、创建ForkJoinPool
2、资源类通过继承RecursiveTask并重写Compute方法,通过对资源类的fork和join实现功能需求
3、ForkJoinTask对任务进行提交
4、通过ForkJoinTask的get方法获取返回结果
代码如下:

public class MergeTest { 
    private static int MAX = 100;
    private static int inits[] = new int[MAX];
    // 随机队列初始化
    static { 
        Random r = new Random();
        for (int index = 1; index <= MAX; index++) { 
            inits[index - 1] = r.nextInt(1000);
        }
    }
    public static void main(String[] args) throws Exception { 
        // 正式开始
        long beginTime = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool();
        MyTask task = new MyTask(inits);
        ForkJoinTask<int[]> taskResult = pool.submit(task);
        try { 
            int[] ints = taskResult.get();
            System.out.println(Arrays.toString(ints));
        } catch (InterruptedException | ExecutionException e) { 
            e.printStackTrace(System.out);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗时=" + (endTime - beginTime));

    }
    static class MyTask extends RecursiveTask<int[]> { 

        private  int source[];

        public MyTask(int source[]) { 
            this.source = source;
        }
        @Override
        protected int[] compute() { 
            int sourceLen = source.length;
            // 如果条件成立,说明任务中要进行排序的集合还不够小
            if (sourceLen > 2) { 
                int midIndex = sourceLen / 2;
                // 拆分成两个子任务
                MyTask task1 = new MyTask(Arrays.copyOf(source, midIndex));
                task1.fork();
                MyTask task2 = new MyTask(Arrays.copyOfRange(source, midIndex, sourceLen));
                task2.fork();
                // 将两个有序的数组,合并成一个有序的数组
                int result1[] = task1.join();
                int result2[] = task2.join();
                int mer[] = joinInts(result1, result2);
// System.out.println("-------"+Thread.currentThread().getName());
                return mer;
            }
            // 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
            else { 
                // 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
                if (sourceLen == 1
                        || source[0] <= source[1]) { 
                    return source;
                } else { 
                    int targetp[] = new int[sourceLen];
                    targetp[0] = source[1];
                    targetp[1] = source[0];
                    return targetp;
                }
            }
        }
        private static int[] joinInts(int array1[], int array2[]) { 
            int destInts[] = new int[array1.length + array2.length];
            int array1Len = array1.length;
            int array2Len = array2.length;
            int destLen = destInts.length;

            // 只需要以新的集合destInts的长度为标准,遍历一次即可
            for (int index = 0, array1Index = 0, array2Index = 0; index < destLen; index++) { 
                int value1 = array1Index >= array1Len ? Integer.MAX_VALUE : array1[array1Index];
                int value2 = array2Index >= array2Len ? Integer.MAX_VALUE : array2[array2Index];
                // 如果条件成立,说明应该取数组array1中的值
                if (value1 < value2) { 
                    array1Index++;
                    destInts[index] = value1;
                }
                // 否则取数组array2中的值
                else { 
                    array2Index++;
                    destInts[index] = value2;
                }
            }
            return destInts;
        }
    }
}

参考:图灵教育—金三银四Java面试突击专题

    原文作者:眼红de熊熊
    原文地址: https://blog.csdn.net/JhonyLin/article/details/116207875
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞