面试总结9——Java多线程

Semaphore信号量

Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来 构建一些对象池,资源池之类的,比如数据库连接池

  1. 实现互斥锁(计数器为 1)

我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量, 表示两种互斥状态。

Semaphore  ReentrantLock

Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与 release()方法来获得和释放临界资源。

默认为可响应中断锁

实现了可轮询的锁请求与定时锁的功能

提供了公平与非公平锁的机制

释放锁的操作也必须在 finally 代码块中完成。

ReadWriteLock 读写锁

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写 锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

Java中,死锁怎么避免

  1. 调整申请锁的顺序:确保所有的线程都是按照相同的顺序获得锁。
  2. 在尝试获取锁的时候加一个超时时间,在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。ReentrantLock.tryLock()方法。
  3. 缩小加锁的范围;

 

死锁的检测:

用jstack检测究竟时候那个线程死锁;

 

线程同步

同步就是协同步调,按预定的先后次序进行运行。

 

同步方法:

  1. 临界区
  2. 互斥量
  3. 信号量
  4. 信号

 

Java实现同步:

  1. synchronized同步方法和同步代码块
  2. volatile关键字
  3. ReentrantLock
  4. 局部变量:Threadlocall
  5. 阻塞队列

 

java中有几种方法可以实现一个线程?

1) 继承Thread类(java.lang.Thread)创建(本质Thread也实现了Runable接口),重写run方法,start启动线程。

2) 实现Runable接口:定义一个实现java.lang.Runnable接口的类,重写run方法,将类的对象传递给Thread构造器,调用start方法。

3) 实现Callable接口:定义一个实现callable接口的类,将类的对象传递给FutureTask构造器,再将FutureTask对象传递给Thread构造器,调用start方法启动线程,通过FutureTask对象的get方法获取线程运行的结果。

4) 使用线程池:使用executors工具类中的静态工厂方法用于闯将线程池,使用execute方法启动线程,使用shutdown方法等待提交的任务执行完成并关闭线程。

 

Callable和Runnable的区别如下:

1)Callable定义的方法是call,而Runnable定义的方法是run。

2)Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。

3)Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。

FutureTask为Runnable的实现类。

FutureTask类

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

Runnable实现和Thread继承的区别

  1. 可以实现多个接口
  2. 适合资源共享,Thread类线程是分别处理的。

终止线程

1,程序运行结束,线程自动结束。

2,使用退出标志退出线程。有些线程是伺服线程。

它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如: 最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while 循环是否退出。

 

3,使用interrupt方法中断线程:设置中断标志位,具体怎么反应依赖于线程本身。

线程处于阻塞状态:抛出异常。通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。

线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。和使用自定义的标志来控制循环是一样的道理。

4,stop方法:直接中断线程不安全,可能会产生不可预料的结果,已弃用。

5,run方法完成后线程中止

 

中断的API支持:

  1. public boolean isInterrupted(): 判断中断标志位是否被标记。
  2. public void interrupte(): 设置中断标志位
  3. public static Boolean interrupted(): 判断并清空中断标志位。

线程状态

 

线程对中断产生的反应:

    1. new,terminated:无任何影响
    2. runnable,blocked:设置中断标志位
    3. waiting,timed waiting:抛出InterruptException异常,并清空中断标志位。、
    4. synchronized在获锁的过程中不能被中断
    5. 死锁状态的线程无法被中断(收不到中断信号)

Start()和run()的区别

1,调用start启动一个线程,只要得到cpu就可以执行run方法。

2,调用run方法,就是普通的调用方法,运行在当前线程中,没有启动另一个线程。

notify()和notifyAll()有什么区别?

  1. notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,进入锁池,选择哪个线程取决于操作系统对多线程管理的实现。
  2. notifyAll 会唤醒所有等待(对象的)线程(等待池中的所有线程),全部进入锁池,尽管哪一个线程将会第一个处理取决于操作系统的实现。

wait()和sleep()的区别

  1. sleep来自Thread类,wait来自Object类。
  2. sleep使线程暂停执行一段时间的方法,把执行机会(CPU)让给其他线程; wait,一种使线程暂停执行的方法。
  3. 调用sleep()方法的过程中,线程不会释放对象锁(监控状态monitor仍然保持)。调用 wait 方法线程会释放对象锁,进入等待此对象的等待锁定池,只有调用notify()/notifyAll方法后本线程才进入锁池,通过竞争对象锁进入运行状态。
  4. sleep睡眠后不出让系统资源,sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。wait让出系统资源其他线程可以占用CPU。
  5. sleep必须捕获异常。有可能被其他对象调用他的interrupt(),产生Interrupted Exception。wait,notify和notifyAll不需要捕获异常。
  6. sleep可以在任何地方使用,wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用

