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方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈
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方法的内存图
1.5调用start方法的内存图
主线程和分线程总是有一个先执行,一个后执行
有先有后 控制台只有一个
有多有少
2.方式二实现Runnable接口(比较常用)
因为还可以继承其他的类
写一个类实现java.lang.Runnable接口,重写run方法
1.创建一个可运行的对象
MyRunnable r=new MyRunnable();
2.把可运行的对象封装成一个线程对象
Thread t=new Thread®;
3.启动线程
t.start();
3.采用匿名内部类创建线程
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方法的面试题
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 合理终止一个线程(掌握)
通过布尔标记
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占有这把锁以后,进入同步代码块执行程序
这样就达到了线程排队执行
注意:这个共享对象一定要选择好了。共享对象一定是我们需要排队执行的这些线程对象所共享的
2. Java中局部变量和常量不会出现线程安全问题
Java有三大变量
实例变量:在堆中
局部变量:在栈中 一个线程一个栈
静态变量:在方法区中
以上三个变量中,局部变量永远都不会存在线程安全问题
因为局部变量不共享(一个线程一个栈)
如果使用局部变量的话,建议使用:StringBuilder
因为局部变量不存在线程安全问题,StringBuffer效率低
ArrayList是非线程安全的
Vector是线程安全的
HashMap,HashSet是非线程安全的
HashTable是线程安全的
3.扩大同步范围
同步代码块中代码越少,效率越高
括号里面不能写this,此时this指的是线程对象,而线程对象new了两次
4.synchronized可以用在实例方法上
如果共享对象是this,并且整个方法都需要同步,我们就用这个方式
5synchronized的三种写法
synchronized出现在静态方法是找类锁,不管创建多少对象,都需要等待
不要嵌套,不然会出现死锁
6.开发中如何解决线程安全问题
六、用户线程和守护线程
垃圾回收线程就是守护线程(后台线程)
Java语言线程有用户线程和守护线程两种
用户线程包括主线程
守护线程又叫后台线程
用户线程也叫工作线程,当线程的任务执行完或通知方式结束
守护线程特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
主线程main方法是一个用户线程
守护线程类似于在后台默默守护
守护线程用在什么地方?
每天00:00的时候,系统数据自动备份
这个需要使用到定时器,并且我们可以把定时器设置为守护线程
一直在哪里看着,每到00:00的时候就备份一次,所有的用户线程如果结束了,
守护线程自动退出,没有必要进行数据备份了
在启动线程之前,把线程设置为守护线程
假如有一个线程t,这个时候只需要调用一个方法
t.setDaemon(true);
七、定时器(不需要我们写,以后用框架)
作用:间隔特定的时间,执行特定的程序
每周要进行银行账户的总账操作
每天要进行数据的备份操作
在实际的开发中,每隔多久执行一段特定的程序,是很常见的
那么在Java中可以采用多种方式实现
可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行
任务,这种方法是最原始的定时器(比较low)
在Java类库总已经写好了一个定时器:java.util.Timer,可以直接拿来用
不管,这种方式在目前的开发中也很少用,因为现在有很大高级框架都是支持定时任务的。
在实际的开发中,目前使用较多的是Spring框架提供的SpringTask框架,
这个框架只要进行简单的配置,就可以完成定时器的任务
TimerTask是抽象类
八、生产者和消费者模式
1.wait和notify概述
2.生产者和消费者模式
- 点赞
- 收藏
- 关注作者
评论(0)