多线程面试总结

总结:

每个对象有一个监视器锁monitor,线程进入同步方法时尝试获取monitor的所有权,其他线程进入阻塞状态。该线程释放monitor的所有权后其他线程重新尝试获取monitor的所有权。

只能有一个线程对同步监视器加锁

1、多线程的问题案例

两个取钱线程,取出来剩余金额为负数

 

2、synchronize优化多线程

我们选用共享资源(account对象)作为同步监视器, 只有一个线程可以获得同步监视器的锁定。

方式一:在线程里对对象加锁

方式二:在对象中对方法加锁 

 3、同步锁 Lock实现多线程同步

 4、线程死锁

案例:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

结果:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

结果说明:

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

死锁的四个必要条件。

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

3. 如何避免线程死锁?

破坏循环等待:将系统中的所有资源统一编号,进程在可在任何时候提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。

我们对上面的代码进行修改,将线程 2 的代码修改成下面这样就不会产生死锁了。

 new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();

结果:

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

线程通信

(1)、synchronize和lock有什么区别?底层都是如何实现的?synchronize是不是可重入锁?

AQS队列到底是什么?

区别:

a、synchronized原始采用的是CPU悲观锁机制,通过monitor对象来完成。而Lock用的是乐观锁方式,采用AQS+CAS的方式进行保证线程安全。所以lock在大量的线程同步下的效率会高。

b、锁的释放方面,synchronized是由jvm来进行控制的,Lock是需要自己释放锁的。

c、是否可以中断:synchronize不可中断,ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

d、是否公平锁:synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。

【“公平锁和非公平锁的区别在于获取锁的顺序。公平锁是按照申请锁的顺序,让线程直接进入队列排队,只有队列中的第一个线程才能获取锁。优点是所有线程都能获取到资源,不会阻塞在队列中。缺点是吞吐量会下降,因为其他线程可能会阻塞,而且CPU唤醒阻塞线程的开销也会增加。非公平锁是多个线程尝试获取锁时,会直接进入等待队列,如果能获取到,就直接获取到锁。优点是可以减少CPU唤醒线程的开销,整体的吞吐效率会稍高】

共同点:

可重入:synchronized和lock都是可重入锁。

AQS:

AQS全称:AbstractQueuedSynchronizer,抽象队列式同步器

AQS是一个抽象类,它定义了一套多线程访问共享资源的同步器框架。通俗解释,AQS就像是一个队列管理员,当多线程操作时,对这些线程进行排队管理。

AQS主要通过维护了两个变量来实现同步机制的。AQS使用一个volatile修饰的私有变量state来表示同步状态,当state=0表示释放了锁,当state>0表示获得锁。

AQS通过内置的FIFO同步队列,来实现线程的排队工作。如果线程获取当前同步状态失败,AQS会将当前线程的信息封装成一个Node节点,加入同步队列中,并且阻塞该线程,当同步状态释放,则会将队列中的线程唤醒,重新尝试获取同步状态。

(2)、synchronize锁对象和锁方法有什么区别?

Synchronized方法锁、对象锁、类锁区别 - 不懂就查 - 博客园

synchronized的底层实现原理 - 望川拓 - 博客园

a、synchronize作用在代码块上,通过指令monitorenter和monitorexit来完成。每个对象有一个监视器锁:monitor。线程执行monitorenter指令时尝试获取monitor的所有权,其他线程进入阻塞状态。执行monitorexit的线程释放monitor的所有权,其他线程重新尝试获取monitor的所有权。

b、作用在方法上时通过ACC_SYNCHRONIZED标示符来指定线程锁定了monitor对象。

JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

(3)、悲观锁乐观锁的区别和应用。乐观锁的CAS机制。AtomicInter的底层实现。

Java:CAS(乐观锁) - 简书

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

乐观锁CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值(这个过程中可能被其他线程修改了)相同时,才会将内存地址V对应的值修改为B。

(4)、线程间通信的方式

信号量机制

synchronize中是wait,notifyAll

lock中是await和signalAll

(5)、threadLocal

每个线程一个新的变量

ThreadLocal的数据结构:ThreadLocal是一个类里面用Thead的ThreadLocalMap属性进行存储数据。其中key是ThreadLocal对象的弱引用,value是存储的数据。

ThreadLocal容易出现内存泄漏的原因:ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

使用 ThreadLocal 一次解决老大难问题 - 知乎

使用案例 1、每个线程需要一个独享对象,例如SimpleFormatDate,SimpleFormatDate是线程不安全的,里面有calcaulate.set操作,当多线程访问时会出现冲突。所以可以将SimpleFormatDate封装为ThreadLocal类型。

(6)、volatile

简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。二是禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致

/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值

加了volatile之后,2个线程之间的数据就可以及时可见。保证2个线程之间,变量的可见性。 volatile并不能解决加锁的问题。

(7)、线程池的原理,核心参数的含义,有哪些拒绝策略

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

corePoolSize:线程池的大小。线程池创建之后不会立即去创建线程,而是等待线程的到来。当当前执行的线程数大于该值时,线程会加入到缓冲队列;
maximumPoolSize:线程池中创建的最大线程数;
keepAliveTime:空闲的线程多久时间后被销毁。默认情况下,该值在线程数大于corePoolSize时,对超出corePoolSize值得这些线程起作用。
unit:TimeUnit枚举类型的值,代表keepAliveTime时间单位,可以取下列值:
TimeUnit.DAYS; //天
  TimeUnit.HOURS; //小时
  TimeUnit.MINUTES; //分钟
  TimeUnit.SECONDS; //秒
  TimeUnit.MILLISECONDS; //毫秒
  TimeUnit.MICROSECONDS; //微妙
  TimeUnit.NANOSECONDS; //纳秒
