一、面试经典
1.为什么使用多线程及其重要
为了使用户体验更好,服务的相应速度更快。现如今硬件不断发展,软件要求也逐渐提高,都是为了一个字:快。
2.进程、线程、管程(monitor 监视器)
3.多线程并行和并发的区别
①并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行(需要多核CPU)
②并发是指两个任务都请求运行,而处理器只能接收一个任务,就是把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行(秒杀系统)
4.wait和sleep的区别?
wait是Object类中的方法,表示释放该线程执行权,需用Object中的notify|notifyAll唤醒。而sleep只是“阻塞”当前线程指定时间,不释放执行权,是Thread类中的方法。
5.synchronized和lock的区别?
(1). 原始构成
a. synchronized是关键字属于JVM层面
monitor对象,每个java对象都自带了一个monitor,需要拿到monitor对象才能做事情
monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象,
只能在同步块或方法中才能调用wait/notify等方法),进入
monitorexit:退出
b. lock是api层面的锁,主要使用ReentrantLock实现
(2). 使用方法
a. synchronized不需要用户手动释放锁,当synchronized代码完成后系统会自动让线程释放
对锁的占用
b. ReentrantLock则需要用户手动释放锁若没有主动释放锁,就有可能会导致死锁的现象
(3). 等待是否可中断?
a. synchronized不可中断,除非抛出异常或者正常运行完成
b. ReentrantLock可中断
(设置超时时间tryLock(long timeout,TimeUnit unit),调用interrupt方法中断)
(4). 加锁是否公平
a. synchronized非公平锁
b. ReentrantLock两者都可以,默认是非公平锁,构造方法可以传入boolean值,true为公平锁,
false为非公平锁
(5). 锁绑定多个Condition
a.synchronized没有
b.ReentrantLock用来实现分组唤醒需要唤醒线程们,可以精确唤醒,而不是像synchronized要么
随机唤醒一个\要么多个
6.多线程的实现方式
①基础Tread类
public class ThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 100; i++) {
System.out.println("我是线程"+Thread.currentThread().getName()+"输出功能:"+i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我是线程"+Thread.currentThread().getName()+"输出功能:"+i);
}
}
}
②实现Runnable接口
public class RunnableDemo {
public static void main(String[] args) {
MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
Thread t = new Thread(myThreadRunnable);
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("我是线程"+Thread.currentThread().getName()+"输出功能:"+i);
}
}
}
class MyThreadRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我是线程"+Thread.currentThread().getName()+"输出功能:"+i);
}
}
}
两种实现多线程方式的区别
(1).查看源码
a.继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()
方法
b.实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用
run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),
运行时执行的是子类的run()方法
(2).继承Thread
a.好处是:可以直接使用Thread类中的方法,代码简单
b.弊端是:如果已经有了父类,就不能用这种方法
(3).实现Runnable接口
a.好处是:即使自己定义的线程类有了父类也没有关系,因为有了父类可以实现接口,而且接口
可以多现实的
b.弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,
代码复杂
③Callable接口
(1)Callable接口中的call方法和Runnable接口中的run方法的区别
- 是否有返回值(Runnable接口没有返回值 Callable接口有返回值)
- 是否抛异常(Runnable接口不会抛出异常 Callable接口会抛出异常)
(2)Future接口概述
- FutureTask是Future接口的唯一的实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Futrue得到Callable的返回值
public class ThreadCallDemo {
public static void main(String[] args) {
CallThread callThread = new CallThread();
FutureTask futureTask = new FutureTask(callThread);
new Thread(futureTask).start();
try {
Object sum = futureTask.get();
System.out.println(sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class CallThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
System.out.println(i);
sum += i;
}
return sum;
}
}
(3) 注意
- get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞)
- 一个FutureTask,多个线程调用call( )方法只会调用一次
- 如果需要调用call方法多次,则需要多个FutureTask
④线程池
此文不做记录,见后续学习笔记。
二、线程基本设置
1.设置线程名
- void setName(String name):将此线程的名称更改为等于参数 name
- String getName( ):返回此线程的名称
- 通过构造函数设置线程名称:
-Thread(String name):通过带参构造进行赋值/Thread(Runnable target , String name) - static Thread currentThread( )返回对当前正在执行的线程对象的引用
- 注意:要是类没有继承Thread,不能直接使用getName( ) ;要是没有继承Thread,要通过Thread.currentThread得到当前线程,然后调用getName( )方法
//FileWriter
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("线程1");
my2.setName("线程2");
my1.start();
my2.start();
2.线程优先级(setPriority)
线程有两种调度模型:
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]
Thread类中设置和获取线程优先级的方法
- public final void setPriority(int newPriority):更改此线程的优先级
- public final int getPriority():返回此线程的优先级
- a. 线程默认优先级是5;线程优先级范围是:1-10; b. 线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
例子:下面不一定是线程2先执行,只是先执行得
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("线程1");
tp2.setName("线程2");
tp3.setName("线程3");
//设置正确的优先级
tp1.setPriority(5);
tp2.setPriority(10);
tp3.setPriority(1);
tp1.start();
tp2.start();
tp3.start();
3.线程控制(sleep、join、setDeamon)
①static void sleep(long millis):使当前正在执行的线程停留(暂停执行)指定的毫秒数 (休眠线程)
② void join():当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (相当于插队加入)
③void join(int millis):可以等待指定的毫秒之后继续 (相当于插队,有固定的时间)
④void yield():让出cpu的执行权(礼让线程)
⑤void setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出(守护线程)(相当于象棋中的帅,要是帅没了,别的棋子都会没用了)
说明:
- 守护线程是区别于用户线程哈,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程
- 守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程
- t1.setDaemon(true)一定要在start( )方法之前使用