Java面试-多线程10问

1、说说进程和线程,以及线程的生命周期?

进程:进程是系统中正在运行的程序,一个程序开始运行就代表开始了一个进程,所以说进程就是程序运行的实例。
线程:线程是进程运行时的实例,一个正在运行的进程至少要有一个线程,也可以有多个,一起并行完成一个进程任务。
对于单核心系统来说,即使使用多线程,在不可分割的单位时间内也只有一个线程在运行,使用多线程,便会导致线程的频繁切换
对于多核心系统来说,多个线程可以并行运行,大大缩短一个进程处理任务的时间,多线程可以更充分利用多核心。

线程的生命周期:

在这里插入图片描述

1、新建(New):线程被创建但还没有开始运行,此时线程处于新建状态。此时仅仅是个对象。
2、就绪(Runnable):调用的线程的start()方法后,线程被启动并进入就绪状态,等待CPU调度执行。此时,线程已经分配了所有需要的资源,包括堆栈和CPU时间片等。线程进入就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器。线程的执行是由底层平台控制, 具有一定的随机性。
3、运行(Running):线程被调度执行后,进入运行状态,开始执行run()方法中的代码。
4、阻塞(Blocked):当线程需要等待某些操作完成时,比如等待I/O操作或等待某个锁的释放,sleep(),wait(),join()等则进入阻塞状态。此时,线程将不会消耗CPU时间片,直到等待的操作完成。
5、终止(Terminated):线程执行完所有代码或发生异常时,进入终止状态。此时,线程释放所有资源,包括堆栈和CPU时间片等。

如何终止一个线程:

1、可以用interrupt()和isInterrupted()、interrupted()方法来停止线程,interrupt()方法并不会真的停止一个线程,它只是将对应的线程打上true的中断标记,isInterrupted()方法可以返回线程的中断标记状态,根据状态来处理对应的代码。
2、interrupted()也可以返回中断标记状态,方法底层调用的是isInterrupted()方法,但interrupted()是一个静态方法,最好用在当前线程上,而且一旦调用了interrupted()方法之后,还会立刻清除中断标记状态。
3、利用中断异常也可以停止线程,例如catch(InterruptedException e),然后调用interrupt()方法,也可以终止一个线程。

2、说说volatile、synchronized、ThreadLocal?

Volatile

多线程通信的关键字,常见于Atomic原子类value属性的修饰符、ConcurrentHashMap里HashEntry、Node内部类的value、next属性的修饰符。
1、保证在多线程运行对共享变量的 可见性。即每一个线程在自己的 工作内存 中对公共变量的修改,都会被强制刷新回 主内存。注意,volatile只保证 可见性 ,但不保证 原子性。
2、禁止指令重排。即编译器和处理器会对指令重排序以提高运行性能,但不会对存在依赖的指令进行重排序,单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。而volatile就是对修饰的属性在读、写时,在内存中插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行,经典的用法就是双重检查锁。
普通的单例模式在单程环境是没有问题的,但在多线程环境下,不加锁会创建多个对象实例,从而违背单例原则,所以用双重锁来判断是否需要实例和初始化;其次,如果没有volatile修饰,对象在线程一未完全初始化完成就被线程二使用,可能会抛出初始化未完成异常,例如空指针等。

synchronized

在Java程序中,synchronized关键字可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),所以synchronized即可以保证 可见性,又可以保证 原子性。
	1、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
	2、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
	3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized代码块底层原理
	某个线程获取锁,通过monitorenter 和 monitorexit 指令来获取锁对象的 monitor 持有权。当执行monitorenter指令时,当前线程将试图获取 对象锁 所对应的 monitor 的持有权,当对象锁 的 monitor 的进入计数器 state 为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
	如果当前线程已经拥有对象锁 的 monitor 的持有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1,所以synchronized也是可重入锁。
	倘若某个线程已经拥有对象锁 的 monitor 的所有权,那其他线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor 并设置计数器值为0 ,其他线程将有机会持有 monitor 。
	值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。
	为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
