Java多线程

举报
Java的学习之路 发表于 2022/03/24 11:18:14 2022/03/24
【摘要】 一、基本概念 1.进程和线程什么是线程? 什么是进程?进程是一个应用程序 (1个进程是一个软件)线程是一个进程中的执行场景/执行单元一个进程可以启动多个线程对于Java程序来说,在DOS命令窗口输入java HelloWorld回车以后会先启动JVM,而JVM就是一个进程JVM再启动一个主线程调用main方法同时启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的Java程序至少要有两...

一、基本概念

1.进程和线程

什么是线程? 什么是进程?

进程是一个应用程序 (1个进程是一个软件)

线程是一个进程中的执行场景/执行单元

一个进程可以启动多个线程

对于Java程序来说,在DOS命令窗口输入

java HelloWorld回车以后

会先启动JVM,而JVM就是一个进程

JVM再启动一个主线程调用main方法

同时启动一个垃圾回收线程负责看护,回收垃圾。

最起码,现在的Java程序至少要有两个线程并发

一个是垃圾回收线程,一个是执行main方法的主线程

2.进程和线程的关系

例如

阿里巴巴:进程

​ 马云:阿里巴巴的一个线程

​ 童文红:阿里巴巴的一个线程

京东:进程

强东:京东的一个线程

妹妹:京东的一个线程

进程可看成现实生活的公司,线程可看成公司的某个员工

注意:

进程A和进程B的内存独立不共享------阿里巴巴和京东的资源不共享

魔兽游戏是一个进程

酷狗音乐是一个线程

这两个线程是独立的,不共享资源

线程A和线程B呢?

​ 在Java中:

​ 线程A和线程B,在堆内存和方法区共享

​ 但是栈内存独立,一个线程一个栈

加入启动10个线程,就会有10个栈空间,每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

火车站可以看成一个进程

火车站中的每一个售票窗口可以看成是一个线程,我在窗口1买票,你可以在窗口2买票,不需要等我,我也不需要等你,各个窗口售票互不干扰,所多线程并发可以提高效率

Java之所以有多线程机制,目的是为了提高程序的处理效率

思考

使用多线程以后,那么以后main方法结束,是不是可能程序还没有结束。main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrNiw5KR-1645706524755)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220207232714642.png)]

3. 多线程并发的理解

对单核cpu来说,真的可以做到多线程并发吗?

对于多核的cpu电脑来说,真正的多线程并发是没有问题的

​ 4核cpu表示同一个时间点上,可以真正的有4个进程进行并发执行

什么是真正的多线程并发?

​ t1线程执行t1的

​ t2线程执行t2的

​ t3线程执行t3的

​ t1,t2,t3互不影响,这就是真正的多线程并发

单核cpu表示只有一个大脑

​ 不能做到真正的多线程并发,但是可以给人做到一种多线程并发的感觉

​ 对于单核cpu来说,在某一个时间点上实际上只能处理意见事情,但是由于cpu的处理速度很快,多个线程之间频繁切换执行,人的感 觉是:多个事情同时在做

​ 线程A:播放音乐

​ 线程B:运行魔兽游戏

电影院 用胶卷来播放电影,一个胶卷一个胶卷播放速度打的一定程度以后

人类的感觉就产生了错觉,感觉是动画的,说明人类的反应速度很慢,就像一根针扎到手中,到最终感觉到疼,这个过程是需要“很长”时间的,在这个期间计算机可以进行亿万次的循环,所以计算机的执行速度很快

二、 创建线程的方式

1.方式一继承Thread类

编写一个类直接继承java.lang.Thread类,重写run方法

在run方法中写的程序是运行在分支线程中(分栈)

在main方法中的代码运行在主线程中,在主栈中运行

注意:方法体的代码永远都是自上而下执行,前一个代码没有执行完毕后面的代码是不会执行的

1.1步骤

假设有一个叫做MyThread类继承Thread类

1.创建分支线程对象

MyThread myThread=new MyThread();

2.启动线程

myThread.start(); 调用线程对象的start方法

3.如果后面接着写代码片段,则运行在主线程中

1.2start方法的作用

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成后,瞬间就结束了。这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开辟出来,start()方法就结束了,线程就启动成功。run()方法在分支栈的底部,main方法在主栈的底部。run和main是平级的。

1.3如果直接调用run方法

不会启动线程,不会分支新的分支,只有主线程

1.4直接调用run方法的内存图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A5owvown-1645706524756)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208095411219.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YZlaokRW-1645706524756)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208094355928.png)]

