摘要:
最近在整理Android岗位面试题的答案,虽然工作已有两年,独立开发了好几个APP,但在不查资料的情况下,回答这些试题非常的困难,瞬间感觉一万点伤害。即使不为了找工作,整理这样一份Android面试题答案,也可以加深对各个知识点的理解,这一整套面试题基于《最全的BAT大厂面试题整理》,然后将每一个分类拆分成附带参考答案的独立的文章,在此非常感谢整理试题的原作者。
Note that:文章中给出的答案尽管参考了网上的很多资料,但肯定回答得还不够到位,更需要在实际的环境中运用、验证
- 开启线程的三种方式?
第一种方式:继承Thread,重写run()方法
public class NewThread extends Thread{
@Override
public void run(){
System.out.println(getName());
}
public static void main(String[] args){
NewThread thread=new NewThread();
thread.start();
}
}
第二种方式:实现Runnable接口
public class NewRunnable implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args){
NewRunnable runnable=new NewRunnable();
Thread thread=new Thread(runnable);
thread.start();
}
}
第三种方式:实现Callable接口
public class NewCallable implements Callable{
public Object call() throws Exception {
return null;
}
public static void main(String[] args) {
NewCallable callable=new NewCallable();
FutureTask task=new FutureTask(callable);
Thread thread=new Thread(task);
thread.start();
}
}
- 线程和进程的区别?
进程,是一个应用程序分配资源(CPU资源、内存资源)的基本单位,应用程序为每一个进程分配独立的地址空间,使得进程间的通信比较困难,进程间的切换开销比较大
线程,是进程内部的一个控制流,一个进程至少包含一个线程(称为主线程),多个线程共享进程内部资源,多线程的好处保证应用程序可以并发、流畅执行,但不利于资源的管理和保护。
- 为什么要有线程,而不是仅仅用进程?
因为进程间的通信比较困难,切换开销比较大,频繁的切换会造成应用程序性能问题,影响用户体验。
而线程的特点,不仅通信方便,切换开销小,同时可以多个线程并发操作,实现多个任务同时执行,保证应用程序流畅运行
- run()和start()方法区别
查看start()方法源码,Thread调用start()方法会将自身添加到线程组队列中,在一个线程组中的线程等待抢夺CPU资源,获得资源的线程开始执行run()方法,否则不会执行run()方法
一个线程中的run()方法可以被执行多次,但start()方法只可以调用一个,加入队列中的线程处于不停的抢夺状态,直到线程销毁
public synchronized void start() {
if(this.threadStatus != 0) {
throw new IllegalThreadStateException();
} else {
this.group.add(this);
boolean var1 = false;
try {
this.start0();
var1 = true;
} finally {
try {
if(!var1) {
this.group.threadStartFailed(this);
}
} catch (Throwable var8) {
;
}
}
}
}
public void run() {
if(this.target != null) {
this.target.run();
}
}
- 如何控制某个方法允许并发访问线程的个数?
使用java.util.concurrent.Semaphore
工具类可以有效地控制并发访问线程的个数,初始化Semaphore实例,在方法体的开始处调用acquire()
方法,在方法体的结束处调用release()
方法
public class NewThread extends Thread {
private Semaphore semaphore=new Semaphore(3);
@Override
public void run(){
try {
println();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args){
//创建15个线程
int i=15;
while(i>0){
NewThread thread=new NewThread();
thread.start();
i--;
}
}
private void println() throws InterruptedException{
semaphore.acquire();
println(getName()+":进来了");
semaphore.release();
println(getName()+":出去了");
}
private void println(String str){
System.out.println(str);
}
}
- 在Java中wait和sleep方法的不同
wait()方法,是Object声明的方法,在同步控制的方法或同步控制块中调用wait()方法,使当前线程进入暂停状态,同时释放方法或控制块的同步锁,只有当调用notify()、notifyAll()方法或暂停的时间到了,当前线程才可以重新进入运行状态
sleep()方法,是Thread声明的方法,可以在任何地方调用sleep()方法,使当前线程进入暂停状态,但不会释放同步锁,只有当暂停的时间到了,当前线程才可以重新进入运行状态
- 谈谈wait/notify关键字的理解
在同步控制的方法或同步控制代码块中使用wait()方法,使当前线程进入暂停状态,进入暂停状态的线程除非调用notify()/notifyAll()方法或暂停的时间点到了,否则无法重新恢复运行状态
wait()方法的调用,同时运行其他线程调用同步控制的方法或同步控制代码块,在暂停状态时调用interrupt()方法,会抛出InterruptedException异常
wait()方法、notify()方法来自Object声明的方法,在多线程的环境中通常配合一起使用
- 什么导致线程阻塞?
当前线程调用sleep()方法,释放了CPU资源,进入了阻塞状态
在一个同步控制的方法或同步控制的代码块中,调用了wait(),使线程暂停运行,进入阻塞状态
线程试图访问一个执行一段未获取同步锁的代码,需要等待同步锁被释放,进入了阻塞状态
- 线程如何关闭?
在run()方法外添加一个标记位flag,run()方法执行完,改变标记位的状态,实现关闭线程的目的
调用sleep()方法,使线程进入暂停状态,然后调用interrupt()方法,让线程抛出InterruptedException异常,停止运行,实现关闭线程的目的
stop()是一个过时的方法,不建议直接调用该方法关闭线程
- 讲一下java中的同步的方法
同步方法,在方法返回类型前添加synchronized修饰符,多个线程访问对象内的某个同步方法,其他线程不允许访问该方法和其他同步方法,当且仅当线程释放同步锁后,其他线程才可以访问,缺点:该同步方法开销很大,参考例子:Vector、HashTable
同步代码块,在方法体内需要同步的代码块前添加synchronized修饰符,表明该代码块当且仅当线程释放同步锁后其他线程才可以访问,推荐使用该方法代替同步方法
重入锁ReentrantLock同步,被ReentrantLock对象锁定的代码块,在没有解锁之前只允许当前线程访问,其他线程等待获取同步锁
Lock lock=new ReentrantLock();
int mCount=0;//账户余额
public void addMoney(int money){
lock.lock();//锁定代码块开始处
try{
mCount+=money;
}finally{
lock.unlock();//释放同步锁
}
}
其他同步的方法:
使用volatile关键字修饰成员变量,表明该变量对多线程是可见的,一个线程修改该变量,其他线程访问获取最新的值
使用ThreadLocal类,表明多个线程共享同一个成员变量
还包括一些java.util.concurrent
包下的其他同步类
- 数据一致性如何保证?
synchronized关键字,同步代码块、同步方法可以保证数据一致性,缺点开销较大
final关键字修饰的类、方法、成员变量、局部变量,初始化后不可变,保证多线程访问数据的一致性
java.util.concurrent
包下提供的类,可以保证数据一致性,比如:AutomicInteger、ReentrantLock、ArrayBlockingQueue等
- 如何保证线程安全?
使用线程同步的相关方法,可以保证线程安全,参考上文:讲一下java中的同步的方法
- 如何实现线程同步?
参考上文:讲一下java中的同步的方法
- 两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
无法实现两个进程同时写或读操作,但可以实现两个进程交替写或读,因为它们切换的时间非常的短,感觉两个进程写或读是同时发生的。
防止进程同步:
- 线程间操作List
线程间的List操作需要考虑同步的问题,实现List同步的方法有:Vector、Collections.synchronized()、CopyOnWriteArrayList,前两种同步的方式还需要注意在遍历List的同时,不要尝试去添加、删除List,否则抛ConcurrentModificationException异常
解决的办法,如果确实想要在遍历List的同时,执行添加、删除List操作,可以在遍历代码块外面添加一个synchronized同步锁或者用CopyOnWriteArrayList代替Vector、Collections.synchronized()
如何线程安全地遍历List:Vector、CopyOnWriteArrayList
java 多线程操作List,已经做了同步synchronized,还会有ConcurrentModificationException,知道为什么吗?
- Java中对象的生命周期
创建阶段,初始化对象并分配内存地址
引用阶段,声明的变量持有一个实例化对象的引用,可以使用对象提供的属性、方法
不可见阶段,超出了作用域范围外的变量,对其他类、对象是不可见的,比如:方法内的局部变量
不可达阶段,一个类有其引用的层次结构,以某一个强引用对象为GC Root,无法通过层次结构查询到的对象,变成了不可达阶段
回收阶段,垃圾回收器扫描堆区内的对象,不可达阶段的对象标识为回收对象,进入回收阶段
终止阶段,回收对象,释放内存,准备重新分配新的实例化对象
- Synchronized用法
锁机制,每一个对象都有一个内置锁,称为对象内置锁,使用synchronized关键字可以触发对象内置锁,只有获取对象内置锁的线程才可以调用同步的方法或同步的代码块
同理,每一个类也都有一个内置锁,称为类内置锁,使用synchronized关键字可以触发类内置锁,只有获取类内置锁的线程才可以访问同步的类方法
同步代码块,作用于调用这个代码块的对象,同一个对象共享同一把锁
同步方法,作用于调用这个方法的对象,同一个对象共享同一把锁
同步静态方法,作用于该类的所有对象,多个对象共享同一把锁
同步类,作用于该类的所有对象,多个对象共享同一把锁
- synchronize的原理
JVM在编译的时候,检测到synchronized关键字,自动给synchronized修饰的实例方法或代码块添加对象内置锁,只有抢占了对象内置锁的线程才可以访问该实例方法或代码块
同理,JVM在编译的时候,检测到synchronized关键字,自动给synchronized修饰的静态方法添加类内置锁,只有抢占了类内置锁的线程才可以访问该静态方法
同理,类在调用静态方法的时候,检测到
- 谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
Synchronized关键字、类锁、方法锁的理解参考上述:Synchronized用法、synchronize的原理
重入锁的含义是,一个已获取对象锁的线程,可以重新获取该对象上的对象锁,synchronized、ReentrantLock是常用的重入锁
手写一个重入锁
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
手写一个非重入锁
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){ //不用if,而用while,是为了防止假唤醒
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
- static synchronized 方法的多线程访问和作用
static synchronized修饰的方法,称为同步类方法,使用的是类内置锁,一把类内置锁作用于该类所有声明的对象,只有抢占该类内置锁的线程可以访问同步类方法,其他线程必须等待
- 同一个类里面两个synchronized方法,两个线程同时访问的问题
一个类里面有两个synchronized的成员方法,该类声明的同一个实例使用同一个对象内置锁,两个线程同时访问,只有抢占到对象锁的线程可以访问,而另一个线程必须阻塞,直到抢占到对象锁的线程释放锁
一个类里面有两个synchronized的类方法,该类声明的多个实例使用同一个类内置锁,两个线程同时访问,只有抢占到类锁的线程可以访问,而另一个线程必须阻塞,直到抢占到类锁的线程释放
- volatile的原理
每个线程维护着自己的一份工作内存,工作内存记录变量的执行结果,线程执行完成会将结果刷新到主存,多个线程共享同一个进程下的主存,这样可能会产生一些问题。
线程A从主存中读取了i的值到工作内存中并修改,但还没有刷新到主存,这时候线程B从主存中读取到的是旧值(实际上i已经被修改),造成变量不同步的问题
volatile关键字的原理是保证工作内存变量的修改能及时刷新到主存,其他想要访问该变量的线程,必须重新从主存中读取到最新值,保证了volatile修饰变量的可见性
- 谈谈volatile关键字的用法
volatile关键字用于修饰变量,变量前面添加volatile关键字,表明变量执行的操作在多线程环境中对每个线程都是可见的,同时保证程序运行的有序性
- 谈谈volatile关键字的作用
参考上述:谈谈volatile关键字的用法
- 谈谈NIO的理解
- synchronized 和volatile 关键字的区别
synchronized通常应用于方法、代码块,保证在多线程的环境中数据访问的原子性、可见性和有序性。synchronized修饰的成员方法或代码块,表明只有抢占对象内置锁的线程可以访问,其他线程想要访问同步的方法或代码块,必须等待对象锁释放并抢占到,否则一种阻塞下去
volatile仅可以应用于变量,保证在多线程的环境中数据访问的可见性和有序性,但不保证数据访问的原子性。volatile实现的原理是将当期线程的执行结果刷新回主存,然后告诉其他线程它们在运行之前需要重新读取主存内的数据
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞
- synchronized与Lock的区别
synchronized应用于方法、代码块,只有获取内置锁的线程可以访问同步的方法或代码块,其它线程必须阻塞直到内置锁被释放
Lock除了具备synchronized内置锁的特点外,还添加了可以具体直到阻塞的时间,在阻塞的时候可以被中断,阻塞的时候还可以去处理别的事情
synchronized使用的是内置锁,该锁是在JVM层面是自动添加的;Lock常用的锁是ReentranLock,需要在代码块中手动添加锁、释放锁
synchronized在抢占资源不激烈的情况下,性能优于ReentranLock;相反,在抢占资源激烈的情况下,synchronized的性能大大降低
- ReentrantLock 、synchronized和volatile比较
三者之间的区别,可以集合上述:synchronized 和volatile 关键字的区别和synchronized与Lock的区别
- ReentrantLock的内部实现
加锁的方式 | 描述 |
---|---|
lock() |
请求获取一把没有被另一个线程持有的锁并返回,同时将当前线程持有锁的个数设为1;如果当前线程已经常用锁,则记录锁个数的变量(count )加1;如果锁被另一个线程占有,当前线程被禁用并“休眠”到获取锁 |
lockInterruptibly() |
除非当前线程被中断,否则线程一直等到直到获取锁(可以被其它线程或当前线程中断等待的状态,并抛出InterruptedException 异常) |
tryLock() |
即使tryLock 被设置为公平锁策略模式,但只要锁是可用的,即便是其它获取锁的线程同时处于等待状态,调用tryLock 当前线程将会优先抢占锁,返回true,表明锁是空闲的并被当前线程持有 |
tryLock(long timeout, TimeUnit unit) |
如果锁被另一个线程持有,在超出等待时长后,当前线程“休眠”状态会被中断 |
相同点:
如果锁是空闲的,调用上述任一方法,由当前线程抢占锁,并将记录锁个数的变量(count)加1
如果锁被另一个线程持有,调用上述任一方法,当前线程被阻塞(不同的加锁方法,处理阻塞状态的策略不一样)
不同点:
加锁的方式 | 区别 |
---|---|
lock() |
一直阻塞,直到当前线程获取锁 |
lockInterruptibly() |
除非被其它线程或当前线程中断,否则一直阻塞,直到当前线程获取锁 |
tryLock() |
在锁可用时,优先获取锁;在阻塞状态,当前线程同样会一直等待 |
tryLock(long timeout,TimeUnit unit) |
在锁可用时,优先获取锁;在阻塞状态,超出了指定时长,当前线程会被中断 |
Note that:ReentrantLock区分公平锁、非公平锁,默认使用非公平锁
- lock原理
- 死锁的四个必要条件?
title | content |
---|---|
互斥条件 | 一个资源每次只能被一个进程占有 |
请求和保持条件 | 进程因请求新的资源而阻塞,对已获得的资源保持不放 |
不可剥夺条件 | 对于进程已获得的资源,在没有使用完成之前,不可以强行剥夺 |
循环条件 | 若干个进程首尾相连形成循环等待的状态 |
- 怎么避免死锁?
尽量避免或破坏死锁的四个必要条件之一,同时系统对于进程请求的每一个系统能够分配的资源进行动态的检查,将资源分配给进程后是否会造成死锁,如果会造成死锁,系统则不予分配;否则,予以分配
- 对象锁和类锁是否会互相影响?
对象锁,所有Java对象包含一个内置对象锁,该锁由JVM添加和释放
类锁,同一个类的多个Java对象共享一个类锁,即同一个类的多个Java对象共享同一个Class对象锁,因为使用static和synchronized修饰的方法在JVM中共享的是同一份Class文件,该Class也包含一个内置对象锁,未获得Class对象内置锁的线程必须阻塞
因为对象锁指的是当前实例化的对象包含的锁,而类锁指的是Class类实例化的对象包含的锁,它们属于不同的两个实例,所以相互不受影响
- 什么是线程池,如何使用?
线程池有线程管理器、工作线程、工作任务和工作队列组成,使用ThreadPoolExecutor或Executors初始化一个线程池,在一个线程池中根据需要同时创建多个线程,需要使用线程的时候,直接从线程池中拿取一个工作线程,任务执行完成再把线程放回线程池中,避免频繁创建和销毁线程的开销
首先获取一个线程池实例,可以直接使用Executors封装好的静态方法,然后实例多个工作任务并添加到工作队列中,线程池管理器负责分配线程来执行工作任务
执行一个工作任务
Executor taskDistributor = createTaskDistributor();
taskDistributor.execute(task);
实例化一个线程管理器
/** Creates default implementation of task distributor */
public static Executor createTaskDistributor() {
return Executors.newCachedThreadPool(createThreadFactory(Thread.NORM_PRIORITY, "uil-pool-d-"));
}
实例化一个线程工厂
/** Creates default implementation of {@linkplain ThreadFactory thread factory} for task executor */
private static ThreadFactory createThreadFactory(int threadPriority, String threadNamePrefix) {
return new DefaultThreadFactory(threadPriority, threadNamePrefix);
}
封装一个线程工厂,负责创建线程
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final int threadPriority;
DefaultThreadFactory(int threadPriority, String threadNamePrefix) {
this.threadPriority = threadPriority;
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = threadNamePrefix + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) t.setDaemon(false);
t.setPriority(threadPriority);
return t;
}
}
- Java的并发、多线程、线程模型
- 谈谈对多线程的理解
多线程在同一时间可以执行多个任务,充分利用CPU资源,提高资源利用率,可以解决负载不均衡的问题。
在一个应用程序中,为了给用户提供更好的体验,对于比较耗时的操作,如:IO操作,可以开启独立的后台线程,等任务执行完成,这个时候用户可以执行其他操作
使用多线程,也可能造成内存不足或线程阻塞等问题,运用不当不仅给不了用户更好的体验,很可能影响程序的正常运行
- 多线程有什么要注意的问题?
你可能感兴趣的文章
转载请注明出处: https://www.teachcourse.cn/2613.html ,谢谢支持!