目录
一、多线程
1.1 基本介绍
1.2 进程和线程的关系
1.3 多线程并发概念
二、实现线程的方式
2.1 继承Thread类
2.2 实现java.lang.Runnable接口
2.3 匿名类
2.4 实现Callable接口(JDK8新特性)
2.5 run和start的区别
2.6 线程声明周期
三、线程中易错及常用的方法
3.1 获取当前线程对象、修改线程的名字、获取线程对象的名字
3.2 线程 sleep方法
3.2.1 面试题
3.2.2 终止睡眠
3.2.3 强行终止线程
3.2.4 合理终止线程
3.3 线程调度(了解)
3.3.1 获取线程优先级、设置线程优先级、合并线程
四、线程安全(重要)
4.1 对synchronized理解
4.1.1 哪些变量有线程安全问题?
4.1.2 扩大同步范围
4.1.3 synchronized出现在实例方法上
4.1.4 局部变量使用StringBuffer(线程安全)还是StringBuilder(不安全)
4.1.5 synchronized总结:3种用法
4.1.6 面试题1
4.1.7 面试题2
4.1.8 面试题3
4.1.9 面试题4
4.2 死锁
4.2.1 怎么解决死锁
五、守护线程
5.1 实现守护线程
5.2 定时器(比较重要,但是也很少用,因为框架支持定时任务)
六、生产者消费模式(wait和notify)
6.1 wait和notify作用
6.2 生产者消费者模式
一、多线程
1.1 基本介绍
进程:一个应用程序,一个进程就是一个软件
线程:一个进程中的执行场景/执行单元
一个进程可以启动多个线程
对于java程序来说,当在DOS命令窗口中输入java HelloWorld回车之后,会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾,也就是说java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程
1.2 进程和线程的关系
我们拿公司举例:
阿里巴巴:进程
马云:阿里巴巴的一个线程
童文红:阿里巴巴的一个线程
京东:进程
强东:京东的线程
妹妹:京东的线程
进程可以看做公司,线程可以看做公司中的某个员工
十个线程十个栈,每个栈不会相互干扰,各自执行各自的,这就是多线程并发
主栈空了,其他的栈可能还在执行
注意:进程A和进程B的内存独立不共享,即阿里巴巴和京东的资源不共享
在Java语言中线程A和线程B的堆内存和方法区内存共享,但是栈内存独立
多线程机制的目的:提高程序的运行效率
1.3 多线程并发概念
t1线程执行t1的,t2线程执行t2的,两者互不影响
那单核的CPU(相当于一个大脑)能实现多线程并发么?
对于多核肯定没问题的。单核的不能做到真正的多线程并发,但是能做到一个多线程并发的感觉。因为CPU的处理速度很快,多个线程之间频繁切换执行,给人一种多线程并发的错觉
二、实现线程的方式
2.1 继承Thread类
编写一个类,继承java.lang.Thread类,重写run方法
public class ThreadTest01 {
public static void main(String[] args) {
// 这里的代码属于主线程
// 新建分支线程独享
MyThread myThread = new MyThread();
// 启动线程,start()方法的作用就是启动一个分支线程,在JVM中开辟一个新的空间,这段代码瞬间就结束
// 只要新的空间开出来,代码就执行完毕。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部
myThread.start();
// 这里的代码还是主线程
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--》"+i);
}
}
}
public class MyThread extends Thread{
@Override
public void run() {
// 编写程序,这段程序运行在分支栈中
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--》"+i);
}
}
}
输出结果有先有后,有多有少
JVM图示
2.2 实现java.lang.Runnable接口
下面这种用的多些,因为一个类可实现多个接口但是只能实现一个类
public class ThreadTest01 {
public static void main(String[] args) {
// 这里的代码属于主线程
// 将可运行的对象封装成一个线程对象
Thread t = new Thread(new MyThread());
t.start();
// 这里的代码还是主线程
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--》"+i);
}
}
}
//仅仅是一个线程类,是一个可运行的类,此时还不是个线程
public class MyThread implements Runnable{
@Override
public void run() {
// 编写程序,这段程序运行在分支栈中
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--》"+i);
}
}
}
2.3 匿名类
public class ThreadTest01 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--》"+i);
}
}
});
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--》"+i);
}
}
}
2.4 实现Callable接口(JDK8新特性)
这种方式实现的线程可以回去线程的返回值
2.5 run和start的区别
start()方法的作用就是启动一个分支线程,在JVM中开辟一个新的空间,这段代码瞬间就结束。只要新的空间开出来,代码就执行完毕。启动成功的线程会自动调用run方法(不需要我们主动调用),并且run方法在分支栈的栈底部
run()方法只是一个方法,如果是t.run()调用的话并不会出现新的线程,仅仅是对象调用其方法,不会启动分线程
2.6 线程声明周期
面试挺重要的
三、线程中易错及常用的方法
3.1 获取当前线程对象、修改线程的名字、获取线程对象的名字
怎么获取当前线程对象?
static Thread currentThread
public class ThreadTest01 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
Thread t1 = Thread.currentThread();
System.out.println(t1);
System.out.println(t.currentThread());
}
}
为什么我们用t线程调用的时候输出的结果和t1输出的一个样呢?
因为这个方法是静态方法,是在主线程中调用的,所以输出的线程是主线程
简单地说 static Thread currentThread这段代码出现在哪里,就获取的哪个线程对象
线程对象.setName("线程名字");
线程对象.getName();
线程没有设置名字的时候就是Thread-0,Thread-1.....
public class ThreadTest01 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
t.setName("ttttt");
System.out.println(t.getName());
t.start();
}
}
3.2 线程 sleep方法
此方法能使线程进入阻塞状态,可以做到间隔特定的时间执行特定的程序
静态方法,参数是毫秒,让当前线程进入阻塞状态,放弃占有CPU时间片,让给其他线程使用
简单的说,在哪个线程中使用,哪个线程就会进行休眠状态
public class ThreadTest01 {
public static void main(String[] args) {
System.out.println("休眠前");
try {
//休眠五秒
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("休眠后");
}
}
3.2.1 面试题
下面这段代码,不会让t线程进入休眠状态,反而是让当前线程进入休眠状态
让哪个线程进入休眠状态,取决于在哪个线程进行使用
3.2.2 终止睡眠
注意:run方法中的异常不能抛出,只能try,因为run方法在父类中没有抛出异常,子类不能比父类抛出更多的异常
不是中断线程的执行,而是中断线程的睡眠
public class ThreadTest01 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
t.setName("t");
t.start();
// 希望五秒后t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 干扰,这段代码会让t线程中睡眠出现异常,然后进入catch语句块,然后整个try...catch结束了
t.interrupt();
}
}
//仅仅是一个线程类,是一个可运行的类,此时还不是个线程
public class MyThread implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000*500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 编写程序,这段程序运行在分支栈中
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--》"+i);
}
}
}
干扰,这段代码会让t线程中睡眠出现异常,然后进入catch语句块,然后整个try...catch结束了
t.interrupt();
3.2.3 强行终止线程
不建议使用,已经过时了
这个方法容易丢失数 据,这是一个很坏的结果,非常大的缺点
3.2.4 合理终止线程
public class ThreadTest01 {
public static void main(String[] args) {
MyThread r = new MyThread();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 希望五秒后t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 干扰,这段代码会让t线程中睡眠出现异常,然后进入catch语句块,然后整个try...catch结束了
r.run=false;
}
}
//仅仅是一个线程类,是一个可运行的类,此时还不是个线程
public class MyThread implements Runnable{
// 打标记
public boolean run = true;
@Override
public void run() {
if(run){
try {
Thread.sleep(1000*500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 编写程序,这段程序运行在分支栈中
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--》"+i);
}
}else {
// 终止
return;
}
}
}
3.3 线程调度(了解)
- 抢占式调度模型
哪个线程的优先级别高,抢到CPU时间片的概率高一些。java便采用的是此模型
- 均分式调度模型
平均分配CPU时间片,每个线程占有CPU时间片长度一样。平均分配,一切平等
3.3.1 获取线程优先级、设置线程优先级、合并线程
四、线程安全(重要)
多线程并发条件下数据的安全问题
什么时候会存在安全问题?
多线程并发、有共享数据、共享数据有修改的行为
怎么解决线程安全问题?
线程排队执行,不能并发,使用线程同步机制(牺牲一部分效率)
- 同步编程模型
两个线程各自执行各自的,谁也不等谁,其实就是多线程并发
- 异步编程模型
两个线程,t1执行的时候,必须等待t2线程执行,效率较低
4.1 对synchronized理解
下面这个不一定写this,只要是共享对象就行
对synchronized解决线程安全问题的理解:(仔细阅读)
只要进入了synchronized就进入了线程同步模式,加入t1线程过来,遇到synchronized之后就会找后面括号里面的对象锁,每个java对象都有一把锁。此时synchronized便把这把锁给占有了,然后执行里面的代码。假如在这段代码的执行过程中,t2线程也遇到了这个synchronized,也会占用这个对象锁,但是很可惜,这把锁已经被t1线程给占用了,所以t2线程只能等待。当t1线程执行完这段代码后,t1线程会归还这把锁。归还之后,在等待的t2线程便不必等待,就拿到这个对象锁,再去执行代码。这样就达到了线程排队执行。这个共享对象一定要选好,这个共享对象一定是你需要排队执行的这些线程对象所共享的。
类似厕所的茅坑,这个茅坑一个人用完了另一个人才能用。
java语言中,任何一个独享都有一把锁,这把锁本质是一个标记,只是叫做锁。
100个对象,100把锁。1个对象1个锁
4.1.1 哪些变量有线程安全问题?
实例变量:堆中
静态变量:方法区中
局部变量: 栈中
以上变量中,局部变量永远不会存在线程安全问题,因为不会共享。堆和方法区都只有一个,堆和方法区都是多线程共享的,所以可能存在线程安全问题。
常量也没有线程安全问题,常量不可修改
4.1.2 扩大同步范围
同步代码块越小,效率越大。
4.1.3 synchronized出现在实例方法上
优点:代码写的少,简洁了。 如果共享的对象就是this,并且同步的代码块是整个方法体,就用这种方式。
4.1.4 局部变量使用StringBuffer(线程安全)还是StringBuilder(不安全)
因为局部变量没有线程安全问题,所以选择StringBuilder,效率高,不会走锁池
4.1.5 synchronized总结:3种用法
4.1.6 面试题1
问:doOther方法的执行需不需要等待doSome方法的结束?
t1线程和t2线程是同一个。此时doOther方法的执行并不需要等doSome方法的结束,执行doOther的时候没有锁
4.1.7 面试题2
问:doOther方法的执行需不需要等待doSome方法的结束?
此时是需要的。doSome一直占用着一把锁,doOther方法无法执行,锁被占用了
4.1.8 面试题3
问:doOther方法的执行需不需要等待doSome方法的结束?
不用要等待,因为对象mc1和对象mc2不是共享对象,所以是两把锁
4.1.9 面试题4
问:doOther方法的执行需不需要等待doSome方法的结束?
需要等待,是类锁,出现在静态方法上,虽然new了两次但是同一个类。类锁只有一把
这种锁叫做排他锁,t1线程拿到后,其他线程拿不到
4.2 死锁
让程序停止不前,也不出什么异常但是就是不动了,很诡异,程序僵持住了。
这种错误很难调试。
死锁代码
4.2.1 怎么解决死锁
synchronized在开发中不要嵌套使用
我们在实际开发中只有在不得已的情况下才使用此关键字。此关键字会导致用户体验不好,系统用户的吞吐量降低,用户体验差。在不得已的情况下再选择线程同步机制。
第一种方案:尽量使用局部变量代替实例变量和静态变量
第二种方案: 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应一个对象),对象不共享便没有线程安全问题
第三种方案:synchronized线程同步机制
五、守护线程
垃圾回收器就是守护线程(后台线程,默默的在后面),主线程是用户线程,我们自己创建的线程也是用户线程
守护线程的特点:死循环,所有的用户线程只要结束,守护线程自动结束
守护线程一般用在系统数据自动备份,我们一般将定时器设置为守护线程
所有的用户线程结束,自动退出,也不需要数据备份
5.1 实现守护线程
5.2 定时器(比较重要,但是也很少用,因为框架支持定时任务)
间隔特定时间执行特定的程序。比如每天的数据备份、每天的流水分总计
public class Test {
public static void main(String[] args) throws ParseException {
// 创建定时器对象
Timer timer = new Timer();
// 指定定时任务 timer.schedule(定时任务,第一次执行时间,间隔多久);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2022-12-12 09:30:00");
// 间隔十秒
timer.schedule(new LogTimerTask(),firstTime,1000*10);
}
}
//编写定时任务类
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 编写需要执行的任务
System.out.println("执行任务");
}
}
六、生产者消费模式(wait和notify)
wait和notify是Object类的方法。并不是线程对象调用的
6.1 wait和notify作用
wait方法和notify方法建立在线程同步的基础上
wait方法,让正在对象是哪个获取地t线程进入等待状态,并且释放掉t线程之前占有的锁
6.2 生产者消费者模式
模拟生产者和消费者的代码
//生产线程负责生产,消费线程负责消费
public class Test {
public static void main(String[] args) throws ParseException {
// 模拟仓库 模拟生产一个消费一个
List list = new ArrayList<>();
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
class Producer implements Runnable{
private List list;
@Override
public void run() {
// 生产
while (true){
// 加锁
synchronized (list){
if(list.size()>0){
// 仓库满了表示生产够了,不再生产
try {
// 当前线程进入等待状态,并释放锁。如果不释放这个锁的话,消费线程无法操作
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序运行到这里说明仓库是空的,可以生产
Object object = new Object();
list.add(object);
System.out.println(Thread.currentThread().getName()+"---->"+object);
// 唤醒消费者消费
list.notify();
}
}
}
public Producer(List list) {
this.list = list;
}
}
class Consumer implements Runnable{
private List list;
@Override
public void run() {
// 消费
while (true){
synchronized (list){
if(list.size() ==0){
// 仓库空了,等待
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费,运行到这里说明仓库中有剩余
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName()+"---->"+obj);
// 唤醒生产者生产 这个地方唤醒所有也没问题,因为唤醒不会释放锁
list.notify();
}
}
}
public Consumer(List list) {
this.list = list;
}
}
交替效果