多线程面试题_java多线程面试题 - CSDN
精华内容
参与话题
  • Java多线程常用面试题(含答案,精心总结整理)

    万次阅读 多人点赞 2017-11-23 15:42:43
    Java并发编程问题是面试过程中很容易遇到的问题,提前准备是解决问题的最好办法,将试题总结起来,时常查看会有奇效。 ...这个多线程问题比较简单,可以用join方法实现。 核心: thread.Jo

    Java并发编程问题是面试过程中很容易遇到的问题,提前准备是解决问题的最好办法,将试题总结起来,时常查看会有奇效。

    现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

    这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。

    核心:

    thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。 
    比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。 
    想要更深入了解,建议看一下join的源码,也很简单的,使用wait方法实现的。

    t.join(); //调用join方法,等待线程t执行完毕 
    t.join(1000); //等待 t 线程,等待时间是1000毫秒。

    代码实现:

    public static void main(String[] args) {
            method01();
            method02();
        }
    
        /**
         * 第一种实现方式,顺序写死在线程代码的内部了,有时候不方便
         */
        private static void method01() {
            Thread t1 = new Thread(new Runnable() {
                @Override public void run() {
                    System.out.println("t1 is finished");
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override public void run() {
                    try {
                        t1.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2 is finished");
                }
            });
            Thread t3 = new Thread(new Runnable() {
                @Override public void run() {
                    try {
                        t2.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t3 is finished");
                }
            });
    
            t3.start();
            t2.start();
            t1.start();
        }
    
    
        /**
         * 第二种实现方式,线程执行顺序可以在方法中调换
         */
        private static void method02(){
            Runnable runnable = new Runnable() {
                @Override public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行完成");
                }
            };
            Thread t1 = new Thread(runnable, "t1");
            Thread t2 = new Thread(runnable, "t2");
            Thread t3 = new Thread(runnable, "t3");
            try {
                t1.start();
                t1.join();
                t2.start();
                t2.join();
                t3.start();
                t3.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

    这个题的原答案我认为不是很全面。 
    Lock接口 和 ReadWriteLock接口 如下:

    public interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        void unlock();
        Condition newCondition();
    }
    
    public interface ReadWriteLock {
        Lock readLock();
        Lock writeLock();
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

    ReadWriteLock是对Lock的运用,具体的实现类是 ReentrantReadWriteLock ,下面用这个类来实现读写类型的高效缓存:

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Random;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 用ReadWriteLock读写锁来实现一个高效的Map缓存
     * Created by LEO on 2017/10/30.
     */
    public class ReaderAndWriter<K, V> {
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();
        private final Map<K, V> map;
    
        public ReaderAndWriter(Map<K, V> map) {
            this.map = map;
        }
    
        /*************   这是用lock()方法写的   ********************/
    //    public V put(K key, V value){
    //        writeLock.lock();
    //        try {
    //            return map.put(key, value);
    //        }finally {
    //            writeLock.unlock();
    //        }
    //    }
    //    public V get(K key){
    //        readLock.lock();
    //        try {
    //            return map.get(key);
    //        }finally {
    //            readLock.unlock();
    //        }
    //    }
    
        /*************   这是用tryLock()方法写的   ********************/
        public V put(K key, V value){
            while (true){
                if(writeLock.tryLock()){
                    try {
                        System.out.println("put "+ key +" = " + value);
                        return map.put(key, value);
                    }finally {
                        writeLock.unlock();
                    }
                }
            }
        }
        public V get(K key){
            while (true){
                if (readLock.tryLock()) {
                    try {
                        V v = map.get(key);
                        System.out.println("get "+ key +" = " + v);
                        return v;
                    } finally {
                        readLock.unlock();
                    }
                }
            }
        }
    
    
        /********************    下面是测试区       *********************************/
        public static void main(String[] args) {
            final ReaderAndWriter<String, Integer> rw = new ReaderAndWriter<>(new HashMap<>());
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < 100; i++) {
                exec.execute(new TestRunnable(rw));
            }
            exec.shutdown();
        }
    
        static class TestRunnable implements Runnable{
            private final ReaderAndWriter<String, Integer> rw;
            private final String KEY = "x";
    
            TestRunnable(ReaderAndWriter<String, Integer> rw) {
                this.rw = rw;
            }
    
            @Override
            public void run() {
                Random random = new Random();
                int r = random.nextInt(100);
                //生成随机数,小于30的写入缓存,大于等于30则读取数字
                if (r < 30){
                    rw.put(KEY, r);
                } else {
                    rw.get(KEY);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    在java中wait和sleep方法的不同?

    通常会在电话面试中经常被问到的Java线程面试问题。 
    最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

    此处我想理一下Java多线程的基础知识: 
    - Java的多线程锁是挂在对象上的,并不是在方法上的。即每个对象都有一个锁,当遇到类似synchronized的同步需要时,就会监视(monitor)每个想使用本对象的线程按照一定的规则来访问,规则也就是在同一时间内只能有一个线程能访问此对象。 
    - Java中获取锁的单位是线程。当线程A获取了对象B的锁,也就是对象B的持有标记上写的是线程A的唯一标识,在需要同步的情况下的话,只有线程A能访问对象B。 
    - Thread常用方法有:start/stop/yield/sleep/interrupt/join等,他们是线程级别的方法,所以并不会太关心锁的具体逻辑。 
    - Object的线程有关方法是:wait/wait(事件参数)/notify/notifyAll,他们是对象的方法,所以使用的时候就有点憋屈了,必须当前线程获取了本对象的锁才能使用,否则会报异常。但他们能更细粒度的控制锁,可以释放锁。

    用Java实现阻塞队列。

    这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用Java线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和notify()方法来实现阻塞队列,你可以要求他用最新的Java 5中的并发类来再写一次。

    下面是实现了阻塞的take和put方法的阻塞队列(分别用synchronized 和 wait/notify 实现):

    import java.util.LinkedList;
    import java.util.List;
    import java.util.Random;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 实现了阻塞的take和put方法的阻塞队列
     *      分别用synchronized 和 wait/notify 实现
     * @author xuexiaolei
     * @version 2017年11月01日
     */
    public class MyBlocingQueue<E> {
        private final List list;
        private final int limit;//有大小限制的
    
        public MyBlocingQueue(int limit) {
            this.limit = limit;
            this.list = new LinkedList<E>();
        }
    
        //这是用synchronized写的,在list空或者满的时候效率会低,因为会一直轮询
    //    public void put(E e){
    //        while(true){
    //            synchronized (list){
    //                if (list.size() < limit) {
    //                    System.out.println("list : " + list.toString());
    //                    System.out.println("put : " + e);
    //                    list.add(e);
    //                    return;
    //                }
    //            }
    //        }
    //    }
    //    public E take(){
    //        while (true) {
    //            synchronized (list) {
    //                if (list.size() > 0){
    //                    System.out.println("list : " + list.toString());
    //                    E remove = (E) list.remove(0);
    //                    System.out.println("take : " + remove);
    //                    return remove;
    //                }
    //            }
    //        }
    //    }
    
        //用wait,notify写的,在list空或者满的时候效率会高一点,因为wait释放锁,然后等待唤醒
        public synchronized void put(E e){
            while (list.size() == limit){
                try {
                    wait();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
            System.out.println("list : " + list.toString());
            System.out.println("put : " + e);
            list.add(e);
            notifyAll();
        }
        public synchronized E take() {
            while (list.size() == 0){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("list : " + list.toString());
            E remove = (E) list.remove(0);
            System.out.println("take : " + remove);
            notifyAll();
            return remove;
        }
    
    
        /********************    下面是测试区       *********************************/
        public static void main(String[] args) {
            final MyBlocingQueue<Integer> myBlocingQueue = new MyBlocingQueue(10);
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < 100; i++) {
                exec.execute(new TestRunnable(myBlocingQueue));
            }
            exec.shutdown();
        }
    
        static class TestRunnable implements Runnable{
            private final MyBlocingQueue<Integer> myBlocingQueue;
    
            TestRunnable(MyBlocingQueue<Integer> myBlocingQueue) {
                this.myBlocingQueue = myBlocingQueue;
            }
    
            @Override
            public void run() {
                Random random = new Random();
                int r = random.nextInt(100);
                //生成随机数,按照一定比率读取或者放入,可以更改!!!
                if (r < 30){
                    myBlocingQueue.put(r);
                } else {
                    myBlocingQueue.take();
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    BlockingQueue介绍:

    Java5中提供了BlockingQueue的方法,并且有几个实现,在此介绍一下。

    BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:

    Throws exception Special value Blocks Times out
    add(e) offer(e) put(e) offer(Object, long, TimeUnit)
    remove() poll() take() poll(long, TimeUnit)
    element() peek()    

    - Throws exception 抛异常:如果试图的操作无法立即执行,抛一个异常。 
    - Special value 特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false) 
    - Blocks 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。 
    - Times out 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。

    BlockingQueue 的实现类 
    ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。 
    DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。 
    LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。 
    PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。 
    SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

    BlocingQueue的实现大多是通过 lock锁的多条件(condition)阻塞控制,下面我们自己写一个简单版:

    import java.util.LinkedList;
    import java.util.List;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 模仿ArrayBlockingQueue实现阻塞队列
     * @author xuexiaolei
     * @version 2017年11月01日
     */
    public class MyBlocingQueue2<E> {
        private final List list;
        private final int limit;//有大小限制的
        private final Lock lock = new ReentrantLock();
        private final Condition notFull = lock.newCondition();
        private final Condition notEmpty = lock.newCondition();
    
        public MyBlocingQueue2(int limit) {
            this.limit = limit;
            this.list = new LinkedList<E>();
        }
    
        public void put(E e) throws InterruptedException {
            lock.lock();
            try {
                while (list.size() == limit){
                    notFull.await();
                }
                list.add(e);
                notEmpty.signalAll();
            }finally {
                lock.unlock();
            }
        }
        public E take() throws InterruptedException {
            lock.lock();
            try {
                while (list.size() == 0){
                    notEmpty.await();
                }
                E remove = (E) list.remove(0);
                notFull.signalAll();
                return remove;
            }finally {
                lock.unlock();
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    用Java写代码来解决生产者——消费者问题。

    与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

    生产者、消费者有很多的实现方法: 
    - 用wait() / notify()方法 
    - 用Lock的多Condition方法 
    - BlockingQueue阻塞队列方法

    可以发现在上面实现阻塞队列题中,BlockingQueue的实现基本都用到了类似的实现,将BlockingQueue的实现方式稍微包装一下就成了一个生产者-消费者模式了。

    import java.util.Random;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 用阻塞队列快速实现生产者-消费者
     * @author xuexiaolei
     * @version 2017年11月01日
     */
    public class ProduceAndConsumer {
        public static void main(String[] args) {
            final BlockingQueue<Integer> list = new ArrayBlockingQueue<Integer>(10);
            Procude procude = new Procude(list);
            Consumer consumer = new Consumer(list);
            procude.start();
            consumer.start();
        }
    
        static class Procude extends Thread{
            private final BlockingQueue<Integer> list;
            Procude(BlockingQueue<Integer> list) {
                this.list = list;
            }
            @Override public void run() {
                while(true){
                    try {
                        Integer take = list.take();
                        System.out.println("消费数据:" + take);
    //                    Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        static class Consumer extends Thread{
            private final BlockingQueue<Integer> list;
            Consumer(BlockingQueue<Integer> list) {
                this.list = list;
            }
            @Override public void run() {
                while (true){
                    try {
                        int i = new Random().nextInt(100);
                        list.put(i);
                        System.out.println("生产数据:" + i);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    此处不再详细地写另外几种实现方式了:wait() / notify()方法、Lock的多Condition方法、信号量等,甚至可以考虑用CyclicBarrier、CountDownLatch也可以实现生产者-消费者的,难易程度、效率不一样罢了。

    用Java写一个会导致死锁的程序,你将怎么解决?

    这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。

    /**
     * 简单死锁程序
     *      lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
     *      但A线程先拿lockA、再拿lockB
     *      线程先拿lockB、再拿lockA
     * @author xuexiaolei
     * @version 2017年11月01日
     */
    public class SimpleDeadLock {
        public static void main(String[] args) {
            Object lockA = new Object();
            Object lockB = new Object();
            A a = new A(lockA, lockB);
            B b = new B(lockA, lockB);
            a.start();
            b.start();
        }
    
        static class A extends Thread{
            private final Object lockA;
            private final Object lockB;
            A(Object lockA, Object lockB) {
                this.lockA = lockA;
                this.lockB = lockB;
            }
    
            @Override public void run() {
                synchronized (lockA){
                    try {
                        Thread.sleep(1000);
                        synchronized (lockB){
                            System.out.println("Hello A");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        static class B extends Thread{
            private final Object lockA;
            private final Object lockB;
            B(Object lockA, Object lockB) {
                this.lockA = lockA;
                this.lockB = lockB;
            }
    
            @Override public void run() {
                synchronized (lockB){
                    try {
                        Thread.sleep(1000);
                        synchronized (lockA){
                            System.out.println("Hello B");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    产生死锁的四个必要条件: 
    - 互斥条件:一个资源每次只能被一个进程使用。 
    - 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 
    - 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 
    - 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    如何避免死锁? 
    - 从死锁的四个必要条件来看,破坏其中的任意一个条件就可以避免死锁。但互斥条件是由资源本身决定的,不剥夺条件一般无法破坏,要实现的话得自己写更多的逻辑。 
    - 避免无限期的等待:用Lock.tryLock(),wait/notify等方法写出请求一定时间后,放弃已经拥有的锁的程序。 
    - 注意锁的顺序:以固定的顺序获取锁,可以避免死锁。 
    - 开放调用:即只对有请求的进行封锁。你应当只想你要运行的资源获取封锁,比如在上述程序中我在封锁的完全的对象资源。但是如果我们只对它所属领域中的一个感兴趣,那我们应当封锁住那个特殊的领域而并非完全的对象。 
    - 最后,如果能避免使用多个锁,甚至写出无锁的线程安全程序是再好不过了。

    什么是原子操作,Java中的原子操作是什么?

    非常简单的java线程面试问题,接下来的问题是你是否需要同步一个原子操作。

    原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。 
    多个原子操作合并起来后就不是一个原子操作了,就需要同步了。 
    i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。 
    另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。

    Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?

    自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性。

    volatile关键字的作用是:保证变量的可见性。 
    在java内存结构中,每个线程都是有自己独立的内存空间(此处指的线程栈)。当需要对一个共享变量操作时,线程会将这个数据从主存空间复制到自己的独立空间内进行操作,然后在某个时刻将修改后的值刷新到主存空间。这个中间时间就会发生许多奇奇怪怪的线程安全问题了,volatile就出来了,它保证读取数据时只从主存空间读取,修改数据直接修改到主存空间中去,这样就保证了这个变量对多个操作线程的可见性了。换句话说,被volatile修饰的变量,能保证该变量的 单次读或者单次写 操作是原子的。

    但是线程安全是两方面需要的 原子性(指的是多条操作)和可见性。volatile只能保证可见性,synchronized是两个均保证的。 
    volatile轻量级,只能修饰变量;synchronized重量级,还可修饰方法。 
    volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。

    什么是竞争条件(race condition)?你怎样发现和解决的?

    这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验。关于这方面最好的书是《java并发编程实战》。

    竞争条件,在《java并发编程实战》叫做竞态条件:指设备或系统出现不恰当的执行时序,而得到不正确的结果。

    下面是个最简单的例子,是一个单例模式实现的错误示范:

    @NotThreadSafe
    public class LazyInitRace {
        private ExpensiveObject instance = null;
    
        public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在上述例子中,表现一种很常见的竞态条件类型:“先检查后执行”。根据某个检查结果来执行进一步的操作,但很有可能这个检查结果是失效的!还有很常见的竞态条件“读取-修改-写入”三连,在多线程条件下,三个小操作并不一定会放在一起执行的。

    如何对待竞态条件? 
    首先,警惕复合操作,当多个原子操作合在一起的时候,并不一定仍然是一个原子操作,此时需要用同步的手段来保证原子性。 
    另外,使用本身是线程安全的类,这样在很大程度上避免了未知的风险。

    你将如何使用thread dump?你将如何分析Thread dump?

    在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

    SIGQUIT(kill -3 pid)用来打印Java进程trace,并不会影响程序运行,不用担心他把程序杀死了;SIGUSR1(kill -10 pid)可触发进程进行一次强制GC。

    java线程的状态转换介绍

    后续分析要用到,所以此处穿插一下这个点: 
    java线程的状态转换

    • 新建状态(New) 
      用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。
    • 就绪状态(Runnable) 
      当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。
    • 运行状态(Running) 
      处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。
    • 阻塞状态(Blocked) 
      阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。 
      阻塞状态可分为以下3种: 
      1. 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
      2. 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
      3. 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
    • 死亡状态(Dead) 
      当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。

    我们运行之前的那个死锁代码SimpleDeadLock.java,然后尝试输出信息(/*这是注释,作者自己加的*/):

    /* 时间,jvm信息 */
    2017-11-01 17:36:28
    Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
    
    /* 线程名称:DestroyJavaVM
    编号:#13
    优先级:5
    系统优先级:0
    jvm内部线程id:0x0000000001c88800
    对应系统线程id(NativeThread ID):0x1c18
    线程状态: waiting on condition [0x0000000000000000]  (等待某个条件)
    线程详细状态:java.lang.Thread.State: RUNNABLE  及之后所有*/
    "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000001c88800 nid=0x1c18 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018d49000 nid=0x17b8 waiting for monitor entry [0x0000000019d7f000]
    /* 线程状态:阻塞(在对象同步上)
        代码位置:at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
        等待锁:0x00000000d629b4d8 
        已经获得锁:0x00000000d629b4e8*/
       java.lang.Thread.State: BLOCKED (on object monitor)
        at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
        - waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
        - locked <0x00000000d629b4e8> (a java.lang.Object)
    
    "Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc waiting for monitor entry [0x000000001907f000]
       java.lang.Thread.State: BLOCKED (on object monitor)
        at com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
        - waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
        - locked <0x00000000d629b4d8> (a java.lang.Object)
    
    "Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000018c46000 nid=0xb8c waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000018be4800 nid=0x1db4 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000018be3800 nid=0x810 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000018bcc800 nid=0x1c24 runnable [0x00000000193ce000]
       java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x00000000d632b928> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x00000000d632b928> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
    
    "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000017781800 nid=0x524 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001778f800 nid=0x1b08 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001776a800 nid=0xdac in Object.wait() [0x0000000018b6f000]
       java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d6108ec8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x00000000d6108ec8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
    
    "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000017723800 nid=0x1670 in Object.wait() [0x00000000189ef000]
       java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
    
    "VM Thread" os_prio=2 tid=0x000000001771b800 nid=0x604 runnable 
    
    "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000001c9d800 nid=0x9f0 runnable 
    
    "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000001c9f000 nid=0x154c runnable 
    
    "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000001ca0800 nid=0xcd0 runnable 
    
    "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000001ca2000 nid=0x1e58 runnable 
    
    "VM Periodic Task Thread" os_prio=2 tid=0x0000000018c5a000 nid=0x1b58 waiting on condition 
    
    JNI global references: 33
    
    
    /* 此处可以看待死锁的相关信息! */
    Found one Java-level deadlock:
    =============================
    "Thread-1":
      waiting to lock monitor 0x0000000017729fc8 (object 0x00000000d629b4d8, a java.lang.Object),
      which is held by "Thread-0"
    "Thread-0":
      waiting to lock monitor 0x0000000017727738 (object 0x00000000d629b4e8, a java.lang.Object),
      which is held by "Thread-1"
    
    Java stack information for the threads listed above:
    ===================================================
    "Thread-1":
        at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
        - waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
        - locked <0x00000000d629b4e8> (a java.lang.Object)
    "Thread-0":
        at com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
        - waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
        - locked <0x00000000d629b4d8> (a java.lang.Object)
    
    Found 1 deadlock.
    
    /* 内存使用状况,详情得看JVM方面的书 */
    Heap
     PSYoungGen      total 37888K, used 4590K [0x00000000d6100000, 0x00000000d8b00000, 0x0000000100000000)
      eden space 32768K, 14% used [0x00000000d6100000,0x00000000d657b968,0x00000000d8100000)
      from space 5120K, 0% used [0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
      to   space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
     ParOldGen       total 86016K, used 0K [0x0000000082200000, 0x0000000087600000, 0x00000000d6100000)
      object space 86016K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000087600000)
     Metaspace       used 3474K, capacity 4500K, committed 4864K, reserved 1056768K
      class space    used 382K, capacity 388K, committed 512K, reserved 1048576K
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132

    为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

    这是另一个非常经典的java多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级Java面试的第一轮被问到。这个问题的回答应该是这样的,当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。

    简单点来说: 
    new一个Thread,线程进入了新建状态;调用start()方法,线程进入了就绪状态,当分配到时间片后就可以开始运行了。 
    start()会执行线程的相应准备工作,然后自动执行run()方法的内容。是真正的多线程工作。 
    而直接执行run()方法,会把run方法当成一个mian线程下的普通方法去执行,并不会在某个线程中执行它,这并不是多线程工作。

    Java中你怎样唤醒一个阻塞的线程?

    这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了IO阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。

    这个我们先简单粗暴地对某些阻塞方法进行分类: 
    - 会抛出InterruptedException的方法:wait、sleep、join、Lock.lockInterruptibly等,针对这类方法,我们在线程内部处理好异常(要不完全内部处理,要不把这个异常抛出去),然后就可以实现唤醒。 
    - 不会抛InterruptedException的方法:Socket的I/O,同步I/O,Lock.lock等。对于I/O类型,我们可以关闭他们底层的通道,比如Socket的I/O,关闭底层套接字,然后抛出异常处理就好了;比如同步I/O,关闭底层Channel然后处理异常。对于Lock.lock方法,我们可以改造成Lock.lockInterruptibly方法去实现。

    在Java中CycliBarriar和CountdownLatch有什么区别?

    这个线程问题主要用来检测你是否熟悉JDK5中的并发包。这两个的区别是CyclicBarrier可以重复使用已经通过的障碍,而CountdownLatch不能重复使用。

    还要注意一点的区别: 
    CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 
    CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。 
    这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。 
    从api上理解就是CountdownLatch有主要配合使用两个方法countDown()和await(),countDown()是做事的线程用的方法,await()是等待事情完成的线程用个方法,这两种线程是可以分开的(下面例子:CountdownLatchTest2),当然也可以是同一组线程(下面例子:CountdownLatchTest);CyclicBarrier只有一个方法await(),指的是做事线程必须大家同时等待,必须是同一组线程的工作。

    CountdownLatch例子:

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 线程都准备完成后一起执行的例子
     * @author xuexiaolei
     * @version 2017年11月02日
     */
    public class CountdownLatchTest {
        private final static int THREAD_NUM = 10;
        public static void main(String[] args) {
            CountDownLatch lock = new CountDownLatch(THREAD_NUM);
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < THREAD_NUM; i++) {
                exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
            }
            exec.shutdown();
        }
    
        static class CountdownLatchTask implements Runnable{
            private final CountDownLatch lock;
            private final String threadName;
            CountdownLatchTask(CountDownLatch lock, String threadName) {
                this.lock = lock;
                this.threadName = threadName;
            }
            @Override public void run() {
                //循环多次是为了证明,CountdownLatch只会阻挡一次
                for (int i = 0; i < 3; i++) {
                    System.out.println(threadName + " 准备完成");
                    lock.countDown();
                    try {
                        lock.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(threadName + " 执行完成");
                }
    
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 各个线程执行完成后,主线程做总结性工作的例子
     * @author xuexiaolei
     * @version 2017年11月02日
     */
    public class CountdownLatchTest2 {
        private final static int THREAD_NUM = 10;
        public static void main(String[] args) {
            CountDownLatch lock = new CountDownLatch(THREAD_NUM);
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < THREAD_NUM; i++) {
                exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
            }
            try {
                lock.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("大家都执行完成了,做总结性工作");
            exec.shutdown();
        }
    
        static class CountdownLatchTask implements Runnable{
            private final CountDownLatch lock;
            private final String threadName;
            CountdownLatchTask(CountDownLatch lock, String threadName) {
                this.lock = lock;
                this.threadName = threadName;
            }
            @Override public void run() {
                System.out.println(threadName + " 执行完成");
                lock.countDown();
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    CyclicBarrier例子:

    import java.util.concurrent.*;
    
    /**
     *
     * @author xuexiaolei
     * @version 2017年11月02日
     */
    public class CyclicBarrierTest {
        private final static int THREAD_NUM = 10;
        public static void main(String[] args) {
            CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() {
                @Override public void run() {
                    System.out.println("大家都准备完成了");
                }
            });
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < THREAD_NUM; i++) {
                exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
            }
            exec.shutdown();
        }
    
        static class CountdownLatchTask implements Runnable{
            private final CyclicBarrier lock;
            private final String threadName;
            CountdownLatchTask(CyclicBarrier lock, String threadName) {
                this.lock = lock;
                this.threadName = threadName;
            }
            @Override public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println(threadName + " 准备完成");
                    try {
                        lock.await();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(threadName + " 执行完成");
                }
    
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    什么是不可变对象,它对写并发应用有什么帮助?

    另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个java面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么String是不可变的。

    immutable Objects(不可变对象)就是那些一旦被创建,它们的状态就不能被改变的Objects,每次对他们的改变都是产生了新的immutable的对象,而mutable Objects(可变对象)就是那些创建后,状态可以被改变的Objects.

    如何在Java中写出Immutable的类? 
    1. immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。 
    2. immutable类的所有的属性都应该是final的。 
    3. 对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。 
    4. 对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。 
    5. 如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)

    使用Immutable类的好处: 
    1. Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享 
    2.Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享 
    3. Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用 
    4. Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。

    /**
     * 不可变对象
     * @author xuexiaolei
     * @version 2017年11月03日
     */
    public class ImmutableObjectPerson {
        private final String name;
        private final String sex;
    
        public ImmutableObjectPerson(String name, String sex) {
            this.name = name;
            this.sex = sex;
        }
    
        public String getName() {
            return name;
        }
        public String getSex() {
            return sex;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?

    多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。

    此类问题请大家面试的时候提前准备,方便交流,如果实在找不出来,可以想想自己平时解决问题的思路,总结下来告诉考官。

    展开全文
  • Java多线程常见面试题

    千次阅读 2019-09-29 13:41:23
    并发(Concurrent):指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继...

    1. 并行和并发有什么区别?

    1. 并行(Parallel):指两个或者多个事件在同一时刻发生,即同时做不同事的能力。例如垃圾回收时,多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    2. 并发(Concurrent):指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

    2. 线程和进程的基本概念、线程的基本状态以及状态之间的关系?

    1. 一个线程是进程的一个顺序执行流程。一个进程中的全部线程共享同一个堆空间。线程本身有一个供程序执行时的栈,一个进程中可以包含多个线程。
    2. 线程的基本状态:新建、就绪、运行状态、阻塞状态、死亡状态
    3. 新建状态:利用NEW运算创建了线程对象,此时线程状态为新建状态,调用了新建状态线程的start()方法,将线程提交给操作系统,准备执行,线程将进入到就绪状态。
    4. 就绪状态:由操作系统调度的一个线程,没有被系统分配到处理器上执行,一旦处理器有空闲,操作系统会将它放入处理器中执行,此时线程从就绪状态切换到运行时状态。
    5. 运行状态:线程正在运行的过程中,碰到调用Sleep()方法,或者等待IO完成,或等待其他同步方法完成时,线程将会从运行状态,进入到阻塞状态。
    6. 死亡状态:线程一旦脱离阻塞状态时,将重新回到就绪状态,重新向下执行,最终进入到死亡状态。一旦线程对象是死亡状态,就只能被GC回收,不能再被调用。

    3. 守护线程是什么?

    1. 守护线程又称为后台线程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件
    2. 正常创建的线程都是普通线程,或称为前台线程,守护线程与普通线程在使用上没有什么区别,但是他们有一个最主要的区别是在于进程的结束中。当一个进程中所有普通线程都结束时,那么进程就会结束。如果进程结束时还有守护线程在运行,那么这些守护线程就会被强制结束
    3. 在 Java 中垃圾回收线程就是特殊的守护线程

    4. 创建线程有哪几种方式?

    1. 继承Thread类(真正意义上的线程类),是Runnable接口的实现。
    2. 实现Runnable接口,并重写里面的run方法。
    3. 使用Executor框架创建线程池。Executor框架是juc里提供的线程池的实现。

    5. sleep() 和 wait() 有什么区别?

    1. 类的不同:sleep() 来自 Thread,wait() 来自 Object。
    2. 释放锁:sleep() 不释放锁;wait() 释放锁。
    3. 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

    6. 线程的 run() 和 start() 有什么区别?

    1. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。
    2. run() 可以重复调用,而 start() 只能调用一次。
    3. 第二次调用start() 必然会抛出运行时异常

    7. 创建线程池有哪几种方式?

    1. newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
    2. newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
    3. newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
    4. newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
    5. newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
    6. newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
    7. ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

    8. 在 Java 程序中怎么保证多线程的运行安全?

    1. 使用安全类,比如 Java. util. concurrent 下的类。
    2. 使用自动锁 synchronized。
    3. 使用手动锁 Lock。

    9. 什么是死锁?怎么防止死锁?

    1. 当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
    2. 防止死锁方法:
      1. 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
      2. 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
      3. 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
      4. 尽量减少同步的代码块。

    10. synchronized 和 volatile 的区别是什么?

    1. volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
    2. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
    3. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

    11. synchronized 和 Lock 有什么区别?

    1. synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
    2. synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
    3. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    12. synchronized 和 ReentrantLock 区别是什么?

    1. ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
    2. ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
    3. ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

    13. 为什么使用线程池?

    由于创建和销毁线程都需要很大的开销,运用线程池就可以大大的缓解这些内存开销很大的问题;可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存。

    展开全文
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...

    史上最强多线程面试47题(含答案),建议收藏

    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~

    1、并发编程三要素?

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

    2)防止阻塞

    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

    4)通过线程池创建

    5、创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    7、Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就
    绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

    9、四种线程池的创建:

    1)newCachedThreadPool创建一个可缓存线程池

    2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    CountDownLatch
    CyclicBarrier
    Semaphore
    Exchanger

    12、CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

    13、synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

    14、volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

    15、什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS的问题

    1)CAS容易造成ABA问题

    一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

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

    3)CAS造成CPU利用率增加

    之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

    18、什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    19、AQS支持两种同步方式:

    1)独占式

    2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

    1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

    2)ReentrantLock可以获取各种锁的信息

    3)ReentrantLock可以灵活地实现多路通知

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    23、什么是乐观锁和悲观锁

    1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    24、线程B怎么知道线程A修改了变量

    volatile修饰变量
    synchronized修饰修改变量的方法
    wait/notify
    while轮询

    25、synchronized、volatile、CAS比较

    synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
    volatile提供多线程共享变量可见性和禁止指令重排序优化。
    CAS是基于冲突检测的乐观锁(非阻塞)

    26、sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    27、ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

    28、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    29、多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    1)线程体中调用了yield方法让出了对cpu的占用权利

    2)线程体中调用了sleep方法使线程进入睡眠状态

    3)线程由于IO操作受到阻塞

    4)另外一个更高优先级线程出现

    5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    32、Linux环境下如何查找哪个线程使用CPU最长

    1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

    2)top -H -p pid,顺序不能改变

    33、Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

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

    2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

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

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

    这里区分一下:

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

    39、Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    40、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    41、什么是自旋

    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

    42、Java
    Concurrency API中的Lock接口(Lock
    interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    可以使锁更公平
    可以使线程在等待锁的时候响应中断
    可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    可以在不同的范围,以不同的顺序获取和释放锁

    43、单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

    1)饿汉式单例模式的写法:线程安全

    2)懒汉式单例模式的写法:非线程安全

    3)双检锁单例模式的写法:线程安全

    44、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    45、Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

    展开全文
  • 50个多线程面试题,你会多少?

    万次阅读 多人点赞 2019-10-15 15:14:04
    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。 什么是线程? 什么是线程安全和线程不安全? 什么是自旋锁? 什么是Java内存模型? 什么是CAS? 什么是乐观锁和悲观锁? 什么是AQS? 什么是...

    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。

    1. 什么是线程?
    2. 什么是线程安全和线程不安全?
    3. 什么是自旋锁?
    4. 什么是Java内存模型?
    5. 什么是CAS?
    6. 什么是乐观锁和悲观锁?
    7. 什么是AQS?
    8. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
    9. 什么是Executors框架?
    10. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
    11. 什么是Callable和Future?
    12. 什么是FutureTask?
    13. 什么是同步容器和并发容器的实现?
    14. 什么是多线程?优缺点?
    15. 什么是多线程的上下文切换?
    16. ThreadLocal的设计理念与作用?
    17. ThreadPool(线程池)用法与优势?
    18. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
    19. synchronized和ReentrantLock的区别?
    20. Semaphore有什么作用?
    21. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
    22. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
    23. ConcurrentHashMap的并发度是什么?
    24. ReentrantReadWriteLock读写锁的使用?
    25. CyclicBarrier和CountDownLatch的用法及区别?
    26. LockSupport工具?
    27. Condition接口及其实现原理?
    28. Fork/Join框架的理解?
    29. wait()和sleep()的区别?
    30. 线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
    31. start()方法和run()方法的区别?
    32. Runnable接口和Callable接口的区别?
    33. volatile关键字的作用?
    34. Java中如何获取到线程dump文件?
    35. 线程和进程有什么区别?
    36. 线程实现的方式有几种(四种)?
    37. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
    38. 如果你提交任务时,线程池队列已满,这时会发生什么?
    39. 锁的等级:方法锁、对象锁、类锁?
    40. 如果同步块内的线程抛出异常会发生什么?
    41. 并发编程(concurrency)并行编程(parallellism)有什么区别?
    42. 如何保证多线程下 i++ 结果正确?
    43. 一个线程如果出现了运行时异常会怎么样?
    44. 如何在两个线程之间共享数据?
    45. 生产者消费者模型的作用是什么?
    46. 怎么唤醒一个阻塞的线程?
    47. Java中用到的线程调度算法是什么
    48. 单例模式的线程安全性?
    49. 线程类的构造方法、静态块是被哪个线程调用的?
    50. 同步方法和同步块,哪个是更好的选择?
    51. 如何检测死锁?怎么预防死锁?

    什么是线程?

    线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。

    比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒

    什么是线程安全和线程不安全?

    通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的

    线程安全

    线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染

    一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。

    线程不安全

    线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

    什么是自旋锁?

    基本概念

    自旋锁是SMP架构中的一种low-level的同步机制

    当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。

    锁需要注意

    • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
    • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁

    实现自旋锁

    参考

    segmentfault.com/q/101000000…

    一个简单的while就可以满足你的要求。

    目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

    public class MyWaitNotify3{
    
      MonitorObject myMonitorObject = new MonitorObject();
      boolean wasSignalled = false;
    
      public void doWait(){
        synchronized(myMonitorObject){
          while(!wasSignalled){
            try{
              myMonitorObject.wait();
             } catch(InterruptedException e){...}
          }
          //clear signal and continue running.
          wasSignalled = false;
        }
      }
    
      public void doNotify(){
        synchronized(myMonitorObject){
          wasSignalled = true;
          myMonitorObject.notify();
        }
      }
    }
    

    什么是Java内存模型?

    Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情

    Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。

    “一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。

    此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。

    看下面代码展示的一个简单例子:

    ClassReordering {
        
        int x = 0, y = 0;
       
        public void writer() {
            x = 1;
            y = 2;
        }
    
        public void reader() {
            int r1 = y;
            int r2 = x;
        }
    }
    

    让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到1。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是r1变量的值是2,但是r2变量的值为0。

    但是面试官,有时候不这么认为,认为就是JVM内存结构

    JVM内存结构主要有三大块:堆内存、方法区和栈

    堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

    JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

    java堆(Java Heap)

    • 可通过参数 -Xms 和-Xmx设置
    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    1. Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的)。

    据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1.Java虚拟机栈是线程私有的,它的生命周期与线程相同

    1. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    2. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧用于存储 局部变量表操作数栈动态链接方法出口等信息。
    • 局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型
    • 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
    • 动态连接每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
    • 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
    1. 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。

    2. 在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用 (reference类型)、returnAddress类型)

    java虚拟机栈,规定了两种异常状况:

    1. 如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
    2. 如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

    本地方法栈

    可通过参数 栈容量可由-Xss设置

    1. 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
    2. 本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation)

    2. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

    3. 如何实现方法区,属于虚拟机的实现细节,不受虚拟机规范约束。

    4. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    5. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载

    6. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    运行时常量池

    JDK1.6之前字符串常量池位于方法区之中JDK1.7字符串常量池已经被挪到堆之中

    可通过参数-XX:PermSize和-XX:MaxPermSize设置

    • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量
    • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
    • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。
    1. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

    2. 字面量:文本字符串、声明为final的常量值等。

    3. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

    直接内存

    可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样

    • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

    总结的简单一点

    java堆(Java Heap)

    可通过参数 -Xms 和-Xmx设置

    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1. Java虚拟机栈是线程私有的,它的生命周期与线程相同
    2. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    3. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表操作数栈动态链接方法出口等信息

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域),用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码方法区也称持久代(Permanent Generation)

    2. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    3. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    4. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    什么是CAS?

    CAS(compare and swap)的缩写,中文翻译成比较并交换

    CAS 不通过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。

    利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的

    整个java.util.concurrent都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    CAS应用

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

    CAS优点

    确保对内存的读-改-写操作都是原子操作执行

    CAS缺点

    CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

    总结

    1. 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用
    2. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS

    参考blog.52itstyle.com/archives/94…

    什么是乐观锁和悲观锁?

    悲观锁

    Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

    乐观锁

    乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

    什么是AQS?

    AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

    CAS 原子操作在concurrent包的实现

    参考blog.52itstyle.com/archives/94…

    由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

    • A线程写volatile变量,随后B线程读这个volatile变量。
    • A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

    Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

    首先,声明共享变量为volatile;然后,使用CAS的原子条件更新来实现线程之间的同步;

    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    AQS,非阻塞数据结构和原子变量类(Java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

     

    image

     

    AQS没有锁之类的概念,它有个state变量,是个int类型,在不同场合有着不同含义。

    AQS围绕state提供两种基本操作“获取”和“释放”,有条双向队列存放阻塞的等待线程,并提供一系列判断和处理方法,简单说几点:

    • state是独占的,还是共享的;
    • state被获取后,其他线程需要等待;
    • state被释放后,唤醒等待线程;
    • 线程等不及时,如何退出等待。

    至于线程是否可以获得state,如何释放state,就不是AQS关心的了,要由子类具体实现。

    AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性

    AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

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

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。

    到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。  

    什么是Executors框架?

    Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。

    Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。

    利用Executors框架可以非常方便的创建一个线程池,

    Java通过Executors提供四种线程池,分别为:

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。  

    什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

    JDK7提供了7个阻塞队列。(也属于并发容器)

    1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    5. SynchronousQueue:一个不存储元素的阻塞队列。
    6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    什么是阻塞队列?

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

    2个附加操作:

    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

    阻塞队列的应用场景

    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

    几个方法

    在阻塞队列不可用的时候,上述2个附加操作提供了四种处理方法

    方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
    插入方法 add(e) offer(e) put(e) offer(e,time,unit)
    移除方法 remove() poll() take() poll(time,unit)
    检查方法 element() peek() 不可用 不可用

    JAVA里的阻塞队列

    JDK 7 提供了7个阻塞队列,如下

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    2、LinkedBlockingQueue一个由链表结构组成的有界阻塞队列

    此队列按照先出先进的原则对元素进行排序

    3、PriorityBlockingQueue支持优先级的无界阻塞队列

    4、DelayQueue支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素

    5、SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。

    6、LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法

    transfer方法

    如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。

    tryTransfer方法

    用来试探生产者传入的元素能否直接传给消费者。,如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

    7、LinkedBlockingDeque链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。

    如何使用阻塞队列来实现生产者-消费者模型?

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

    使用BlockingQueue解决生产者消费者问题

    为什么BlockingQueue适合解决生产者消费者问题

    任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题。

    Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程创建资源,另一个消费资源)。java.util.concurrent包下的BlockingQueue接口是一个线程安全的可用于存取对象的队列。

    BlockingQueue是一种数据结构,支持一个线程往里存资源,另一个线程从里取资源。这正是解决生产者消费者问题所需要的,那么让我们开始解决该问题吧。

    生产者

    以下代码用于生产者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述:生产者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:52
     **/
    class Producer implements Runnable {
    
        protected BlockingQueue<Object> queue;
    
        Producer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object justProduced = getResource();
                    queue.put(justProduced);
                    System.out.println("生产者资源队列大小= " + queue.size());
                }
            } catch (InterruptedException ex) {
                System.out.println("生产者 中断");
            }
        }
    
        Object getResource() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                System.out.println("生产者 读 中断");
            }
            return new Object();
        }
    }
    

    消费者

    以下代码用于消费者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述: 消费者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:54
     **/
    class Consumer implements Runnable {
        protected BlockingQueue<Object> queue;
    
        Consumer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object obj = queue.take();
                    System.out.println("消费者 资源 队列大小 " + queue.size());
                    take(obj);
                }
            } catch (InterruptedException ex) {
                System.out.println("消费者 中断");
            }
        }
    
        void take(Object obj) {
            try {
                Thread.sleep(100); // simulate time passing
            } catch (InterruptedException ex) {
                System.out.println("消费者 读 中断");
            }
            System.out.println("消费对象 " + obj);
        }
    }
    

    测试该解决方案是否运行正常

    package io.ymq.example.thread;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    /**
     * 描述: 测试
     *
     * @author yanpenglei
     * @create 2018-03-14 15:58
     **/
    public class ProducerConsumerExample {
    
        public static void main(String[] args) throws InterruptedException {
    
            int numProducers = 4;
            int numConsumers = 3;
    
            BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5);
    
            for (int i = 0; i < numProducers; i++) {
                new Thread(new Producer(myQueue)).start();
            }
    
            for (int i = 0; i < numConsumers; i++) {
                new Thread(new Consumer(myQueue)).start();
            }
    
            Thread.sleep(1000);
    
            System.exit(0);
        }
    }
    

    运行结果

    生产者资源队列大小= 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    生产者资源队列大小= 3
    消费对象 java.lang.Object@1e1aa52b
    生产者资源队列大小= 2
    生产者资源队列大小= 5
    消费对象 java.lang.Object@6e740a76
    消费对象 java.lang.Object@697853f6
    
    ......
    
    消费对象 java.lang.Object@41a10cbc
    消费对象 java.lang.Object@4963c8d1
    消费者 资源 队列大小 5
    生产者资源队列大小= 5
    生产者资源队列大小= 5
    消费者 资源 队列大小 4
    消费对象 java.lang.Object@3e49c35d
    消费者 资源 队列大小 4
    生产者资源队列大小= 5
    

    从输出结果中,我们可以发现队列大小永远不会超过5,消费者线程消费了生产者生产的资源

    什么是Callable和Future?

    Callable 和 Future 是比较有趣的一对组合。当我们需要获取线程的执行结果时,就需要用到它们。Callable用于产生结果,Future用于获取结果

    Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题。

    在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法,等待Callable结束并获取它的执行结果。

    代码示例

    Callable 是一个接口,它只包含一个call()方法。Callable是一个返回结果并且可能抛出异常的任务

    为了便于理解,我们可以将Callable比作一个Runnable接口,而Callable的call()方法则类似于Runnable的run()方法

    public class CallableFutureTest {
    
        public static void main(String[] args) throws InterruptedException, ExecutionException {
    
            System.out.println("start main thread ");
    
            ExecutorService exec = Executors.newFixedThreadPool(2);
    
            //新建一个Callable 任务,并将其提交到一个ExecutorService. 将返回一个描述任务情况的Future.
            Callable<String> call = new Callable<String>() {
    
                @Override
                public String call() throws Exception {
                    System.out.println("start new thread ");
                    Thread.sleep(5000);
                    System.out.println("end new thread ");
                    return "我是返回的内容";
                }
            };
    
            Future<String> task = exec.submit(call);
            Thread.sleep(1000);
            String retn = task.get();
            //关闭线程池
            exec.shutdown();
            System.out.println(retn + "--end main thread");
        }
    }
    

    控制台打印

    start main thread 
    start new thread 
    end new thread 
    我是返回的内容--end main thread
    

    什么是FutureTask?

    FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。

    1.执行多任务计算

    FutureTask执行多任务计算的使用场景

    利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    
    public class FutureTaskForMultiCompute {
    
        public static void main(String[] args) {
    
            FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();
            // 创建任务集合
            List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
            // 创建线程池
            ExecutorService exec = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 10; i++) {
                // 传入Callable对象创建FutureTask对象
                FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i));
                taskList.add(ft);
                // 提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;
                exec.submit(ft);
            }
    
            System.out.println("所有计算任务提交完毕, 主线程接着干其他事情!");
    
            // 开始统计各计算线程计算结果
            Integer totalResult = 0;
            for (FutureTask<Integer> ft : taskList) {
                try {
                    //FutureTask的get方法会自动阻塞,直到获取计算结果为止
                    totalResult = totalResult + ft.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
    
            // 关闭线程池
            exec.shutdown();
            System.out.println("多任务计算后的总结果是:" + totalResult);
    
        }
    
        private class ComputeTask implements Callable<Integer> {
    
            private Integer result = 0;
            private String taskName = "";
    
            public ComputeTask(Integer iniResult, String taskName) {
                result = iniResult;
                this.taskName = taskName;
                System.out.println("生成子线程计算任务: " + taskName);
            }
    
            public String getTaskName() {
                return this.taskName;
            }
    
            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
    
                for (int i = 0; i < 100; i++) {
                    result = +i;
                }
                // 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。
                Thread.sleep(5000);
                System.out.println("子线程计算任务: " + taskName + " 执行完成!");
                return result;
            }
        }
    }
    
    生成子线程计算任务: 0
    生成子线程计算任务: 1
    生成子线程计算任务: 2
    生成子线程计算任务: 3
    生成子线程计算任务: 4
    生成子线程计算任务: 5
    生成子线程计算任务: 6
    生成子线程计算任务: 7
    生成子线程计算任务: 8
    生成子线程计算任务: 9
    所有计算任务提交完毕, 主线程接着干其他事情!
    子线程计算任务: 0 执行完成!
    子线程计算任务: 2 执行完成!
    子线程计算任务: 3 执行完成!
    子线程计算任务: 4 执行完成!
    子线程计算任务: 1 执行完成!
    子线程计算任务: 8 执行完成!
    子线程计算任务: 7 执行完成!
    子线程计算任务: 6 执行完成!
    子线程计算任务: 9 执行完成!
    子线程计算任务: 5 执行完成!
    多任务计算后的总结果是:990
    

    2.高并发环境下

    FutureTask在高并发环境下确保任务只执行一次

    在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。对于这样的应用场景,通常采用的方法为使用一个Map对象来存储key和连接池对应的对应关系,典型的代码如下面所示:

      private Map<String, Connection> connectionPool = new HashMap<String, Connection>();
        private ReentrantLock lock = new ReentrantLock();
    
        public Connection getConnection(String key) {
            try {
                lock.lock();
                if (connectionPool.containsKey(key)) {
                    return connectionPool.get(key);
                } else {
                    //创建 Connection  
                    Connection conn = createConnection();
                    connectionPool.put(key, conn);
                    return conn;
                }
            } finally {
                lock.unlock();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    
    

    在上面的例子中,我们通过加锁确保高并发环境下的线程安全,也确保了connection只创建一次,然而确牺牲了性能。改用ConcurrentHash的情况下,几乎可以避免加锁的操作,性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象。这时最需要解决的问题就是当key不存在时,创建Connection的动作能放在connectionPool之后执行,这正是FutureTask发挥作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:

      private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();
    
        public Connection getConnection(String key) throws Exception {
            FutureTask<Connection> connectionTask = connectionPool.get(key);
            if (connectionTask != null) {
                return connectionTask.get();
            } else {
                Callable<Connection> callable = new Callable<Connection>() {
                    @Override
                    public Connection call() throws Exception {
                        // TODO Auto-generated method stub  
                        return createConnection();
                    }
                };
                FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
                connectionTask = connectionPool.putIfAbsent(key, newTask);
                if (connectionTask == null) {
                    connectionTask = newTask;
                    connectionTask.run();
                }
                return connectionTask.get();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    

    经过这样的改造,可以避免由于并发带来的多次创建连接及锁的出现。

    什么是同步容器和并发容器的实现?

    一、同步容器

    主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。锁的粒度为当前对象整体。迭代器是及时失败的,即在迭代的过程中发现被修改,就会抛出ConcurrentModificationException。

    二、并发容器

    主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。锁的粒度是分散的、细粒度的,即读和写是使用不同的锁。迭代器具有弱一致性,即可以容忍并发修改,不会抛出ConcurrentModificationException。

    JDK 7 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    JDK 8 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    三、阻塞队列

    主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。适用于生产者、消费者模式(线程池和工作队列-Executor),同时也是同步容器

    四、双端队列

    主要代表有ArrayDeque和LinkedBlockingDeque。意义:正如阻塞队列适用于生产者消费者模式,双端队列同样适用与另一种模式,即工作密取。在生产者-消费者设计中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作。具有更好的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,他们都只是访问自己的双端队列,从而极大的减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争。适用于:网页爬虫等任务中

    五、比较及适用场景

    如果不需要阻塞队列,优先选择ConcurrentLinkedQueue;如果需要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,队列大小不固定优先选择LinkedBlockingQueue;如果需要对队列进行排序,选择PriorityBlockingQueue;如果需要一个快速交换的队列,选择SynchronousQueue;如果需要对队列中的元素进行延时操作,则选择DelayQueue。

    什么是多线程?优缺点?

    什么是多线程?

    多线程:是指从软件或者硬件上实现多个线程的并发技术。

    多线程的好处:

    1. 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
    2. 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好

    多线程的缺点:

    1. 大量的线程降低代码的可读性;
    2. 更多的线程需要更多的内存空间
    3. 当多个线程对同一个资源出现争夺时候要注意线程安全的问题。

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

    即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)

    上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态

    • 从任务保存到再加载的过程就是一次上下文切换

    ThreadLocal的设计理念与作用?

    Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量

    ThreadLocal

    如何创建ThreadLocal变量

    以下代码展示了如何创建一个ThreadLocal变量:

    private ThreadLocal myThreadLocal = new ThreadLocal();
    

    通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值

    如何访问ThreadLocal变量

    一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

    myThreadLocal.set("A thread local value”);
    

    可以通过下面方法读取保存在ThreadLocal变量中的值:

    String threadLocalValue = (String) myThreadLocal.get();
    

    get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数。

    为ThreadLocal指定泛型类型

    public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
    

    我们可以创建一个指定泛型类型的ThreadLocal对象,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了。下面展示了指定泛型类型的ThreadLocal例子:

    ThreadLocal的设计理念与作用

    http://blog.csdn.net/u011860731/article/details/48733073http://blog.csdn.net/u011860731/article/details/48733073)

    InheritableThreadLocal

    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
    

    InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值

    InheritableThreadLocal 原理

    Java 多线程:InheritableThreadLocal 实现原理

    blog.csdn.net/ni357103403…

    ThreadPool(线程池)用法与优势?

    为什么要用线程池:

    1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    3. Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

    new Thread 缺点

    1. 每次new Thread新建对象性能差。
    2. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
    3. 缺乏更多功能,如定时执行、定期执行、线程中断。

    ThreadPool 优点

    减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

    可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

    • 减少在创建和销毁线程上所花的时间以及系统资源的开销
    • 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存

    Java提供的四种线程池的好处在于

    1. 重用存在的线程,减少对象创建、销毁的开销,提高性能。
    2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    3. 提供定时执行、定期执行、单线程、并发数控制等功能。

    比较重要的几个类:

    描述
    ExecutorService 真正的线程池接口。
    ScheduledExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
    ThreadPoolExecutor ExecutorService的默认实现。
    ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

    Executors提供四种线程池

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    一般都不用Executors提供的线程创建方式

    使用ThreadPoolExecutor创建线程池

    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.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    

    参数:

    1. corePoolSize核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
    2. maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
    3. keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
    4. unit 时间单位
    5. workQueue 保存任务的阻塞队列
    6. threadFactory 创建线程的工厂
    7. handler 拒绝策略

    任务执行顺序:

    1. 当线程数小于corePoolSize时,创建线程执行任务。
    2. 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
    3. 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
    4. 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。

    ThreadPoolExecutor默认有四个拒绝策略:

    1. ThreadPoolExecutor.AbortPolicy() 直接抛出异常RejectedExecutionException
    2. ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行
    3. ThreadPoolExecutor.DiscardPolicy() 直接丢弃后来的任务
    4. ThreadPoolExecutor.DiscardOldestPolicy() 丢弃在队列中队首的任务

    当然可以自己继承 RejectedExecutionHandler 来写拒绝策略.

    java 四种线程池的使用

    juejin.im/post/59df0c…

    Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。

    阻塞队列

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    CountDownLatch

    CountDownLatch 允许一个或多个线程等待其他线程完成操作。

    应用场景

    假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。

    在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的做法是使用join。代码如下:

    public class JoinCountDownLatchTest {
    
    	public static void main(String[] args) throws InterruptedException {
    		Thread parser1 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    			}
    		});
    
    		Thread parser2 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    				System.out.println("parser2 finish");
    			}
    		});
    
    		parser1.start();
    		parser2.start();
    		parser1.join();
    		parser2.join();
    		System.out.println("all parser finish");
    	}
    
    }
    

    join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远wait,代码片段如下,wait(0)表示永远等待下去。

    while (isAlive()) {
     wait(0);
    }
    
    • 方法isAlive()功能是判断当前线程是否处于活动状态。
    • 活动状态就是线程启动且尚未终止,比如正在运行或准备开始运行。

    CountDownLatch用法

    public class Test {
         public static void main(String[] args) {   
    	 
             final CountDownLatch latch = new CountDownLatch(2);
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                        Thread.sleep(3000);
                        System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                        latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                         Thread.sleep(3000);
                         System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                         latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             try {
                 System.out.println("等待2个子线程执行完毕...");
                latch.await();
                System.out.println("2个子线程已经执行完毕");
                System.out.println("继续执行主线程");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
         }
     }
    
    线程Thread-0正在执行
    线程Thread-1正在执行
    等待2个子线程执行完毕...
    线程Thread-0执行完毕
    线程Thread-1执行完毕
    2个子线程已经执行完毕
    继续执行主线程
    

    new CountDownLatch(2)的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。

    当我们调用一次CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的await()会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,你只需要把这个CountDownLatch的引用传递到线程里。

    Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore

    www.importnew.com/21889.html

    synchronized和ReentrantLock的区别?

    java在编写多线程程序时,为了保证线程安全,需要对数据同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock。

    基础知识

    • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁
    • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
    • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
    • CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

    Synchronized

    synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一定的局限性

    例如:

    1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
    2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。

    ReentrantLock

    ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

    代码示例

    private Lock lock = new ReentrantLock();
    public void test(){
     lock.lock();
     try{
     doSomeThing();
     }catch (Exception e){
     // ignored
     }finally {
     lock.unlock();
     }
    }
    
    • **lock()**, 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
    • tryLock(long timeout,TimeUnit unit)****,如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

    ReentrantLock 一些特性

    1. 等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
    2. 公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;

    非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

    Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作

    ReenTrantLock实现的原理:

    简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

    总结一下

    在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

    synchronized

    在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

    ReentrantLock:

    ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。

    ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

    分析ReentrantLock的实现原理(ReentrantLock和同步工具类的实现基础都是AQS)

    www.jianshu.com/p/fe027772e…

    Semaphore有什么作用?

    1. Semaphore就是一个信号量,它的作用是限制某段代码块的并发数
    2. Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问
    3. 如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入
    4. 由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

    //参数permits表示许可数目,即同时可以允许多少线程进行访问  
    public Semaphore(int permits) {  
        sync = new NonfairSync(permits);  
    }  
    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可  
    public Semaphore(int permits, boolean fair) {  
        sync = (fair)? new FairSync(permits) : new NonfairSync(permits);  
    }  
    
    • Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    • acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    • release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    

    这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire() { };  
    //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false  
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };   
    //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire(int permits) { };   
    //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true  
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { };  
    //得到当前可用的许可数目  
    public int availablePermits(); 
    

    示例

    假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

    public class Test {  
        public static void main(String[] args) {  
            int N = 8; //工人数  
            Semaphore semaphore = new Semaphore(5); //机器数目  
            for(int i=0;i<N;i++)  
                new Worker(i,semaphore).start();  
        }      
        static class Worker extends Thread{  
            private int num;  
            private Semaphore semaphore;  
            public Worker(int num,Semaphore semaphore){  
                this.num = num;  
                this.semaphore = semaphore;  
            }          
            @Override  
            public void run() {  
                try {  
                    semaphore.acquire();  
                    System.out.println("工人"+this.num+"占用一个机器在生产...");  
                    Thread.sleep(2000);  
                    System.out.println("工人"+this.num+"释放出机器");  
                    semaphore.release();              
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    } 
    

    运行结果:

    工人0占用一个机器在生产...  
    工人1占用一个机器在生产...  
    工人2占用一个机器在生产...  
    工人4占用一个机器在生产...  
    工人5占用一个机器在生产...  
    工人0释放出机器  
    工人2释放出机器  
    工人3占用一个机器在生产...  
    工人7占用一个机器在生产...  
    工人4释放出机器  
    工人5释放出机器  
    工人1释放出机器  
    工人6占用一个机器在生产...  
    工人3释放出机器  
    工人7释放出机器  
    工人6释放出机器
    

    Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

    Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

    同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。

    而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

    ConcurrentHashMap的并发度是什么?

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势

    ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

    读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

    如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁

    线程进入读锁的前提条件

    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

    线程进入写锁的前提条件

    • 没有其他线程的读锁
    • 没有其他线程的写锁
    • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
    • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
    • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
    • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
    • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

     

    展开全文
  • 史上最全Java多线程面试题及答案

    万次阅读 多人点赞 2018-08-20 11:17:08
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...
  • 面试题:线程是什么?多线程

    万次阅读 多人点赞 2019-08-04 17:26:25
    为什么使用多线程?多线程的示例以及解决方案?线程池是什么? 一.线程是什么? 在Thread类中有这样的明确定义:线程是程序中执行的线程,Java虚拟机允许程序同时运行多个执行线程。 怎么创建一个线程呢? Thread中...
  • 一份经典多线程并发面试题

    千次阅读 2019-05-07 19:49:28
    synchronized关键字解决的是线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,...
  • 想进大厂?50个多线程面试题,你会多少?(一)

    万次阅读 多人点赞 2020-03-19 16:53:01
    最近看到网上流传着,各种面试经验及面试题,往往都是一大堆技术题目贴上去,而没有答案。 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java...
  • 多线程经典面试题

    万次阅读 多人点赞 2018-05-07 17:47:21
    多线程在笔试面试中经常出现,下面列出一些公司的多线程笔试面试题。首先是一些概念性的问答题,这些是多线程的基础知识,经常出现在面试中的第一轮面试(我参加2011年腾讯研究院实习生招聘时就被问到了几个概念性...
  • 多线程面试题

    千次阅读 2017-09-20 09:50:51
    程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个...
  • C++ 多线程 面试题详解

    万次阅读 2019-12-26 14:35:02
    首先看看堆栈 堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。...栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 t...
  • Java多线程面试题

    千次阅读 2020-08-15 19:10:55
    一、线程和进程的区别? 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。 进程在执行过程中拥有独立的内存单元,而线程共享内存资源,减少切换次数,从而效率更高。 ...
  • java多线程面试题

    千次阅读 2018-10-30 15:05:01
    1.启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5 然后是线程2打印6,7,8,9,10然后是线程3打印11,12,13,14,15.接着再由线程1打印16,17,18,19,20....以此类推, 直到打印到75 package com.rj.bd.zy; /** * @desc...
  • 几率大的多线程面试题(含答案)

    万次阅读 多人点赞 2020-03-09 14:46:41
    其他篇章:Java校招极大几率出的面试题(含答案)----汇总 本文面试题如下: 线程和进程的区别? Thread和Runnable的关系,区别 synchronized底层如何实现?锁优化,怎么优化?多线程中 synchronized 锁升级的原理是...
  • 多线程面试题汇总(一)

    万次阅读 2018-07-21 15:16:06
    本文是作者整理的个人笔记,文中可能引用到其他人的成果...同一进程的线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程之间切换快,无需陷入内核态。 线程状态 阻塞状态分分为...
  • Linux多线程面试题

    千次阅读 2018-11-29 17:27:58
    前面的选择那些跳过,直接看最后的编程。 第三(某培训机构的练习): 子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次,试写出代码...
  • C/C++多线程面试题

    万次阅读 2018-05-20 10:56:06
    第一线程的基本概念、线程的基本状态及状态之间的关系? 线程,有时称为轻量级进程,是CPU使用的基本单元;它由线程ID、程序计数器、寄存器集合和堆栈组成。它与属于同一进程的其他线程共...
  • 引言: 通过一道阿里的Java多线程面试题目分析,来深入理解Java线程的状态转变过程。
  • Java多线程面试题整理及答案

    千次阅读 2020-01-11 13:47:09
    Java多线程面试题整理及答案 1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速...
  • 史上最全 Java 多线程面试题及答案

    万次阅读 多人点赞 2018-08-17 10:41:16
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...
1 2 3 4 5 ... 20
收藏数 109,877
精华内容 43,950
关键字:

多线程面试题