JAVA线程基础----->看这篇就够了
目录
多线程的创建
方法一:继承Thread类
方法二:实现Runnable接口
方法三:实现Callable接口
三种创建方式对比,总结
Thread的常用方法
线程安全
线程同步
方式一:同步代码块
方式二:同步方法
方式三:Lock锁
线程通信
线程池
线程
线程(thread)是一个程序内部的一条执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
多线程是指从软硬件上实现多条执行流程的技术。
多线程的创建
- 方式一:继承Thread类
- 方式二:实现Runnable接口
- 方式三:JDK 5.0新增:实现Callable接口
方式一和方式二的线程创建方式都存在一个问题: 他们重写的run方法均不能直接返回结果。不适合需要返回线程执行结果的业务场景。
方式三的线程创建方式可以得到线程执行的结果。
方法一:继承Thread类
Java是通过java.lang.Thread 类来代表线程的。 按照面向对象的思想,Thread类应该提供了实现多线程的方式。
多线程的实现
- 定义一个子类MyThread继承线程类java.lang.Thread(继承Thread类)
- 重写run()方法
- 创建MyThread类的对象(创建线程对象)
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
优点:编码简单
缺点:存在单继承的局限性,线程类已经继承Thread,无法继承其他类,不利于扩展。
public class ThreadTest1 { public static void main(String[] args) { //3.创建MyThread类的对象(创建线程对象) Thread t = new MyThread(); //4.调用线程对象的start()方法启动线程(启动后还是执行run方法的) t.start(); for (int i = 0; i < 3; i++) { System.out.println(i+" 主线程执行输出!"); } }}//1.定义一个子类MyThread继承线程类java.lang.Thread(继承Thread类)class MyThread extends Thread{ //2.重写run()方法 @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(i+" 子线程执行!"); } }}
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。 只有调用start方法才是启动一个新的线程执行。
方法二:实现Runnable接口
多线程的实现
- 定义一个线程任务类MyRunnable实现Runnable接口
- 重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
Thread的构造器
public Thread(String name) |
可以为当前线程指定名称 |
public Thread(Runnable target) |
封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) |
封装Runnable对象成为线程对象,并指定线程名称 |
public class RunnableTest2 { public static void main(String[] args) { //3.创建MyRunnable任务对象 Runnable r = new MyRunnable(); //4.把MyRunnable任务对象交给Thread处理。-----Thread(Runnable) 构造器 Thread t = new Thread(r); //5.调用线程对象的start()方法启动线程 t.start(); for (int i = 0; i < 3; i++) { System.out.println(i+"主线程执行输出!"); } }}//1.定义一个线程任务类MyRunnable实现Runnable接口class MyRunnable implements Runnable{ //2.重写run()方法 @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(i+"子线程执行!"); } }}
实现Runnable接口(匿名内部类形式)
- 创建Runnable的匿名内部类对象。
- 交给Thread处理。
- 调用线程对象的start()启动线程。
只是把写成了匿名内部类的形式,和上面是一样的;
public class RunnableTest3 { public static void main(String[] args) { //1.创建Runnable的匿名内部类对象。 Runnable r = () -> { for (int i = 0; i < 3; i++) { System.out.println(i + "子线程执行!");} }; //2.交给Thread处理。-----Thread(Runnable) 构造器 Thread t = new Thread(r); //3.调用线程对象的start()方法启动线程 t.start(); for (int i = 0; i < 3; i++) { System.out.println(i+"主线程执行输出!"); } }}
方法三:实现Callable接口
多线程的实现
- 得到任务对象
- 定义类实现Callable接口,重写call方法,封装要做的事情。
- 用FutureTask把Callable对象封装成线程任务对象。
- 把线程任务对象交给Thread处理。
- 调用Thread的start方法启动线程,执行任务
- 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。 可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
FutureTask的API
public FutureTask(Callable call) |
把Callable对象封装成FutureTask对象。 |
public V get() throws Exception |
获取线程执行call方法返回的结果。 |
import java.util.concurrent.Callable;import java.util.concurrent.FutureTask;public class CallableTest4 { public static void main(String[] args) throws Exception { //3.创建Callable任务对象 Callable c = new MyCallable(99); //4.用FutureTask把Callable对象封装成线程任务对象。 FutureTask f = new FutureTask(c); //FutureTasks对象实现了Runnable接口,可以交给Thread-----用get方法可以得到线程执行完成的结果 //5.把线程任务对象交给Thread处理。 Thread t = new Thread(f); //6.调用Thread的start方法启动线程,执行任务 t.start(); //7.线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果 f.get(); System.out.println(f.get()); }}//1.得到任务对象,实现Callable接口-----应该声明线程任务执行完毕后的结果数据类型,如class MyCallable implements Callable { private int n; public MyCallable(int n){ this.n = n; } //2.重写call方法,封装要做的事情。 @Override public String call() throws Exception { int s = 0; for (int i = 1; i <= n; i++) s += i; return "1~"+n+"的累加和为:"+s; }}
三种创建方式对比,总结
方式 |
优点 |
缺点 |
继承Thread类 |
编程比较简单,可以直接使用Thread类中的方法 |
扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 |
扩展性强,实现该接口的同时还可以继承其他的类。 |
编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 |
扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 |
编程相对复杂 |
Thread的常用方法
Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
使用:当很多线程在执行的时候,我们可以去区分这些线程;
//1.定义一个子类MyThread继承线程类java.lang.Thread(继承Thread类)public class MyThread extends Thread{ //2.重写run()方法 @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(i+" "+Thread.currentThread().getName()+" 子线程执行!"+Thread.currentThread()); } }
public class ThreadTest1 { public static void main(String[] args) { Thread t0 = new MyThread(); t0.setName("0号"); //Thread t0 = new MyThread("0号"); t0.start(); Thread t1 = new MyThread(); t1.setName("1号"); //Thread t1 = new MyThread("1号"); t1.start(); //当前线程对象---谁执行它,它得到哪一个线程对象 Thread p = Thread.currentThread(); //主线程的名称是main System.out.println(p.getName()); for (int i = 0; i < 3; i++) { System.out.println(i+" "+Thread.currentThread().getName()+" 主线程执行输出!"+Thread.currentThread()); } }}
public class ThreadTest2 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 3; i++){ System.out.println("执行输出:"+i); if (i == 1){ //让当前线程进行休眠状态 Thread.sleep(300); } } }}
Thread常用方法、构造器
String getName() |
获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) |
设置线程名称 |
public static Thread currentThread(): |
返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) |
让线程休眠指定的时间,单位为毫秒。 |
public void run() |
线程任务方法 |
public void start() |
线程启动方法 |
线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
出现原因
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
模拟线程安全问题案例:取款
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Account { private String cardId; private double money; // 余额 关键信息 // final修饰后:锁对象是唯一和不可替换的,非常专业 private final Lock lock = new ReentrantLock(); public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } public void drawMoney(double money) { // 1、拿到是谁来取钱 String name = Thread.currentThread().getName(); // 2、判断余额是否足够 // 小白 小黑 if(this.money >= money){ // 钱够了 System.out.println(name+"来取钱,吐出:" + money); // 更新余额 this.money -= money; System.out.println(name+"取钱后,余额剩余:" + this.money); }else{ // 3、余额不足 System.out.println(name+"来取钱,余额不足!"); } }}
public class DrawThread extends Thread{ //接收要处理的账户 private Account account; public DrawThread(Account account, String name){ super(name); this.account = account; } @Override public void run() { //取钱 account.drawMoney(999999); }}
public class ThreadTest { public static void main(String[] args) { //定义线程类,创建一个共享的账户对象 Account account = new Account("ICPC-999", 999999); //创建2个线程对象,代表2个人 new DrawThread(account, "小白").start(); new DrawThread(account, "小黑").start(); }}
两个人同时取款出现问题:同时进入了取钱区,导致错误
线程同步
保证线程安全:让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的核心思想:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
方式一:同步代码块
- 作用:把出现线程安全问题的核心代码给上锁
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
- 锁对象要求: 理论上,锁对象只要对于当前同时执行的线程来说是同一个对象即可
锁对象的规范要求
- 规范上:建议使用共享资源作为锁对象。
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
public void drawMoney(double money) { // 1、拿到是谁来取钱 String name = Thread.currentThread().getName(); // 同步代码块 // this == account 共享账户 synchronized (this) { // 2、判断余额是否足够 if(this.money >= money){ // 钱够了 System.out.println(name+"来取钱,吐出:" + money); // 更新余额 this.money -= money; System.out.println(name+"取钱后,余额剩余:" + this.money); }else{ // 3、余额不足 System.out.println(name+"来取钱,余额不足!"); } } }
对于上述案例安全啊问题修改代码,进行同步,即解决问题
对出现问题的核心代码使用synchronized进行加锁 每次只能一个线程占锁进入访问
方式二:同步方法
- 作用:把出现线程安全问题的核心方法给上锁。
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
同步代码块锁的范围更小,同步方法锁的范围更大。
修饰符 synchronized 返回值类型 方法名称(形参列表) { 操作共享资源的代码;}
对出现问题的核心方法使用synchronized修饰 每次只能一个线程占锁进入访问
对于实例方法默认使用this作为锁对象。 对于静态方法默认使用类名.class对象作为锁对象。
方式三:Lock锁
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
public ReentrantLock() |
获得Lock锁的实现类对象 |
void lock() |
获得锁 |
void unlock() |
释放锁 |
private final Lock lock = new ReentrantLock(); public void drawMoney(double 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(name+"来取钱,余额不足!"); } } finally { lock.unlock(); //解锁 } }
线程通信
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
线程通信常见形式
- 通过共享一个数据的方式实现。
- 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全
Object类的等待和唤醒方法
void wait() |
让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法 |
void notify() |
唤醒正在等待的单个线程 |
void notifyAll() |
唤醒正在等待的所有线程 |
注意 :上述方法应该使用当前同步锁对象进行调用。
线程池
线程池就是一个可以复用线程的技术
创建线程池对象
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- 参数一:指定线程池的线程数量(核心线程): corePoolSize -----不能小于0
- 参数二:指定线程池可支持的最大线程数: maximumPoolSize-----最大数量 >= 核心线程数量
- 参数三:指定临时线程的最大存活时间: keepAliveTime -----不能小于0
- 参数四:指定存活时间的单位(秒、分、时、天): unit -----时间单位
- 参数五:指定任务队列: workQueue -----不能为null
- 参数六:指定用哪个线程工厂创建线程: threadFactory -----不能为null
- 参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler -----不能为null
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
ExecutorService接口:代表线程池
ExecutorService的常用方法
void execute(Runnable command) |
执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future submit(Callable task) |
执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() |
等任务执行完毕后关闭线程池 |
List shutdownNow() |
立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
1.线程池处理Runnable任务
处理Runnable任务:
- 使用ExecutorService的方法: void execute(Runnable target)
public class ThreadPoolTest1 { public static void main(String[] args) { //1.创建线程池对象 ExecutorService pool =new ThreadPoolExecutor(2, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue(3), new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Runnable r = new MyRunnable(); //2个任务处理 pool.execute(r); pool.execute(r); //3个任务在任务队列 pool.execute(r); pool.execute(r); pool.execute(r); //1个任务临时线程 //新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。 pool.execute(r); //核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝 //关闭线程池// pool.shutdown(); //会等待全部任务执行完毕后再关闭// pool.shutdownNow(); //立即关闭,会丢失任务 }}class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i "+i); try { System.out.println(Thread.currentThread().getName()+"----->睡着了!"); Thread.sleep(1000000); } catch (InterruptedException e) { throw new RuntimeException(e); } }}
2.线程池处理Callable任务
使用ExecutorService的方法:
- Future submit(Callable command)
import java.util.concurrent.*;public class ThreadPoolTest2 { public static void main(String[] args) throws ExecutionException, InterruptedException { //1.创建线程池对象 ExecutorService pool =new ThreadPoolExecutor(2, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue(3), new ThreadPoolExecutor.AbortPolicy()); //2.给任务线池处理 Future f1 = pool.submit(new MyCallable(100)); Future f2 = pool.submit(new MyCallable(200)); Future f3 = pool.submit(new MyCallable(300)); Future f4 = pool.submit(new MyCallable(400)); String s1 = f1.get(); String s2 = f2.get(); String s3 = f3.get(); String s4 = f4.get(); System.out.println(s1); System.out.println(s2); System.out.println(s3); System.out.println(s4); }}class MyCallable implements Callable { private int n; public MyCallable(int n ){ this.n = n; } @Override public String call() throws Exception { int s = 0; for (int i = 0; i <= n; i++) { s += i; } return Thread.currentThread().getName()+":1~"+n+"的累加和为:"+s; }}
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
创建线程池对象的常用方法:
public static ExecutorService newCachedThreadPool() |
线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。 |
public static ExecutorService newFixedThreadPool(int nThreads) |
创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor () |
创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
陷阱:大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadPoolTest3 { public static void main(String[] args) { //1.创建固定线程池----2个线程 ExecutorService pool = Executors.newFixedThreadPool(2); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); //没有多余线程 }}
建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。