1.5调用start方法的内存图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ucdnfctO-1645706524757)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208095506844.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfADnjeu-1645706524757)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208100037370.png)]

主线程和分线程总是有一个先执行,一个后执行

有先有后 控制台只有一个

有多有少

2.方式二实现Runnable接口(比较常用)

因为还可以继承其他的类

写一个类实现java.lang.Runnable接口,重写run方法

1.创建一个可运行的对象

MyRunnable r=new MyRunnable();

2.把可运行的对象封装成一个线程对象

Thread t=new Thread®;

3.启动线程

t.start();

3.采用匿名内部类创建线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMpoB362-1645706524758)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208113521369.png)]

4.线程对象的生命周期

新建,就绪,运行,死亡

当阻塞以后,重新抢到执行权,不会重新执行run方法,而是从上一次没有执行完的run

方法继续往下执行

5.相关方法

5.1设置线程名字 和获取线程名字

setName()

getName()

如果没有设置线程的名字,当我们创建了线程对象,

默认的名字为Thread-0 Thread-1 Thread-2 以此类推

5.2获取当前线程对象

static Thread currentThread() 返回当前正在执行的线程对象的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BbtjwIQl-1645706524758)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208120559921.png)]

5.3 sleep方法

static void sleep(long millis)

静态方法

参数是毫秒

作用是让当前线程进入休眠,处于阻塞状态,放弃占有的cpu执行权,让给其他线程使用

Thread.sleep()方法,可以做到这种效果:

​ 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次

5.3.1 sleep方法的面试题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hhoz490I-1645706524758)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208122919502.png)]

t.sleep(1000*5) 在执行的时候会转换成Thread.sleep(1000*5);

这行代码的作用是让当前线程进入休眠,也就是说main线程进入休眠

主线程休眠5秒以后才会输出hello world

sleep方法出现在main方法中,main线程休眠

5.4如何唤醒休眠的线程

run方法当中的异常不能throws

因为run方法在父类中没有抛出任何异常,子类不能比父类抛出更多异常

sleep睡眠太久了,如果希望半途醒过来,该怎么叫醒一个正在睡眠的线程?

​ 注意:这个不是终断线程的执行,是终止线程的睡眠

interrupt()方法可以中断线程的睡眠,这种中断线程睡眠的方式依靠了Java的异常处理机制

5.5如何终止一个线程
5.5.1 强制终止一个线程

使用stop方法

已经过时了

可以强制关闭一个线程

这种方式容易丢失数据,因为这种方式直接把线程给杀死了,

线程没有保存的数据将会丢失,不建议使用

5.5.2 合理终止一个线程(掌握)

通过布尔标记

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gsKhkTjb-1645706524759)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208143231157.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EnyH9brr-1645706524760)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208143321217.png)]

6. 方式三实现Callable接口(JDK8新特性)

这种方式实现的线程可以获取线程的返回值 但是效率低

之前讲解的两种方式是无法获取线程返回值的,因为run()方法返回void

思考:

系统委派一个线程去执行一个任务,该线程执行完任务以后,可能会有一个执行结果,我们要怎么能拿到这个执行结果呢?

使用第三种方式:实现Callable接口

步骤

1.创建未来任务类对象

//参数很重要:需要给一个Callable接口的实现类对象

FutureTask task=new FutureTask(里面传的参数是Callable接口的实现类对象); Callable是抽象类,需要重写call方法

call方法相当于run方法

2.创建线程对象

Thread t=new Thread(task);

3.启动线程

t.start();

//在主线程中,怎么获得t线程的执行结果

Object obj=task.get();

//main方法这里的程序要想执行,必须等待get方法的结束

而get方法可能需要很久,因为get方法是用来获取另外一个线程的执行结果

另外一个线程的执行是需要时间的

get方法会导致当前线程阻塞

package com.bjpower.java;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author zengyihong
 * @create 2022--02--09 10:55
 */
public class ThreadTest04 {
 public static void main(String[] args) throws ExecutionException, InterruptedException {
  CallableImpl CallableImpl=new CallableImpl();
  FutureTask task=new FutureTask(CallableImpl);

  Thread t=new Thread(task);
  t.start();

  Object o = task.get();

  System.out.println("执行结果"+o);



 }

static class CallableImpl implements Callable{

 @Override
 public Object call() throws Exception {

  System.out.println("begin");
  Thread.sleep(1000*10);
  System.out.println("over");

  int a=45;
  int b=456;
  //自动装箱
  return a+b;



 }
}



}

