首页 > 编程语言 >Java多线程

Java多线程

时间:2023-02-10 21:01:12浏览次数:63  
标签:Runnable Java Thread start 线程 new 多线程 public

​Java多线程 超详细!​

什么是线程?多线程?

线程是一个程序内部的一条执行路径,我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

多线程是指从软硬件上实现多条执行路径的技术。


线程

Thread类

Java是通过 java.lang.Thread 类来代表线程的,其提供了实现多线程的方式,其直接继承了Object类,并实现了Runnable接口。

Java多线程_多线程

构造器

Thread([String name]) ;

Thread(Runnable [,String name]);//若不取名则有默认名:Thread-0、Thread-1……

线程对象操作

String getName()  //获取线程名字

setName(String name) //设置线程名字

static Thread currentThread() //获取当前线程对象

线程状态操作

static sleep(long time);  //让当前线程休眠指定的时间后再继续执行,单位为毫秒。

run(); //线程的任务方法,在这个书写需要执行的代码逻辑

start(); //启动一个线程,启动成功后JVM会自动调用该线程的run方法(任务)

start()和run()的区别

  1. **run()**不会启动线程,只是普通的调用方法而已,不会分配新的分支栈。
  2. start()的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。

:speech_balloon:为什么启动线程不能直接调用run()方法,而要调用start()方法?

​start()​​方法包含了创建新线程的特殊代码逻辑,而​​run()​​方法是我们自己写的代码,很显然没有这个能力。

当你调用一个线程对象的​​run()​​方法时,你实际上只是在调用一个普通的函数,它将在你的主线程中顺序执行,而不是在新的线程中并行执行。

相反,如果你调用线程对象的​​start()​​方法,它会创建一个新的线程,并在新的线程中执行线程对象的​​run()​​方法。这样,你的程序中就有了两个并行执行的线程:主线程和新创建的线程。

线程生命周期

Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中。(Thread.state

<img src="​​https://agust.oss-cn-guangzhou.aliyuncs.com/images/202302101946145.png​​" alt="img" style="zoom:67%;" />

状态

描述

NEW(新建)

线程刚被创建,但是并未启动。

Runnable(可运行)

线程已经调用了start()等待CPU调度

Blocked(锁阻塞)

线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态

Waiting(无限等待)

一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同waiting状态,有些方法带有超时参数(Thread.sleep、0bject.wait),调用他们将进入Timed Waiting状态。

Teminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

Java多线程_守护线程_02

并发与并行

正在运行的程序就是一个独立的进程,线程是属于进程的,多个线程是并发运行的。

并发

  • CPU同时处理线程的数量有限,会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行。
  • 在一段时间内(很小)多个线程被调度,可以称这几个线程并发。

并行

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行。

现在的java程序中至少有两个线程并发,一个是 垃圾回收线程,一个是 执行main方法的主线程

父线程和子线程是并发运行的,哪个先执行是未知的。

内存共享

进程和线程是什么关系?

进程:可以看做是现实生活当中的公司。

线程:可以看做是公司当中的某个员工。

线程A和线程B,堆内存方法区 内存共享,但是 栈内存 独立,一个线程一个栈。

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

守护线程 & 用户线程

java语言中线程分为两大类:用户线程 和 守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

:speech_balloon:守护线程的特点:

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

注意:主线程main方法是一个用户线程。

:speech_balloon:守护线程用在什么地方呢?

每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。

一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。

方法:

void setDaemon(boolean on)// on为true表示把线程设置为守护线程

boolean isDaemon()// 判断是否为守护线程

下面是示例:

public class Main {
public static void main(String[] args) {
Thread t = new DaemonThread();//DaemonThread——自定义的守护线程
t.setName("Back Data Thread");

// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();

// 主线程:主线程是用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//自定义的守护线程
class DaemonThread extends Thread {

@Override
public void run() {
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程任务

Java中定义了一些接口用来定义线程中所需要执行的任务,如Runnable、Callable、FutureTask

Runnable接口

Runnable是函数式接口,只有一个任务方法​​void run()​(位于Java.lang)

Java多线程_守护线程_03

Thread类实现了Runnable接口,实现Runnable接口的类对象可以创建线程,线程启动后就会运行 ​​run()​​ 。

Callable接口

(位于java.util.concurrent)

Callable也是一个函数式接口,只有一个​​V call()​​:用于计算结果,其中V为返回结果的类型;如果不能正常计算结果则抛出异常。

Java多线程_System_04

Callable接口类似于Runnable,但是 Runnable的​​run()​​不返回结果,也不能抛出检查异常。

Future接口

Java多线程_多线程_05

Future接口的作用就是为了调用其他线程完成好后的结果,再返回到当前线程中,如上图举例:小王自己是主线程,叫外卖等于使用Future接口,叫好外卖后小王就接着干自己的事去了,当外卖到了的时候,Future.get()获取,继续做接下来的事情;要注意的是当还没获取外卖的时候,主线程中用餐这一步是卡住的。

Java多线程_System_06

future接口定义的方法:

  • cancel():如果等太久,你可以直接取消这个任务
  • isCancelled():任务是不是已经取消了
  • isDone():任务是不是已经完成了
  • get():有2个get()方法,不带参数的表示无穷等待,或者你可以只等待给定时间

FutureTask类

FutureTask类实现了Runnable接口和Future接口的集合——RunnableFuture接口,Runnable为其提供了多线程的能力,Future为其提供了获取线程任务结果的能力。

FutureTask实现了Runnable接口,可以作为​​Thread(Runnable)​​的参数,以此实现多线程。

FutureTask类有一个成员变量 ​​private Callable<V> callable​​,可以通过构造器将Callable对象封装成FutureTask对象,通过​​get()​​获取Callable对象的任务结果(​​Call()​​)。

构造器

FutureTask(Runnable runnable, V result) 把Runnable对象封装成FutureTask对象

FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象

获取计算结果的返回值

V get() throws Exception 获取线程执行call方法返回的结果

**FutureTask(Runnable runnable, V result)**的用法

Creates a ​​FutureTask​​ that will, upon running, execute the given ​​Runnable​​, and arrange that ​​get​​ will return the given result on successful completion.

这里构造函数说明中提示到:当Runnable执行完毕之后,可以用Future接口的get()获取执行结果,但是Runnable是没有返回结果,那这个result 有什么用呢?实际上这个result是由你设置好传进去的,FutureTask只是在Runnable执行完之后返回预先设置好的result,以便通知任务已完成。

public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
FutureTask<String> futureTask = new FutureTask<>(myThread, "123456");
// 1.构造线程并启动任务
new Thread(futureTask).start();
try {
// 2.获取预设好的线程任务结果
System.out.println("get():" + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}

// 自定义线程类
class MyThread implements Runnable {

// 任务方法
@Override
public void run() {
System.out.println("hello");
}
}

创建多线程

Java中创建多线程有以下几种方法:

继承Thread类

  1. 定义一个类继承Thread类,重写​​run()​
  2. 调用构造器执行​​start()​​运行线程

优点:编码简单

缺点:线程类已经继承了Thread类,无法继承其他类,不利于拓展

public class ThreadDemo1 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new MyThread(i).start();
}
System.out.println("Dad Process");
// 父进程和子进程是并行运行的,先运行哪个是不确定的
}
}

class MyThread extends Thread {
int i;

public MyThread(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println("Child Process:" + i);
}
}

实现Runnable接口

  1. 定义一个类实现Runnable接口并重写​​run()​
  2. 通过​​Thread(Runnable target)​​ 接收Runnable接口(任务对象)创建线程
  3. 再通过​​start()​​ 启动线程的任务。

优点:线程任务类只是实现了Runnale接口,可以继续类和实现接口。

缺点:如果线程有执行结果是不能直接返回的。

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

for (int i = 0; i < 50; i++) {
Runnable runnable = new RunnableImpl(i);
new Thread(runnable).start();
}
}
}

class RunnableImpl implements Runnable {
private int i;

public MyThread(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println("child process output:" + i);
}
}

一个Runnable对象只能创建一个线程,多次创建的线程其实是同一对象。

public class Main {
public static void main(String[] args) {
Runnable runnable = new MyThread();
// 这里使用一个Runnable对象作为参数构造了多个Thread对象,打印出的对象地址是一致的
for (int i = 0; i < 10; ++i) {
new Thread(runnable).start();
}
}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println(this);
}
}

实现Callable接口+FutureTask类

前2种线程创建方式都存在一个问题——重写的run方法均不能直接返回结果。JDK 5.0提供了Callable和FutureTask来解决这个问题:

  1. Callable其实就是可以返回结果的Runnable,其使用FutureTask将自己封装成线程对象。
  2. FutureTask实现了Runnable,可以被​​Thread(Runnable)​​调用。
  3. FutureTask的​​get()​​ 获取线程任务返回的值,解决了无法返回结果的问题 。

new Thread(new FutureTask(new CallableImpl()))

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

public class ThreadDemo3 {
public static void main(String[] args) {
//new Thread(new FutureTask(new CallableImpl()))
Callable<String> callable = new CallableImpl();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {
//通过FutureTask获取返回值
System.out.println("ans = " + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}

//这里的泛型类型也就是返回值类型
class CallableImpl implements Callable<String> {

@Override
public String call() {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
}
return "" + sum;
}
}
  1. Callable是个泛型接口,其传入的参数类型和返回值类型是一致的
  2. 使用同一个Callable对象多次构造的FutureTask对象是同一个,Thread也同理

线程同步

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。线程同步的出现就是为了解决线程安全问题。

如何才能保证线程安全呢? 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。这种依次访问共享资源的方式我们称之为同步同步的核心就是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

同步与互斥:

  • 互斥是指某一资源同时只允许一个访问者对其进行访问,但无法控制对资源的访问顺序。
  • 同步是指在互斥的基础上实现对资源的有序访问。

Java中有三大变量:

  1. 实例变量:在中。
  2. 静态变量:在方法区
  3. 局部变量:在中。

局部变量永远都不会存在线程安全问题,因为局部变量不共享,一个线程一个栈。

**成员变量(实例+静态)**可能存在线程安全问题,堆和方法区都是多线程共享的。

在Java中同步可以有好几种实现方式,如 同步代码块同步方法lock锁

同步代码块

把出现线程安全问题的核心代码加上锁,每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

synchronized(同步锁对象){
操作共享资源的代码
}

同步锁对象相当于锁的名字,也可以使用字符串常量来充当,对不同的同步需要使用不同的锁名,不然会锁错

  1. 建议使用共享资源作为锁对象
  2. 对于实例方法,通常我们使用​​this​​作为同步锁对象,这样就不会造成使用不同同步使用同样的锁了
  3. 对于静态方法,建议使用​​类名.class​​作为所对象
//账户类
class Account {
private String cardId;
private int money;

public Account(String cardId, int money) {
this.cardId = cardId;
this.money = money;
}

//取钱方法(添加了同步锁)
public void withdrawMoney(int money) {
String name = Thread.currentThread().getName();
synchronized (this) {
if (this.money >= money) {
System.out.print(name + "取钱成功,取走了" + money + "元");
this.money -= money;
} else {
System.out.print("余额不足," + name + "取钱失败");
}
System.out.println(" 剩余金额为:" + this.money + "元");
}
}
}


//线程类
class MyThread extends Thread {
private Account account;

public MyThread(Account account, String name) {
super(name);
this.account = account;
}

@Override
public void run() {
//使用账户类的取钱方法
int v = (int) (Math.random() * 1000);
account.withdrawMoney(v);
}
}

public class ThreadSyncDemo1 {
public static void main(String[] args) {
//定义一个账户,余额为100000
Account account = new Account("10086", 100000);
//定义多个线程操作这个账户
new MyThread(account, "小红").start();
new MyThread(account, "小明").start();
new MyThread(account, "小蓝").start();
new MyThread(account, "小绿").start();
new MyThread(account, "小黄").start();
}
}

同步方法

把出现线程安全问题的核心方法给上锁,每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

修饰符 synchronized 返回值类型  方法名称 (形参列表){
操作共享资源的代码
}
  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用​​this​​作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用​​类名.class​​作为的锁对象。

Lock锁

  1. 为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock。
  2. Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
  3. Lock是接口不能直接实例化,通常采用它的实现类​​ReentrantLock​​来构建Lock锁对象。

方法

描述

ReentrantLock()

获得Lock锁的实现类对象

lock()

获得锁

unlock()

释放锁

//加上final修饰,别人就撬不了锁
final Lock lock = new ReentrantLock();
lock.lock();//上锁
//锁的共享内容
lock.unlock();//解锁

线程通信

所谓线程通信就是线程间相互发送数据,线程间通信的模型有两种:共享内存消息传递

案例:有两个线程,A 线程向一个集合里面依次添加元素“abc”字符串,一共添加十次,当添加到第五次的时候,希望 B 线程能够收到 A 线程的通知,然后 B 线程执行相关的业务操作。

volatile 关键字

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务,这也是最简单的一种实现方式。

public class TestDemo1 {
//定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;

public static void main(String[] args) {
List<String> list = new ArrayList<>();
//线程A(这里用lambda实现了RunnableImpl)
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}

Object 类的 wait()/notify()

Object类提供了线程相关方法 :

方法

描述

wait()

让当前线程等待并释放所占锁

标签:Runnable,Java,Thread,start,线程,new,多线程,public
From: https://blog.51cto.com/u_15936519/6049739

相关文章

  • Java 多线程编程
    Java多线程编程Java给多线程编程提供了内置的支持。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任......
  • JAVA - IO流
    JavaIO流学习总结Java流操作有关的类或接口:Java流类图结构:流的概念和作用流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传......
  • Java - 面向对象 - 多态
    多态多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各......
  • Java基础知识(关系运算符/比较运算符、逻辑运算符)
    一:关系运算符符号说明==a==b,判断a和b的值是否相等,成立为true,不成立为false。!=a!=b,判断a和b的值是否相等,成立为true,不成立为false。>a>b,判断a是否大于b,成立为true,不成立f......
  • 学习打卡01- java入门
    1,基础知识点:java的三个版本javaSE(java基础版),javaEE(java企业版),javaME(小型嵌入式开发)LTS(Longterm<时期>support<支持>)长期支持版公司长期维护包括5.......
  • JavaScript迭代器与生成器
    JavaScript的迭代器与生成器前沿:可迭代对象及其相关的迭代器是是ES6的一个特性。数组是可迭代的,字符串、set对象和map对象也是。这意味着这些数据结构的内容可以通过......
  • 浏览器中的JavaScript(3)
    3.操作CSS摘要:我们已经知道了JavaScript可以控制HTML文档的逻辑结构和内容。通过对CSS编程,Javascript也可以控制文档的外观和布局。接下来讲解几种JavaScript可以用来操作......
  • java防止频繁请求、重复提交(防抖动)
    在客户端网络慢或者服务器响应慢时,用户有时是会频繁刷新页面或重复提交表单的,这样是会给服务器造成不小的负担的,同时在添加数据时有可能造成不必要的麻烦。自定义注解/......
  • Java基础知识(自增自减运算符、赋值运算符)
    一:自增自减运算符1.基本用法作用符号说明加++变量的值加1减--变量的值减1注意:++和--既可以放在变量的前边,也可以放在变量的后边。1.单独使用:++和--无论是放在变量的前边还是......
  • java 接口返回空指针问题排查
    java接口返回空指针问题排查问题现象现象:业务流程都能通,数据也正常,就是接口返回【空指针异常】排查:postman接口调用测试,返回200PHP项目中调用接口,返回400空指针异常......