synchronized方法底层原理
	synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
	当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

ThreadLocal

Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的都是当前线程所持有的对象,这样就隔离了多个线程对数据的数据共享。
由ThreadLocal维护的变量在多线程环境中,每一个线程都将其维护在内部静态类ThreadLocalMap当中,ThreadLocalMap的底层数据结构是继承了弱引用的Entry[] 数组,所以在使用ThreadLocal时,要特别注意两个问题。
第一,ThreadLocal是与线程绑定的一个变量,生命周期与线程共存,如果维护的变量没有在方法执行的最后remove(),又恰好系统使用线程池去管理线程,反复使用变量就会让其不断膨胀;
第二,由于ThreadLocal的Key时弱引用,那么在下一次垃圾回收时就会被清理,就会出现Key为null的value,即Entry(null,value),这样的Entry越来越多,即用不到又无法回收,就会导致内存泄漏。
key 如果是强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal),造成ThreadLocal无法被回收,同样无法完全避免内存泄漏。
key 如果是弱引用,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 。这就意味着使用threadLocal , CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocal 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

3、说说CountDownLatch、CyclicBarrier 、Semaphore?

CountDownLatch
	CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务 执行完毕 之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
	CountDownLatch类只提供了一个构造器:public CountDownLatch(int count) {  };  //参数count为计数值
	然后下面这3个方法是CountDownLatch类中最重要的方法:
	1、public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
	2、public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
	3、public void countDown() { };  //将count值减1
CyclicBarrier
	CyclicBarrier类同样位于java.util.concurrent包下字面意思回环栅栏,通过它可以实现让一组线程等待至某个 状态 之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
	CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:
	1、public CyclicBarrier(int parties, Runnable barrierAction) {}
	2、public CyclicBarrier(int parties) {}
	参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
	然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
	1、public int await() throws InterruptedException, BrokenBarrierException { };
	2、public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };
	第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
	第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
Semaphore
	Semaphore类也位于java.util.concurrent包翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
	Semaphore类位于java.util.concurrent包下,它提供了2个构造器:
	1、public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问
		sync = new NonfairSync(permits);
	}
	2、public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
		sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
	}
  下面说一下Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
	1、public void acquire() throws InterruptedException {  }     //获取一个许可
	2、public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
	3、public void release() { }          //释放一个许可
	4、public void release(int permits) { }    //释放permits个许可
	acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
	release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
	以上4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:
	1、public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
	2、public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
	3、public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
	4、public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
	另外还可以通过availablePermits()方法得到可用的许可数目。

4、说说synchronized锁升级过程?

在这里插入图片描述

Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
偏向锁:为了减少同一线程获取锁的代价而引入偏向锁,偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,会记录线程ID。当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提高了程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。
轻量级锁:为了提升程序性能,依据“对绝大部分的锁,在整个同步周期内都不存在竞争”而引入,适应的场景是线程交替执行同步块的场合。但是如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁:为了避免线程在竞争锁的过程中被挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,因此自旋锁会假设在不久将来,当前的线程可以获得锁,虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的,最后没办法也就只能升级为重量级锁了。
锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,JVM会自动将其锁消除。

5、说说synchronized和Lock锁的区别?

1、synchronized是java编程的关键字,底层是通过monitor监视器所来实现线程同步的,而Lock是java并发包下的一个接口,具体的实现类有ReentrantLock、Segment、ReadLock、WriteLock,实现线程同步需要使用具体的lock()/tryLock()方法加锁,unlock()方法来解锁,使用时要注意避免死锁。
synchronized在不断迭代升级后,性能也不逊于Lock锁,而且官方也建议使用synchronized,相信以后还会对其进行升级。而且无论是否出现异常的情况下,synchronized最后都会调用monitorenter指令去释放锁。
2、Lock在编程时,可以指定是公平锁还是非公平锁,而synchronized始终就是非公平锁。这里的公平还是非公平主要是指,在锁释放后,当前线程与阻塞的线程再次获得锁的机会是随机的,还是有一定顺序的。
这主要是由锁的实现方式决定的,Lock锁的实现方式调用的是AQS的acquire()方法,acquire()方法里的tryAcquire()方法是一个模版方法,具体的实现方法要看Lock接口的实现类怎么来维护,例如ReentrantLock类内FairSync、NonfairSync静态内部类都有相应tryAcquire()方法实现。
3、AQS可以简单理解为由volatile修饰的Node节点组成的链表➕volatile修饰的state值共同组成,tryAcquire()方法用CAS算法来操作state值的变化来判断是否加锁成功,并且通过不断循环自旋的方式让线程尝试获取锁,直至封装成Node节点以尾插的方式加入链表。
所以Lock锁的公平性就体现在,在进行CAS操作前,当前线程会判断自己是否是头Node节点,而如果是非公平锁的实现的话,则上来就进行CAS操作。

6、说说线程池?

参数
1、核心线程数:程序第一次执行任务并且当前运行的线程数小于核心线程数,线程池会创建一个新的线程去执行任务。默认情况下,核心线程在执行完任务之后,并不会会被销毁,但也可以设置allowCoreThreadTimeOut来进行超时回收。核心线程的允许超时参数默认为false,线程池比较当前工作线程Worker和核心线程数的大小,通过不断从阻塞队列中拿取任务或者阻塞来保证核心线程的存活。
2、最大线程数:线程池执行任务时,允许存在的最大工作线程数。当阻塞队列已满,工作线程也达到最大线程数,此时会触发相应的拒绝策略。
3、等待时长:工作线程的闲置超时时长。如果超过该时长,除核心线程数外的工作线程就会被回收。
4、等待时长单位:一般有毫秒、秒、分。
5、队列:任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中,其采用阻塞队列实现。当线程池内工作线程执行任务速度,小于外部向线程池提交任务的速度,会提交到队列。
	1、ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
	2、LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
	3、PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照		优先级取任务。
	4、DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
	5、SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调		用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
6、线程工厂:用于指定为线程池创建新线程的方式。可以指定线程名称前缀,方便日志排查。
7、拒绝策略:当达到最大线程数时需要执行的饱和策略。
	1、AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
	2、CallerRunsPolicy:由调用线程处理该任务。
	3、DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
	4、DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
工作流程

在这里插入图片描述

功能线程池
1、定长线程池(FixedThreadPool)
	public static ExecutorService newFixedThreadPool(int nThreads) {
			return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
	}
	public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
			return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
	}
	固定的线程数,核心线程和最大线程数一致,队列为链表结构的有界队列,默认容量为Integre.MAX_VALUE。固定线程数便可以有效的控制线程并发数,但容量为Integre.MAX_VALUE的队列容易造成		OOM。
2、定时线程池(ScheduledThreadPool )
	public ScheduledThreadPoolExecutor(int corePoolSize) {
			super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
	}
	public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
			super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue(), threadFactory);
	}
	
	scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
	scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
	核心线程数量固定,非核心线程数量无限,执行完超过闲置时间后回收,任务队列为延时阻塞队列。执行定时或周期性的任务,但容量为Integre.MAX_VALUE的最大线程数容易造成CPU打满。
3、可缓存线程池(CachedThreadPool)
	public static ExecutorService newCachedThreadPool() {
 		return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
	}
	public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
 		return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory);
	}
	无核心线程,非核心线程数量无限,执行完超过闲置时间后回收,任务队列为不存储元素的阻塞队列。可以用来执行大量、耗时少的任务,同样注意CPU打满。
4、单线程化线程池(SingleThreadExecutor)
	public static ExecutorService newSingleThreadExecutor() {
			return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,  new LinkedBlockingQueue<Runnable>()));
	}
	public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
			return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));
	}
	只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。适合串行执行的任务逻辑。
对比

在这里插入图片描述

7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

8、多线程之间如何进行通信?

9、Fork/Join 框架是干什么的?

10、FutureTask 是什么?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值