1 多线程基本概念
1.1 线程与进程
进程和线程的概念
- 进程:进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
- 线程:线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位
- 管程:Monitor(监视器),也就是我们平时所说的锁
举例:
你打开一个word就是一个进程开启了,你打字的时候,单词打错了,word中会有一个波浪线,这就是一个线程在检查
进程与线程的区别
- 1 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 2 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 3 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间及一些进程级的资源
- 4 调度和切换:线程上下文切换比进程上下文切换要快得多
1.2 并行与并发
并行和并发的概念
- 并行:只两个任务同时运行,就是甲任务进行的同时,乙任务也在进行(需要多核cpu的支持)
- 并发:指两个任务都请求运行,而处理器只能接收一个任务,就是把这两个任务安排轮流进行,由于时间>间隔较短,使人感觉两个任务都在运行
举例:
家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
1.3 同步和异步
同步和异步的概念
- 同步:方法调用开始,调用者必须等到方法执行完毕返回后才能继续后续的行为
- 异步:方法调用开始,就可以立即返回,调用者可以继续后续的操作
举例:
你去商场买冰箱,你向售货员下单,售货员去仓库帮你调配物品,你在商场一直等待调配,直到商家把你和冰箱一起送回家里,这就是同步
你打开电脑,打开购物网,下单了一台冰箱,对于你来说下单已经成功了,你可以去做其他事情了,这就是异步
1.4 阻塞和非阻塞
- 临界区:一种公有的资源或者共享数据,它可以被多个线程使用。临界区资源一次只能被一个线程使用,其它线程必须等待上一个线程执行完成之后,才能使用。
- 阻塞:当一个线程占用了临界区资源,那么其它需要使用这个资源的线程都必须在这个临界区上等待。等待会导致线程挂起,这样就形成了阻塞。如果占用资源的线程一直没有释放资源,那么其它的线程在这个临界区上都不能继续工作。
- 非阻塞:非阻塞表明多个线程之间的执行是不会相互影响的。
举例:
比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。这就是阻塞
如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。这就是非阻塞
2 多线程的创建方式
2.1 继承Thread
public class ThreadDemo1 {
public static void main(String[] args) {
//4.创建Thread类的子类对象
MyThread myThread = new MyThread();
//5.调用start()方法开启线程
//[会自动调用run方法这是JVM做的事情,源码看不到 ]
myThread.start();
}
}
//1.继承Thread
class MyThread extends Thread {
//2.重写run方法
public void run() {
//3.将要执行的代码写在run方法中
System.out.println("我是线程");
}
}
2.2 实现Runnable接口
public class ThreadDemo2 {
public static void main(String[] args) {
//4.创建Runnable的子类对象
MyRunnale mr = new MyRunnale();
//5.将子类对象当做参数传递给Thread的构造函数,并开启线程
new Thread(mr).start();
}
}
//1.定义一个类实现Runnable
class MyRunnale implements Runnable {
//2.重写run方法
@Override
public void run() {
//3.将要执行的代码写在run方法中
System.out.println("我是线程");
}
}
2.3 Callable接口
public class ThreadDemo3 {
public static void main(String[] args) {
//3.创建callable接口实现类的对象
NumThead m = new NumThead();
//4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(m);
//5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
//FutureTask类继承了Runnable接口
//new Runnable = futrueTask;
new Thread(futureTask).start();
//6.获取callable接口中call方法的返回值
try {
//get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
Object sum = futureTask.get();
System.out.println("sum是:" + sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.创建一个实现Callable接口的实现类
class NumThead implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
return 1024;
}
}
注意事项
- get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞)
- 一个FutureTask,多个线程调用call( )方法只会调用一次,如果需要调用call方法多次,则需要多个FutureTask
public class ThreadDemo4 {
public static void main(String[] args) throws Exception {
CallAble c=new CallAble();
FutureTask<Integer> futureTask=new FutureTask<>(c);
new Thread(futureTask,"线程A").start();
new Thread(futureTask,"线程B").start();
Integer integer = futureTask.get();
System.out.println("integer = " + integer);
}
}
class CallAble implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("欢迎你调用call方法");
return 1024;
}
}
// 结果
// 欢迎你调用call方法
// integer = 1024
2.4 线程池
public class ThreadDemo5 {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
new ThreadPoolExecutor.AbortPolicy());
threadPoolExecutor.execute(()->{
System.out.println("线程执行");
});
}
}
3 设置和获取线程名称
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 获取线程名字
String name = Thread.currentThread().getName();
System.out.println("name:" + name);
});
// 设置线程名字
t1.setName("t1");
t1.start();
// 构造方法中第2个参数可以设置名字
// Thread(Runnable target , String name)
Thread t2 = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println("name:" + name);
}, "t2");
t2.start();
}
}
4 设置线程优先级
线程有两种调度模型
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 Java使用的是抢占式调度模型
线程优先级设置和获取方法
public final void setPriority(int newPriority): 更改此线程的优先级
public final int getPriority(): 返回此线程的优先级
线程默认优先级是5;线程优先级范围是:1-10;
线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
代码
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println("name:" + name);
}, "t1");
t1.setPriority(1);
Thread t2 = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println("name:" + name);
}, "t2");
t2.setPriority(10);
t1.start();
t2.start();
}
}
// 结果
name:t2
name:t1
5 线程控制
5.1 sleep
注意点
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出
- InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
- 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
- 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性
代码
@Slf4j
public class ThreadDemo8 {
public static void main(String[] args) throws Exception {
log.info("睡眠开始");
TimeUnit.SECONDS.sleep(2);
log.info("睡眠结束");
}
}
// 结果
22:29:44 [INFO ] [main] c.i.j.ThreadDemo8 - 睡眠开始
22:29:46 [INFO ] [main] c.i.j.ThreadDemo8 - 睡眠结束
5.2 yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程(让出cpu的执行权礼让线程)
代码
public class ThreadDemo9 {
public static void main(String[] args) {
new Thread(new MyRunnable(), "t1").start();
new Thread(new MyRunnable(), "t2").start();
}
}
@Slf4j
class MyRunnable implements Runnable {
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 3; i++) {
if (i % 2 == 0) {
yield();
}
log.info(name + ":" + i);
}
}
}
// 结果
22:49:08 [INFO ] [t2] c.i.j.MyRunnable - t2:0
22:49:08 [INFO ] [t1] c.i.j.MyRunnable - t1:0
22:49:08 [INFO ] [t2] c.i.j.MyRunnable - t2:1
22:49:08 [INFO ] [t1] c.i.j.MyRunnable - t1:1
22:49:08 [INFO ] [t2] c.i.j.MyRunnable - t2:2
22:49:08 [INFO ] [t1] c.i.j.MyRunnable - t1:2
5.3 join
- 主线程暂停,等待调用join的线程执行结束后,主线程再继续
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
@Slf4j
public class ThreadDemo10 {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
log.info("我是线程t1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
// 等待t1执行完毕后再向下执行
t1.join();
log.info("我是主线程");
}
}
// 结果
18:50:40 [INFO ] [t1] c.i.j.ThreadDemo10 - 我是线程t1
18:50:42 [INFO ] [main] c.i.j.ThreadDemo10 - 我是主线程
@Slf4j
public class ThreadDemo10 {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
log.info("开始睡眠,我是线程t1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束睡眠,我是线程t1");
}, "t1");
t1.start();
// 等待t1执行1s后再向下执行
t1.join(1000);
log.info("我是主线程");
}
}
// 结果
18:52:54 [INFO ] [t1] c.i.j.ThreadDemo10 - 开始睡眠,我是线程t1
18:52:55 [INFO ] [main] c.i.j.ThreadDemo10 - 我是主线程
18:52:57 [INFO ] [t1] c.i.j.ThreadDemo10 - 结束睡眠,我是线程t1
5.4 setDeamon
- 守护线程是区别于用户线程哈,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程
- 守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程
- t1.setDaemon(true)一定要在start( )方法之前使用
@Slf4j
public class ThreadDemo11 {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
log.info("我是子线程(用户线程.I am running");
} catch (Exception e) {
}
}
});
// 设置为守护线程
thread.setDaemon(true);
thread.start();
TimeUnit.SECONDS.sleep(2);
log.info("main线程结束");
}
}
// 结果
18:59:30 [INFO ] [Thread-0] c.i.j.ThreadDemo11 - 我是子线程(用户线程.I am running
18:59:31 [INFO ] [Thread-0] c.i.j.ThreadDemo11 - 我是子线程(用户线程.I am running
18:59:31 [INFO ] [main] c.i.j.ThreadDemo11 - main线程结束
6 线程的生命周期
- 1 新建:就是刚使用new方法,new出来的线程
- 2 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行
- 3 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
- 4 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- 5 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源
7 常见面试题
7.1 wait | sleep的区别?功能都是当前线程暂停,有什么区别?
1 wait放开手去睡,放开手里的锁;wait是Object类中的方法
2 sleep握紧手去睡,醒了手里还有锁 ;sleep是Thread中的方法
7.2 synchronized 和 lock的区别
1 synchronized是关键字属于JVM层面, lock是api层面的锁,主要使用ReentrantLock实现
2 synchronized自动释放锁, ReentrantLock需要手动释放锁
3 synchronized不可中断, ReentrantLock可以中断
4 synchronized非公平锁,ReentrantLock支持非公平锁和公平锁
5 ReentrantLock锁可以绑定多个Condition,但是synchronized不可以
7.3 Callable接口中的call方法和Runnable接口中的run方法的区别
1 是否有返回值(Runnable接口没有返回值 Callable接口有返回值)
2 是否抛异常(Runnable接口不会抛出异常 Callable接口会抛出异常)
3 落地方法不一样,一个是call() ,一个是run()