多线程常见面试题

 

synchronized与Lock的区别

(1)synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的,通过队列同步器AQS实现。

(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现发象生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

(3)Lock可以让等待锁的线程响应中断,通过interrupt方法。而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;如果遇到IO操作比较耗时,会影响运行效率。

(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。使用 lock.tryLock()获取true或则false判断判断是否拿到锁。

(5)Lock可以提高多个线程进行读操作的效率。ReadWriteLock,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。提高处理效率。

(6)Lock的优点:公平锁、能被中断地获取锁、尝试非阻塞的获取锁、等待唤醒机制的多个条件变量、可以在不同的范围,以不同的顺序获取和释放锁、超时获取锁。

(7)synchronized修饰非静态方法时锁的是当前实例对象(this);修饰静态方法时锁的是Class类对象;修饰类时,作用的是这个类的所有对象。若同步块内出现异常则会释放锁。

 

sleep() 和 wait()的区别

(1)sleep()方法是Thread的静态方法,而wait是Object实例方法

(2)wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;

(3)sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
 

乐观锁和悲观锁

悲观锁

        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

       总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

       从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
 

volatile关键字

(1)volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。

(2)volatile 禁止指令重排序并保证可见性,并不保证原子性,synchronized可以保证原子性,也可以保证可见性,volatile是synchronized的轻量级实现,性能较好。

(3)volatile修饰后,CPU会加上Lock前缀指令,处理器缓存会写到主存,线程的本地内存失效,别的线程只能从主存中读取数据。而本地内存的值会立马刷新到主存中去。

(4)volatile 还能提供原子性,如读 64 位long 和 double 是非原子操作,但 volatile 类型的 double 和 long 就是原子的。因为对这两种类型的写是分为两部分。

(5)volatile并不能保证原子性,如count++ 。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。JDK8中的LongAdder对象比AtomicLong性能更好(减少乐观锁的重试次数)。

内存屏障

     volatile 提供内存屏障,在写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

内存屏障会提供3个功能:
   

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内 存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

内存屏障有三种类型和一种伪类型:

(1)lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
(2)sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。
(3)mfence,即全能屏障,具备ifence和sfence的能力。
(4)Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能。

happen-before原则

volatile 提供 ,确保一个线程的修改能对其他线程是可见的。

(1)程序顺序规则:一个线程中的每个操作,先行发生于该线程中的任意后序操作。
(2)监视器锁规则:对一个锁的解锁,先行发生于随后对这个锁的加锁
(3)volatile变量规则:对一个volatile域的写,先行发生于任意后续对这个volatile域的读。
 (4)传递性:如果A先行发生于B,而B又先行发生于C,那么A先行发生于C
(5)线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
(6)线程终止规则 :如果线程A执行B.join()方法并成功返回,那线程B中的任意操作先行发生于线程A
(7)线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
(8)可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组

 

ThreadLocal的原理

(1)ThreadLocal提供了线程局部 (thread-local) 变量。而访问变量的每个线程都有自己的局部变量,它独立于变量的初始化副本(线程本地变量)ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

(2)适用场景:用于解决多线程中数据因并发产生不一致问题。

(3)内存泄露:只要该线程对象被gc回收,就不会出现内存泄露,但在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了内存泄露。

(4)ThreadLocal与线程同步无关,ThreadLocal对象建议使用static修饰

(5)在ThreadLocal类中有一个静态内部类ThreadLocalMap(线程的局部变量空间),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

 

死锁的产生

指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。

(1)发生死锁的四个必要条件

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

(2)解决死锁的方法

   预防死锁:破坏产生死锁的四个必要条件中的一个或者几个
   避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态
   检测死锁:允许系统在运行过程中发生死锁。但可及时地检测出死锁的发生
   解除死锁:当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来
   银行家算法

 

死锁实例:

public class DeadLock {
    public static final String obj1 = "obj1";
    public static final String obj2 = "obj2";

    public static void main(String[] args) {
        Thread t1 = new Thread(new Lock1());
        Thread t2 = new Thread(new Lock2());
        t1.start();
        t2.start();
    }
}

class Lock1 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Lock1 running");
            synchronized (DeadLock.obj1) {
                System.out.println("Lock1 lock obj1");
                Thread.sleep(3000);
                synchronized (DeadLock.obj2) {
                    System.out.println("Lock1 lock obj2");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Lock2 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Lock2 running");
            synchronized (DeadLock.obj2) {
                System.out.println("Lock2 lock obj2");
                Thread.sleep(3000);
                synchronized (DeadLock.obj1) {
                    System.out.println("Lock2 lock obj1");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

CAS算法

(1)CAS,即比较并交换,是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。通过unsafe类的compareAndSwap方法实现的,参数是要修改对象,对象的偏移量,修改前的值,预期值。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

(2)CAS 是一种无锁的非阻塞算法的实现,当且仅当内存值V 的值等于预期值A 时,CAS 通过原子方式用更新值B 来更新V 的值,否则不会执行任何操作

CAS的缺点

ABA问题,即在a++之间,a可能被多个线程修改过了,只不过回到了最初的值,这时CAS会认为a的值没有变。解决方法:增加修改计数或引入版本号,AtomicStampedReference:预期引用和预期标志都与当前相等,则更新。
循环时间长开销大,只能保证一个共享变量的原子操作
原子类都是通过CAS实现的,如AtomicInteger,常用方法getAndAdd、getAndIncrement、addAndGet()等

CAS 和 synchronize的区别?

CAS是乐观锁,不需要阻塞,硬件级别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。
使用场景不同,线程冲突严重时CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严重仍然有较高的吞吐量,因为线程都被阻塞了,不会占用CPU。

 

 

 

线程池

   (1) 线程池:将创建的线程对象放在一个容器中,用完的线程也放到这个容器中,用户使用的时候不是开辟一个新的线程,而是直接到这个容器中去获取已经创建好的线程,这样的存放线程的容器就是线程池。

   (2)为什么使用线程池:新线程的创建,会带来时间的开销,通过线程池可以节约开辟新线程的时间,提高响应速度,同时也便于对线程进行管理。线程是稀缺资源,合理的使用线程池对线程进行统一分配、调优和监控;多线程可以解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力

  (3)线程池的处理流程:提交任务先给核心线程池,若核心线程池未满,创建任务,否则放入等待队列,若等待队列未满就将任务存储在等待队列中,否则放入线程池中,若线程池未满创建任务,否则执行拒绝策略。

 (4)常用线程池:newCachedThreadPool  newFixedThreadPool  newSingleThreadPool  newScheduledThreadPool。

 (5)拒绝策略:抛异常(默认)、直接运行该任务、丢弃队列最前端任务执行当前任务,丢弃该任务。

Executors工厂类提供四个方法

(1)newFixedThreadPool方法 

       创建固定大小的线程池,核心线程数等于最大线程数,使用LinkedBlockingQuene,预热后,线程池中的线程数达到corePoolSize,新任务将在无界队列中等待,不会拒绝任务所以最大线程数是一个无效参数。

(2)newCachedThreadPool() 方法 

        大小无界的线程池,适用于执行很多的短期异步任务的小程序,使用不存储元素的SynchronousQueue作为阻塞队列,默认缓存60s,最大线程数是最大整型值。

(3)newSingleThreadExecutor方法

        初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,使用LinkedBlockingQueue作为阻塞队列。

  (4)newScheduledThreadPool方法

         创建固定大小的线程,可以延迟或定时的执行任务,当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或scheduleWith- FixedDelay()方法时,向DelayQueue添加一个ScheduledFutureTask,线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

       总结:使用线程池一般不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,因为这样可以更加明确线程池的运行规则,规避资源耗尽的风险,因为FixedThreadPool和SingleThreadPool允许请求队列的长度为Integer.MAX.VALUE,可能会堆积大量请求,从而导致OOMCachedThreadPool和ScheduledThreadPool允许创建线程的数量为Integer.MAX.VALUE,可能会创建大量的线程,从而导致OOM。


 

 

 

阻塞队列

 (1)BlockingQueue提供了线程安全的队列访问方式,阻塞队列实现的原理:使用通知模式实现。即当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空

 (2)AQS是构建锁的基础框架,通过内置的FIFO队列来完成资源获取线程的排队工作,其中内部状态state,头节点和尾节点,都是通过volatile修饰,保证了多线程之间的可见。子类重写tryAcquire和tryRelease方法,通过CAS指令修改状态变量state,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。

(3)一共有7种阻塞队列

        ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列,底层使用了Condition来实现
        LinkedBlockingQueue :一个由链表结构组成的无界阻塞队列,默认容量Integer.MAX_VALUE
       PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列,当调用take方法时才去排序
       SynchronousQueue:一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素
       DelayQueue:一个使用优先级队列实现的无界阻塞队列,队列中的元素必须实现Delay接口,应用场景:缓存系统、定时任务调度
      LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
      LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

 

   生产者和消费者模型的作用是什么?

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值