线程、多线程和线程池面试专题
阅读五分钟,每日十点,和您一起终身学习,这里是程序员Android
一、开启线程的三种方式?
1.继承Thread
类
继承Thread
类,重写run()
方法,在run()
方法体中编写要完成的任务,然后启动线程 new Thread().start();
2.实现Runnable
接口
实现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();
二、run()和start()方法区别
1.run()
方法
只是线程的主体方法,和普通方法一样,不会创建新的线程。
2.start()
方法
只有调用start()
方法,才会启动一个新的线程,新线程才会调用run()
方法,线程才会开始执行。
三、如何控制某个方法允许并发访问线程的个数?
创建Semaphore
变量,Semaphore semaphore = new Semaphore(5, true);
当方法进入时,请求一个信号,如果信号被用完则等待,方法运行完,释放一个信号,释放的信号新的线程就可以使用。
四、在Java中wait和seelp方法的不同
1.wait()
方法
属于Object
类,调用该方法时,线程会放弃对象锁,只有该对象调用notify()
方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
2.sleep()
方法
属于Thread
类,sleep()
导致程序暂停执行指定的时间,让出CPU
,但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()
方法中线程不会释放对象锁。
五、谈谈wait/notify关键字的理解
1.notify()
唤醒在此对象监视器上等待的单个线程
2.notifyAll()
通知所有等待该竞争资源的线程
3.wait()
释放obj
的锁,导致当前的线程等待,直接其他线程调用此对象的notify()
或notifyAll()
方法
当要调用wait()
或notify()/notifyAll()
方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)
代码中。当调用obj.notify/notifyAll
后,调用线程依旧持有obj
锁,因此等待线程虽被唤醒,但仍无法获得obj
锁,直到调用线程退出synchronized
块,释放obj
锁后,其他等待线程才有机会获得锁继续执行。
6、什么导致线程阻塞?
(1)一般线程阻塞
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、线程如何关闭?
- 使用标志位
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、线程间操作List
多线程数量的问题,一般情况下,多线程数量要等于机器CPU
核数-1
.
1.如何让n个线程顺序遍历含有n个元素的List集合
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
public class Test_4 {
/**
* 多线程处理list
*
* @param data 数据list
* @param threadNum 线程数
*/
public synchronized void handleList(List<String> data, int threadNum) {
int length = data.size();
int tl = length % threadNum == 0 ? length / threadNum : (length
/ threadNum + 1);
for (int i = 0; i < threadNum; i++) {
int end = (i + 1) * tl;
HandleThread thread = new HandleThread("线程[" + (i + 1) + "] ", data, i * tl, end > length ? length : end);
thread.start();
}
}
class HandleThread extends Thread {
private String threadName;
private List<String> data;
private int start;
private int end;
public HandleThread(String threadName, List<String> data, int start, int end) {
this.threadName = threadName;
this.data = data;
this.start = start;
this.end = end;
}
public void run() {
List<String> subList = data.subList(start, end)/*.add("^&*")*/;
System.out.println(threadName+"处理了"+subList.size()+"条!");
}
}
public static void main(String[] args) {
Test_4 test = new Test_4();
// 准备数据
List<String> data = new ArrayList<String>();
for (int i = 0; i < 6666; i++) {
data.add("item" + i);
}
test.handleList(data, 5);
System.out.println(ArrayUtils.toString(data));
}
}
2. List多线程并发读取读取现有的list对象
//测试读取List的线程类,大概34秒
package com.thread.list;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
Map<Long,Integer> map = new HashMap<Long,Integer>();
for(int i = 0;i<1000;i++){
list.add(""+i);
}
int pcount = Runtime.getRuntime().availableProcessors();
long start = System.currentTimeMillis();
for(int i=0;i<pcount;i++){
Thread t = new MyThread1(list,map);
map.put(t.getId(),Integer.valueOf(i));
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(list.get(i));
}
System.out.println("----"+(System.currentTimeMillis() - start));
}
}
//线程类
package com.thread.list;
import java.util.List;
import java.util.Map;
public class MyThread1 extends Thread {
private List<String> list;
private Map<Long,Integer> map;
public MyThread1(List<String> list,Map<Long,Integer> map){
this.list = list;
this.map = map;
}
@Override
public void run() {
int pcount = Runtime.getRuntime().availableProcessors();
int i = map.get(Thread.currentThread().getId());
for(;i<list.size();i+=pcount){
System.out.println(list.get(i));
}
}
}
3.多线程分段处理List集合
场景:大数据List
集合,需要对List
集合中的数据同标准库中数据进行对比,生成新增,更新,取消数据
解决方案:
List
集合分段,
动态创建线程池newFixedThreadPool
将对比操作在多线程中实现
public static void main(String[] args) throws Exception {
// 开始时间
long start = System.currentTimeMillis();
List<String> list = new ArrayList<String>();
for (int i = 1; i <= 3000; i++) {
list.add(i + "");
}
// 每500条数据开启一条线程
int threadSize = 500;
// 总数据条数
int dataSize = list.size();
// 线程数
int threadNum = dataSize / threadSize + 1;
// 定义标记,过滤threadNum为整数
boolean special = dataSize % threadSize == 0;
// 创建一个线程池
ExecutorService exec = Executors.newFixedThreadPool(threadNum);
// 定义一个任务集合
List<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>();
Callable<Integer> task = null;
List<String> cutList = null;
// 确定每条线程的数据
for (int i = 0; i < threadNum; i++) {
if (i == threadNum - 1) {
if (special) {
break;
}
cutList = list.subList(threadSize * i, dataSize);
} else {
cutList = list.subList(threadSize * i, threadSize * (i + 1));
}
// System.out.println("第" + (i + 1) + "组:" + cutList.toString());
final List<String> listStr = cutList;
task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "线程:" + listStr);
return 1;
}
};
// 这里提交的任务容器列表和返回的Future列表存在顺序对应的关系
tasks.add(task);
}
List<Future<Integer>> results = exec.invokeAll(tasks);
for (Future<Integer> future : results) {
System.out.println(future.get());
}
// 关闭线程池
exec.shutdown();
System.out.println("线程任务执行结束");
System.err.println("执行任务消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
}
12、Java中对象的生命周期
1)创建阶段(Created):
为对象分配存储空间,开始构造对象,从超类到子类对static
成员初始化;超类成员变量按顺序初始化,递归调用超类的构造方法,子类成员变量按顺序初始化,子类构造方法调用。
2)应用阶段(In Use):
对象至少被一个强引用持有着。
3)不可见阶段(Invisible):
程序运行已超出对象作用域
4)不可达阶段(Unreachable):
该对象不再被强引用所持有
5)收集阶段(Collected):
假设该对象重写了finalize()
方法且未执行过,会去执行该方法。
6)终结阶段(Finalized):
对象运行完finalize()
方法仍处于不可达状态,等待垃圾回收器对该对象空间进行回收。
7)对象空间重新分配阶段(De-allocated):
垃圾回收器对该对象所占用的内存空间进行回收或再分配,该对象彻底消失。
13、static synchronized 方法的多线程访问和作用
static synchronized
控制的是类的所有实例访问,不管new
了多少对象,只有一份,所以对该类的所有对象都加了锁。限制多线程中该类的所有实例同时访问JVM
中该类对应的代码。
14、同一个类里面两个synchronized方法,两个线程同时访问的问题
如果synchronized
修饰的是静态方法,锁的是当前类的class
对象,进入同步代码前要获得当前类对象的锁;
普通方法,锁的是当前实例对象,进入同步代码前要获得的是当前实例的锁;
同步代码块,锁的是括号里面的对象,对给定的对象加锁,进入同步代码块库前要获得给定对象锁;
如果两个线程访问同一个对象的synchronized
方法,会出现竞争,如果是不同对象,则不会相互影响。
15、volatile的原理
有volatile
变量修饰的共享变量进行写操作的时候会多一条汇编代码,lock addl $0x0,lock
前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存,这个写回内存的操作会引起在其他CPU
里缓存了该内存地址的数据无效。同时lock
前缀也相当于一个内存屏障,对内存操作顺序进行了限制。
16、synchronized原理
synchronized
通过对象的对象头(markword)
来实现锁机制,java
每个对象都有对象头,都可以为synchronized
实现提供基础,都可以作为锁对象,在字节码层面synchronized
块是通过插入monitorenter monitorexit
完成同步的。持有monitor
对象,通过进入、退出这个Monitor
对象来实现锁机制。
17、谈谈NIO的理解
NIO( New Input/ Output)
引入了一种基于通道和缓冲区的I/O
方式,它可以使用 Native
函数库直接分配堆外内存,然后通过一个存储在Java
堆的 DirectByteBuffer
对象作为这块内存的引用进行操作,避免了在Java
堆和Native
堆中来回复制数据。 NIO
是一种同步非阻塞的 IO
模型。同步是指线程不断轮询IO
事件是否就绪,非阻塞是指线程在等待IO
的时候,可以同时做其他任务。同步的核心就是Selector,Selector
代替了线程本身轮询IO
事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当IO
事件就绪时,可以通过写道缓冲区,保证IO
的成功,而无需线程阻塞式地等待。
18.ReentrantLock 、Lock、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
用于读多写少的场合,且读不需要互斥场景。
友情推荐:
Android 干货分享
至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!