什么是多线程?
顾名思义,就是从软硬件上实现多条执行路径的技术。多线程在我们平时的日常生活中也有很多的使用,例如在你登录QQ空间的时候,你输入账号密码进行登录这是属于你自己的两个线程,但是在同时也有其他人也在登录QQ空间,此时服务器那边会再给他分配两个线程,这就是多线程的应用。
一、多线程的创建
多线程的创建一共有三种方式,分别为继承Thread类,定义Runable实现类,实现Callable接口。
1.继承Thread类
Run方法中的代码就是线程用来执行的任务代码。 那么为什么不直接用类名直接调用run方法使用呢?为什么还要再用start方法来使用。如果你是直接调用run方法的话,那么他就会被当成跟main方法中的普通语句,就不会给他当成另一条线程去执行任务,此时程序中只有一条主线程。因此需要用start方法启动线程调用run方法,只有调用了start方法,他才会开启一条新的线程。
使用注意: 需要把子线程任务放在主线程前,如果放在后面的话,就是主线程先跑完再轮到子线程去跑,这样的话就起不到多线程的优势了。
代码:
package com.itheima.d1_create;
/*
Thread线程的创建方式
方式一 定义一个子类MyThread 继承线程类 Thread 重写 run()方法
*/
public class ThreadDemo1 {
public static void main(String[] args) {
//创建线程对象
Thread t= new MyThread();
//启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行" + i);
}
}
}
//1.创建类继承run方法
class MyThread extends Thread
{
@Override
public void run()
{
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行" + i);
}
}
}
因为方式一是继承Thread类重写run方法,因此这个实现类就不能再继承其他类了,不利于扩展。
但是他的书写格式很简单。
2.定义Runable实现类
Thread类的构造器
这种方法用到的就是第二种构造器。因为Runnable接口是函数式接口,因此可以用匿名内部类的方式去简写代码。
代码:
package com.itheima.d1_create;
public class ThreadDemo2 {
public static void main(String[] args) {
//创建任务对象
// Runnable target = new Myrunnable();
//匿名内部类写法
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行");
}
}
};
//把任务对象交给Thread处理
Thread t = new Thread(target);
//启动线程
t.start();
//简写方式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1" + i);
}
}
}).start();
new Thread(() -> {
for (int i = 0; i <5 ; i++) {
System.out.println("子线程2" + i);
}}
).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行" + i);
}
}
}
//定义任务类实现Runnable接口
//class Myrunnable implements Runnable
//{
//
// @Override
// public void run() {
// for (int i = 0; i < 5; i++) {
// System.out.println("子线程执行" + i);
// }
// }
//}
3.实现Callable接口
在上两种方式中,两个方法都是通过run方法来完成任务,然后run方法是void类型,因此如果任务需要返回值的话,上面两种方法是无法满足条件的,这时就用到了第三种方法来解决上两种方法的短板。
因为Callable接口是泛型接口,因此实现接口的时候需要申明线程完毕任务之后的数据类型,并且用FutureTask类创建对象,还可以用他特有的方法,get方法获取任务完毕后的返回结果。下面FutureTask对象会等待线程任务执行结束之后在返回值,因此返回的值一定是最终值。
代码:
package com.itheima.d1_create;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) {
//创建callable任务对象
Callable<String> call = new Mycallable(100);
//把Callable任务对象交给 FutureTask对象
//FT作用1: 是Runnable的对象 (实现了Runnable接口) 可以交给Thread
//作用2 : 可以在线程结束之后调用特有的get方法拿到执行完成的结果。
FutureTask<String> ft = new FutureTask<>(call);
Thread t= new Thread(ft);
t.start();
//get方法一定是在线程结束之后才获取其返回值的。
try {
String rs = ft.get();
System.out.println("执行结果" + rs);
} catch ( Exception e) {
e.printStackTrace();
}
}
}
//定义任务类实现callable接口 声明线程任务结束之后返回的结果的数据类型
class Mycallable implements Callable<String>
{
private int n ;
public Mycallable(int n){
this.n = n;
}
//重写call方法
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n ; i++) {
sum += i;
}
return "执行结果为" + sum;
}
}
三种方式优缺点对比
二、Thread的常用方法
常用方法:getname() 获取线程名, setname()设置线程名,currentThread()获取当前线程对象(也就是谁现在执行就是哪个线程对象), 这里还用到了Thread构造器中的第一种构造器,直接在构造器中给线程设置名称,因此需要在实现类中重写一个带参构造器,又因为name属性是Thread中的属性,因此我们只需要在子类带参构造器中调用父类构造器就可以了。
代码:
package com.itheima.d2_Api;
public class Mythread extends Thread{
public Mythread()
{
}
public Mythread (String name)
{
//为当前线程设置名称,送给父类有参构造器初始化名称
super(name);
}
@Override
public void run()
{
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +"输出:"+ i);
}
}
}
package com.itheima.d2_Api;
public class ApiDemo {
/*
给线程命名
currentThread 获取当前线程对象
setname 给线程取名
getname 给线程取名
*/
public static void main(String[] args) {
Thread t = new Mythread("1号");
// t.setName("1号线程");
t.start();
// System.out.println(t.getName());
//哪个线程执行它, 它就得到哪个线程对象(当前线程对象)
//主线程名称就叫main
Thread m = Thread.currentThread();
System.out.println(m.getName());
}
}
在执行完这个方法后,该线程的任务会停止你设置的毫秒数时间。
代码”:
package com.itheima.d2_Api;
public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <= 5; i++) {
System.out.println("输出" + i);
if (i == 3)
{
//线程休眠
Thread.sleep(3000);
}
}
}
}
三、线程安全
当多个线程同时操作同一个共享资源的时候,可能会诱发业务安全性问题,这种问题称为线程安全问题。接下来会用一个案例来演示这种问题。
分析问题:首先需要定义一个账户类作为共同账户(共享资源),创建线程类,在任务中进行取钱任务,然后在类中定义取钱功能,对于能否取钱进行一个简单的逻辑判断,最后创建两个线程小红和小明同时进行取钱,看看会不会出问题。
代码:
package com.itheima.d3_ThreadSafety;
public class Account {
private String name;
private int money;
public Account() {
}
public Account(String name, int money) {
this.name = name;
this.money = money;
}
public void drawMoney(int money) {
String name = Thread.currentThread().getName();
if (this.money >= money)
{
System.out.println(name + "来取钱了,吐了" + money);
this.money -= money;
System.out.println(name + "取钱成功,余额"+ this.money);
}else {
System.out.println("余额不足");
}
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return money
*/
public int getMoney() {
return money;
}
/**
* 设置
* @param money
*/
public void setMoney(int money) {
this.money = money;
}
public String toString() {
return "Account{name = " + name + ", money = " + money + "}";
}
}
package com.itheima.d3_ThreadSafety;
import com.itheima.d2_Api.Mythread;
public class MyThread extends Thread{
Account acc = new Account();
public MyThread(Account acc,String name)
{
super(name);
this.acc = acc;
}
@Override
public void run ()
{
acc.drawMoney(10000);
}
}
package com.itheima.d3_ThreadSafety;
/*
模拟取钱案例
*/
public class ThreadDemo {
public static void main(String[] args) {
Account acc = new Account("ICBC",10000);
Thread t1 = new MyThread(acc,"小明");
Thread t2 = new MyThread(acc,"小红");
t1.start();
t2.start();
}
}
出现这种问题的原因是因为两个线程是同时访问了账户类,因为线程是交替执行的,并且任务中都执行了该方法,因此如果小明先进入判断条件并且进行取钱操作的话,这个时候余额还没有更新,但是小红这个时候也进入了条件判断,因此就会出现错误。
四、线程同步
线程同步的作用就是为了解决,多线程的线程安全问题,核心思想是给共享资源上锁,这样的话即使是多个线程同时执行任务访问共享资源,也只能先让一个进去,这样就实现了多个线程先后访问共享资源。
线程同步实现方式一: 同步代码块 synchronized{} 同步方法:synchronize关键字直接写在方法名上
锁对象用任意唯一的对象的缺点:会影响其他无关线程的执行,因为同步代码块的原理是使用同步锁对象,而同步锁对象对于其他线程也是唯一的,因此如果其他线程也想执行同步代码块中的操作的话,就会被拦截。因此在设置同步锁对象的时候,规范上建议使用共享资源作为同步锁对象,例如,上面案例中小红和小明的共享账户acc对象,因为在上面的方法是实例方法,属于对象,当创建对象的时候,实例方法也会跟着加载,因此这个时候这个共享账户对象是只属于小红和小明的唯一对象,因此就可以使用this关键字来代替,谁调用的这个方法,那么就用该对象来当锁。因此对于实例方法建议使用this作为对象,对于静态方法建议使用字节码对象,也就是类对象作为锁对象。
同步方法的底层原理与同步代码块一样,只不过作用范围不一样,同步方法的作用范围比同步代码块更大一些。
package com.itheima.d4_thread_synchronized_code.d3_ThreadSafety;
public class Account {
private String name;
private int money;
public Account() {
}
public Account(String name, int money) {
this.name = name;
this.money = money;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return money
*/
public int getMoney() {
return money;
}
/**
* 设置
* @param money
*/
public void setMoney(int money) {
this.money = money;
}
public String toString() {
return "Account{name = " + name + ", money = " + money + "}";
}
public void drawMoney(int money) {
String name = Thread.currentThread().getName();
//同步代码块 索
//小明 小红 括号中的对象只是一个内容而已, 他只要是唯一的对象都可以当成锁使用
synchronized (this)
{
if (this.money >= money)
{
System.out.println(name + "来取钱了,吐了" + money);
this.money -= money;
System.out.println(name + "取钱成功,余额"+ this.money);
}else {
System.out.println("余额不足");
}
}
}
}
方式二:Lock锁
使用方式:
代码:
package com.itheima.d5_thread_synchronized_lock.d3_ThreadSafety;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String name;
private int money;
//用final修饰 不能改变锁 例如 lock = null
private final Lock lock = new ReentrantLock();
public Account() {
}
public Account(String name, int money) {
this.name = name;
this.money = money;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return money
*/
public int getMoney() {
return money;
}
/**
* 设置
* @param money
*/
public void setMoney(int money) {
this.money = money;
}
public String toString() {
return "Account{name = " + name + ", money = " + money + "}";
}
public void drawMoney(int money) {
String name = Thread.currentThread().getName();
lock.lock();
try {
if (this.money >= money)
{
System.out.println(name + "来取钱了,吐了" + money);
this.money -= money;
System.out.println(name + "取钱成功,余额"+ this.money);
}else {
System.out.println("余额不足");
}
} finally {
lock.unlock();
}
}
}
建议将解锁方法写在finally块中,这样即使try中出现异常也必须执行解锁这行代码,这样就不会导致死锁问题(因为无法解锁导致其他线程也访问不了该资源)的出现。
五、线程池
1.线程池概述
线程池复用技术:当一个线程执行完任务之后他不会死亡,而是去处理新的任务。
不用线程池如果每次都有一个新的请求,每次都会创建一个新的线程,这样会造成内存开销很大,同时也会抢夺CPU处理,导致其他方面的线程无法执行。
工作原理:
假如此时线程池中有三个任务,有很多任务进入任务队列来申请线程调度,这个时候线程池中的线程就会与队列中的任务去绑定,此时队列后面的任务就会进行等待,当线程执行完前面的任务后,他会继续调用队列中等待的任务。任务队列中的任务都是实现了Runable和Callable接口的任务。
2.线程池实现的API、参数说明
创建线程池对象一共有两种方法 ExecutorService接口是线程池中的顶级接口
第一种方式中的参数分别代表了什么:
/* 创建线程池对象 public ThreadPoolExecutor(int corePoolSize, 核心线程数 int maximumPoolSize, 指定线程池最大线程数(核心线程数与临时线程数总和) long keepAliveTime, 指定临时线程最大存活时间 TimeUnit unit, 最大时间规定单位是 秒 分钟 小时等等... BlockingQueue<Runnable> workQueue, 任务队列最大数 任务队列是实现了 Runnable和Callable接口 ThreadFactory threadFactory, 线程工厂(用于创建线程)里面封装的是创建线程的三种方法 RejectedExecutionHandler handler) 如果任务队列满了,新任务来了的话需要去拒绝 核心线程和临时线程都在忙 任务队列也满了,新的任务过来的时候才会开始任务拒绝 */
3.线程池处理Runnable任务
线程池处理Runnable方法的执行流程: 创建Runnable实现类,创建线程池,使用线程池的execute方法调用Runnable实现类任务。
线程池的特性:多线程之间并发执行任务,不会像线程一样执行完任务就死亡,线程池在执行完队列中的任务之后,不会停止运行,而是会继续等待任务调用。
代码中测试了线程池参数中特性:
package com.itheima.ThreadPoolDemo1;
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
/*
创建线程池对象
public ThreadPoolExecutor(int corePoolSize, 核心线程数
int maximumPoolSize, 指定线程池最大线程数(核心线程数与临时线程数总和)
long keepAliveTime, 指定临时线程最大存活时间
TimeUnit unit, 最大时间规定单位是 秒 分钟 小时等等...
BlockingQueue<Runnable> workQueue, 任务队列最大数 任务队列是实现了 Runnable和Callable接口
ThreadFactory threadFactory, 线程工厂(用于创建线程)里面封装的是创建线程的三种方法
RejectedExecutionHandler handler) 如果任务队列满了,新任务来了的话需要去拒绝
核心线程和临时线程都在忙 任务队列也满了,新的任务过来的时候才会开始任务拒绝
*/
//线程池的创建
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) ,new ThreadPoolExecutor.AbortPolicy());
// 给任务线程池处理
Runnable target = new MyRunnable();
//让线程池中的线程执行任务
//现在每个任务都绑定了一个线程。
pool.execute(target);
pool.execute(target);
pool.execute(target);
// pool.execute(target);
// pool.execute(target);//也执行的过来 不需要再去额外创建临时线程
//给任务加个线程休眠看看会不会创建新的临时线程 不会 等待队列中最大长度为5
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
//队列满 线程休眠 创建临时线程
pool.execute(target);
pool.execute(target);
//不创建,拒绝策略被触发
pool.execute(target);
}
}
注意:shutdownNow方法一般不会去使用,这个方法即使你线程中的任务没有完成,他也会立即关闭线程池。
最后一个策略,当你的线程池开始拒绝任务的时候,这个任务会绕过线程池,利用main方法中总线程进行处理。
4.线程池处理Callable任务
线程池处理Callable任务方法的执行流程: 创建Callable实现类,创建线程池,使用线程池的submit方法调用Callable实现类任务,这种任务与Runnable方法的不同之处在于任务可以返回值,因此这个方法会返回一个Future的任务对象,利用这个任务对象可以调用get方法获取返回值,Futer任务对象是TaskFuter的父类。
代码:
package com.itheima.ThreadPoolDemo1;
import java.util.concurrent.*;
public class ThreadPoolDemo2 {
//线程池的创建
//线程池处理Callable任务
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) ,new ThreadPoolExecutor.AbortPolicy());
//FutreTask的实现接口的父类接口
Future<String> f1= pool.submit(new MyCallable(100));
Future<String> f2= pool.submit(new MyCallable(200));
Future<String> f3= pool.submit(new MyCallable(300));
//获取返回值
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
}
}
5.Executors工具类实现线程池
线程池第二种创建方式:通过Executors工具类创建。 这些工具类中有个致命的缺点,这四个方法的底层中请求队列的长度为 Integer.maxValue 也就是几乎可以有无限个请求,如果线程来不及处理,内存中就会堆积大量的请求,导致OOM(内存溢出)。因此在大型并发系统中不会用工具类来创建线程池对象。
第二个线程只能创建固定线程的线程池,因此如果三个线程都因为执行异常结束,是没有临时线程来帮他执行新的任务的。
六、定时器
定时器在我们以后的开发中会用到很多,主要的实现方式有两种,通常我们会用第二种。
1.Timer类定时器
Timer类创建定时器中的参数说明(任务,延迟执行时间,执行周期)
然而这种定时器有个致命的缺点,因为Timer是单线程处理的,因此如果线程中的任务出现执行异常,那么后面的任务就不会再执行或者他会违背定时器的原则,比如下面的代码,如果按照正常执行流程第二个任务应该再第一个任务后两秒执行,但是因为第一个线程中线程睡眠所以延迟了五秒,因此我们通常使用第二种定时器方法。
2.ScheduledExecutorService (之前工具类中的方法
通过Executors工具类创建线程池对象再通过调用方法来创建定时器,方法中的参数(Runnable类任务,开始日期,执行周期,执行周期单位(秒 分 时 天)) ,因为这个定时器是基于线程池实现的,因此很好的补足了上面定时器创建的短板,当一个线程中任务出现异常的时候,他不会影响到其他任务的执行,任务会调度其他线程来执行。
代码:
package com.itheima.d7_timer;
import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TimerDemo2 {
public static void main(String[] args) {
//创建线程池做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行 AAA" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS); //1.定时任务 2 初始化延迟时间 3.周期时间 4.周期时间单位
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行 BBB" + new Date());
}
},0,2,TimeUnit.SECONDS);
}
}