1.开启线程的三种方式?
1)继承Thread类,重写run()方法,在run()方法体中编写要完成的任务 new Thread().start();
2)实现Runnable接口,实现run()方法 new Thread(new MyRunnable()).start();
3)实现Callable接口MyCallable类,实现call()方法,使用FutureTask类来包装Callable对象,使用FutureTask对象作为Thread对象的target创建并启动线程;调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());
new Thread(ft).start();
2、run()和start()方法区别
run()方法只是线程的主体方法,和普通方法一样,不会创建新的线程。只有调用start()方法,才会启动一个新的线程,新线程才会调用run()方法,线程才会开始执行。
3、如何控制某个方法允许并发访问线程的个数?
创建Semaphore变量,Semaphore semaphore = new Semaphore(5, true); 当方法进入时,请求一个信号,如果信号被用完则等待,方法运行完,释放一个信号,释放的信号新的线程就可以使用。
4、在Java中wait和seelp方法的不同
wait()方法属于Object类,调用该方法时,线程会放弃对象锁,只有该对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
sleep()方法属于Thread类,sleep()导致程序暂停执行指定的时间,让出CPU,但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()方法中线程不会释放对象锁。
5、谈谈wait/notify关键字的理解
notify: 唤醒在此对象监视器上等待的单个线程
notifyAll(): 通知所有等待该竞争资源的线程
wait: 释放obj的锁,导致当前的线程等待,直接其他线程调用此对象的notify()或notifyAll()方法
当要调用wait()或notify()/notifyAll()方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)代码中。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。
6、什么导致线程阻塞?
1)线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;
2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;
3)线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;
4)线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。
5)线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。
6)线程执行suspend()使线程进入阻塞态,必须resume()方法被调用,才能使线程重新进入可执行状态。
7、线程如何关闭?
1) 使用标志位
2)使用stop()方法,但该方法就像关掉电脑电源一样,可能会发生预料不到的问题,弃用
3)使用中断interrupt()
public class Thread {
// 中断当前线程
public void interrupt();
// 判断当前线程是否被中断
public boolen isInterrupt();
// 清除当前线程的中断状态,并返回之前的值
public static boolen interrupted();
}
但调用interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。
8、讲一下java中的同步的方法
之所以需要同步,因为在多线程并发控制,当多个线程同时操作一个可共享的资源时,如果没有采取同步机制,将会导致数据不准确,因此需要加入同步锁,确保在该线程没有完成操作前被其他线程调用,从而保证该变量的唯一一性和准确性。
1)synchronized修饰同步代码块或方法
由于java的每个对象都有一个内置锁,用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需获得内置锁,否则就处于阴塞状态。
2)volatile修饰变量
保证变量在线程间的可见性,每次线程要访问volatile修饰的变量时都从内存中读取,而不缓存中,这样每个线程访问到的变量都是一样的。且使用内存屏障。
3)ReentrantLock重入锁,它常用的方法有ReentrantLock():创建一个ReentrantLock实例
lock()获得锁 unlock()释放锁
4)使用局部变量ThreadLocal实现线程同步,每个线程都会保存一份该变量的副本,副本之间相互独立,这样每个线程都可以随意修改自己的副本,而不影响其他线程。常用方法ThreadLocal()创建一个线程本地变量;get()返回此线程局部的当前线程副本变量;initialValue()返回此线程局部变量的当前线程的初始值;set(T value)将此线程变量的当前线程副本中的值设置为value
5) 使用原子变量,如AtomicInteger,常用方法AtomicInteger(int value)创建个有给定初始值的AtomicInteger整数;addAndGet(int data)以原子方式将给定值与当前值相加
6)使用阻塞队列实现线程同步LinkedBlockingQueue<E>
9、如何保证线程安全?
线程安全性体现在三方法:
1)原子性:提供互斥访问,同一时刻只能有一个线和至数据进行操作。
JDK中提供了很多atomic类,如AtomicInteger\AtomicBoolean\AtomicLong,它们是通过CAS完成原子性。JDK提供锁分为两种:synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。
2)可见性:一个线程对主内存的修改及时被其他线程看到。
JVM提供了synchronized和volatile,volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。
3)有序性:指令没有被编译器重排序。
可通过volatile、synchronized、Lock保证有序性。
10、两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
我认为可以实现,比如两个进程都读取日历进程数据是没有问题,但同时写,应该会有冲突。
可以使用共享内存实现进程间数据共享。
11、Java中对象的生命周期
1)创建阶段(Created):为对象分配存储空间,开始构造对象,从超类到子类对static成员初始化;超类成员变量按顺序初始化,递归调用超类的构造方法,子类成员变量按顺序初始化,子类构造方法调用。
2)应用阶段(In Use):对象至少被一个强引用持有着。
3)不可见阶段(Invisible):程序运行已超出对象作用域
4)不可达阶段(Unreachable):该对象不再被强引用所持有
5)收集阶段(Collected):假设该对象重写了finalize()方法且未执行过,会去执行该方法。
6)终结阶段(Finalized):对象运行完finalize()方法仍处于不可达状态,等待垃圾回收器对该对象空间进行回收。
7)对象空间重新分配阶段(De-allocated):垃圾回收器对该对象所占用的内存空间进行回收或再分配,该对象彻底消失。
12、static synchronized 方法的多线程访问和作用
static synchronized控制的是类的所有实例访问,不管new了多少对象,只有一份,所以对该类的所有对象都加了锁。限制多线程中该类的所有实例同时访问JVM中该类对应的代码。
13、同一个类里面两个synchronized方法,两个线程同时访问的问题
如果synchronized修饰的是静态方法,锁的是当前类的class对象,进入同步代码前要获得当前类对象的锁;
普通方法,锁的是当前实例对象,进入同步代码前要获得的是当前实例的锁;
同步代码块,锁的是括号里面的对象,对给定的对象加锁,进入同步代码块库前要获得给定对象锁;
如果两个线程访问同一个对象的synchronized方法,会出现竞争,如果是不同对象,则不会相互影响。
14、volatile的原理
有volatile变量修饰的共享变量进行写操作的时候会多一条汇编代码,lock addl $0x0,lock前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存,这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。同时lock前缀也相当于一个内存屏障,对内存操作顺序进行了限制。
15、synchronized原理
synchronized通过对象的对象头(markword)来实现锁机制,java每个对象都有对象头,都可以为synchronized实现提供基础,都可以作为锁对象,在字节码层面synchronized块是通过插入monitorenter monitorexit完成同步的。持有monitor对象,通过进入、退出这个Monitor对象来实现锁机制。
16、谈谈NIO的理解
NIO( New Input/ Output) 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。 NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。
17、synchronized 和volatile 关键字的区别、synchronized与Lock的区别、ReentrantLock 、synchronized和volatile比较
1)volatile:解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。
2)synchronized:互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。
3)lock是一个接口,而synchronized是java中的关键字,synchronized是内置语言的实现。lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
4)ReentrantLock可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。
18、在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
19、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?
20、悲观锁与乐观锁
-
悲观锁
-
乐观锁
1)两种锁的使用场景
2)乐观锁常见的两种实现方式
1. 版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
2. CAS 算法
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
3)乐观锁的缺点
1 ABA 问题
2 循环时间长开销大
3 只能保证一个共享变量的原子操作
4)CAS 与 synchronized 的使用情景
21、线程的状态
New:新建状态,new出来,还没有调用start
Runnable:可运行状态,调用start进入可运行状态,可能运行也可能没有运行,取决于操作系统的调度
Blocked:阻塞状态,被锁阻塞,暂时不活动,阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
Waiting:等待状态,不活动,不运行任何代码,等待线程调度器调度,wait sleep
Timed Waiting:超时等待,在指定时间自行返回
Terminated:终止状态,包括正常终止和异常终止
22、线程中断
一般情况下,线程不执行完任务不会退出,但是在有些场景下,我们需要手动控制线程中断结束任务,Java中有提供线程中断机制相关的Api,每个线程都一个状态位用于标识当前线程对象是否是中断状态
public boolean isInterrupted() //判断中断标识位是否是true,不会改变标识位public void interrupt() //将中断标识位设置为truepublic static boolean interrupted() //判断当前线程是否被中断,并且该方法调用结束的时候会清空中断标识位
需要注意的是interrupt()方法并不会真的中断线程,它只是将中断标识位设置为true,具体是否要中断由程序来判断,如下,只要线程中断标识位为false,也就是没有中断就一直执行线程方法
new Thread(new Runnable(){
while(!Thread.currentThread().isInterrupted()){
//执行线程方法
}
}).start();
前边我们提到了线程的六种状态,New Runnable Blocked Waiting Timed Waiting Terminated,那么在这六种状态下调用线程中断的代码会怎样呢,New和Terminated状态下,线程不会理会线程中断的请求,既不会设置标记位,在Runnable和Blocked状态下调用interrupt会将标志位设置位true,在Waiting和Timed Waiting状态下会发生InterruptedException异常,针对这个异常我们如何处理?
1.在catch语句中通过interrupt设置中断状态,因为发生中断异常时,中断标志位会被复位,我们需要重新将中断标志位设置为true,这样外界可以通过这个状态判断是否需要中断线程
try{
....
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
2.更好的做法是,不捕获异常,直接抛出给调用者处理,这样更灵活
23、Thread为什么不能用stop方法停止线程
从SUN的官方文档可以得知,调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
- 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。
- 释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。
24、volatile关键字
volatile为实例域的同步访问提供了免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就直到该域可能被另一个线程并发更新
25、原子性 可见性 有序性
原子性:对基本数据类型的读取和赋值操作是原子性操作,这些操作不可被中断,是一步到位的,例如x=3是原子性操作,而y = x就不是,它包含两步:第一读取x,第二将x写入工作内存;x++也不是原子性操作,它包含三部,第一,读取x,第二,对x加1,第三,写入内存。原子性操作的类如:AtomicInteger AtomicBoolean AtomicLong AtomicReference
可见性:指线程之间的可见性,既一个线程修改的状态对另一个线程是可见的。volatile修饰可以保证可见性,它会保证修改的值会立即被更新到主存,所以对其他线程是可见的,普通的共享变量不能保证可见性,因为被修改后不会立即写入主存,何时被写入主存是不确定的,所以其他线程去读取的时候可能读到的还是旧值
有序性:Java中的指令重排序(包括编译器重排序和运行期重排序)可以起到优化代码的作用,但是在多线程中会影响到并发执行的正确性,使用volatile可以保证有序性,禁止指令重排
volatile可以保证可见性 有序性,但是无法保证原子性,在某些情况下可以提供优于锁的性能和伸缩性,替代sychronized关键字简化代码,但是要严格遵循使用条件。
26、线程池的种类
1.FixedThreadPool:可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队
2.SingleThreadExecutor:单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量
3.CachedThreadPool:按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交
任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务
4.ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE
26、线程同步机制与原理,举例说明
为什么需要线程同步?当多个线程操作同一个变量的时候,存在这个变量何时对另一个线程可见的问题,也就是可见性。每一个线程都持有主存中变量的一个副本,当他更新这个变量时,首先更新的是自己线程中副本的变量值,然后会将这个值更新到主存中,但是是否立即更新以及更新到主存的时机是不确定的,这就导致当另一个线程操作这个变量的时候,他从主存中读取的这个变量还是旧的值,导致两个线程不同步的问题。线程同步就是为了保证多线程操作的可见性和原子性,比如我们用synchronized关键字包裹一端代码,我们希望这段代码执行完成后,对另一个线程立即可见,另一个线程再次操作的时候得到的是上一个线程更新之后的内容,还有就是保证这段代码的原子性,这段代码可能涉及到了好几部操作,我们希望这好几步的操作一次完成不会被中间打断,锁的同步机制就可以实现这一点。一般说的synchronized用来做多线程同步功能,其实synchronized只是提供多线程互斥,而对象的wait()和notify()方法才提供线程的同步功能。JVM通过Monitor对象实现线程同步,当多个线程同时请求synchronized方法或块时,monitor会设置几个虚拟逻辑数据结构来管理这些多线程。新请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有锁的线程unlock之后,则排队队列里的线程竞争上岗(synchronized是不公平竞争锁,下面还会讲到)。如果运行的线程调用对象的wait()后就释放锁并进入wait线程集合那边,当调用对象的notify()或notifyall()后,wait线程就到排队那边。这是大致的逻辑。
27、死锁的产生条件,如何避免死锁
死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个进程使用
2.请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
3.不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
4.循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
避免死锁的方法:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。 一般来说互斥条件是无法破坏的,所以在预防死锁时主要从其他三个方面入手 :
(1)破坏请求和保持条件:在系统中不允许进程在已获得某种资源的情况下,申请其他资源,即要想出一个办法,阻止进程在持有资源的同时申请其它资源。
方法一:在所有进程开始运行之前,必须一次性的申请其在整个运行过程中所需的全部资源,
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源
(2)破坏不可抢占条件:允许对资源实行抢夺。
方式一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方式二:如果一个进程请求当前被另一个进程占有的资源,则操作系统可以抢占另一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。
(3)破坏循环等待条件
对系统所有资源进行线性排序并赋予不同的序号,这样我们便可以规定进程在申请资源时必须按照序号递增的顺序进行资源的申请,当以后要申请时需检查要申请的资源的编号大于当前编号时,才能进行申请。
利用银行家算法避免死锁:
所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。
按照银行家算法的思想,当进程请求资源时,系统将按如下原则分配系统资源:
28、Android单线程模型
Android单线程模型的核心原则就是:只能在UI线程(Main Thread)中对UI进行处理。当一个程序第一次启动时,Android会同时启动一个对应的 主线程(Main Thread),主线程主要负责处理与UI相关的事件,如:用户的按键事件,用户接触屏幕的事件以及屏幕绘图事 件,并把相关的事件分发到对应的组件进行处理。所以主线程通常又被叫做UI线 程。在开发Android应用时必须遵守单线程模型的原则: Android UI操作并不是线程安全的并且这些操作必须在UI线程中执行。
Android的单线程模型有两条原则:
1.不要阻塞UI线程。
2.不要在UI线程之外访问Android UI toolkit(主要是这两个包中的组件:android.widget and android.view
29、进程线程的区别
1.地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
2.资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
3.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
4.进程切换时,消耗的资源大,效率不高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
5.执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6.线程是处理器调度的基本单位,但是进程不是。
7.两者均可并发执行。
30、Binder的内存拷贝过程
相比其他的IPC通信,比如消息机制、共享内存、管道、信号量等,Binder仅需一次内存拷贝,即可让目标进程读取到更新数据,同共享内存一样相当高效,其他的IPC通信机制大多需要2次内存拷贝。Binder内存拷贝的原理为:进程A为Binder客户端,在IPC调用前,需将其用户空间的数据拷贝到Binder驱动的内核空间,由于进程B在打开Binder设备(/dev/binder)时,已将Binder驱动的内核空间映射(mmap)到自己的进程空间,所以进程B可以直接看到Binder驱动内核空间的内容改动
31、传统IPC机制的通信原理(2次内存拷贝)
1.发送方进程通过系统调用(copy_from_user)将要发送的数据存拷贝到内核缓存区中。
2.接收方开辟一段内存空间,内核通过系统调用(copy_to_user)将内核缓存区中的数据拷贝到接收方的内存缓存区。
种传统IPC机制存在2个问题:
1.需要进行2次数据拷贝,第1次是从发送方用户空间拷贝到内核缓存区,第2次是从内核缓存区拷贝到接收方用户空间。
2.接收方进程不知道事先要分配多大的空间来接收数据,可能存在空间上的浪费。