面试题总结——Java多线程与并发

Java多线程与并发

线程与线程通信

1. 请完整描述线程的生命周期与5中状态之间流转过程2

1.1 新建状态:当线程对象创建后,即进入新建状态。
1.2 就绪状态:当调用线程对象的start()方法时,线程进入就绪状态,处于就绪状态的线程,只能说明已经做好了准备,随时等待CPU调用,并不是说执行了start()方法,线程立即就会执行。
1.3 运行状态:当CPU调用处于就绪状态的线程时,线程才等到真正的执行,即进入到运行状态,注意:就绪状态时进入到运行状态的唯一入口,也就是说线程想进入到运行状态,首先必须处于就绪状态。
1.4 阻塞状态:处于运行状态的线程,由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,在有可能再次被CPU调用进入到运行状态,根据阻塞产生的原因不同,阻塞可以分为3种:
1.4.1 等待阻塞:运行状态线程调用wait()方法,是线程进入到等待阻塞状态。
1.4.2 同步阻塞:线程在获取synchronized锁同步锁失败(被其他线程占用),线程会进入同步阻塞状态。
1.4.2 其他阻塞:通过调用线程的sleep()或join()或I/O请求时,线程会进入阻塞状态,当sleep()超时或join()等的线程终止或超时或I/O处理完毕,线程重新进入就绪状态。
1.5 死亡状态:线程执行完毕或由于异常退出了run()方法,此线程结束生命周期。
在这里插入图片描述

2. 谈一下线程之间的通信与协作

