40个java多线程问题总结
1、并发:某一段时间内,宏观上看多个程序在同时运行,在微观上看多个程序之间是串行执行的。
并发编程三个问题:原子性问题,可见性问题,有序性问题
并行:两个或以上的任务同时运行,无论宏观微观都是同时运行。(与串行区分)
2、线程安全:多线程访问某个类时,不管线程之间如何交替执行,总能得到正确执行结果。(重点是正确性)
线程安全:Vector、Stack、HashTable、Emeriable
3、线程:一个进程包含多个线程,同一进程的各个线程之间可以共享相同的内存空间,由共享空间实现数据交换、实施其他同步工作。
多任务:针对操作系统,一个OS可同时运行多个应用程序
多线程:对一个程序,一个程序内部可以同时执行多个线程
进程:程序执行过程;
线程:系统中最小的执行单元
4、java中线程都由java.lang.Thread类定义描述
生命周期:创建--就绪--运行--阻塞i--终止
创建:使用new声明一个对象实例时
两种方法:1、继承类Thread,在子类中重写run() ;2、实现Runnable接口,重写run(),并将该类的实例对象作为类Thread的构造方法的参数(还要再创建Thread对象)
eg.1、Worker继承Thread,构造函数,重写run();
在主类中:
Worker worker=new Worker(xx);
worker.start()
2、Worker实现Runnable接口,构造函数,重写run();
主类中:
Worker worker=new Worker(xx);
Thread t=new Thread(worker);
t.start()
就绪:处于新建态的线程通过start方法启动后,进入就绪态。进入线程队列排队等待CPU调度,获得CPU时间片后,转换为运行态
运行:就绪态线程被调度并获得CPU资源是,进入运行态。考虑线程优先级。
阻塞:可能进入阻塞的情况(某些共享资源被占用;等待I/O操作;调用了wait/sleep/suspend方法;锁被其他线程占有。
终止:可能到达终止的情况(线程的run方法执行结束;线程通过某些方法(destroy())提前终止;在run()执行期间发生未捕获的异常)
5、线程属性:
(1)线程标识符:getId()
(2)线程名:使用继承Thread方法创建,则使用getName();setName
(3)优先级(用于线程调度)
(4)线程状态:getState()
6、线程管理:
(1) join():一个线程等待另一个线程执行结束再开始执行;Thread.join()包含在try-catch中。
A先执行,B后执行,则比如在线程B中调用了线程A的A.Join()方法,直到线程A执行完毕后,才会继续执行线程B。 具体:A照常写;B中run()中,先写join()(try-catch语句),再写要执行的。
还有join(long millis)和 join(long millis,int nanos):线程在给定的超时时间里未终止,则会从该方法返回。
(2) sleep():使线程暂停运行一段时间,把CPU让给优先级低的线程,sleep(long millis)包含在try-catch中。
(3) yeild():让当前执行线程暂停一下
(4) interrput():中断线程,静态
(5)suspend():线程暂停,但调用后线程不会释放已占有的资源(比如锁),而占有着资源进入睡眠状态,易引发死锁问题。
resume():线程恢复
stop():线程终止,在终止一个线程时不会保证线程的资源正常释放,通常不给予线程完成资源释放工作的机会,会导致程序可能工作在不确定状态下。
以上3个API是过期的,不建议使用
7、带返回值的线程:接口Callable定义,由接口Future获得线程返回值
(1)Callable接口(一定要实现call方法,有返回值)
执行方法 call(),可抛出异常,有返回值
public interface Callable{
V call() throws Exception;
}
常用场景:可以定义一个返回Integer类型的线程Worker:
public class Worker implements Callable{
public Integer call(){ //... }
}
(2)接口Future:允许在未来某个时间获得线程允许的结果,保存了使用Callable接口定义的线程异步运行的结果。
(当启动一个Future对象后,相当于启动一个计算,去做别的事,Future对象的计算结果在计算后得到)
a.常用方法:
V get():获得线程的返回值,若结果未计算出,则阻塞直到有结果
V get(long time,TimeUnit unit):指定时间内获得线程返回值,否则抛出TimeOutException
b.常用场景:FutureTask包装器,同时实现Callable和Runnable接口,对Callable对象进行封装并转换为Future对象
构造方法:
FutureTask(Callable task) 传入实现了implements Callable的线程实现类
FutureTask(Runnable task,V result)
比如:实现在一堆很多的数中找最大值,可以先分成多个数组,再在各自数组中找最大的数字。
创建Worker类implements Callable
public Integer call()
在主函数中:
FutureTask
Thread t=new Thread(task);
t.start();
8、线程同步:
(1)临界区:线程中访问共享数据的那段代码(多线程可同时请求进入临界区,但同一时间只允许一个进入;临界区被一个线程拥有,其他线程等待;OS随机选取一个线程进入)
(2)线程阻塞:线程可能被CPU挂起,等待某一时间后,再去尝试获取资源
(3)锁:加锁--操作共享数据--解锁(一般是对共享数据)
同步锁,开始就有;其他两种从jdk 1.5后开始
1、同步锁:synchronized,被修饰的方法不能被子类继承,互斥锁,但依靠隐藏再对象后的内置监视器,不直观,修饰方法或块。
本质是对一个对象的监视器进行获取,这个获取过程时排他的,也就是同一时刻只有一个线程获取到由synchronized所保护对象的监视器。
任意一个对象都有自己的监视器,当这个对象由同步块/同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块/方法,而没有获取到监视器(没能执行该方法)的线程被阻塞在同步块/方法的入口处,进入BLOCKED
(1)同步方法:使用synchronized修饰的方法都处于锁保护状态,同一时刻只能有一个线程对其进行访问,进入synchronized控制范围内的线程称其为持有锁的线程,其他想要访问该方法的线程只能等待,当持有锁的线程执行完毕且释放锁后,其他线程又开始竞争获得该锁。
在这过程中,无论是正常执行完毕,或者抛出异常,都会释放锁。
eg. a,b是两个窗口同时在作业(加1),但是要保证任意时刻a和b的值相等。
public class Data{
int a=0;
int b-0;
public synchronized void increase(){
//该方法同一时间只能有一个线程访问,所以可以保证相加a和b的值一致
a++; b++;
}
}
(2)同步块:使用synchronized修饰的一块代码,需要明确指出监视器对象,
使用当前对象 synchronized(this) {...}
使用属性: synchronized(obj){...}
嵌套的同步块:先得到A的锁,再得到B的锁
synchronized(A){
synchronized(B){...}
}
2、可重入锁:无阻塞同步机制,创建:ReentranLock();加锁:Lock()、解锁unlock(),通常放入try-catch语句,比如用于多个线程同时对ArrayList进行读写操作时进行同步控制,因为它是非线程安全的,互斥锁
Lock lock=new ReentrantLock()
lock.lock();
try{...
}finally{
lock.unlock();
}
3、读写锁:读多写少;读锁由多个线程同时持有,写锁是排他锁,一个线程持有。ReentrantReadWriterLock,
4、volatile:修饰域变量在被线程访问时,都强迫从共享内存中重读该成员变量的值,当域变量的值发生变化是,必须将该成员变量值写回共享内存。修饰字段。 volatile保证了其他线程的立即可见性,就没有保证原子性。对volatile变量的操作不会造成阻塞。主要作用:保证变量的可见性!
对一个域并发访问是安全的的情况:
1、域是volatile的
2、 域是final的,且在构造器调用完成后被访问
3、 对域的访问有锁保护
5、原子操作:操作要么全不做,要么全做。原子操作作为一个不可分割的整体完成,执行完毕前不会被任何其他任务或事件中断,这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性。
“a++”不是原子操作,它可以细分:
(1)取出a的值;
(2)执行加一操作
(3)将结果存回a
所以多线程对共享变量执行a++操作时,需要使用同步控制
CAS是基本原子操作之一。CAS(compare and swap)
内存值V,旧期望值A,新值B,如果V==A,将V替换为B,返回V
int compare_and_swap(Mem m,int oldVal, int newVal){
int old_reg_val=m;
if(old_reg_val==oldVal) m=newVal;
return old_reg_val;
}
6、ThreadLocal:线程本地变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
(1)数据结构:
每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals是用来存储实际的变量副本的,是一个以ThreadLocal对象为键,任意对象为值的存储结构,该结构附带在线程上,也就是一个线程可根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
private static final ThreadLocal t=new ThreadLocal();
可由set(T)初始化设置值,在当前线程下通过get()获取原先设置的值。
(2)方法:
public T get() { } //获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //设置当前线程中变量的副本
public void remove() { } //移除当前线程中变量的副本
protected T initialValue() { } //protected,用于使用时重写初始化,是一个延迟加载方法。
(3)应用
定义Profilter类使用到ThreadLocal
import java.util.concurrent.TimeUnit;
public class Profiler {
//创建一个存储结构ThreadLocal,在这里重写了初始化值函数,返回当前时间
private static final ThreadLocal TIME_THREADLOCAL=new ThreadLocal(){
protected Long initialValue(){
return System.currentTimeMillis();
}
};
public static final void begin(){
TIME_THREADLOCAL.set(System.currentTimeMillis());
//通过set(T)设置线程变量的值
}
public static final long end(){
return System.currentTimeMillis()-TIME_THREADLOCAL.get();
//通过get(),获取到之前设置的值
}
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost:"+Profiler.end()+" millis");
}
}
最常用于解决数据库连接、session管理。
数据库连接例子:
数据库连接库中,有创建数据库连接和得到连接的方法,如果在多线程中不用ThreadLocal,也不使用同步,就会产生线程安全问题。第一,2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
所以,必须使用同步。但同步效率太低,一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
如果在各个类使用数据库操作时,再在方法中建立数据库连接,虽然也可以,因为每次都是在方法内部创建的连接,不存在线程安全问题。但由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。
所以可以使用线程副本,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
缺点是资源消耗大,内存大。
举例
private static ThreadLocal connectionHolder= new ThreadLocal() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
session管理例子:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
7、线程间通信:这三个方法不是在Thread类中,而是在Object类中
(1 )wait():让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定时间”,当前线程被唤醒(进入“就绪状态”),常放入synchronized修饰的语句块或方法中,放在try-catch中
(2)** notify()和notifyAll()**的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程
常考实例:生产者消费者模型:
(1)考虑只有一个缓冲区
缓冲区CubbyHole.java:
消费者:Customer.java
生产者Producer.java:
主函数:
(2)考虑有多个缓冲区的情况:
缓冲区被安排为头尾相接的模式,生产者生产的数据放入缓冲区尾部,放入后尾指针rear+1,消费者消费时从头部取得数据消费,消费后头指针front+1
缓冲区CubbyHole.java:
生产者Producer.java:
消费者Customer.java:
主函数Index.java:
二、线程池:
1、原理:线程池包含若干个准备运行的空闲线程,线程在程序运行的开始传教,把创建的Runnable对象交给线程池中线程运行,运行完成,无其他任务,线程转入休眠状态,有任务时唤醒,所有任务执行完成,关闭线程池。线程池机制分离了任务的创建和执行,减少了重复创建和销毁线程的开销。
2、线程执行器:
维护和管理线程
仅需要实现Runnable对象,把对象交给执行器,执行器会使用到线程池中的线程执行
jdk1.5开始 Executor框架:
Executor接口、其子接口ExecutorSevice、实现两个接口的类ThreadPoolExecutor;
工厂类Executors
面试题:
1、Thread 类中的start() 和 run() 方法有什么区别?
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。
2、Java中Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
3、Java中的volatile 变量是什么?
volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的volatile变量规则。
如果一个成员变量发生了变化,强制要求其将值写回共享内存。但不保证原子性。
4、一个线程运行时发生异常会怎样?
先捕获异常,如果没有被捕获,线程会停止执行。
5、如何在两个线程间共享数据?
使用共享对象,用wait()和notify()方法
6、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
7、Java中interrupted 和 isInterruptedd方法的区别?
interrupted()***** 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。*简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
8、 为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件
9、什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
9、死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可抢占:进程已获得的资源,在末使用完之前,不能强行抢占。
循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。
10、Java中活锁和死锁有什么区别?
活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
11、有三个线程T1,T2,T3,怎么确保它们按顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
12、Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
13、Java中Semaphore是什么?
Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。
14、Swing是线程安全的吗? 为什么?
你可以很肯定的给出回答,Swing不是线程安全的,但是你应该解释这么回答的原因即便面试官没有问你为什么。当我们说swing不是线程安全的常常提到它的组件,这些组件不能在多线程中进行修改,所有对GUI组件的更新都要在AWT线程中完成,而Swing提供了同步和异步两种回调方法来进行更新。
15、什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
16、volatile 变量和 atomic 变量有什么不同?
首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
***关于原子操作:
原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。
多个原子操作合并起来后就不是一个原子操作了,就需要同步了。
i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。
另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。
17、如果同步块内的线程抛出异常会发生什么?
无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。
18、如何强制启动一个线程?
这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。
19、Java多线程中调用wait() 和 sleep()方法有什么不同?
Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
20、单例模式的双检锁是什么?
这个问题在Java面试中经常被问到,但是面试官对回答此问题的满意度仅为50%。一半的人写不出双检锁还有一半的人说不出它的隐患和Java1.5 是如何对它修正的。它其实是一个用来创建线程安全的单例的老方法,当单例实例第一次被创建时它试图用单个锁进行性能优化,但是由于太过于复杂在 JDK1.4中它是失败的,我个人也不喜欢它。无论如何,即便你也不喜欢它但是还是要了解一下,因为它经常被问到。
21、 如何在Java中创建线程安全的Singleton?
这是上面那个问题的后续,如果你不喜欢双检锁而面试官问了创建Singleton类的替代方法,你可以利用JVM的类加载和静态变量初始化特征来创建Singleton实例,或者是利用枚举类型来创建Singleton,我很喜欢用这种方法。
22、写出3条你遵循的多线程最佳实践
我相信你在写并发代码来提升性能的时候也会遵循某些最佳实践。以下三条最佳实践我觉得大多数Java程序员都应该遵循:
·** 给你的线程起个有意义的名字。**这样可以方便找bug或追踪,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
· 避免锁定和缩小同步的范围锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
· 多用同步类少用wait 和 notify。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断 优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
· 多用并发集合少用同步集合。这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。
24、synchronized和volatile区别?
synchronized:同步锁;volatile:保证变量的可见性
线程安全是两方面需要的 原子性(指的是多条操作)和可见性。volatile只能保证可见性,synchronized是两个均保证的。
volatile轻量级,只能修饰变量;synchronized重量级,还可修饰方法。
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。
25、如果唤醒一个阻塞的线程?
(1)针对会抛出异常的方法wait、sleep、join、Lock.lockInterruptibly等:
线程内部处理好异常(要不完全内部处理,要不把这个异常抛出去)来唤醒。
(2)不会抛出的:Socket的I/O,同步I/O,Lock.lock等。对于I/O类型,我们可以关闭他们底层的通道,比如Socket的I/O,关闭底层套接字,然后抛出异常处理就好了;比如同步I/O,关闭底层Channel然后处理异常。对于Lock.lock方法,我们可以改造成Lock.lockInterruptibly方法去实现
26、进程间通信方式:管道、消息队列、信号、信号量、共享内存
管道:速度慢,容量有限
【管道:半双工的通信方式。一个管道可以实现双向的数据传输,而同一个时刻只能最多有一个方向的传输,不能两个方向同时进行。所以不能用于在一个基于分布式的游戏服务器系统中,不同的服务器之间。】
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
信号量:不能传递复杂消息,只能用来同步
共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了一块内存的。
线程间通信方式:信号、信号量、锁机制
27、Callable:有返回值,抛出异常,执行入口方法call()
Runnable:无返回值,不可抛出异常,执行入口方法run()
面试代码题:
一、现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
方法1:在t2执行前先调用t1.join(),在t3执行前先调用t2.join() ;最后是按t3 t2 t1的顺序启动线程。
方法二:线程执行顺序可以在方法中调换
二、实现死锁:同步块嵌套容易产生死锁,简单死锁: lockA、lockB分别是两个资源,线程A、B必须同时拿到才能工作 ,但A线程先拿lockA、再拿lockB;B线程先拿lockB、再拿lockA
A. semaphore:信号量。用于表示共享资源数量。用acquire()获取资源,用release()释放资源。
B. CyclicBarrier 线程到达屏障后等待,当一组线程都到达屏障后才一起恢复执行
C. CountDownLatch 初始时给定一个值,每次调用countDown值减1,当值为0时阻塞的线程恢复执行
28、进程间通信方式:管道(半双工)、信号量(semphore,相当于计数器)、消息队列、共享内存、套接字、信号
线程间通信方式:锁机制(互斥锁、读写锁等)、信号量、信号
29、
线程工具类:
1、执行器:Executor(接口):定义线程的自定义子系统,包括线程池,异步IO和轻量级任务框架
2、同步器:
Semaphore:信号量,并发工具,限制可以访问某些资源的线程数,维持一个许可集,许可可用前阻塞每一个acquire(),再获许可,每个release()加一个许可,从而释放一个正阻塞的获取集。
CountDownLatch:倒计时门栓,保持给定数目的信号 事件或条件前阻塞执行
CyclicBarrier:障栅,可重置的多路同步前,一组线程互相等待,每次barrier.await() 在try-catch中使用,即要等待所有线程都到达再执行下面的
Exchanger:2线程在集合点交换对象
Future:表示异步计算的结果,可检查计算是否完成,检索计算结果。
Lock机制:保证多线程执行过程中不会因为共享数据而产生竞争条件(即相互干扰的现象)
分为synchronized【修饰方法/代码块】、Lock对象【比如ReentrantLock类,可重入锁,常会用到递归调用,tryLock()检测此时有无其他线程的锁】
30、2套线程的协作机制:
1、Object类的wait() notify()
【wait:Object类,等待,会释放线程锁,要在同步块/方法中使用;
sleep:Thread类,不会释放对象锁,随时使用
】
2、Condition类的await()和 signal()
线程等待,结束等待
Java线程面试题Top50 - CSDN博客
Java多线程常用面试题(含答案,精心总结整理) - CSDN博客