Tread多线程
Tread多线程
什么是线程?
-
线程(Thread)是一个程序内部的一条执行流程。
-
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
多线程是什么?
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由cpu负责调度执行)。
多线程的创建方式
方式一:继承Thread
①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
//(1)让自定义的MyThread继承Thread线程类【自定义的类也就具备线程的特性】 public class MyThread extends Thread { //(2)想要声明自定义的线程执行的时候到底执行什么代码,主动重写父类的run方法 @Override public void run() { for (int i = 1; i <= 20; i++) { System.out.println("【自定义线程】的run方法执行了第" + i + "次!"); } } } public class ThreadTest1 { public static void main(String[] args) { //(3)创建自定义线程类对象并调用start方法启动线程 MyThread myThread = new MyThread(); myThread.start(); //补:在自定义线程启动之后,继续编写代码让主线程执行 for (int i = 1; i <= 20; i++) { System.out.println("【主线程】的run方法执行了第" + i + "次!"); } } }
方式一优缺点
-
优点:编码简单
-
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
方式二:实现Runnable接口
①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
②创建MyRunnable任务对象
③把MyRunnable任务对象交给Thread处理。
public class ThreadTest2 { public static void main(String[] args) { //(3)创建MyRunnable线程任务对象 【和线程还没有关系】 MyRunnable myRunnable = new MyRunnable(); //(4)创建Thread线程对象,并且线程任务作为构造方法的参数传递【枪:√ 弹夹:√】 Thread t = new Thread(myRunnable); t.start(); //补:在自定义线程启动之后,继续编写代码让主线程执行 for (int i = 1; i <= 20; i++) { System.out.println("【主线程】的run方法执行了第" + i + "次!"); } } }
方式二优缺点:
-
优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强。
-
缺点:需要多一个Runnable对象。
前两种线程创建文件都存在的一个问题
假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。
解决(多线程的第三种创建方式)
利用Callable,FutureTask类型实现。
①创建任务对象
定义一个类实现Callable接口,重写call方法,封装要做的事情和要返回的数据。
把Callable类型的对象封装成FutureTask对象(线程任务对象)。
②把线程任务对象封装成Thread对象。
③调用Thread对象的start方法启动线程。
④线程执行完毕后,通过FutureTask对象的的get方法去获取线程任务执行的结果。
public class ThreadTest4 { public static void main(String[] args) throws ExecutionException, InterruptedException { //(3)Thread类不支持直接传递一个Callable线程任务对象【封装FutureTask对象并且将Callable线程任务作为构造参数传递】 MyCallable myCallable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(myCallable); //(4)创建Thread类对象并且将FutureTask作为参数传递 Thread t = new Thread(futureTask); t.start(); //★(5)通过futureTask对象获取结果 Integer result = futureTask.get(); System.out.println("带有返回值的线程任务执行完成后返回的结果是:" + result); //补:在自定义线程启动之后,继续编写代码让主线程执行 for (int i = 1; i <= 20; i++) { System.out.println("【主线程】的run方法执行了第" + i + "次!"); } } }
方式三优缺点:
-
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
-
缺点:编码复杂一点。
三种线程创建方法比较
Thread的常见用法
public void run() 线程的任务方法
public void start() 启动线程
public Strign getName() 获取1当前线程名称,线程名称默认是Thread-索引
public void setName(String name) 为线程设置名称
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
public void join() 让调用这个方法的线程先执行完
public class MyRunRunable implements Runnable { @Override public void run() { for (int i = 1; i <= 200; i++) { //在打印的时候,想要【获取到执行当前这行代码的线程】的线程名称 //通过★Thread.currentThread():获取当前执行此方法的线程对象 System.out.println(Thread.currentThread().getName() + "已经跑了" + i + "米!"); } } } public class ThreadTest5 { public static void main(String[] args) throws ExecutionException, InterruptedException { String threadName = Thread.currentThread().getName(); System.out.println("【主线程名称】:" + threadName); MyRunRunable myRunRunable = new MyRunRunable(); //线程起名方式(1):通过线程对象调用setName方法传递名称 Thread t1 = new Thread(myRunRunable); t1.setName("张二狗"); //思考:模拟两个人跑 => 两个线程跑【跑的逻辑一样 所以使用同一个线程任务】不会干扰【底层:线程栈 线程执行过程中产生的变量数据都在线程栈中保存】 //线程起名方式(2):通过new Thread构造方法的时候,将参数一作为线程任务,参数二作为线程名称 Thread t2 = new Thread(myRunRunable, "刘铁柱"); t1.start(); //t1.join(); 【让调用此方法的线程先执行完:插队】 t2.start(); } }
线程安全
什么是线程安全问题?
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
取钱的线程安全问题
场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
线程安全问题出现的原因?
-
存在多个线程在同时执行
-
多个线程同时访问一个共享资源
-
存在修改共享资源的情况
定义一个账号类
package com.itheima.safe; import java.time.LocalTime; public class Account { private String accoundId; private Integer money; //取钱:takeMoney public void takeMoney(Integer money) { //获取当前取钱的线程名称 String name = Thread.currentThread().getName(); System.out.println(LocalTime.now() + " " + name + "准备开始取钱!"); if (this.money >= money) { System.out.println(LocalTime.now() + " " + name + "取出了" + money + "元!"); this.money -= money; } else { System.out.println(LocalTime.now() + " " + name + "余额不足!"); } System.out.println(LocalTime.now() + " 账户的余额是:" + this.money + "元!"); } public String getAccoundId() { return accoundId; } public void setAccoundId(String accoundId) { this.accoundId = accoundId; } public Integer getMoney() { return money; } public void setMoney(Integer money) { this.money = money; } public Account() { } public Account(String accoundId, Integer money) { this.accoundId = accoundId; this.money = money; } }
定义线程类
public class TakeMoneyRunnable implements Runnable { //线程任务需要访问到Account账户对象【将账户对象作为线程任务的构造方法 并且只给出一个有参构造】 private Account account; public TakeMoneyRunnable(Account account) { this.account = account; } @Override public void run() { account.takeMoney(100000); } }
测试类
package com.itheima.safe; public class TakeMoneyThreadTest { public static void main(String[] args) { Account account = new Account("CHINA-BANK-62261728738", 100000); //创建线程任务【由于两个线程的逻辑一样 只需要一个线程任务】 TakeMoneyRunnable takeMoneyRunnable = new TakeMoneyRunnable(account); //创建线程对象并且传递线程任务和线程名称 Thread t1 = new Thread(takeMoneyRunnable, "张二狗"); Thread t2 = new Thread(takeMoneyRunnable, "王美丽"); t1.start(); t2.start(); } }
线程同步(解决线程安全)
线程同步的思想
让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程次才可以来执行。
对线程安全改造
public void takeMoney(Integer money) { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱"); //(同步代码块)加锁 synchronized (this) { if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "余额不足"); } System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元"); } }
同步锁的注意事项
-
对于当前同时执行的线程来说,同步锁必须是同一把锁(同一个对象),否则会出bug。
锁对象的使用规范
-
建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
-
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
同步方法
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程次才可以来执行。
对线程安全改造
//同步方法加锁(修饰符后面,放回值类型前面) public synchronized void takeMoney(Integer money) { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱"); if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "余额不足"); } System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元"); } }
同步方法底层原理
-
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
-
如果方法是实例方法:同步方法默认用this作为的锁对象。
-
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
1.同步方法是如何保证线程安全的?
-
对出现问题的核心方法使用**synchronized修饰**
-
每次只能一个线程占锁进入访问
2.同步方法的同步锁对象的原理?
-
对于实例方法默认使用**this作为锁对象。**
-
对于静态方法默认使用**类名.class对象作为锁对象。**
Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
对线程安全改造
private static final Lock LOCK = new ReentrantLock(); public void takeMoney(Integer money) { LOCK.lock(); try { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱"); if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "余额不足"); } System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元"); }finally { LOCK.unlock(); } }
线程池
了解线程池
什么是线程池?
线程池就是一个可以复用线程的技术。
不使用线程池的问题
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程,这样会严重影响系统的性能。
线程池的工作原理
创建线程池
如何得到线程池对象?
方式一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象。
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
ThreadPoolExecutor**构造器**
-
参数一:corePoolSize : 指定线程池的核心线程数量。
-
参数二:maximumPoolSize:指定线程池的最大线程数量。
-
参数三:keepAliveTime :指定临时线程的存活时间。
-
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
-
参数五:workQueue:指定线程池的任务队列。
-
参数六:threadFactory:指定线程池的线程工厂。
-
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
public class PoolDemo1 { public static void main(String[] args) { //基于ThreadPoolExecutor的构造方法创建线程池对象 //核心线程:【线程任务:Cpu密集型(运算):当前机器Cpu的核心数+1 Runtime.getRuntime().availableProcessors()+1】 //核心线程:【线程任务:IO密集型(读写):当前机器Cpu的核心数*2 Runtime.getRuntime().availableProcessors()*2】 //ThreadFactory:线程工厂 【Exectors.defaultThreadFactory】 获取默认的线程工厂 ThreadPoolExecutor pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() + 1, 15, 40L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); //可以基于线程池规范接口的execute方法提交线程任务交给线程池执行 for (int i = 1; i <= 25; i++) { pool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行了线程任务!"); } }); } //AbortPolicy:默认丢弃新任务并且抛出异常 //DiscardPolicy:默认丢弃新任务并且不抛出异常 //DiscardOldestPolicy:默认将等待时间最长的任务丢弃,并且让新任务添加到队列中 //CallerRunsPolicy:使用主线程执行新任务绕过当前线程池 //线程池一旦提交任务就持久运行【想要关闭调用shutdown/shutdownNow】 pool.shutdown(); } }
线程池的注意事项
1、临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2、什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
线程池如何处理Runnable任务?
-
使用ExecutorService的方法:
-
void execute(Runnable target)
线程池如何处理Callable任务,并得到任务执行完后返回的结果?
-
使用ExecutorService的方法:
-
Future<T> submit(Callable<T> command)
并发,并行
并发的含义
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
在同一个时刻上,同时有多个线程在被CPU调度执行。
简单说说多线程是怎么执行的?
-
并发:CPU分时轮询的执行线程。
-
并行:同一个时刻多个线程同时在执行。
线程的生命周期
Java线程的状态
-
Java总共定义了6种状态
-
6种状态都定义在Thread类的内部枚举类中。