三、关于线程的调度

1.常见的线程调度模型

抢占式调度模型

哪一个线程的优先级比较高,抢到的cpu时间片的概率就高一些

Java采用的是抢占式调度模型

均分式调度模型

平均分配cpu时间片,每个线程占有的cpu时间片时间长度一样,平均分配,一切平等。

有一些编程语言,线程调度模型采用的就是这种方式

2.线程调度有关的方法

实例方法

void setPriority(int newPriority) 设置线程的优先级

int getPriority() 获取线程优先级

最低优先级1

最高优先级10

默认优先级5

有一些关于优先级的常量

MAX_PRIORITY

MIN_PRIORITY

NORM_PRIORITY

优先级高的获取cpu时间片可能多一些,但也不是完全是,大概率是这样

静态方法:

2.1yield方法

static void yield()让位方法

暂停当前在执行的线程对象,去执行其他线程

注意:yield方法不是阻塞方法,它只是使得当前线程让位,给其他线程使用,变成就绪状态。

yield方法会让当前线程从 运行状态 回到 就绪状态

yield方法执行以后,当前线程还是会继续抢占cpu的执行权,只是抢夺成功的概率变得很小了

2.2join方法

void join()合并线程

class MyThread1 extends Thread{

public void doSome(){

MyThread2 t= new MyThread2();

t.join(); //让当前线程进入阻塞,线程t执行,直到线程t结束,当前线程才可以继续

}

}

class MyThread2 extends Thread{

}

 
public class TestJoin {
    public static void main(String[] args) {

        MyThread02 t =new MyThread02();
        Thread.currentThread().setName("main线程");
        t.start();

        for (int i = 1 ;i <= 10; i++) {
            System.out.println(Thread.currentThread().getName()+"----"+"hi"+i);
            //每隔一秒输出,输出5次以后,然后让子线程运行完毕
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (i ==5){
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }



    }
}
class MyThread02 extends Thread{

    @Override
    public void run() {
        Thread.currentThread().setName("分线程");
        for (int i = 1; i <=10; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+"hello"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 
main线程----hi1
分线程---hello1
分线程---hello2
main线程----hi2
分线程---hello3
main线程----hi3
main线程----hi4
分线程---hello4
main线程----hi5
分线程---hello5
分线程---hello6
分线程---hello7
分线程---hello8
分线程---hello9
分线程---hello10
main线程----hi6
main线程----hi7
main线程----hi8
main线程----hi9
main线程----hi10

Process finished with exit code 0

四、线程安全(重点)

以后在开发当中,我们的项目都是运行在服务器当中的
而服务器已经把线程的定义,线程对象的创建,线程的启动等都已经实现完了
这些代码不需要我们编写

最重要的是:我们 要知道,我们编写的程序要放在一个多线程的环境下运行,我们
          更需要关注的是这些数据在多线程并发的环境下是否安全

1.线程不安全的条件

如果只是读取余额,没有线程不安全的问题

如果对数据进行操作,会出现线程不安全的情况

我们应该让一个线程在操作的时候,另外一个线程要进行等待

三个条件:

多线程并发、有共享数据、共享数据有修改的行为

满足这三个条件,就会存在线程安全问题

2.如何解决线程安全问题(线程同步)

线程安全如何解决:线程排队执行,不能并发

用排队机制来解决线程安全问题

这种机制被称为:线程同步机制

线程同步就是线程排队,线程排队会牺牲一部分的效率,但是没有办法,我们要保证数据安全第一位,数据安全以后,我们才可以谈效率

3.异步和同步的理解

3.1异步编程模型

线程t1和线程t2,各自执行各自的,不需要等待,t1不管t2,t2不管t1,谁也不需要等待谁,这种编程模型叫做:异步编程模型

其实就是:多线程并发(效率较高)

异步就是并发

3.2同步编程模型

线程t1和线程t2,在线程t1执行的时候,t2必须等待线程t1执行结束,才能执行

两个线程之间发生了等待关系,这就是线程同步模型

效率较低,线程排队执行

同步就是排队

五、线程同步机制

线程同步就算当有一个线程在对内存进行操作的时候,其他线程不可以进入,不可以对这个内存进行操作

1.对synchronized的理解

线程同步机制的语法:

synchronized(){

//线程同步代码块

}

synchronized后面小括号里面传的数据是相当关键的

这个数据必须是多线程共享的数据,才能达到多线程排队

()中写什么?

​ 那就要看我们想让哪一些线程同步

​ 假如有5个线程,t1, t2, t3, t4, t5

我们只希望t1, t2 , t3排队,另外两个不需要排队,怎么做?

我们就一定要在()中写一个 t1, t2, t3共享的对象

而这个对象对于 t4 , t5来说不是共享的

这里的共享对象是:账户对象

如果账户对象是共享的,那么this就是账户对象

括号中不一定是this, 这里只要是多线程共享的那个对象就可以了

在Java语言中,任何一个对象都有一把锁,其实这把锁就是标记(只是把它叫做锁)

100个对象,100把锁,1个对象1把锁

以下代码的执行原理:

1.假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先 一个后

2.假设t1先执行,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到以后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放

3.假设t1占有了这把锁,t2也遇到了synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1结束,直到t1把同步代码块执行结束了,t1才会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁以后,进入同步代码块执行程序

这样就达到了线程排队执行

注意:这个共享对象一定要选择好了。共享对象一定是我们需要排队执行的这些线程对象所共享的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytOvJUie-1645706524760)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208183310191.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCoTCdtC-1645706524761)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208180846446.png)]

2. Java中局部变量和常量不会出现线程安全问题

Java有三大变量

实例变量:在堆中

局部变量:在栈中 一个线程一个栈

静态变量:在方法区中

以上三个变量中,局部变量永远都不会存在线程安全问题

因为局部变量不共享(一个线程一个栈)

如果使用局部变量的话,建议使用:StringBuilder

因为局部变量不存在线程安全问题,StringBuffer效率低

ArrayList是非线程安全的

Vector是线程安全的

HashMap,HashSet是非线程安全的

HashTable是线程安全的

3.扩大同步范围

同步代码块中代码越少,效率越高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFNLp19Q-1645706524761)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208182634233.png)]

括号里面不能写this,此时this指的是线程对象,而线程对象new了两次

4.synchronized可以用在实例方法上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PlxnaKnw-1645706524762)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208183347526.png)]

如果共享对象是this,并且整个方法都需要同步,我们就用这个方式

5synchronized的三种写法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JeKGzzdo-1645706524762)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208184123191.png)]

synchronized出现在静态方法是找类锁,不管创建多少对象,都需要等待

不要嵌套,不然会出现死锁

6.开发中如何解决线程安全问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VQx99bo7-1645706524763)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220208222654291.png)]

六、用户线程和守护线程

垃圾回收线程就是守护线程(后台线程)

Java语言线程有用户线程和守护线程两种

用户线程包括主线程

守护线程又叫后台线程

用户线程也叫工作线程,当线程的任务执行完或通知方式结束

守护线程特点:

​ 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束

主线程main方法是一个用户线程

守护线程类似于在后台默默守护

守护线程用在什么地方?

每天00:00的时候,系统数据自动备份

这个需要使用到定时器,并且我们可以把定时器设置为守护线程

一直在哪里看着,每到00:00的时候就备份一次,所有的用户线程如果结束了,

守护线程自动退出,没有必要进行数据备份了

在启动线程之前,把线程设置为守护线程
假如有一个线程t,这个时候只需要调用一个方法
t.setDaemon(true);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L3gHTUZF-1645706524763)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209002423966.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4OaNYFZM-1645706524763)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209002454526.png)]

七、定时器(不需要我们写,以后用框架)

作用:间隔特定的时间,执行特定的程序

每周要进行银行账户的总账操作

每天要进行数据的备份操作

在实际的开发中,每隔多久执行一段特定的程序,是很常见的

那么在Java中可以采用多种方式实现

可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行

任务,这种方法是最原始的定时器(比较low)

在Java类库总已经写好了一个定时器:java.util.Timer,可以直接拿来用

不管,这种方式在目前的开发中也很少用,因为现在有很大高级框架都是支持定时任务的。

在实际的开发中,目前使用较多的是Spring框架提供的SpringTask框架,

这个框架只要进行简单的配置,就可以完成定时器的任务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pVO5s0TY-1645706524764)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209003633458.png)]

TimerTask是抽象类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDwiRXX3-1645706524764)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209011735077.png)]

八、生产者和消费者模式

1.wait和notify概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irR4EKvS-1645706524764)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209115648606.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A31XIHGA-1645706524765)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209120450418.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0v4stWK-1645706524765)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209120233340.png)]

2.生产者和消费者模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F8hP8njw-1645706524765)(C:\Users\17614\AppData\Roaming\Typora\typora-user-images\image-20220209121231544.png)]

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。