一、Thread 基础知识
1、线程的状态
- 创建(new)状态:准备好了一个多线程对象。
- 就绪(runable)状态:调用了start()方法,等待CPU的调度。
- 运行(running)状态: 执行run()方法.
- 阻塞(blocked)状态:暂时停止,可能将资源交给其他线程。
- 终止(dead)状态:线程销毁。
2、线程执行流程
- 1、线程创建后不会立即进入就绪状态,因为线程运行需要一些条件(比如划分一块内存空间)。只有线程就绪需要的条件都准备好了,才能进入就绪状态。
- 2、当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
- 3、线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去。进入阻塞状态。time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)
- 4、当由于突然中断或者子任务执行完毕,线程就会被消亡。
3、sleep和wait的区别:
- wait是Thread的方法,sleep是Object中的方法。
- Thread.sleep() 不会改变锁的行为,不会让线程释放锁。
- Thread.sleep和Object.wait都会暂停当前的线程,调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.
4、停止一个线程
- 1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 2、使用stop方法强行终止线程,但是不推荐使用这个方法,是作废过期的方法,使用他们可能产生不可预料的结果。
- 3、使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
二、Android 面试题
1、开启线程的三种方式?
- 1)继承thread 类
public class MyThread extends Thread {
private int i;
@Override
public void run() {
...
}
}
-
- 实现Runable接口
//1实现Runnable接口
public class SecondThread implements Runnable{
public void run() {
}
}
//2使用
SecondThread s1=new SecondThread();
Thread t1=new Thread(s1,"线程1");
t1.start();
2、线程和进程的区别?
- 1)一个程序至少有一个进程.并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。
- 2)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 3)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
- 4)线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所以系统中的执行功能通常是创建线程去完成的
3、为什么要有线程,而不是仅仅用进程?
- 1)进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 2)进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
4、run()和start()方法区别
- 1)用start方法来启动线程,真正实现了多线程运行。
- 2)run方法只是thread的一个普通方法调用,还是在主线程里执行。
5、如何控制某个方法允许并发访问线程的个数?
- 通过samePhore方法
- semaphore.acquire() 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
- semaphore.release() 释放一个信号量,此时信号量个数+1
public class SemaphoreTest {
private Semaphore mSemaphore = new Semaphore(5);
public void run(){
for(int i=0; i< 100; i++){
new Thread(new Runnable() {
@Override
public void run() {
test();
}
}).start();
}
}
private void test(){
try {
mSemaphore.acquire(); //Thread.interrupted()阻塞线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 进来了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 出去了");
mSemaphore.release();
}
}
6、在Java中wait和seelp方法的不同;
-
- Thread类的方法:sleep(),yield()等 ,Object的方法:wait()和notify()等
-
- Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。调用wait()方法的时候,线程会放弃对象锁,
-
- 而且wait存在notify方法来唤醒调用wait的线程,这个是sleep没有的。
7、谈谈wait/notify关键字的理解
-
- wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类,每个对象都有wait( ),notify( ),notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。
-
- notify( ) 方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程)
-
- notifyAll( ) 通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行)
-
- wait( ) 导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
8、什么导致线程阻塞?
- 1)特点:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才恢复运行。或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException
- 2)线程执行了Thread.sleep(intmillsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
- 3)线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法
- 4)线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
- 5)线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
9、线程如何关闭?
- 1)关于线程的关闭,可能会用stop() 方法,但是stop是线程不安全的,一般采用interrupt,判断线程是否中止采用isInterrupted,
Thread thread = null;
thread = new Thread(new Runnable() {
@Override
public void run() {
/*
* 在这里为一个循环,条件是判断线程的中断标志位是否中断
*/
while (true&&(!Thread.currentThread().isInterrupted())) {
try {
Log.i("tag","线程运行中"+Thread.currentThread().getId());
// 每执行一次暂停40毫秒
//当sleep方法抛出InterruptedException 中断状态也会被清掉
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
//如果抛出异常则再次设置中断请求
Thread.currentThread().interrupt();
}
}
}
});
thread.start();
//触发条件设置中断
thread.interrupt();
10、讲一下java中的同步的方法
-
- 同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
-
- 同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
- 3)使用特殊域变量(volatile)实现线程同步
- 4)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
- 5)使用局部变量ThreadLocal实现.
- 总结:volatile和ThreadLoacl都是通过修饰线程共享的变量来实现同步.剩余3种类似个域加锁.
11、数据一致性如何保证?
- 这个没使用过!不会
12、如何保证线程安全?
- 确保线程安全的方法:竞争与原子操作、同步与锁、可重入、过度优化
- 1)竞争与原子操作:
多线程访问和修改一个数据,可能造成严重的后果。原因是很多的操作被操作系统编译为汇编代码后不止一条指令,因此执行的时候可能执行到一半就被调度系统打乱执行别的代码了。一般单指令的操作称为原子操作,不管怎么样,单指令的执行是不会被打乱的。
因此为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。 - 2)同步与锁:
在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问,同步的最常用的方法是使用锁(Lock),- 1、每个线程访问数据或者资源前首先试图获取锁,
- 2、访问结束后释放锁 ,
- 3、在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可以用。
- 3)可重入:
一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。什么是可重入函数?
可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
//只使用非静态局部变量;不调用不可重入的函数。
public class Reentrant1 {
private int count = 0;
/**
* 不可重入
* 当有多个线程调用同一个Reentrant对象的increament(),输出数值是不可预测的。count可能是1,也可能是任何正整数。
*/
public void increament(){
count ++;
System.out.println(count);
}
/**
* 可重入
* 无论多少线程调用同一个Reentrant对的象decreament方法,输出数值都是传入参数length的值减1。
*/
public void decreament(int length){
length--;
System.out.println(length);
}
}
- 4)过度优化
在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。 可以使用volatile关键字试图阻止过度优化,它可以做两件事:- 第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;
- 第二,阻止编译器调整操作volatile变量的指令顺序。
13、如何实现线程同步?
同 问题10
14、两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
15、线程间操作List
通过 :Collections.synchronizedList(new ArrayList());
public class MultiThread {
public static final List<Long> list = Collections.synchronizedList(new ArrayList<Long>());
public static void main(String[] args) {
for(int i = 1;i<=100;i++){
list.add(Long.valueOf(i));
}
MyThread myThread = new MultiThread().new MyThread();
Thread t1 = new Thread(myThread);
t1.setName("线程1");
t1.start();
Thread t2 = new Thread(myThread);
t2.setName("线程2");
t2.start();
Thread t3 = new Thread(myThread);
t3.setName("线程3");
t3.start();
Thread t4 = new Thread(myThread);
t4.setName("线程4");
t4.start();
}
public class MyThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<list.size();i++){
// 同步list,打印数据并删除该数据
synchronized (list) {
try {
//当前线程睡眠,让其它线程获得执行机会
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.print(Thread.currentThread().getName() + ":" + list.get(i)+"\n");
list.remove(i);
}
}
}
16、Java中对象的生命周期
-
1、创建阶段(Created)
- 1 为对象分配存储空间
- 2 开始构造对象
- 3 从超类到子类对static成员进行初始化
- 4 超类成员变量按顺序初始化,递归调用超类的构造方法
- 5 子类成员变量按顺序初始化,子类构造方法调用
-
2、应用阶段(In Use)
- 对象至少被一个强引用持有着
-
3、不可见阶段(Invisible)
- 说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
publec int getNum(){
int num =0;
reture num+1;
}
//不可见
Log.e("error--->",num);
-
4、不可达阶段(Unreachable)
- 对象处于不可达阶段是指该对象不再被任何强引用所持有。
-
5、收集阶段(Collected)
- 当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。
-
6、终结阶段
- 当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
-
7、对象空间重新分配阶段
- 垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
17、Synchronized用法
- 1)synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 1.修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 2.修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 3.修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 4.修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
- 2)总结:
- 1.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 2.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 3.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
注意:作用于一个类,和作用于一个对象的区别!!!
18、synchronize的原理
- 1)通过反编译synchronize代码块可以得到 monitorenter 和 monitorexit:
- 1.monitorenter:每个对象都有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程中执行monitorenter就是尝试获取monitor的所有权。其过程如下:
- a.如果moniter的进入数为0,则该线程进入monitor。设置monitor的进入数为1,既该线程成为monitor的所有者。
- b.如果该线程已占有该monitor,则只需重新进入,monitor的进入数加1。
- c.如果其他线程占有了该monitor,则该线程进入阻塞状态。直到monitor的进入数为0.再尝试获取monitor的所有权
- 2.monitorexit:
- a.该指令执行时,monitor的进入数减一,当monitor的进入数为0时,该线程退出monitor,不再是线程的持有者。等着其他线程尝试进入。
- 1.monitorenter:每个对象都有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程中执行monitorenter就是尝试获取monitor的所有权。其过程如下:
- 2)通过分析:synchronize的底层语义原理是通过monitor对象来完成的。
其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
19、谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
- 1)Synchronized关键字:
- 2)类锁:当Synchronized修饰某个代码块的时候,那么就程这段代码为同步代码块
Synchronized(Object){ //注意Object被称为锁对象
........ //这里面的就是同步的代码块
}
- 3)方法锁:当Synchronized修饰某个方法的时候,那么就称这个方法为同步方法。
- 4)重入锁:所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。可重入锁的意义在于防止死锁
20、static synchronized 方法的多线程访问和作用
- 1)Synchronized(实例锁):锁在每一个对象上。如果该类是一个单例(比如单例模式),那么它就具有全局锁的概念。
- 2)Static Synchronized(全局锁):该锁针对的是类的,无论实例化了多少个对象,那么线程都是共享这个锁的。
21、同一个类里面两个synchronized方法,两个线程同时访问的问题
- 1)在同一个类的一个实例中的两个synchronized方法,并发访问。
不可以!!!
多个线程访问同一个类的synchronized方法时, 都是串行执行的!
synchronized方法使用了类java的内置锁, 即锁住的是方法所属对象本身.
同一个锁某个时刻只能被一个执行线程所获取, 因此其他线程都得等待锁的释放.
- 2)在同一个类的两个实例中的两个synchronized方法,并发访问。
22、volatile的原理
- 1)volatile作用:
- 1.防止重排序,实例化一个对象其实可以分为三个步骤:
- a.分配内存空间
- b.初始化对象
- c.将内存空间地址赋值给相应的对象。
- ※ 操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
- a.分配内存空间
- c.将内存空间地址赋值给相应的对象。
- b.初始化对象
- 1.防止重排序,实例化一个对象其实可以分为三个步骤:
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,
从而导致不可预料的结果。
因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
- 2.实现可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。
引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。
volatile关键字能有效的解决这个问题
- 3.保证原子性
volatile只能保证对单次读/写的原子性
- 2)volatile原理:
23、谈谈volatile关键字的用法
- 同上
24、谈谈volatile关键字的作用
- Synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。
25、谈谈NIO的理解
nio 是non-blocking的简称,NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO最重要的三个组成部分:
- 1)通道 Channels
- 2)缓冲区 Buffers
- 3)选择器 Selectors
26、synchronized 和volatile 关键字的区别
- 1)volatile是变量修饰符,而synchronized则作用于一段代码或方法。
- 2)volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
27、synchronized与Lock的区别
- 1)Lock是一个接口,而synchronized是Java中的关键字。
- 2)synchronized是在JVM层面上实现的,在代码执行时出现异常,JVM会自动释放锁定,使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
- 3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- 5)Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
28、ReentrantLock 、synchronized和volatile比较
- 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
29、ReentrantLock的内部实现
- 1)ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型
30、lock原理
31、死锁的四个必要条件?
- 1)死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
- 2)条件:
- 1.互斥条件:一个资源每次只能被一个进程使用。
- 2.占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 3.不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
32、怎么避免死锁?
- 1)避免一个线程获取多个锁。
- 2)避免一个线程在锁内占用多个资源,尽量保证每个锁只占用一个资源。
- 3)尝试定时锁,使用 lock.tryLock(timeout) 来替代使用内部索机制。
- 4)对于数据库锁,加锁解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
33、对象锁和类锁是否会互相影响?
-
1)对象锁:在代码中的方法上加了synchronized的锁,或者synchronized(this)的代码段,java的所有对象都含有1个对象锁,这个锁由JVM自动获取和释放。
-
2)类锁:在代码中的方法上加了static和synchronized的锁,或者synchronized(xxx.class)的代码段
34、什么是线程池,如何使用?
- 1)newSingleThreadExecutor:
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务 - 2)newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行 - 3)newCacheThreadExecutor
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。 - 4)newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程
35、Java的并发、多线程、线程模型
36、谈谈对多线程的理解
37、多线程有什么要注意的问题?
38、谈谈你对并发编程的理解并举例说明
39、谈谈你对多线程同步机制的理解?
40、如何保证多线程读写文件的安全?
41、多线程断点续传原理
42、断点续传的实现