yield()和join()

1)yield()方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。   

2)join方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

什么是Daemon线程?它有什么意义?

指在程序运行的时候在后台提供一种通用服务的线程,如计时线程和垃圾回收线程。

只剩下守护线程,虚拟机退出。

永远不去访问固有资源,因为可能在任何情况下发生中断。

java如何实现多线程之间的通讯和协作?

  1. 共享内存:JMM(Java内存模型):java线程<->工作内存<->主内存
  2. 消息传递:

syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll():  Object类中声明的方法,因为每个对象都可以拥有锁;是本地方法,且为final方法,无法被重写;调用wait时,当前线程必须拥有此对象的锁,否则抛出错误。

ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll():实现线程间协作更加安全和高效,调用Condition的await()和signal()方法,都必须在lock保护之内。

为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

Wait-notify机制是在获取对象锁的前提下不同线程间的通信机制。在Java中,任意对象都可以当作锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在Object类里。

为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?

wait/notify机制是依赖于Java中Synchronized同步机制的,其目的在于确保等待线程从Wait()返回时能够感知通知线程对共享变量所作出的修改。如果不在同步范围内使用,就会抛出java.lang.IllegalMonitorStateException的异常。

什么是线程池?如何创建一个Java线程池?

  一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行的任务的队列。线程池可以避免线程的频繁创建与销毁,降低资源的消耗,提高系统的反应速度。java.util.concurrent.Executors提供了几个java.util.concurrent.Executor接口的实现用于创建线程池,其主要涉及四个角色:

线程池:Executor

工作线程:Worker线程,Worker的run()方法执行Job的run()方法

任务Job:Runable和Callable

阻塞队列:BlockingQueue

 

构造函数的参数及意义

  1. corePoolSize:核心线程池的大小,核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
  2. maximunPoolSize:线程池能创建最大的线程数量。
  3. keepAliveTime:非核心线程能够空闲的最长时间,超过时间,线程终止。只要线程数量不超过核心线程大小,就不会起作用。但是,如果设置了  allowCoreThreadTimeOut = true,则会作用于核心线程。
  4. unit:时间单位,和keepAliveTime配合使用。
  5. workQueue:缓存队列,用来存放等待被执行的任务。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
  6. threadFactory:线程工厂,用来创建线程,这是一个接口,new它的时候需要实现他的Thread newThread(Runnable r)方法。
  7. handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略。主要是用来抛异常的。

workQueue类型

SynchronousQueue:同步队列,这个队列接收到任务的时候,会直接提交给线程处理,而不保留它。假设所有线程都在工作怎么办?这种情况下,SynchronousQueue就会新建一个线程来处理这个任务。所以为了保证不出现(线程数达到了maximumPoolSize而不能新建线程)的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大,去规避这个使用风险。

LinkedBlockingQueue:链表阻塞队列,这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。

ArrayBlockingQueue:数组阻塞队列,可以限定队列的长度(既然是数组,那么就限定了大小),接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

DelayQueue:延迟队列,队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

线程池是如何处理这些批量任务?

1:如果线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务

2:如果线程数量达到了corePools,则将任务移入队列等待

3:如果队列已满,新建线程(非核心线程)执行任务

4:如果队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler抛出异常

线程数量大于最大线程数就会采用拒绝处理策略

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException
  2. CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
  3. DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  4. DiscardPolicy:丢弃任务,不做任何处理。

四种常用的线程池

  1. SingleThreadExecutor:用于创建一个单线程的线程池. 此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  2. FixedThreadPool:用于创建使用固定线程数的ThreadPool,只有核心线程。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. CachedThreadPool:用于创建一个可缓存的线程池,无核心线程,无限大,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
  4. ScheduledThreadPoolExecutor:用于创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。核心线程池固定,大小无限的线程池。

线程池状态

在ThreadPoolExecutor类中定义了一个volatile变量runState来表示线程池的状态,线程池有四种状态,分别为RUNNING、SHURDOWN、STOP、TERMINATED。

  1. 线程池创建后处于RUNNING状态。
  2. 调用shutdown后处于SHUTDOWN状态,线程池不能接受新的任务,会等待缓冲队列的任务完成。
  3. 调用shutdownNow后处于STOP状态,线程池不能接受新的任务,并尝试终止正在执行的任务。
  4. 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

如何在 Java 线程池中提交线程?

1. execute():ExecutorService.execute 方法接收一个 Runnable 实例

2. submit():ExecutorService.submit() 方法返回的是 Future 对象。

ThreadLocal及其引发的内存泄露

ThreadLocal(本地线程副本)为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值