2.1 sleep():Thread类方法,使当前线程(调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不是释放对象锁,也就是synchronized同步块其他线程仍不能访问共享数据。例如两个线程同时执行一个线程优先级为Max,一个优先级为Min,如果没有Sleep()方法,那么只有优先级高的线程执行完毕后,优先级低的线程才能执行,但是优先级高的线程调用sleep(500),低优先级的线程就有机会执行了。注意该方法需要捕捉异常。总之,sleep()方法既可以让优先级低的线程得到执行机会,也是让同级或优先级高的线程得到执行机会。
2.2 join():使调用该方法的线程在此之前执行完毕,也就是等待执行该方法的线程执行完毕后在往下继续进行,注意该方法也需要处理捕捉异常。
2.3 yield():该方法与sleep()方法类似,只是不能设置等待时间,而且该方法只能让同优先级的线程获得执行机会。
2.4 wait()和notify()和notifyAll():这三个方法由于协调多线程访问synchronized同步代码块的共享数据的存取,synchronized用于保护共享数据,阻止其他线程对共享数据的存取,但这样程序的流程就不灵活了,如何才能在当前线程还没有退出,synchronized数据块时让其他线程有机会访问共享数据呢?此时这三个方法来灵活控制。
2.4.1 wait():使当前线程暂停,并释放对象锁标志,让其他程序可以进入到synchronized数据块中,当前线程被放入到对象等待池中。
2.4.2 notify():从对象等待池中移走任意一个线程并添加到锁标志等待池中,只有在锁标志等待池中的线程才有机会获得锁标志,锁标志等待池中没有线程时,notify()不起作用。
2.4.3 notifyAll():将对象等待池中的所有等待对象锁的线程转移指锁标志等待池中。

3. run()方法和start()方法的区别?

  1. run()方法:Runnable接口,当通过实现Runnable接口来创建线程时,启动线程会使得run()方法在那个独立执行的线程中被调用,是线程启动后要进行回调的方法。
  2. start()方法:Thread接口,会使得该线程执行,java虚拟机会去调用该线程的run()方法。调用start方法会创建一个新的线程并启动。

4. 多线程场景下启动线程是调用run()方法还是start()方法?

调用start()方法,new Thread(),线程进入到了新建状态。调用start()方法,会启动一个线程并进入到就绪状态,当获得时间片的执行时间后就可以开始运行了。start()方法会执行线程的相应准备工作,然后自动执行run()方法的内容,这才是多线程工作;而直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中心它,所以并不是多线程。所以调用start()方法可以启动线程并是线程进入继续状态,而run()方法只是Thread的一个普通方法的调用,还是在主线程里执行。

5. sleep()、yield()、wait()方法的区别?

  1. sleep()方法:Thread类方法,使当前线程暂停执行一段时间,让其他线程有机会执行,当并不释放对象锁,也就是synchronized同步锁,其他线程仍不能访问共享数据。sleep()方法既可以是优先级较低的线程获得执行机会,也可以让优先级高的或者同优先级线程获得执行机会,注意该方法需要捕捉异常。
  2. yield()方法:Thread类方法,该方法和sleep()方法类似,只是不能由用户指定暂停时间,而且该方法只能让同优先级的线程获得执行机会。
  3. wait()方法:Object类方法,用于协调对共享数据的存取,所以必须在synchronized语句块中使用,使当前线程暂停,并释放对象锁标识,让其他线程可以进入到synchronized数据块中,当前线程被放入对象等待池中。wait()方法通常被用于线程间通信,sleep()通常被用于暂停执行。

6. Thread.sleep(0)作用?

该方法并非真的要线程暂停0毫秒,意义在于调用该方法的当前线程确实被“冻结”了一下,让其他线程有机会执行,也就意味着线程暂时放弃CPU的执行时间,释放一些未使用的时间片给其他线程使用,相当于一个让位动作,这样可以让操作系统来执行其他线程,提高效率。

7. 为什么wait, notify 和 notifyAll这些方法不在thread类里面而是在Object类?

wait()、notify()、notifyAll()这三个方法的主要作用是配合synchronized关键字使用,来实现Java中两个线程之间的通信机制,而synchronized可以对任何对象进行加锁操作,而Object是所有类的父类,那么wait()、notify()、notifyAll()放在Object里面最合适不过了。

8. 有三个线程T1,T2,T3,怎么确保它们按顺序执行?

/**
 * @Description [假设一条流水线上有三个工作者:work1、work2、work3,有一个任务需要他们三者协作完成,work3可以开始这个任务的前提是
 * work1和work2完成了它们的工作,而work1和work2是可以并行他们各自的工作的]
 * @Date 2020/12/9 13:44
 **/
public class ThreadCommunicationDemo {


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

        Work work1 = new Work("work1", (long) (Math.random() * 2000 + 3000));
        Work work2 = new Work("work2", (long) (Math.random() * 2000 + 3000));
        Work work3 = new Work("work3", (long) (Math.random() * 2000 + 3000));

        work1.start();
        work2.start();


        work1.join();
        work2.join();

        System.out.println("work1和work2已执行完毕;开始执行work3");

        work3.start();

        /**
         * CountDownLatch和join()的主要区别:调用Thread.join()方法必须等待thread执行完毕,当前线程才能继续往下执行,而
         * CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行,不用管相应的thread是否执行完毕。
         */
        CountDownLatch countDownLatch = new CountDownLatch(2);

        Worker worker1 = new Worker("worker1", (long) (Math.random() * 2000 + 3000), countDownLatch);
        Worker worker2 = new Worker("worker2", (long) (Math.random() * 2000 + 3000), countDownLatch);
        Worker worker3 = new Worker("worker3", (long) (Math.random() * 2000 + 3000), countDownLatch);


        worker1.start();
        worker2.start();
        countDownLatch.await();
        System.out.println("work1和work2已执行完毕;开始执行work3");
        worker3.start();


    }


    /**
     * join()方法实现
     */
    public static class Work extends Thread {

        private String name;

        private long time;

        Work(String name, long time) {
            this.name = name;
            this.time = time;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "-" + name + "-开始工作");
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() + "-" + name + "-工作完成;耗时:" + time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * CountDownLatch类实现
     */
    public static class Worker extends Thread {

        private String name;

        private long time;

        private CountDownLatch countDownLatch;

        Worker(String name, long time, CountDownLatch countDownLatch) {
            this.name = name;
            this.time = time;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "-" + name + "-开始工作");
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() + "-" + name + "-工作完成;耗时:" + time);
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + "-countDownLatch:" + countDownLatch.getCount());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

9. 生产者-消费者模型(哲学家用餐问题)?

https://blog.csdn.net/m0_37741420/article/details/110629976
生产者-消费者模型是一个十分经典的多线程并发协作的模式。所谓生产者-消费者问题,实际上主要是包含了两类线程,一类是生成这线程用于生产数据,一类是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会有一个共享数据的区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为,而消费者只需要从共享数据区中去获取数据,就不在需要关心生产者的行为。但是,这个共享数据区中应该具备线程间并发协作的功能:

  1. 如果共享区域满的话,阻塞生产者继续生产数据放置其中;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据。

在实现生产者消费者问题时,可以采用三种方式:

  1. 使用Object的wai/notify的消息通知机制;
  2. 使用Lock的Condition的await/signal的消息通知机制;
  3. 使用BlockingQueue实现。

10. 使用Object的wait/notifyAll的消息通知机制实现生产者-消费者模型?

// 基本使用范式
synchronized (sharedObject) { 
    while (condition) { 
    sharedObject.wait(); 
        // (Releases lock, and reacquires on wakeup) 
    } 
    // do action based upon condition e.g. take or put into queue 
}

11. wait/notify的消息通知机制潜在的问题?

  1. notify过早通知:在使用线程的等待/通知机制时,一般都要配合一个boolean变量值(或者其他能够判断真假的条件),在notify之前改变该boolean变量的值,让wait返回后退出while循环(一般又要在wait方法外围加一层while,以防止早期通知),而在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。
  2. 等待wait的条件发生变化:在使用线程的等待/通知机制时,一般都要在while循环中调用wait()方法,因此配合使用一个boolean变量(或者其他能判断真假的条件),满足while循环的条件时,进入while循环,执行wait()方法,不满足while条件时跳出循环,执行后面的代码。
  3. 假死状态:假设当前多个生产者线程调用wait方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify通知处于WAITING状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。将notify方法替换成notifyAll方法,如果使用的是lock的话,就将signal方法替换成signalAll方法。

12. 使用Lock的Condition的await/signal的消息通知机制实现生产者-消费者模型?

public class ProducerConsumerByCondition {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition full = lock.newCondition();
    private static Condition empty = lock.newCondition();

    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        ExecutorService service = Executors.newFixedThreadPool(15);
        for (int i = 0; i < 5; i++) {
            service.submit(new Productor(linkedList, 8, lock));
        }
        for (int i = 0; i < 10; i++) {
            service.submit(new Consumer(linkedList, lock));
        }
    }


    /**
     * 定义生产者方法
     */
    static class Productor implements Runnable {
        private List<Integer> list;
        private int maxLength;
        private Lock lock;

        public Productor(List<Integer> list, int maxLength, Lock lock) {
            this.list = list;
            this.maxLength = maxLength;
            this.lock = lock;
        }

        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (maxLength == list.size()) {
                        System.out.println("生产者:" + Thread.currentThread().getName() + "- list已达到最大容量:" + maxLength);
                        full.await();
                        System.out.println("生产者:" + Thread.currentThread().getName() + "- 退出await方法");
                    }
                    Random random = new Random();
                    int i = random.nextInt();
                    System.out.println("生产者:" + Thread.currentThread().getName() + "- 生产数据" + i);
                    list.add(i);
                    empty.signalAll();
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    /**
     * 定义消费者方法
     */
    static class Consumer implements Runnable {
        private List<Integer> list;
        private Lock lock;

        public Consumer(List<Integer> list, Lock lock) {
            this.list = list;
            this.lock = lock;
        }

        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    if (CollectionUtils.isEmpty(list)) {
                        System.out.println("消费者:" + Thread.currentThread().getName() + "- list为空");
                        empty.await();
                        System.out.println("消费者:" + Thread.currentThread().getName() + "- 退出await方法");
                    }
                    Integer ele = list.remove(0);
                    System.out.println("消费者:" + Thread.currentThread().getName() + "- 消费数据:" + ele);
                    full.signalAll();
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                	e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

13. Condition接口原理?和Object类中wait/notifyAll的区别?

https://thinkwon.blog.csdn.net/article/details/102469889
参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

  1. await()方法:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
  2. awaitNanos(long nanos TimeOut)方法:当前线程进入等待状态直到被通知,中断或者超时;
  3. await(long time,TimeUnit unit)方法:同第二种,支持自定义时间单位;
  4. awaitUntil(Date deadline)方法:当前线程进入等待状态直到被通知,中断或者到了某个时间;
  5. signal()方法:唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回;
  6. signalAll()方法:与signal()的区别在于能够唤醒所有等待在condition上的线程。

14. ArrayBlockingQueue实现原理?

https://blog.csdn.net/ThinkWon/article/details/102508971
在多线程编程过程中,为了业务解耦和架构设计,经常会使用并发容器用于存储多线程间的共享数据,这样不仅可以保证线程安全,还可以简化各个线程操作。阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的消费者线程;当前队列满时,会阻塞插入数据的线程,直至队列未满时,通知被阻塞的生产者线程。从源码中可以看出 ArrayBlockingQueue内部采用数组进行数据存储,为了保证线程安全,采用ReentrantLock lock类,为了保证可阻塞式的插入删除数据使用的是Condition对象,当消费数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将线程放置到到notFull等待队列中。而notEmpty和notFull等中属性在构造方法中进行创建。

15. 使用BlockingQueue实现实现生产者-消费者模型?

public class ProducerConsumerByCondition {

    private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(15);
        for (int i = 0; i < 5; i++) {
            service.submit(new Productor(queue));
        }
        for (int i = 0; i < 10; i++) {
            service.submit(new Consumer(queue));
        }
    }


    /**
     * 定义生产者方法
     */
    static class Productor implements Runnable {

        private BlockingQueue queue;

        public Productor(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Random random = new Random();
                    int i = random.nextInt();
                    System.out.println("生产者:" + Thread.currentThread().getName() + "- 生产数据" + i);
                    queue.put(i);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 定义消费者方法
     */
    static class Consumer implements Runnable {

        private BlockingQueue queue;

        public Consumer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println("消费者:" + Thread.currentThread().getName() + "- 消费数据:" + queue.take());
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

1. 广义上“锁”的分类?

  1. 公平锁/非公平锁:申请锁的顺序分类。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁,非公平锁的有点再有吞吐量比公平锁大。对于Sychronized而言,也是一种非公平锁。
  2. 可重入锁/不可重入锁:https://blog.csdn.net/qq_29519041/article/details/86583945
  3. 独享锁/共享锁:锁持有线程的数量分类。独占锁是指该锁一次只能被一个线程锁持有,比如ReentrantLock、Synchronized;共享锁是指该锁可被多个线程锁持有,比如ReadWriteLock,其读锁是共享锁,其写锁是独占锁;Semaphore、CountDownLatch。
  4. 乐观锁/悲观锁:并发同步来分类。乐观锁认为对于同一数据并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断更新的方式更新数据。乐观的认为,不加锁的并发操作是不会发生的,适合读操作比较多的场景,不加锁会带来大量的性能提升,常常采用CAS算法,典型的例子就是原子类,通过CAS自旋实现原子类操作的更新;悲观锁认为对于同一数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一数据的并发操作,悲观锁采取加锁的形式,悲观的认为,不加锁一定会出问题。悲观锁适合写操作非常多的场景,Java中使用的各种锁就是悲观锁。

2. synchronzied的实现原理

1.1 synchronized同步语句块:synchronized经过编译后,会在代码的前后生成monitorenter和monitorexit指令,在执行monitorenter时,首先会获取对象锁,如果这个锁没有被锁定或者当前线程已经拥有了这个线程锁了,锁计数器加1,在执行monitorexit指令时,将会将所计数器减1,当减为0时就释放锁,如果获取锁对象一致失败,那当前线程就要阻塞等待,直到对象锁被另一个对象释放为止。

1.2 synchronized方法:方法级的同步是隐式的,无需通过字节码指令控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标识,得知一个方法是否被声明为一个同步方法。当方法被调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标识是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能这行方法,最后当方法执行完毕(无论是正常完成还是异常退出)时释放monitor对象,在方法运行期间,线程持有了管程,其他线程无法在获取同一个管程。
通过这两条指令,可以很清楚的看到synchronized的实现原理,synchronized的底层是通过以一个monitor对象来完成的,其实wait()/notify()也依赖于monitor对象,这就是为什么只有在同步代码块或者同步方法中才能调用wait()/notify()等方法。

3. 锁的存储?

每个对象都分为3块区域:对象头,实例数据和对其填充

对象头包含两部分:

Mark Word:用于存储自身运行时数据,如HashCode、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,占一个字节;

Klass point(类型指针):是对象指向他的类的元数据类型的指针,虚拟机通过这个指针确认该对象是哪个类的实例,也占一个字节。

在这里插入图片描述
synchronized通常被称为重量级锁,从对象头的存储内容来看,锁状态都是保存在对象头中的,关于synchronized的实现在对象都中也较为简单,就是改变一个锁标志位,并将指针指向monitor对象的起始地址。

4. 锁的类型?

Java 1.6为了减少获得锁和释放锁带来的性能消耗,锁一共有4中状态(从低到高,锁可以升级但是不能降级)

  1. 无锁状态
  2. 偏向锁:根据调查发现,大多数情况下,锁总是被同一个线程所调用,所有出现了偏向锁,总是偏向第一次获取到锁的线程,一般无竞争状态使用就是偏向锁,可以使用-XX: +/- UsebiasedLocking参数启动/关闭偏向锁,默认开启(线程数过多时,适合关闭偏向锁,因为竞争激烈,锁会升级,关闭后可以省略升级过程,直接使用轻量级锁。)
  3. 轻量级锁:自旋锁
  4. 重量级锁:synchronized关键字修饰

5. 锁膨胀升级过程?

  1. 无锁状态:线程A和线程B要去争抢锁对象,但还未开始争抢,锁对象的对象头是无锁的状态也就是25bit位存的是hashCode,4bit存的是对象的分代年龄,1bit记录是否为偏向锁,2bit位记录状态,优先看最后2bit位,是01,所以对象可能是无锁或者偏向锁状态,继续前移一个位置,有1bit专门记录是否为偏向锁,1代表偏向锁,0代表无锁,刚刚开始时一定是一个无锁状态,虽然系统不同bit位存的东西可能有略微差异,但是关键信息是一致的。
  2. 偏向锁:
    2.1 这时线程A得到锁对象了,将偏向锁标志位改成1,并且将原有的hashCode位置为23bit位存放A的线程ID(用CAS算法得到线程A的ID),2bit存放epoch,偏向锁永远不会被释放;接下来线程B开始运行,线程B也希望得到这把锁,于是线程B会检查23bit位存的是不是自己的线程ID,因为被线程A已经持有了,那么23bit位一定不是线程B的线程ID了;
    2.2 然后线程B也不甘示弱,会尝试修改一次23bit位的对象头存储,如果这是恰好线程A释放了锁,那么可以修改成功,然后线程B就可以持有该偏向锁。如果修改失败,开始锁膨胀。自己无法修改,线程B只能找“大哥”了,线程B会通知虚拟机撤销偏向锁,然后虚拟机会撤销偏向锁,并告知线程A到达安全点进行等待。线程A到达安全点,会再次判断线程是否已经退出同步块,如果退出了,将23bit位值为空,这时锁不升级,线程B可以直接进行使用,会将23bit改为线程B的线程ID就可以了;
  3. 轻量级锁:如果线程B没有拿到锁,就会升级到轻量级锁,首先会在线程A和线程B都开辟一块LockRecord空间,然后把锁对象复制一份到自己的LockRecord空间下,并且开辟一块owner空间留作执行锁使用,并且与锁对象的前30bit位合并,等待线程A和线程B来修改指向自己的线程,假如线程A修改成功,则锁对象的前30bit为会存线程A的LockRecord内存地址,并且线程A的owner也会存一份锁对象的内存地址,形成一个双向指向形式。而线程B修改失败,则进入一个自旋状态,就是持续来修改锁对象。
  4. 重量级锁:如果说线程B多次自旋以后还是迟迟拿到锁,它会继续上告,告知虚拟机,多次自旋还是没有拿到锁,这时线程B会由用户态切换到内核态,申请一个互斥量matux,并且将锁对象的前30bit指向互斥量地址,并且进入睡眠状态,然后线程A继续运行直到完成时,当线程A想要释放锁资源,并且去唤醒哪些处于睡眠状态的线程,锁升级到重量级锁。

6. synchronized锁优化?

  1. 减少锁的持有时间:只用在有线程安全要求的程序上加锁;

  2. 减小锁力度:将大对象(这个对象可能被多个线程访问),拆成多个小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁的成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

  3. 锁分离:最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程安全,又提高了性能,读写分离思想可以延伸,只要操作互不影响,锁就可以分离,比如LinedBlockingQueue从头部取出,从尾部放入数据。

  4. 锁粗化:锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。
    在这里插入图片描述
    这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到一些列连串的对同一对象的加锁解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束进行解锁。

  5. 锁消除:锁消除即删除不必要的加锁操作。根据代码逃逸分析技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必加锁。
    在这里插入图片描述
    虽然StringBuffer是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,不会从该方法中逃逸出去,所以其实这个过程是线程安全的,可以将锁消除。

  6. 适应性自旋:从轻量级锁获取的流程可以看出,当线程获取轻量级锁的过程中执行CAS操作失败时,是通过自旋等待来获取锁的,问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那么线程一直处于自旋状态,白白浪费CPU资源。解决这个问题最简单的方法就是指定自旋次数,例如让其循环10,如果没有获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果失败了,则自旋的次数就会减少。

7. ReentrantLock的原理?

https://blog.csdn.net/java6888/article/details/108621760
ReentrantLock的实现原理是基于AQS的,AQS是众多Java并发包中的同步组件的构成基础,它通过一个int类的状态变化state和一个FIFO队列来完成共享资源的获取,线程的排队等待等等,AQS是底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,例如线程的排队、阻塞唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件使用者无需关心的,使用者只需要重写一些简单的指定方法即可(其实就是对共享变量state的一些简单获取释放的操作)。

8. ReentrantLock获取锁的流程以及公平锁与非公平锁的区别?

  1. 非公平锁:
    1.1 获取state值,若为0,意味着此时没有线程获取到资源,CAS将state设置为1,设置成功则代表获取到排他锁;
    1.2 若state大于0,肯定有线程占用了该资源,此时再去判断是不是自己占用的,是的话,state累加,返回true,重入成功,state值为线程重入次数;
    1.3 其他情况则获取锁失败。
  2. 公平锁:大致逻辑与非公平锁一致,不同的地方是即便state为0,也不能贸然的直接获取锁,要先看看有没有排队的线程,如果没有才尝试去获取,做后面的操作,反之返回false,获取失败。
  3. 区别:
    3.1 非公平锁:如果有另外一个线程进来尝试获取锁,那么有可能让这个线程抢先获取;
    3.2 公平锁:如果同时还有另外一个线程进来尝试获取,当它发现自己不在队首的话,就会排队队尾,由队首的线程获取锁。

9. AQS核心成员和数据模型?

AQS就是一个抽象的队列同步器,核心成员包括:队头节点、队尾节点、资源共享标识,三个变量都是Volatile修饰的,通过Volatile保证变量的可见性。
数据模型

10. AQS锁的实现原理?

  1. 锁获取流程:线程A获取锁,state设置为1,线程A占用,在A没有释放锁期间,线程B来获取锁;线程B获取state为1,表示锁被占用,线程B创建Node节点放入队尾(tail),并阻塞线程B,同理线程C获取state为1,表示锁被占用,线程C创建Node节点,放入队尾,且阻塞线程。
  2. 锁释放流程:线程A执行完,将state从1设置为0,唤醒下一个Node B节点,然后在删除线程A节点,线程B节点占用,获取state状态位,执行完毕后唤醒下一个节点Node C再删除B节点。

11. AQS的资源共享方式?

  1. 独占锁:独占模式下,其他线程试图获取该锁,将无法获取成功,只有一个线程能执行,如:ReentranLock。
  2. 共享锁:多个线程获取某个锁可能会获取成功,多个线程可同时执行,如:Semaphore、CountDownLatch。

12. Java内存模型?

Java内存模型规定:所有变量都在主内存中存储,线程工作内存保存了变量在主内存中的副本,线程对变量的所有操作都是在工作内存中进行,执行结束后同步到主内存中,这里必然会造成时间差,这个时间差内该线程对副本的操作,对其他线程是不可见的,从而造成可见性问题。

13. Java内存模型的三大特性?

  1. 原子性:指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。比如,一个静态全局变量 int i,两个线程同时对他赋值,线程A赋值为1,线程B赋值为-1,那么不管这两个线程以何种方式,何种步调工作,i的值要么是1,要么是-1,线程A和线程B之间是没有干扰的。这是原子性的一个特点,不可被中断。
  2. 可见性:指当一个线程修改某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的,因为在任何一个操作步骤中,修改某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并发场景中就不见得了。如果一个线程修改了某个全局变量,那么其他线程未必可以马上知道这个改动。
  3. 有序性:对于一个线程的执行代码而言,我们从事习惯性的认为代码执行时是从前往后依次执行的,就一个线程而言,确实会这样,但是在并发时,程序的执行可能就会出现乱序,给人直观的感受就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重拍后的指令与原指令的顺序未必一致。

14. volatile关键字的作用以及实现原理?

  1. 保证共享变量的可见性:线程本身并不直接与主内存进行数据交互,而是通过线程的工作内存来完成相应的操作,这也是导致线程间数据不可见的本质原因,使用volatile关键字修饰的变量,任何线程对其进行操作都是在主内存中进行的,不会产生副本,从而保证共享变量的可见性。对volatile变量的写操作与普通变量的主要区别有两点:
    1.1 修改volatile变量会强制将修改后的值刷新到主内存;
    1.2 修改volatile变量后会导致其他线程工作内存中对应的变量值失效,因此,再读取该变量的时候就需要重新读取主内存中的值。
  2. 防止指令重排:JVM会对代码进行编译优化,导致代码可能并不是按照代码编写顺序执行的,而是按照JVM进行优化编译后的顺序执行的,执行重排对并发编程安全性有很大影响,所以提供了一些Happens-before规则定义一些禁止编译优化的场景,Volatile关键字规定了一个线程先去写一个Volatile变量,然后另一个线程再去读这个变量,那么这个写的操作结果一定对读的这个线程可见。

15. 内存屏障防止指令重排的实现原理?

硬件层面内存屏障分为Load Barrier 和 Store Barrier 即读屏障和写屏障:

  1. 对于Load Barrier来说,在指令前插入Load Barrier可以让缓存中的数据失效,强制从主内存中加载数据;
  2. 对于Store Barrier来说,在指令后插入Store Barrier可以让写入到缓存中的最新数据更新,并写入主内存让其他线程可见。
  3. 内存屏障的类型:
    3.1 LoadLoad屏障:执行顺序:Load1——>LoadLoad——>Load2;确保Load2及后续Load指令加载数据之前能访问到Load1加载的数据;
    3.2 StoreStore屏障:执行顺序:Store1——>StoreStore——>Store2;确保Store2以及后续Store指令执行前,Store操作的数据对其他线程可见;
    3.3 LoadStore屏障:执行顺序:Load1——>LoadStore——>Store2;确保Store2和后续Store指令执行前,可以访问到Load1加载的数据;
    3.4 StoreLoad屏障:执行顺序:Store1——>StoreLoad——>Load2;确保Load2和后续的Load执行读取之前,Store1的数据对其他线程是可见的。

16. CAS的实现原理?

  1. CAS的全程是Compare-and-swap,它是CPU并发原语,它的功能是判断某个位置的值是否是预期值,如果是则更新该值,这个过程是原子的, CAS包含三个操作数:内存位置(V)、预期原值(A)、新值(B),如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值,否则不会做任何处理,无论那种情况,它都会在CAS指令之前返回该位置的值,CAS说明了“我认为位置V应该包含值A,则将B放在这个位置,否则不更新该位置,只告诉我该位置的值即可”,通常将CAS用于同步的方式是从地址V读取A,执行多种计算来获得新值B,然后CAS将V值从A改为B,如果V处的值未同时更新,则CAS操作成功。.
  2. CAS并发原语体现在Java语言中就是sun.miscUnsafe类中的各个方法,调用miscUnsafe类中的CAS方法,JVM通过JNI帮助我们实现CAS汇编指令,这是一种完全依赖硬件的功能,通过它实现原子操作,由于CAS是一种系统原语,属于操作系统范畴,是有若干条指令构成,用于完成某个功能的过程,并且原语的执行是连续的,在执行过程中不允许中断,所以说CAS是一条原子指令,不会造成所谓的数据不一致的问题。

17. CAS存在的问题?

  1. ABA问题:当获得对象当前的数据后,在准备为修改为新值之前,对象的值被连续修改过多次后对象值恢复为了旧值,这样当前线程无法判断对象是否修改过。
    1.1 解决方案:JDK 1.5提供了AtomicStampedReference类来解决这个问题,它不仅维护了对象的值,还维护了一个时间戳,当它对应的值被修改过后,除了更新数据本身外,还必须更新时间戳,对象值和时间戳都必须满足期望值,才能成功写入。
  2. 循环时间开销过大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
    2.1 解决方案:JVM支持处理器提供的pause指令,使得效率有一定提升。
  3. 不能保证多个变量的原子操作:当一个共享变量执行操作时,我们可以使用循环CAS的方法来保证原子操作,但对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以使用锁保证原子性。
    3.1 解决方案:从JDK 1.5开始JDK提供了AtomicReference类保证引用对象之间的原子性,你可以把多个变量放在一个对象里进行CAS操作。

18. ReentrantLock和synchronized有哪些核心区别?

  1. 原始构成:ReentrantLock-它是JDK1.5之后提供的API层面的互斥锁类;synchronized-它是Java语言的关键字,是原生语言层面的互斥,需要JVM实现。
  2. 实现方式:ReentrantLock-API层面的加锁、解锁,需要手动释放锁;synchronized-通过JVM加锁、解锁。
  3. 代码编写:ReentrantLock-用户必须手动释放锁,如果没有主动释放锁,可能会导致死锁,需要lock()和unlock()配合try/finally语句块来实现;synchronized-不需要用户手动释放锁,当synchronized方法或者语句块执行完成后,系统会自动让线程释放对锁的占用,更加安全。
  4. 灵活性:ReentrantLock-因为是方法调用,可以跨方法,较为灵活;synchronized-整个方法或者synchronized语句块部分。
  5. 等待是否可中断:ReentrantLock-持有锁的线程,长期不释放锁的时候,正在等待的线程可以选择放弃等待;synchronized-等待不可中断,除非抛出异常。
  6. 是否公平锁:ReentrantLock-两者都可以,默认非公平锁,构造器可传入Boolean值,true为公平锁;false为非公平锁;synchronized-非公平锁。
  7. 条件Condition:ReentrantLock-通过多次new Condition可获得多个Condition对象,可以实现比较复杂的线程同步功能;synchronized-无。
  8. 提供高级功能:ReentrantLock-提供多种方法来监听当前锁的信息;synchronized-无。
  9. 便利性:ReentrantLock-需要手动申明加锁和释放锁;synchronized-使用比较简洁,由编译器保证锁的加锁和释放。
  10. 适用场景:ReentrantLock-提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步等,在资源竞争不激烈的情况下,性能稍微比synchronized差一点点,当竞争非常激烈的时候,synchronized的性能一下子能下降好几十倍,而ReentrantLock却还能维持常态;synchronized-资源竞争不激烈,偶尔会有同步的情况下,synchronized是很适合的,原因在于编译程序会尽可能的优化synchronized,另外可读性非常好。

多线程

1. 实现多线程的方法有哪些?

  1. 继承Thread类,重写run()方法;
  2. 实现Runnable接口,重新run()方法;
  3. 实现Callable接口,重写run()方法,通过FutureTask获取包装器获取返回值;
  4. 使用线程池。

2. 实现 Runnable 接口和继承Thread类实现多线程,哪种方式更好?

(实现 Runnable 接口)更好,继承Thread类,从代码架构去考虑,具体执行的任务也就是run方法中的内容,他应该和线程的创建、运行的机制也就是Thread类就是解耦的。所以不应该把他们混为一谈;继承Thread类,每次如果想新建一个任务,只能去新建一个独立的线程,而建立一个线程的损耗是比较大的,它需要创建、执行、销毁;而实现 Runnable 接口的方式,可以利用线程池之类的工具,来减小管理线程所带来的损耗,更加节约资源;由于Java不支持多继承,如果继承了Thread类了,那么这个类就无法继承其他类了,这样大大限制了程序的可扩展性。

3. callable和runnable有什么本质区别?

  1. Runnable接口run方法无返回值;Callable接口call方法有返回值,支持泛型。
  2. Runnable接口run方法只能抛出运行时异常,且无法捕获异常;Callable接口call方法允许抛出异常,可以捕获异常信息。

4. 线程池原理?

线程池原理:Java为了提升并发度,可以使用多线程共同执行,但是如果大量线程在短时间内被创建和销毁,会占用大量系统时间,影响系统效率,为了解决上述问题,Java引入了线程池,可以使创建好的线程在指定时间内,由系统统一管理,而不是在执行是创建,执行后销毁,从而避免频繁创建、销毁线程带来的系统开销。

5. 线程池核心参数?

  1. corePoolSize(线程池基本大小):当提交一个任务时,线程池会创建一个线程来执行该任务,即便其他空闲的基本线程能够执行新任务,也会创建线程,等到需要执行的任务数大于线程池基本大小时就不会创建了,如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  2. runnableTaskQueue(任务队列):用于保存等待执行任务的队列。
  3. maxinumPoolSize(线程池最大大小):线程池允许创建的最大线程数,如果任务队列满了,并且创建的线程数小于最大线程数,线程池会再创建新线程执行任务,如果使用了PriorityBlockQueue任务队列,那么这个参数没有什么效果。
  4. keepAliveTime:当前线程池数量超过keepAliveTime时,多余的空闲线程的存活时间,即多长时间内会被销毁。
  5. unit:keepAliveTime的单位。
  6. threadFactory(线程工厂):通过线程工厂为每个线程设置名字,用于Debug和定位问题。
  7. RejectExecutorHandler(拒绝策略):如果队列和线程池都满了,说明线程池处于饱和状态,那么必须采用一种策略处理提交的新任务,默认AbortPolicy:无法处理时,抛出异常,JDK 1.5以后又提供了以下策略:
    5.1 CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是任务提交线程的性能极有可能会急剧下降。
    5.2 DiscardOldestPolicy:丢弃队列最老的一个任务,就是即将被执行的任务,并尝试提交当前任务;
    5.3 DiscardPolicy:该策略默默地丢弃无法处理的任务,不做任何处理。如果允许任务丢弃,这是最好的一种方案;
    5.4 可以通过实现RejectExecutorHandler接口自定义策略。

6. Java中的阻塞队列?

  1. ArrayBlockingQueue:由数组结构构成的有界阻塞队列此队列按照FIFO的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。
  2. LinkedBlockingQueue(两个独立锁提高并发):由链表结构组成的有界阻塞队列,默认类似无限大小的容量(Integer.MAX_VALUE);同ArrayBlockingQueue类似,此队列按照FIFO的原则对元素进行排序。而LinkedBlockingQueue之所以能够高效的处理并发数据,还应为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行的操作队列中的数据,以此来提高整个队列的并发性能。
  3. PriorityBlockingQueue(compareTo排序实现优先):支持优先级排序的无界阻塞队列;默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
  4. DelayQueue:支持延时获取元素的无界阻塞队列;队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用到一下应用场景:
    4.1 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,是一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
    4.2 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦DelayQueue中获取到任务就开始执行。
  5. SynchronousQueue:不存储元素的阻塞队列;每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适用于传递性场景,比如一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。
  6. LinkedTransferQueue:由链表结构组成的无界阻塞队列;相对于其他队列,LinkedTransferQueue多了tryTransfer方法和transfer方法:
    6.1 transfer方法:如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以吧生产者传入的元素立即transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等待该元素被消费者消费了才返回。
    6.2 tryTransfer方法:用来试探下生产者是否能直接传递给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立刻返回。而transfer方法必须等到消费者消费才返回。
  7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列;所谓双向队列指的是可以从队列的两端插入和移除元素,双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少一半的竞争。在初始化LinkedBlockingDeque时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

7. 线程池核心流程?

线程池核心流程:

  1. 任务提交后,先判断核心线程池中创建的线程是否已经达到线程池的基本大小,如果没有达到,则创建新的线程执行该任务,如果达到了,则判断核心线程池中是有空闲线程,如果有,则指派一个空闲线程去执行该任务;
  2. 如果线程池中的线程已经达到最大核心线程数,并且这些线程都处于繁忙状态,就会把新提交的任务放到等待队列中,如果等待队列又满了,那么检查一下当前线程数是否达到线程池最大线程数,如果还未达到,就继续创建线程执行任务;
  3. 如果已达到线程池最大线程数,那么任务就会交给RejectExecutorHandler(拒绝策略)来决定这个任务应该怎么处理。
    这里是引用

8. 如果corePoolSize = 0会怎么样?

在JDK1.6之前,如果corePoolSize = 0时,则会判断等待队列的容量,如果还有容量,则排队且不会创建新线程;而在JDK1.6版本之后,如果corePoolSize = 0,提交任务时如果线程池为空,则会立即创建一个线程来执行任务(先排队再获取),如果提交任务的时候,线程池不为空,则优先在等待队列中排队,只有队列满了才会创建新线程。所以,优化在于,在队列没有满的这段时间,会有一个线程在消费提交任务;1.6之前的实现是,必须等待队列满了之后,才开始消费。

9. keepAliveTime = 0会怎么样?

在JDK1.8中,keepAliveTime = 0表示非核心线程执行完立即终止;默认情况下,keepAliveTime 小于0,初始化的时候才会报错,但如果allowsCoreThreadTimeOut,keepAliveTime必须大于0,不然初始化报错。

10. 线程池创建之后,是否立即创建线程?

不会,从源码上看,在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是等到有任务提交时才会启动,除非调用prestartCoreThread/prestartAllCoreThread事先启动核心线程。

11. 核心线程永远不会销毁吗?

在JDK1.6之前,线程池会尽量保持corePoolSize核心线程,即使这些线程闲置了很长时间,而从JDK1.6开始,提供放allowsCoreThreadTimeOut,如果传参为true,则允许闲置的核心线程被终止。

12. 空闲线程过多会有什么问题?

线程池保持空闲的核心线程是它的默认配置,一般来讲是没问题的,因为它占用的内存一般不大,怕的是业务代码中使用ThreadLocal缓存数据过大又不清理;如果应用线程处于高位,那么需要观察一下YoungGC的情况,估算一下Eden大小是否足够,如果不够的话,可能要谨慎的创建新线程,并且让空闲的线程终止,必要时候,可能需要对JVM进行调参等。

13. shutdown和shutdownNow的区别?

shutdown:平稳关闭,等待所有已添加到线程池中的任务执行完毕在关闭;
shutdownNow:立即关闭,停止正在执行的任务,并返回队列中未执行的任务。

14. 线程池需不需要关闭?

一般来讲,线程池的生命周期跟随服务的生命周期,如果一个服务停止服务了,那么需要调用shutdown方法进行关闭,所以Java以及一些中间件的源码中,是封装在Service的shutdown方法内的。所以不需要特意的关闭线程池。

15. 线程池初始化示例?

 private static final ThreadPoolExecutor pool;

    static {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("po-detail-pool-%d").build();
        pool = new ThreadPoolExecutor(4, 8, 60L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(512),
            threadFactory, new ThreadPoolExecutor.AbortPolicy());
        pool.allowCoreThreadTimeOut(true);
    }

16. 常见线程池以及应用场景?

  1. newCacheThreadPool:
    1.1 ThreadPoolExecutor;corePoolSize为0;maxinumPoolSize为Integer.MAX_VALUE,KeepAliveTime为60L;unit为TimeUnit.SECONDS;WorkQueue为SynchronizedBlockQueue;
    1.2 处理流程:当有新任务到来时,则插入到SynchronizedBlockQueue队列中,由于SynchronizedBlockQueue是同步队列,因此会在线程池中寻找可用线程执行。如果有可用线程则执行,如没有可用线程,则创建一个线程执行任务,若线程空闲时间超过指定大小,线程则被销毁;
    1.3 适用场景:执行很多短期异步的小程序或者负载较轻的服务器。
  2. newFixThreadPool:
    2.1 ThreadPoolExecutor;corePoolSize为n;maxinumPoolSize为n,KeepAliveTime为无限;unit为TimeUnit.MILLISECONDS;WorkQueue为LinkedBlockQueue;
    2.2 处理流程:创建可容纳固定线程的线程池,每个线程的存活时间是无限的,当线程池满了就不再创建线程,如果线程池所有线程处于繁忙状态,新任务会进入到阻塞队列;
    2.3 适用场景:执行长期任务,性能会好很多。
  3. newSingleThreadPool:
    3.1 FinalizableDetegateExecutorService包装的ThreadPoolExecutor对象,corePoolSize为1;maxinumPoolSize为1,KeepAliveTime为0L;unit为TimeUnit.MILLISECONDS;WorkQueue为LinkedBlockQueue;
    3.2 处理流程:创建只有一个线程的线程池,且线程存活时间是无限的,当该线程繁忙时,新任务会进入到阻塞队列;
    3.3 适用场景:一个任务一个任务执行的场景。
  4. newScheduledThreadPool:
    4.1 ScheduledThreadExecutor对象,corePoolSize为n;maxinumPoolSize为Integer.MAX_VALUE,KeepAliveTime为0L;unit为TimeUnit.NANOSECONDS;WorkQueue为DelayQueue(一个按照超时时间升序排列的队列结构);
    4.2 处理流程:创建一个固定大小的线程池,线程池中的线程存活时间是无限的,线程池可以支持定时及周期性任务执行。
    4.3 适用场景:周期性执行任务的场景。

线程安全

1. 死锁形成?

死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都无法推进下去,此时成系统处于死锁转台或系统产生了死锁,这些永远处于互相等待的线程成为死锁线程。多线程同时被阻塞,它们中的一个或者全部都在等待某个资源的释放。由于线程被无限期的阻塞,因此程序不可能正常终止。行程死锁的四个必要条件:

  1. 互斥条件:线程对于所分配到的资源具有排他性,即一个资源只能被一个线程占用,直到被该线程释放。
  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程在已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才能释放资源。
  4. 循环等待条件:当发生死锁时,所有等待线程必定会线程一个死循环,造成永久阻塞。

2. 如何避免线程死锁?

  1. 破坏互斥条件:这个条件没有办法破坏,因为使用锁本来就是想让线程之间互斥(临界资源需要互斥访问);
  2. 破坏请求和保持条件:一次性申请申请所有资源;
  3. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源,如果申请不到,可以主动释放它占用的资源;
  4. 破坏循环等待条件:靠按顺序申请资源来预防,按某一顺序申请资源,释放资源则反顺序释放。破坏循环等待条件。

3. 什么是线程安全?

当多个线程访问某个类时,不管运行时环境采s用何种调度方式或者这些线程将如何交替执行,并且在主调用代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就成这个类是线程安全的,简而言之,就是程序总能按照单线程那样,不出预料的运行,那么就说明程序是线程安全的,要保证线程安全,有如下三种思路:

  1. 在单线程环境下编程;
  2. 在多线程环境下,不存在对共享变量的写操作;
  3. 在多线程环境下,存在对共享变量的修改操作时,需要同时满足可见性、原子性和有序性。

4. 保证集合线程安全的几种方式?

  1. 使用HashTable、Stack、Vector等通过Sychronized关键字进行加锁的集合;这些集合性能比较低下,因为它们的实现基本就是将put、get、size等方法上加上“synchronized”。简单来说,这就是导致所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发效率。
  2. 可以调用Conllections工具类提供的包装方法,来获取一个包装容器,但是他们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下;
  3. J.U.C提供的并发容器,比如ConcurrentHashMap、CopyOnWriteArrayList等;
  4. J.U.C提供的各种安全队列,比如ArrayBlockingQueue、SychronousQueue等。

4. 单例模式与线程安全?


/**
 * @Description [单例的几种实现方式与线程安全]
 * @Date 2020/12/9 13:44
 **/
public class SingletonDemo {

    /**
     * 懒汉式,线程不安全,最大的问题是不支持多线程,没有同步锁,严格意义上不算是单例模式,这种方式lazyLoading明显,不要求线程安全,
     * 在多线程不能正常工作。
     */
    public static class LazyManSingleton {

        private static LazyManSingleton singleton;

        private LazyManSingleton() {
        }

        public static LazyManSingleton getLazyManSingleton() {
            if (singleton == null) {
                singleton = new LazyManSingleton();
            }
            return singleton;
        }

    }

    /**
     * 线程安全的懒汉式,多线程安全,容易实现,具有很好的lazyLoading,在多线程下也能很好工作,效率低,多说情况不需要同步,第一次调用
     * 才初始化,避免内存浪费,必须加锁synchronized才能保证单例,但加锁会影响效率
     */
    public static class SynchronizedLazyManSingleton {

        private static SynchronizedLazyManSingleton singleton;

        private SynchronizedLazyManSingleton() {
        }

        public static synchronized SynchronizedLazyManSingleton getSynchronizedLazyManSingleton() {
            if (singleton == null) {
                singleton = new SynchronizedLazyManSingleton();
            }
            return singleton;
        }

    }

    /**
     * 饿汉式,它基于ClassLoader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,所以没有达到lazyLoading的效果,线程安全,
     * 没有使用锁,执行效率高,但类加载时就初始化,浪费空间。
     */
    public static class HungerManSingleton {

        private static HungerManSingleton singleton = new HungerManSingleton();

        private HungerManSingleton() {
        }

        public static HungerManSingleton getHungerManSingleton() {
            return singleton;
        }

    }

    /**
     * 双检锁式单例模式,采用双锁机制,安全且在多线程情况下能保证高性能。
     */
    public static class DoubleCheckedLockingSingleton {
)
        private static volatile DoubleCheckedLockingSingleton singleton;

        private DoubleCheckedLockingSingleton() {
        }

        public static DoubleCheckedLockingSingleton getDoubleCheckedLockingSingleton() {
            if (singleton == null) {
                synchronized (DoubleCheckedLockingSingleton.class) {
                    if (singleton == null) {
                        singleton = new DoubleCheckedLockingSingleton();
                    }
                }
            }
            return singleton;
        }
    }

    /**
     * 登记式单例模式,这种方式能达到双检锁方式一样的功效,单丝实现更简单。对静态域使用延迟初始化,应使用这种方式而不是
     * 双检锁方式,双检锁方式可在实例域需要延迟初始化时使用,这种凡是同样利用了ClassLoader机制来保证初始化instance时只用一个线程。
     * 它和双检锁式单例模式不同的是:双检锁式单例模式只要Singleton类被装载了,那么instance就和被实例化,没有达到lazyLoading的效果,
     * 而这种方式是Singleton类被装载了,instance不一定被初始化,因为SingletonHolder类没有被主动调用,只有通过显示调用getInstance方法
     * 时,才能实例化。
     */
    public static class RegisterSingleton {

        private static class SingletonHolder {
            private static final RegisterSingleton instance = new RegisterSingleton();
        }

        public RegisterSingleton() {
        }

        public static final RegisterSingleton getRegisterSingleton() {
            return SingletonHolder.instance;
        }
    }

5. SpringMVC中的bean是否是单例?什么是有状态bean和无状态bean?bean的作用域?

  1. Spring中的Bean默认是单例模式,框架并没有对bean进行多线程的封装处理。
  2. 有状态bean和无状态bean:
    2.1 有状态bean(Stateful Bean):有实例变量的数据,可以保存数据,是非线程安全的。每个用户有自己特有的一个实例,在对象的生命周期内,Bean保存了实例信息,即“有状态”;一旦对象灭亡(调用结束),Bean的生命周期也结束。即会得到一个初始的bean。例如:Spring中的DaoBean、ServiceBean、ControllerBean等。
    2.2 无状态bean(Stateless Bean):没有实例变量的对象,不保存数据,是不变类,是线程安全的。Bean一旦实例化就会被加入到对象池中,各个用户可以公用,即使用户已经消亡,Bean的生命周期也不一定结束,它可能依然存在于对象池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态Bean,但无状态Bean并非总是没有状态的,如果它存在自己的属性(变量),那么这些变量就会受所有调用它的用户的影响。例如:Struts2中的Action等。
  3. bean的作用域:
    3.1 singleton:单例,Spring默认作用域;
    3.2 prototype:原型,每次创建一个新对象,Struts2默认作用域;
    3.3 request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下;
    3.4 session:会话,同一个会话共享一个实例,不同会话使用不同实例;
    3.5 global-session:全局会话,所有会话共享一个实例。

6. SpringMVC如何解决单例bean线程不安全的问题?

  1. 尽量不要在@Controller、@Service等容器你定义变量;
  2. 一定要定义变量的话,用ThreadLocal来封装。

7. 多并发场景下ThreadLocal与事务相结合结合的原理分析,ThreadLocal内存泄露问题?

/**
 * 申请单票券生成任务
 */
public class GenSaleTicketTask implements TaskCommon {

    Logger log = LoggerFactory.getLogger(GenSaleTicketTask.class);

    // 业务逻辑service对象
    private PlatformTransactionManager transactionManager;
    private ThreadLocal<TransactionStatus> transactionStatus = new ThreadLocal<TransactionStatus>();

    @Override
    public void execute(TicketTask task) {
        try {
            beginTransaction();
            // 生成优惠券逻辑
            commitTransaction();
            // 根据票券申请单编号获取新生成的票券券码列表
        } catch (Exception e) {
            log.error("票券销售申请单applyCode:" + saleApplyId + "生成券码异常", e);
        }
    }

    /**
     * 开始事务处理
     */
    private void beginTransaction() {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status = getTransactionManager().getTransaction(definition);
        getTransactionStatus().set(status);
    }

    /**
     * 结束事务处理
     */
    private void commitTransaction() {
        TransactionStatus status = null;
        try {
            status = getTransactionStatus().get();
            getTransactionManager().commit(status);
        } catch (Exception e) {
            getTransactionManager().rollback(status);
        } finally {
            getTransactionStatus().remove();
        }
    }

    public PlatformTransactionManager getTransactionManager() {
        return transactionManager;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public ThreadLocal<TransactionStatus> getTransactionStatus() {
        return transactionStatus;
    }
    public void setTransactionStatus(ThreadLocal<TransactionStatus> transactionStatus) {
        this.transactionStatus = transactionStatus;
    }

}

https://cloud.tencent.com/developer/article/1457903

  1. 基础定义:ThreadLocal提供线程本地变量,每个线程拥有本地变量的副本,各个线程之间的变量互不干扰。ThreadLocal实现在多线程环境下保证变量的线程安全。每个Thread内有自己的实例副本,且该副本只能由当前Thread使用,其他Thread不可访问,那就不存在多线程共享问题。它与普通变量的区别在于,每个使用该线程变量都会初始化一个完全独立的实例副本,ThreadLocal变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal相对应的实例副本都可以被回收。总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。(ThreadLocal解决的是线程安全问题,并不是解决线程线程协作问题,所以ThreadLocal无法解决共享对象的更新问题!)
  2. 核心成员:每个Thread类中有一个成员变量threadLocals,这成员变量是一个ThreadLocalMap对象,每个ThreadLocalMap对象底层事项位Entry[]数组,每个Entry[]键值对的键是一个ThreadLocal对象,值是一个Object对象,这个ThreadLocalMap类提供了以下方法:
    2.1 set()方法(储存对象值):当调用set方法时,ThreadLocal对象会获取到当前线程的引用,根据这个引用获取到线程的成员变量ThreadLocalMap对象,然后调用ThreadLocalMap对象的set方法存储到这个Map中,看似是把数据存储在了ThreadLocal对象中,但其实是把数据存储在当前线程的ThreadLocalMap中,而threadLocal只是用来在线程中查找这个对象而已。
    2.2 get()方法(获得对象值):当调用get方法时,,ThreadLocal对象会获取到当前线程的引用,然后获取这个线程的对象成员ThreadLocalMap,以ThreadLocal引用位键,取出键值对对应的值。如果成功取出,则直接返回这个键值对应的值,如果查找键值对失败,则调用setInitialValue()方法,重新在ThreadLocalMap中添加这个数据对象,然后返回值。
    2.3 remove方法(移除对象值):remove方法也是先获取当前线程对象引用,然后获取这个线程对象ThreadLocalMap,最后一处一ThreadLocal引用为键的键值对。注意:由于ThreadLocalMap存在“哈希冲突”问题,由于ThreadLocalMap的结构非常简单,只是一个数组存储,并没有链表结构,当出现“哈希冲突”时,采用线性查找的方式,所谓线性查找,就是根据初始key的HashCode值确定元素在table数组中的位置,如果发现这个位置上已经有元素占用,则利用固定的算法寻找一定步长的下一个位置(调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647),依次判断,直至找到能够存储的位置,如果产生多次hash冲突,处理起来就没有HashMap的效率高了,为了避免hash冲突,使用尽量少的threadLocal变量。
  3. 原理分析:ThreadLocal的set()、get()、remove()方法实际上是在操作当前线程成员变量threadLocals,这个变量的类型是一个ThreadLocalMap,所以向ThreadLocal添加值实际上是把值添加到当前线程中,从ThreadLocal对象中取值实际上是从当前线程中取值,从ThreadLocal对象中移除值实际上是把这个值从当前线程中移除,所以一切操作都是在操作当前线程中的值,ThreadLocal相当于一个索引,那么对ThreadLocal中存储的对象进行操作就是线程安全的了,因为始终都是操作的当前线程,不实际到其他线程,所有不会出现线程安全问题。
  4. 适用场景:
    4.1 每个线程需要有自己单独的实例
    4.2 实例需要在多个方法中共享,但是不希望被多线程共享:
    4.2.1 存储用户session;
    4.2.2 解决线程安全问题;
    4.2.3 Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
  5. 内存泄漏问题:
    5.1 为什么Entry中键ThreadLocal对象时弱引用?
    5.2.1 如果键 ThreadLocal对象是强引用,当一条线程中的ThreadLocal使用完毕,这个ThreadLocal就会被垃圾收集器回收,本应该销毁当前线程的ThreadLocal,由于Entry数组始终对当前线程的ThreadLocal强引用的关系,GC无法销毁当前ThreadLocal,此时会导致Entry中的键值对永远无法释放导致内存泄露,而使用弱引用时,那么下一次垃圾回收时就会被回收。
    5.2.2 为了尽快回收这个线程变量,因为这个线程变量可能使用场景不是特别多,所以希望使用完后尽快释放掉。因为线程拥有的资源越多,就越臃肿,线程切换的开销就越到,所以希望尽量降低线程拥有的资源量。
    5.2 由于弱引用的关系当键值Key被GC回收变为null,此时Value还存在就会导致Value始终无法被回去,此时Entry数组就会存在很多Key为null的键值对,此时还会造成内存泄漏?当ThreadLocal在get()、set()、remove()的是否会去检查Entry数组中是否有Key为null的键值对,如果有就会被删除。(set方法使用replaceStaleEntry()、remove方法使用expungStaleEntry())
    5.3 如果一个线程运行周期较长,而且将一个大对象放入ThreadLocalMap后便不再使用该对象,仍有可能出现内存泄露问题?需要手动在完成ThreadLocal后手动调用remove()方法,从而避免内存泄露。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java多线程和高并发Java中非常重要的概念和技术,它们可以让Java程序更加高效地利用计算机多核CPU的性能,提升程序的并发能力,提高程序的性能和响应速度。 Java多线程机制是基于线程对象的,它允许程序同时执行多个任务,从而提高程序的并发能力。Java中的线程有两种创建方式:继承Thread类和实现Runnable接口。其中继承Thread类是比较简单的一种方式,但是它会限制程序的扩展性和灵活性,因为Java只支持单继承。而实现Runnable接口则更加灵活,因为Java支持多实现。 Java的高并发编程主要包括以下几个方面: 1. 线程池技术:线程池技术是Java中非常重要的高并发编程技术,它可以实现线程的复用,减少线程创建和销毁的开销,提高程序的性能和响应速度。 2. 锁机制:Java中的锁机制包括synchronized关键字、ReentrantLock锁、ReadWriteLock读写锁等,它们可以保证多个线程之间的互斥访问,避免数据竞争和线程安全问题。 3. CAS操作:CAS(Compare and Swap)操作是Java中一种非常高效的并发编程技术,它可以实现无锁并发访问,避免线程之间的互斥和阻塞,提高程序的性能和响应速度。 4. 并发集合类:Java中的并发集合类包括ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList等,它们可以实现线程安全的数据访问和操作,有效地提高程序的并发能力。 总之,Java多线程和高并发Java编程中非常重要的概念和技术,掌握这些技术可以让Java程序更加高效地利用计算机多核CPU的性能,提升程序的并发能力,提高程序的性能和响应速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值