java多线程
概念
进程 :
是程序的基本执行实体.
线程:
线程是操作系统能够进行运算的调度的最小单位.它被包含在进程 之中,是进程中的实际运算单位.
线程简单理解 :
应用软件中相互独立,可以同时运行的功能.
过去的代码都是单线程代码,从上到下依次运行.
多线程程序特点:
CPU可以在多个程序中来回执行,把内存创建等,等待时间充分利用起来.
多线程的应用场景
让多个功能同时运行.
例如:
在软件的耗时操作:在拷贝,迁移大文件时同时 进行其他操作.
多线程作用:
多线程可以让程序同时做很多事情.提高效率.
并发和并行
并发:
在同一时刻,有多个指令在单个CPU交替 执行.
并行:
在同一时刻,有多个指令在多个CPU上同时 执行.
CPU线程数表示计算机最大同时可以运行线程个数,超过并行.
多线程的实现方式(三种)
方式一:
java
/*
* 多线程的第一种启动方式:
* 1.自己定义一个类继承Thread类
* 2.重写run方法
* 3.创建子类对象,并启动线程
* */
自己定义类继承Thread类和重写run方法
java
public class MyThread extends Thread {
//2.重写run方法
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 50; i++) {
System.out.println("hello,world");
}
}
}
java
//创建子类对象,启动线程(一个对象表示一个线程)
//多个线程需要创建多个对象
MyThread myThread1 = new MyThread();
//开启线程
//不能直接用对象调用run方法
myThread1.start();
给线程命名(setName方法)
java
//给线程命名
myThread1.setName("线程1");
myThread2.setName("线程2");
获取线程名字(getName方法)
java
System.out.println(getName()+"hello,world");
方式二:
java
/*
* 多线程的第二种启动方式
* 1.自己定义一个类实现Runnable接口
* 2.重写里面run方法
* 3.创建自己类的对象
* 4.创建Thread类的对象
* 5.开启线程
* */
自己定义一个类实现Runnable接口
java
public class MyRun implements Runnable {
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 50; i++) {
/*
* 不能直接调用getName方法(没有继承Thread)
* 调用getName方法前要获取当前线程对象
* 获取方法:Thread.currentThread();
* */
Thread t = Thread.currentThread();
System.out.println(t.getName()+"hello,world");
}
}
}
java
//创建自己类的对象
//表示多线程要执行的任务
MyRun mr=new MyRun();
//创建线程对象(直接用Thread创建,不需要再继承)
//参数表示:要执行的任务
Thread t1=new Thread(mr);
//创建第二个线程(参数相同:执行任务一样)
Thread t2=new Thread(mr);
//设置线程名称
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
方式三:
java
/*
* 多线程第三种实现方式:
* 特点:可以获取多线程运行的结果(前面两种都不可以)
* 步骤:
* 1.创建一个类实现Callable接口
* 2.重写call方法(是有返回值的,表示多线程的运行结果)
* 3.创建实现类的对象(表示多线程要执行的任务)
* 4.创建Future接口实现类(FutureTask)的对象(作用管理多线程运行的结果)
* 5.创建Thread对象
* 6.启动线程.
* 7.用FutureTask的对象调用get方法获取结果
* */
创建一个类实现Callable接口
java
//Callable的泛型是指结果的类型
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
java
//3.创建实现类的对象(表示多线程要执行的任务)
MyCallable mc=new MyCallable();
//4.创建Future接口实现类(FutureTask)的对象(作用管理多线程运行的结果)
//参数表示:要管理那个任务的结果
FutureTask<Integer> ft=new FutureTask<>(mc);
Thread thread=new Thread(ft);
//启动线程
thread.start();
//获取多线程运行的结果
Integer i = ft.get();
System.out.println(i);
有多条线程时
要创建多个线程结果管理者对象
java
//创建多线程运行的参数对象
MyCallable mc = new MyCallable(list);
//创建多线程运行结果的管理者对象
//线程1
FutureTask<Integer> ft1 = new FutureTask<>(mc);
//线程2
FutureTask<Integer> ft2 = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft1);
Thread t2 = new Thread(ft2);
//设置线程名字
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
//开启线程
t1.start();
t2.start();
//获取线程返回值
//线程1返回值
Integer max1 = ft1.get();
//线程2返回值
Integer max2 = ft2.get();
三种方法的选择
如果不需要获取运行结果
选择前两类.
前两种的区别:
第一种方式可可扩展性差的原因:
java中只能单继承,不支持多继承 .
第二种方法:
如果要调用Thread中的方法,需要先获取线程对象再调用方法,
获取方法:Thread.currentThread();
Thread中常见的成员方法
getName方法细节:
1.如果我们没有给线程设置名字,线程也有默认名字
格式 :Thread-X(线程序号,从0开始)
setName方法细节:
除了setName方法给线程设置名字,还可以通过构造方法设置名字
java
//子类不能继承父类的构造方法(需要重新创造)
//快捷方法alt+insert
MyThread t1 = new MyThread("线程1");
MyThread t2 = new MyThread("线程2");
获取当前线程对象方法的细节:
java
//那条线程执行这个方法,此时就获取那条线程的对象
//此时没有创建线程对象,所以获取的是执行main方法的线程
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);//main
/*
* 细节:
* 当JVM虚拟机启动后,会自动启动多条线程
* 其中一条就是调用main方法,并执行里面的代码(名字就是main)
* 在之前,我们写的所有代码,都是运行在main方法当中
* */
让线程休眠方法细节:
1.那条线程执行到这个方法,那条线程就会在这里停留对应的时间
2.停留时间与方法参数有关:方法的参数就表示停留的时间.单位毫秒
3.时间到了后,线程会自动醒来继续执行后续代码.
java
//停留10s再执行输出语句
Thread.sleep(10000);
System.out.println(11111);
让自己创建线程休眠
java
@Override
public void run() {
//让自己创建的线程休眠
//此时异常不能抛(父类中run方法没有抛,所以子类中也不能抛)
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 50; i++) {
System.out.println(getName()+"@"+i);
}
}
线程的优先级
线程的调度:
1.抢占式调度(强调随机性):
多条线程抢占CPU的执行权,执行的线程不确定,执行时间也不确定.
2.非抢占式调度:所有线程轮流执行,执行时间也差不多.
Java中采取的就是抢占式调度.
优先级表示抢占到CPU的概率
java中线程优先级分为10档,最小时1,最大是10,没有设置默认为5
优先级最大不能表示100%抢占到CPU,只是概率大.
java
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
t1.setPriority(10);
t2.setPriority(1);
守护线程的细节 :
应用场景:
如:在QQ中聊天时,发送了一个文件当中途退出QQ传输文件这个线程就没必要再继续下去了.
java
/*
* 守护线程细节:当其他的非守护线程结束,守护线程也会陆续结束.
* 守护线程不会一下子结束,会陆续结束
* */
MyThread1 t1 = new MyThread1("女神");
MyThread2 t2 = new MyThread2("备胎");
//把第二个设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
礼让线程
线程的执行是随机的.
礼让线程作用:让线程尽可能均匀执行.
java
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
//出让线程(出让当前CPU的执行权)
Thread.yield();
}
}
插入线程细节 :
作用:
将一个线程插入再另一个线程前,等插入的线程执行完,再执行后面的线程
java
MyThread t1 = new MyThread("飞机");
t1.start();
//把线程t1插在当前线程之前
//t1线程:飞机
//当前线程:main线程
t1.join();
for (int i = 0; i < 10; i++) {
//当前执行在main线程下
System.out.println("main线程"+i);
}
线程的生命周期
在 Java 中,当一个线程的run方法执行完毕后,这个线程就进入了终止状态。处于终止状态的线程不会再被调度获取 CPU 的执行权来执行run方法中的内容了。
java
MyThread t1 = new MyThread("窗口1");
MyThread t2 = new MyThread("窗口2");
MyThread t3 = new MyThread("窗口3");
t3.start();
t1.start();
t2.start();
MyThread t1 = new MyThread("窗口1");
MyThread t2 = new MyThread("窗口2");
MyThread t3 = new MyThread("窗口3");
//首先进入运行状态获取第一张票,但没有执行权
t3.start();
//进入运行状态获取第第二张张票,但没有执行权
t1.start();
//进入运行状态获取第第三张张票,并且抢到执行权先执行
t2.start();
//票从第一张开始卖
/*
窗口1卖出第3张票
窗口1卖出第4张票
窗口1卖出第5张票
窗口3卖出第1张票
窗口2卖出第2张票
窗口1卖出第6张票
*/
线程安全问题
线程在执行代码的时候,CPU的执行权随时会被抢走(代码可能执行了一半).
解决方法:
把操作共享数据的代码锁起来.
(锁):表示锁对象
java
/*
* 锁对象是任意的如:Object类型的
* 但是要保证锁对象的唯一性(static)
* static Object lock = new Object();
* */
//锁对象(一般写当前类的字节码文件对象)
//synchronized (MyThread.class)
static Integer lock = 0;
@Override
public void run() {
while (true) {
//同步代码块:把共享数据的代码锁起来
synchronized (lock) {
if (counter < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
counter++;
System.out.println("正在卖第" + counter + "张票");
} else {
break;
}
}
}
同步代码块的细节
synchronized ()中的锁对象必须是唯一的.
如果锁不唯一,不能起到效果.
注意:
锁对象一般写当前类的字节码文件对象(这个对象是唯一的).
java
synchronized (MyThread.class)
同步方法
如果要把一个方法中所有代码锁起来时 ,此时没必要用同步代码块,可以直接将synchronized
关键字直接加到方法上.
java
//前面不需要写static,因为此时MyRunnable表示线程要执行的任务
//只会创建一次所以不需要添加static表示共享.
int ticket = 0;
@Override
public void run() {
while (true) {
if (method()) break;
}
}
private synchronized boolean method() {
if (ticket == 100) {
return true;
} else {
ticket++;
Thread t = Thread.currentThread();
System.out.println(t.getName() + "在卖第" + ticket + "张票");
}
return false;
}
StringBuffer和StringBuilder区别
StringBuffer的和StringBuilder功能一样都是用于字符串的拼接.
区别:
StringBuffer中方法都是添加了synchronized
关键字的同步方法.在多线程情况下可以保证数据安全.
StringBuilder方法适用于不考虑多线程数据安全的情况.
Lock对象
Lock实现提供比使用synchronized
方法和语句可以获得更加广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock(): 获得锁
void unlock(): 释放锁
java
//创建Lock对象
//Lock是一个接口,不能直接创建对象要创建接口的实现对象
//因为对象的反复创建,为了保证锁的唯一性添加static
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//同步代码块:把共享数据的代码锁起来
//synchronized (MyThread.class) {
lock.lock();//上锁
try {
if (counter == 100) {
break;
} else {
Thread.sleep(100);
counter++;
System.out.println("正在卖第" + counter + "张票");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//finally:保证开锁代码,一定被执行.
lock.unlock();//开锁(释放锁)
}
//}
}
死锁
死锁不是一个知识点是一个错误.
死锁是指:
在程序中出现了锁的嵌套.
例:
出现死锁时程序会卡死,不会继续往下执行.
生产者和消费者(等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作模式
情况一 :
情况二 :
等待唤醒机制
上述过程涉及的方法
如果有多个等待的线程:notify()方法会随机唤醒一个,所以notifyAll()方法更常用.
消费者代码
java
public class Foodie extends Thread{
//消费者
@Override
public void run() {
/*
* 书写多线程的套路
* 1.循环
* 2.同步代码块(可以该为同步方法/Lock锁)
* 3.判断共享数据是否到末尾(到末尾的情况)
* 4.判断共享数据是否到末尾(没到末尾的情况,执行核心逻辑)
* */
while(true){
//Desk.lock:在控制生产者和消费者的执行类中定义了
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else {
//判断桌子上是否有面条
if(Desk.foodFlag==0){
//如果没有,等待
//不能直接用wait方法等待,要用锁对象调用
try {
Desk.lock.wait();//让当前线程和锁进行绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//Desk.lock.notifyAll();//唤醒这把锁对应的所有线程
}else {
//把吃的总数-1
Desk.count--;
//如果有,开吃
System.out.println("吃货在吃面条,还能吃"+Desk.count+"碗");
//吃完唤醒Cook继续做
Desk.lock.notifyAll();
//修改桌子状态
Desk.foodFlag=0;
}
}
}
}
}
}
生产者代码
java
public class Cook extends Thread {
//生产者
@Override
public void run() {
/*
* 书写多线程的套路
* 1.循环
* 2.同步代码块(可以该为同步方法/Lock锁)
* 3.判断共享数据是否到末尾(到末尾的情况)
* 4.判断共享数据是否到末尾(没到末尾的情况,执行核心逻辑)
* */
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
//判断桌子上是否有食物
if (Desk.foodFlag == 1) {
try {
//有,就等待
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//唤醒消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
控制生产者和消费者的执行
java
public class Desk {
/*
* 作用:
* 控制生产者和消费者的执行
* */
//表示桌子上是否有面条: 0:没有面条 1:桌子上有面条
public static int foodFlag=0;
//吃货最多可以吃10碗面
public static int count=10;
//锁对象
public static Object lock=new Object();
}
测试类代码
java
//创建线程对象
Cook c=new Cook();
Foodie f=new Foodie();
//给线程设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
上述代码用厨师和吃货分别代表一个线程,模拟了多线程协作模式.
等待唤醒机制(阻塞队列方式实现)
阻塞队列的继承体系图
实现步骤
1.创建阻塞队列的实现类
java
//1.创建阻塞队列(接口)的实现类
//使用数组实现类要指定大小(构造方法的参数)
ArrayBlockingQueue<String> queue=new ArrayBlockingQueue<>(1);
2.通过创建对象的方式将queue传递给生产者和消费者
生产者
java
public class Cook extends Thread{
//消费者
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
}
}
消费者
java
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
}
}
3.补全生产者和消费者的run方法
生产者
java
public class Cook extends Thread{
//消费者
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断把面条放到阻塞队列中
try {
//队列的添加方法
//put方法底层带有锁,不要再添加锁,否则形成锁的嵌套(死锁)
queue.put("面条");
System.out.println("厨师做了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
消费者
java
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//不断从队列获取面条
try {
//队列获取方法
//take方法底层带有锁,不要再添加锁,否则形成锁的嵌套(死锁)
queue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
测试类代码
java
/*
* 阻塞队列实现等待唤醒机制
* 细节:
* 生产者和消费者使用同一个阻塞队列
* */
//1.创建阻塞队列(接口)的实现类
//使用数组实现类要指定大小(构造方法的参数)
ArrayBlockingQueue<String> queue=new ArrayBlockingQueue<>(1);
//2.通过创建对象的方式将queue传递给生产者和消费者
Cook c=new Cook(queue);
Foodie f=new Foodie(queue);
//3.开启线程
c.start();
f.start();
线程的6种状态
没有蓝色的状态,蓝色的状态是为了方便理解.
线程池
线程池作用:
实现线程的复用.
线程池的代码实现
创建线程池的两种方式
第一种(无上限)
java
//1.获取线程池对象(无上限)
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程(池内所有线程随之消失)
//线程池一般不关闭
pool1.shutdown();
第二种(有上限)
java
//1.获取线程池对象(无上限)
//参数指定最大线程数
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程(池内所有线程随之消失)
//线程池一般不关闭
pool1.shutdown();
自定义线程池
自定义线程池路实现思路
临时线程创建的时机
核心线程都在处理任务,队列也排满时.
任务执行的顺序
不一定是先提交,先执行.如:此时任务4,5,6在排队,任务7,8已经执行了.
当任务数超过核心线程数+队列长度+临时线程数
超出的任务会触发任务拒绝策略.
四种任务拒绝策略
自定义线程池的代码实现
创建自定义线程池
java
/*
* ThreadPoolExecutor tpe=new ThreadPoolExecutor
* (核心线程数,最大线程数,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
* 参数一:核心线程数目 不能小于0
* 参数二:最大线程数 不能小于等于0, 最大线程数>=核心线程数
* 参数三:空闲线程最大存活时间 不能小于0
* 参数四:时间单位 用TimeUnit指定
* 参数五:任务队列(阻塞队列) 不能为null
* 参数六:创建线程工厂 不能为null
* 参数七:
* 任务的拒绝策略 是ThreadPoolExecutor的静态内部类 不能为null
* 创建方式new ThreadPoolExecutor(外部类).内部类
*
* 定义内部类的情况:
* 内部类依赖外部类存在,单独出现没有意义
* 内部类本身是一个单独的个体
*/
//创建自定义线程池
ThreadPoolExecutor tpe=new ThreadPoolExecutor(
3,//核心线程数目不能小于0
6,//最大线程数 不能小于等于0, 最大线程数>=核心线程数
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位=>此时是秒
new ArrayBlockingQueue<>(3),//任务队列(阻塞队列)不能为null
Executors.defaultThreadFactory(),//创建线程工厂不能为null
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
提交任务
java
//提交任务
tpe.submit(new MyRunnable());
关闭线程池
java
//关闭线程池
tpe.shutdown();
线程池的大小
thread dump工具是用来测CPU计算时间和等待时间的.
获取JVM虚拟机可用处理器数目
java
//获取JVM虚拟机可用线程数
int count=Runtime.getRuntime().availableProcessors();
System.out.println(count);
根据公式创建线程池大小的两种情况
最大并行数 =JVM虚拟机可用线程数.
类型一(项目是CPU密集型)
CPU密集型 :计算比较多,读取本地文件或数据库的操作比较少.
公式 :最大并行数+1
类型二(项目是I/O密集型)
CPU密集型 :计算比较少,读取本地文件或数据库的操作比较多.
公式:如图
多线程后续需要学习知识
P167