目录
多线程
概念
线程概念
- 线程(Thread)是一个程序内部的一条执行流程
public static void main(String[] args){
//代码块
...
}
- 程序中如果只有一条执行流程,那这个程序就是单线程的程序
多线程概念
- 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
多线程应用场景:12306、百度网盘(上传、下载)、消息通信、淘宝等都离不开多线程技术
创建多线程
- Java是通过java.lang.Thread类的对象来代表线程的
多线程的创建方式一:继承Thread类
- 定义一个MyThread继承线程类java.lang.Thread,重写Thread的run()方法(将希望这个线程执行的任务写入)
- 创建MyThread类的对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
/**
* 1.让子类继承Thread线程类
*/
public class Mythread extends Thread{
//2.1必须重写Thread类的run方法
@Override
public void run() {
//描述该子线程的执行任务
for (int i = 0; i < 5; i++) {
System.out.println("子线程Mythread输出:" + i);
}
}
}
/**
* 目标:掌握线程的创建方式一:继承Thread类
*/
public class ThreadTest1 {
//main方法是由一条默认的主线程负责执行
public static void main(String[] args) {
//3.创建Mythread线程类的对象代表一个子线程
Thread t = new Mythread();
//4.启动线程(自动执行run方法)
t.start();//main主线程 t子线程
//两条线程并发执行,每次运行程序顺序不一定跟之前的顺序相同
for (int i = 0; i < 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
方法一优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread类,而Java是单继承,无法继承其他类,不利于功能的扩展
多线程的创建方式二:实现Runnable接口
-
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
-
创建MyRunnable任务对象
-
将MyRunnable任务对象交给Thread处理
public Thread(Runnable target); 封装Runnable对象成为线程对象
-
调用线程对象的start()方法启动线程
方式二的优缺点
- 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
- 缺点:需要多一个Runnable对象(??这也算缺点嘛
/**
* 1.创建一个任务类继承Runnable接口
*/
public class MyRunnable implements Runnable{
//2.重写Runneable接口的run方法
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3.创建一个任务类对象
Runnable target = new MyRunnable();
//4.将任务类对象作为参数new一个线程类对象
// public Thread(Runnable target)
new Thread(target).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
线程创建方式二的匿名内部类写法
- 可以创建Runnable的匿名内部类对象(不用再创建MyRunnable类实现Runnable)
- 再交给Thread线程对象
- 再调用线程对象的start()启动线程
public class ThreadTest2_2 {
public static void main(String[] args) {
//1.直接创建Runnable接口的匿名内部类形式(任务对象)
//使用匿名内部类创建一个Runnable任务对象
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target).start();
//简化形式1
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();
//简化形式2 Lambda表达式
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
前两种线程创建方式都存在一个问题
- 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果
- 解决:JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
- 这种方法最大的优点:可以返回线程执行完毕后的结果
多线程的第三种创建方式:利用Callable接口、FutureTask类来实现
-
创建任务对象
a. 定义一个类实现Callable接口(泛型接口),重写call方法,封装要做的事情,和要返回的数据
b. 把Callable类型的对象封装成FutureTask(线程任务对象,FutureTask类实现了Runnable接口)
-
把线程任务交给Thread对象
-
调用Thread对象的start方法启动线程
-
线程执行完毕后,通过FutureTask对象的get方法去获取线程任务执行的结果
import java.util.concurrent.Callable;
/**
* 1.创建一个类实现Callable接口
*/
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
//2.重写call方法
@Override
public String call() throws Exception {
//计算从1-n的和
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "从1-" + n + "的和为:" + sum;
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 目标:掌握吸纳成都创建方式三:实现Callable接口
*/
public class ThreadTest3 {
public static void main(String[] args) throws Exception {
//3.new一个Callable接口实现类的对象
Callable<String> call1 = new MyCallable(100);
//4.将Callable接口实现类的对象封装成FutureTask对象(FutureTask继承Runnable,所以本质是任务对象)
// TODO 未来任务对象的作用?
// a.是一个任务对象,实现了Runnable接口,可封装为一个Thread线程对象
// b.可以在线程执行完毕之后,用未来任务对象调用get方法获取执行完毕后的结果
FutureTask<String> f1 = new FutureTask<>(call1);
//5.将FutureTask任务对象封装成一个Thread线程对象,调用start方法开始执行线程
new Thread(f1).start();
//再来一组实验
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
new Thread(f2).start();
//6.获取线程执行完毕后的返回结果
// 注意:如果执行到这,加入上面的线程还没有执行完毕
// 这里的代码会先暂停,等待上面线程执行完毕后才会获取结果
String rs = f1.get();
System.out.println(rs);
String rs2 = f2.get();
System.out.println(rs2);
}
}
线程创建方式三的优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
- 缺点:编码较复杂
多线程的注意事项
- 启动线程必须是调用start方法,不是调用run方法
- 直接调用run方法会当成Java对象调用方法执行,此时相当于还是单线程执行
- 只有调用start方法才是启动一个新的线程执行
- 不要把主线程任务放在启动子线程之前
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了
Thread的常用方法
public class ThreadTest1 {
public static void main(String[] args) {
Thread t1 = new MyThread("1号线程");
// t1.setName("1号线程");
t1.start();
System.out.println(t1.getName());
Thread t2 = new MyThread("2号线程");
// t2.setName("2号线程");
t2.start();
System.out.println(t2.getName());
//哪个线程执行它,它就会得到哪个线程对象
Thread m = Thread.currentThread();
System.out.println(m.getName());
for (int i = 1; i <= 3; i++) {
System.out.println(m.getName() + "主线程输出:" + i);
}
for (int i = 1; i <= 5; i++) {
System.out.println(i);
if (i == 3){
//会让当前执行的线程暂停5秒,再继续执行
Thread.sleep(5000);
}
}
//join方法作用:让当前调用这个方法的线程先执行完
Thread t1 = new MyThread("1号线程");
t1.start();
t1.join();
Thread t2 = new MyThread("2号线程");
t2.start();
t2.join();
Thread t3 = new MyThread("3号线程");
t3.start();
t3.join();
}
}
Thread类还提供了诸如: yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候提及
线程安全
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
取钱的线程安全问题
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
//1.创建一个账户对象,代表两个人的共享账户
Account account = new Account("12345678","87654321",100000);
//2.创建两个线程,分别代表 小明、小红,再去同一个账户取钱100000
new d3_thread_safe.MyThread(account,"小明").start();
new d3_thread_safe.MyThread(account,"小红").start();
// System.out.println(account.getMoney());//运行的比两个线程要快,把这个放到线程的run方法体现
//再来一组
Account account1 = new Account("123456789","987654321",100000);
new MyThread(account1,"小黑").start();
new MyThread(account1,"小白").start();
}
}
----------------------------------------------------
public class MyThread extends Thread{
private Account account;
public MyThread(Account account,String name){
super(name);//给当前线程对象命名
this.account = account;
}
@Override
public void run() {
account.drawMoney(100000);
}
}
----------------------------------------------------
public class Account {
private String cardCode;
private String password;
private double money;
public Account() {
}
public Account(String cardCode, String password, double money) {
this.cardCode = cardCode;
this.password = password;
this.money = money;
}
public String getCardCode() {
return cardCode;
}
public void setCardCode(String cardCode) {
this.cardCode = cardCode;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
//小明、小红同时过来
public void drawMoney(double money) {
//1.取到当前线程对象的名字
Thread t = Thread.currentThread();
String name = t.getName();
//2.判断余额够不够
if (this.money >= money){
System.out.println(name + "取了100000元成功~");
this.money -= money;
System.out.println(name + "取钱后账户余额剩余" + this.money);
} else {
System.out.println(name + "取钱失败...");
}
}
}
线程同步
- 用来解决线程安全问题的方案
线程同步的思想
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的常见方案
- 加锁: 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能加锁进来
同步代码块
- 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
synchronized(同步锁){
访问共享资源的核心代码
}
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
针对取钱问题的同步代码块的部分代码:
public void drawMoney(double money) {
//1.取到当前线程对象的名字
Thread t = Thread.currentThread();
String name = t.getName();
//实例方法使用共享资源上锁!此时this正好为共享资源
synchronized (this) {
//2.判断余额够不够
if (this.money >= money){
System.out.println(name + "取了100000元成功~");
this.money -= money;
System.out.println(name + "取钱后账户余额剩余" + this.money);
} else {
System.out.println(name + "取钱失败...");
}
}
}
同步锁的注意事项
- 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
锁对象随便选择一个唯一的对象好不好呢?
- 不好,会影响其他无关线程的执行。
锁对象的使用规范
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
同步方法
- 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
针对取钱问题的同步方法的部分代码:
public synchronized void drawMoney(double money) {
//1.取到当前线程对象的名字
Thread t = Thread.currentThread();
String name = t.getName();
//2.判断余额够不够
if (this.money >= money){
System.out.println(name + "取了100000元成功~");
this.money -= money;
System.out.println(name + "取钱后账户余额剩余" + this.money);
} else {
System.out.println(name + "取钱失败...");
}
}
Lock锁
- Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
public ReentrantLock()
获取Lock锁的实现类对象
Lock的常用方法
void lock();//加锁
void unlock();//释放锁
针对取钱问题的Lock锁的部分代码
public class Account{
//创建了一个锁对象
private Lock lk = new ReentrantLock();
public class Account{
//创建一个Lock锁对象
private Lock l = new ReentrantLock();
//小明、小红同时过来
public void drawMoney(double money) {
//1.取到当前线程对象的名字
Thread t = Thread.currentThread();
String name = t.getName();
//2.判断余额够不够
try {
lk.lock();//加锁
if (this.money >= money){
System.out.println(name + "取了100000元成功~");
this.money -= money;
System.out.println(name + "取钱后账户余额剩余" + this.money);
} else {
System.out.println(name + "取钱失败...");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock();//解锁
}
}
}
线程通信
- 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态以相互协调,并避免无效的资源争夺。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据。
- 注意:生产者生产完数据应该等待自己,通知消费者消费(其实应该先唤醒别人,再等待自己);消费者消费完数据也应该等待自己,再通知生产者生产!
**ps:**线程通信的前提是一定要保证线程安全!!!
需求:3个生产者线程(负责生产包子),每个线程每次只能生产一个放在桌子上;2个消费者线程(负责吃包子),每个线程每次只能吃桌子上的一个包子
public class ThreadTest {
public static void main(String[] args) {
//需求:3个生产者线程(负责生产包子),每个线程每次只能生产一个放在桌子上
// 2个消费者线程(负责吃包子),每个线程每次只能吃桌子上的一个包子
//创建一个桌子对象
Desk desk = new Desk();
//创建3个生产者线程(3个厨师)
new Thread(() -> {
while(true){
desk.put();
}
},"厨师1").start();
new Thread(() -> {
while(true){
desk.put();
}
},"厨师2").start();
new Thread(() -> {
while(true){
desk.put();
}
},"厨师3").start();
//创建2个消费者线程(2个吃货)
new Thread(() -> {
while(true){
desk.get();
}
},"吃货1").start();
new Thread(() -> {
while(true){
desk.get();
}
},"吃货2").start();
}
}
import java.util.ArrayList;
import java.util.List;
public class Desk {
private List<String> list = new ArrayList<>();
public synchronized void put() {
try{
String name = Thread.currentThread().getName();
//判断有没有包子
if(list.size() == 0){
list.add(name + "做的包子");
System.out.println(name + "做了一个肉包子");
Thread.sleep(2000);
//唤醒别人,等待自己
this.notifyAll();
this.wait();
} else{
//有包子了,不用做了
//唤醒别人,等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e){
e.printStackTrace();
}
}
public synchronized void get() {
try {
String name = Thread.currentThread().getName();
if (list.size() == 0){
//没包子
//唤醒别人,等待自己
this.notifyAll();
this.wait();
} else{
//有包子 可以吃
System.out.println(name + "吃了" + list.get(0));
list.clear();
Thread.sleep(1000);
//唤醒别人,等待自己
this.notifyAll();
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池
- 线程池就是一个可以复用线程的技术。
不使用线程池的问题
- 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新仕务来了肯定又要创建新线任处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
线程池工作原理
线程池不会产生过多的线程,可以固定线程的数量,重复利用这些线程去处理任务;也可以控制任务数量,将任务暂时缓存起来,让线程处理,因此线程池不会因为线程过多或任务过多,导致占用系统资源过多而导致系统瘫痪,从而提高系统工作性能
ExecutorService
- JDK 5.0起提供了代表线程池的接口:ExecutorService。
如何得到线程池对象?
- 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
import java.util.concurrent.*;
/**
* 目标:掌握线程池的创建
*/
public class ThreadPoolTest1 {
public static void main(String[] args) {
//1.通过ThreadPoolExecutor创建一个线程池对象
/*public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)*/
ExecutorService pool = new ThreadPoolExecutor(3,5,8,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
线程池的注意事项
- 临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。 - 什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象(本质还是调用线程池的实现类ThreadPoolExecutor创建线程池对象,不推荐此方法)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest3 {
public static void main(String[] args) {
//1.通过Executors创建一个线程池对象
ExecutorService pool = Executors.newFixedThreadPool(17);
// Q:核心线程的数量到底配置多少呢??
// 计算密集型的任务:核心线程数量 = CPU的核数 + 1 //计算一些东西
// IO密集型的任务: 核心线程数量 = CPU核数 * 2 //文件、通信等
}
}
ExecutorService的常用方法
a.执行Runnable任务:
public class ThreadPoolTest1 {
public static void main(String[] args) {
//1.通过ThreadPoolExecutor创建一个线程池对象
/*public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)*/
ExecutorService pool = new ThreadPoolExecutor(3,5,8,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//从此处开始新增的任务暂时存入任务队列(4个)
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
//此时任务队列已满,且三个核心线程在执行,需要创建临时线程
pool.execute(target);
pool.execute(target);
//此时任务队列已满,且三个核心线程和两个临时线程都在执行,需要拒绝任务对象
pool.execute(target);
pool.shutdown();//等线程池的任务全部执行完毕后,再关闭线程池
// pool.shutdownNow();//立即关闭线程池!不管任务是否执行完毕
}
}
b.执行Callable任务
public class ThreadPoolTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.通过ThreadPoolExecutor创建一个线程池
/*public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)*/
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//2.使用线程处理Callable(任务?)对象,并返回一个未来任务类对象,可以得到线程执行完毕后的返回值
Future<String> f1 = pool.submit(new MyCallable(100));//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
Future<String> f2 = pool.submit(new MyCallable(200));//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
Future<String> f3= pool.submit(new MyCallable(300));//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
Future<String> f4 = pool.submit(new MyCallable(400));//复用
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
pool.shutdown();
}
}
并发、并行、生命周期
进程
- 正在运行的程序(软件)就是一个独立的进程。
- 线程是属于进程的,一个进程中可以同时运行很多个线程。
- 进程中的多个线程其实是并发和并行执行的。
并发
- 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行
- 在同一时刻,同时有多个线程在被CPU调度执行,这就叫并行。
拿我自己电脑举例:
16个逻辑处理器,每过极短时间都切换16个线程叫并发,同一时刻处理16个线程这叫并行
生命周期
- 线程的生命周期就是线程从生到死的过程中,经历的各种状态及状态转换。
- 理解线程这些状态有利于提升并发编程的理解能力。
Java线程的状态
-
Java总共定义了6种状态
-
6种状态都定义在Thread类的内部枚举类中。