目录
3.1 获取当前线程对象、修改线程的名字、获取线程对象的名字
4.1.4 局部变量使用StringBuffer(线程安全)还是StringBuilder(不安全)
5.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;
- }
- }
-
-
交替效果
评论记录:
回复评论: