什么是线程?多线程?
线程是一个程序内部的一条执行路径,我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
多线程是指从软硬件上实现多条执行路径的技术。
线程
Thread类
Java是通过 java.lang.Thread 类来代表线程的,其提供了实现多线程的方式,其直接继承了Object类,并实现了Runnable接口。
构造器:
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()的区别:
- **run()**不会启动线程,只是普通的调用方法而已,不会分配新的分支栈。
- 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方法而死亡。 |
并发与并行
正在运行的程序就是一个独立的进程,线程是属于进程的,多个线程是并发运行的。
并发:
- 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)
Thread类实现了Runnable接口,实现Runnable接口的类对象可以创建线程,线程启动后就会运行 run()
。
Callable接口
(位于java.util.concurrent)
Callable也是一个函数式接口,只有一个V call()
:用于计算结果,其中V为返回结果的类型;如果不能正常计算结果则抛出异常。
Callable接口类似于Runnable,但是 Runnable的run()
不返回结果,也不能抛出检查异常。
Future接口
Future接口的作用就是为了调用其他线程完成好后的结果,再返回到当前线程中,如上图举例:小王自己是主线程,叫外卖等于使用Future接口,叫好外卖后小王就接着干自己的事去了,当外卖到了的时候,Future.get()获取,继续做接下来的事情;要注意的是当还没获取外卖的时候,主线程中用餐这一步是卡住的。
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类
- 定义一个类继承Thread类,重写
run()
- 调用构造器执行
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接口
- 定义一个类实现Runnable接口并重写
run()
- 通过
Thread(Runnable target)
接收Runnable接口(任务对象)创建线程 - 再通过
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来解决这个问题:
- Callable其实就是可以返回结果的Runnable,其使用FutureTask将自己封装成线程对象。
- FutureTask实现了Runnable,可以被
Thread(Runnable)
调用。 - 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;
}
}
- Callable是个泛型接口,其传入的参数类型和返回值类型是一致的
- 使用同一个Callable对象多次构造的FutureTask对象是同一个,Thread也同理
线程同步
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。线程同步的出现就是为了解决线程安全问题。
如何才能保证线程安全呢? 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。这种依次访问共享资源的方式我们称之为同步。同步的核心就是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
同步与互斥:
- 互斥是指某一资源同时只允许一个访问者对其进行访问,但无法控制对资源的访问顺序。
- 同步是指在互斥的基础上实现对资源的有序访问。
Java中有三大变量:
- 实例变量:在堆中。
- 静态变量:在方法区。
- 局部变量:在栈中。
局部变量永远都不会存在线程安全问题,因为局部变量不共享,一个线程一个栈。
**成员变量(实例+静态)**可能存在线程安全问题,堆和方法区都是多线程共享的。
在Java中同步可以有好几种实现方式,如 同步代码块、同步方法、lock锁等
同步代码块
把出现线程安全问题的核心代码加上锁,每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象){
操作共享资源的代码
}
同步锁对象相当于锁的名字,也可以使用字符串常量来充当,对不同的同步需要使用不同的锁名,不然会锁错
- 建议使用共享资源作为锁对象
- 对于实例方法,通常我们使用
this
作为同步锁对象,这样就不会造成使用不同同步使用同样的锁了 - 对于静态方法,建议使用
类名.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锁
- 为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- 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相关文章
|