workQueue:阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略,有以下取值:
  ArrayBlockingQueue;
  LinkedBlockingQueue;
  SynchronousQueue;

SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者。使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行。其他阻塞队列会被延迟执行。


  threadFactory:线程工厂,是用来创建线程的。默认new Executors.DefaultThreadFactory();
handler:线程拒绝策略。当创建的线程超出maximumPoolSize,且缓冲队列已满时,新任务会拒绝,有以下取值:
  ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
  ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
  ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
  ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

线程池按以下行为执行任务
    1. 当线程数小于核心线程数时,创建核心core线程。
    2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
    3. 当线程数大于等于核心线程数,且任务队列已满
        - 若线程数小于最大线程数,创建线程
        - 若线程数等于最大线程数,抛出异常,拒绝任务

	<bean id="suggestThreadPool" class="java.util.concurrent.ThreadPoolExecutor" >
		<constructor-arg index="0" type="int" value="4"/>
		<constructor-arg index="1" type="int" value="30"/>
		<constructor-arg index="2" type="long" value="60"/>
		<constructor-arg index="3" type="java.util.concurrent.TimeUnit" value="SECONDS"/>
		<constructor-arg index="4" type="java.util.concurrent.BlockingQueue">
			<bean class="java.util.concurrent.SynchronousQueue">
			</bean>
		</constructor-arg>
		<constructor-arg index="5" type="java.util.concurrent.RejectedExecutionHandler">
			<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
		</constructor-arg>
	</bean>

Excutor类:   
 public static ExecutorService getExecutor() {
        if(executor == null){
            synchronized (Executor.class){
                if(executor == null){
                    executor = Executors.newCachedThreadPool();
                }
            }
        }
        return executor;
    }

使用过程:

Future<List<BroadListModel>> mainFuture = Executor.getExecutor().submit(new Callable<List<BroadListModel>>() {
    @Override
    public List<BroadListModel> call() throws Exception {
        return broadListDao.getBroadListTrend(params);
    }
});
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class MutiThread {

    public static ExecutorService getExecutor() {
        ExecutorService executor = null;
        if(executor == null){
            synchronized (Executor.class){
                if(executor == null){
                    executor = Executors.newCachedThreadPool();
                }
            }
        }
        return executor;
    }

    public static void main(String[] args) {
        List<Future> futures = new ArrayList<>(3);
        for (int i=0;i<3;i++) {
            int finalI = i;
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    if(finalI==2){
                        Thread.sleep(6000);
                    }
                    return finalI;
                }
            };
            futures.add(getExecutor().submit(callable));
        }
        for (Future future : futures) {
            try {
                Object result = future.get();
                System.out.println(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.out.println("finish");
    }
}

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

下面我们来分析newCachedThreadPool:
这种类型的线程池特点是:

工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。


8、怎么保证线程按顺序执行

强行进入使用join方法的线程,其他线程等待该线程完全执行完后才会进来。

1)现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?_xuehuagongzi000的博客-CSDN博客

thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();

9、常见面试题

手写多线程交替打印ABC

package com.demo.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class syncPrinter implements Runnable{
    // 打印次数
    private static final int PRINT_COUNT = 10;
    private final ReentrantLock reentrantLock;
    private final Condition thisCondtion;
    private final Condition nextCondtion;
    private final char printChar;
    public syncPrinter(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition, char printChar) {
        this.reentrantLock = reentrantLock;
        this.nextCondtion = nextCondition;
        this.thisCondtion = thisCondtion;
        this.printChar = printChar;
    }
    @Override
    public void run() {
        // 获取打印锁 进入临界区
        reentrantLock.lock();
        try {
            // 连续打印PRINT_COUNT次
            for (int i = 0; i < PRINT_COUNT; i++) {
                //打印字符
                System.out.print(printChar);
                // 使用nextCondition唤醒下一个线程
                // 因为只有一个线程在等待,所以signal或者signalAll都可以
                nextCondtion.signal();
                // 不是最后一次则通过thisCondtion等待被唤醒
                // 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
                if (i < PRINT_COUNT - 1) {
                    try {
                        // 本线程让出锁并等待唤醒
                        thisCondtion.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            reentrantLock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();
        Thread printA = new Thread(new syncPrinter(lock, conditionA, conditionB,'A'));
        Thread printB = new Thread(new syncPrinter(lock, conditionB, conditionC,'B'));
        Thread printC = new Thread(new syncPrinter(lock, conditionC, conditionA,'C'));
        printA.start();
        Thread.sleep(100);
        printB.start();
        Thread.sleep(100);
        printC.start();
    }
}

题目二:

有100个任务需要分成10批执行,每批执行有顺序(即第一批执行完执行第二批)。

  • 说明:10批任务有序执行,每批任务的10个任务要做到并发执行
//===========================================================================
//方案A——考察CountDownLatch熟练使用
private static void planA() throws Exception {
    for (int i = 1; i <= OUT; i++) {
        CountDownLatch cd = new CountDownLatch(10);
        for (int j = 0; j < INNER; j++) {
            int finalJ = j;
            new Thread(() -> {
                try {
                    // do something
                    // 模拟任务耗时不同
                    Thread.sleep(new Random().nextInt(10));
                    System.out.println(finalJ);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    cd.countDown();
                }
            })
                .start();
        }
        cd.await();
        System.out.println("第" + i + "批结束");
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值