该文章是我在学习Java多线程技术过程中自己做的笔记(基于 JDK 11),如果文章中有什么漏洞或错误,欢迎批评指正,在撰写该文章的时候,引用借鉴了以下几位作者的文章:
- 作者:梦里藍天
来源:CSDN
原文:https://blog.csdn.net/ren365880/article/details/80289532 - 作者:日常自闭的HXH
来源:CSDN
原文:https://blog.csdn.net/qq_43570075/article/details/106243873 - 作者:爱_码_仕
来源:CSDN
原文:https://blog.csdn.net/sinat_36710456/article/details/107221342
要想学习多线程技术,我们首先得认识它,知道什么是线程,它有什么用处,下面是我记录的一些关于多线程技术的基础知识。
基本概念
进程
内存中运行的应用程序,每一个进程都有一个独立的内存空间。
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程至少有一个线程。
- 线程实际上是在进程基础上的进一步划分,一个进程启动后,里面的若干个执行路径又可以划分为若干个线程。
线程的调度
分时调度
所有线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间
抢占式调度
- 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,Java就是抢占式调度
- CPU使用抢占式调度模式,在多个线程间进行高速的切换,即对于一个CPU而已,在某个时刻内,只能执行一个线程。多线程程序不能提高程序的运行速度,但能提高程序运行效率,让CPU的使用率更高。
线程的切换需要时间,虽然这个时间非常短,但仍然是有的。
同步与异步
同步
排队执行,安全但效率低
异步
同时执行,不安全但效率高
并发和并行
并发
指两个或多个事件在同一个时间段内发生
并行
指两个或多个事件在同一时刻内发生,即同时发生
在初步的了解了多线程技术后,接下来,我们就看看在Java中多线程技术的实现吧。
多线程技术在Java中的使用
线程的创建
继承Thread类
public class Thread extends Object implements Runnable
在继承了Thread类后,我们就可以重写run()方法。
public class MyThread extends Thread { public void run() { //这里的代码,就是一条新的执行路径 } }
当然执行的时候,并不需要我们去直接调用run()方法,而是调用继承自Thread类的start()方法
MyThread m = new MyThread(); m.start();
这样线程就被启动了。
当然我们也可以使用更简单的实现方式public class Test { public static void main(String[] args) { new Thread() { public void run() { //要执行的任务 } }.start(); } }
Thread类中还有一些比较常用到的方法,在这里也给大家列举出来:
public final String getName() //该方法用于返回线程的名称
public long getId() //该方法用于返回线程的标识符,和getName()的作用差不多,都是用来标识线程的。
public static void sleep(long millis) //该方法用于让线程休眠 millis 毫秒 public static void sleep(long millis, int nanos) //该方法用于让线程休眠 millis 毫秒 + nanos 纳秒 /** * 异常 * * IllegalArgumentException - 如果 millis 值为负数,或者值 nanos 不在 0-999999 范围内 * * InterruptedException - 如果有任何线程中断了当前线程。抛出此异常时,将清除当前线程的中断状态。 */
public final void setPriority(int newPrority) //该方法用来更改此线程的优先级 /** * Thread类提供了三个静态常量,来代表线程的三种优先级: * 1. MAX_PRIORITY——最大优先级 * 2. MIN_PRIORITY——最小优先级 * 3. NORM_PRIORITY——默认优先级 * * 异常 * IllegalArgumentException - 如果优先级不在 MIN_PRIORITY 至 MAX_PRIORITY 范围内。 * SecurityException - 如果当前线程无法修改此线程。 */
public final void setDaemon(boolean on) //该方法用来设置线程是守护线程还是用户线程,该方法应该在线程start()之前调用 /** * true - 该线程是守护线程 * false - 该线程数是用户线程 * * 异常 * IllegalThreadStateException - 在线程start()后仍调用该方法,会抛出此异常 * SecurityException - 如果 checkAccess() 确定当前线程无法修改此线程。 * * checkAccess()方法是用确定当前运行的线程是否具有修改此线程的权限。如果不允许当前线程访问此线程,则会抛出SecurityException 异常 */
实现Runnable接口
实现Runnable接口是我们在实际中比较常见,也比较常用的,开启多线程的技术
Runnable中只有一个需要重写的抽象方法,即run()方法
public class MyRunnable implements Runnable { @Override public void run() { //要执行的任务 } }
在我们重写完run()方法后,我们依旧要使用Thread类的start()方法去开启线程
public class Test { public static void main(String[] args){ MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); t.start(); } }
实现Runnable对比继承Thread的优势
更适合多个线程同时执行相同任务的情况
可以避免单继承所带来的局限性
任务与线程本身是分离的,提高了线程的健壮性
线程池技术,只接受Runnable类型的任务,不接受Thread类型的线程
虽然实现Runnable的方式对比起继承Thread的方式拥有更多的优势,但这不代表要抛弃通过继承Thread类来创建线程的方式,Thread类的简单实现方式在某些开发场景中会更加合适,要根据实际的开发需求来选择使用。
实现Runnable对比继承Thread的优势
- 更适合多个线程同时执行相同任务的情况
- 可以避免单继承所带来的局限性
- 任务与线程本身是分离的,提高了线程的健壮性
- 线程池技术,只接受Runnable类型的任务,不接受Thread类型的线程
虽然实现Runnable的方式对比起继承Thread的方式拥有更多的优势,但这不代表要抛弃通过继承Thread类来创建线程的方式,Thread类的简单实现方式在某些开发场景中会更加合适,要根据实际的开发需求来选择使用。
实现Callable
Callable是一个带返回值的线程,它的使用方法和实现Runnable差不多,都是使用Thread类的start()方法启动。下面我们来看下它的使用方式
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException{ Callable<Integer> callable = new MyCallable(); FutureTask<Integer> task = new FutureTask<>(callable); new Thread(task).start(); System.out.println(task.get()); } static class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { Thread.sleep(1000); return 1; } } }
如果我们不调用 get() 方法主线程就不会等待结果的返回,但如果调用了,主线程就一定会等待。这里再列举几个比较常见的FutureTask类的方法
public V get (long timeout, TimeUnit unit) /** * 这个方法和之前的get方法作用一致,用来获得返回值,但不同的是,我们可以通过传入参数来设定最多等待时间 * timeout - 等待的最长时间 * unit - 超时参数的时间单位 * * 该方法在一些特定情况下会抛出以下异常 * 异常 * CancellationException - 如果计算被取消 * InterruptedException - 如果当前线程在等待时被中断 * ExecutionException - 如果计算引发异常 * TimeoutException - 如果等待超时 */
public boolean isDone() //该方法用来判断子线程是否已经执行完毕了
public boolean cancel(boolean mayInterruptIfRunning) /** * 用来取消线程,传入true就是取消 * 同时该方法也会返回一个boolean值,true代表取消成功通常是因为已经正常完成,false表示无法完成任务 */
线程的中断
前面描述了,如何启动一个线程,下面我们要介绍如何关闭一个线程。
当我们调用Thread类的interrupt()方法时,会给线程添加一个中断标记,当线程在一些特殊操作时,线程就会抛出 InterruptedException 异常,这个时候我们就可以捕获异常并处理。
try{ Thread.sleep(1000); }catch (InterruptedException e) { //当线程发现了中断标记时,就会抛出 InterruptedException 异常 //我们在捕获了异常后,只需要 return 就可以关闭该线程 return; }
Thread类中的stop()方法已经被弃用,使用这种方法关闭线程并不安全,最好不要用这种方法关闭线程。
会触发异常的操作:
wait()
wait(long)
wait(long, int)
sleep(long)
interrupt()
interrupted()
线程安全问题
我们先来看下面这串代码
public class Test { public static void main(String[] args) { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable { //票数 private int count = 10; @Override public void run() { while (count > 0) { System.out.println("准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println("出票成功,余票:" + count); } } } }
正常而言,我们卖票时不可能将票数卖到为负数的,当票数为0的时候,循环也就结束了,然而在这串代码中,票数却会有出现负数的情况
这就是我们所说的线程不安全,如果单从逻辑上判断,当count<=0的时候,应当是不能进入while循环的。但当count>0的时候,三个线程均能进入while循环,即使这时候票数只剩下一张了,三个线程仍在卖票,这个时候余票就会出现负数了。
这种情况显然是不合理的,所以我们应当采取措施,来预防这种情况的出现。
解决方案1:同步代码块
代码如下
public class Test { public static void main(String[] args) { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable { //票数 private int count = 10; private Object o = new Object(); @Override public void run() { while (true) { synchronized (o) { if (count > 0) { System.out.println(Thread.currentThread().getName() + "准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count); } else { break; } } } } } }
我们再运行一次,就会发现余票不会再为负数了。
解决方法2:同步方法
代码如下
public class Test { public static void main(String[] args) { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable { //票数 private int count = 10; private Object o = new Object(); @Override public void run() { while (true) { boolean flag = sale(); if (!flag) { break; } } } public synchronized boolean sale() { if (count > 0) { System.out.println(Thread.currentThread().getName() + "准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count); return true; } return false; } } }
解决方法3:显示锁 Lock 子类 ReentrantLock
代码如下
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author huangyong */ public class Test { public static void main(String[] args) { Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable { //票数 private int count = 10; Lock l = new ReentrantLock(); @Override public void run() { while (true) { l.lock(); if (count > 0) { System.out.println(Thread.currentThread().getName() + "准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count); }else { l.unlock(); break; } l.unlock(); } } } }
这三种方法均能解决线程不安全的问题,这里简单介绍下显示锁和隐式锁的区别
显示锁和隐式锁的区别
一、层面不同
synchronized是Java中的关键字,是由JVM来维护的,是JVM层面的锁
Lock是JDK5以后才出现的具体的类。使用lock是调用对应的API,是API层面的锁
二、使用方式不同
- synchronized能自动获取锁和释放锁,由系统维护
- Lock需要通过 lock() 方法手动获取锁,并通过 unlock() 方法手动释放锁
三、等待是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成。
- Lock可以中断,中断方式:
- 调用设置超时方法tryLock(long timeout ,timeUnit unit)
- 调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
四、能否设置为公平锁
- synchronized不能设置,只能为非公平锁
- Lock可以通过在创建实例时传入boolean值来设置,true表示为公平锁,false表示为非公平锁。
五、能否精确唤醒
- synchronized不能精确唤醒,要么随机唤醒,要么唤醒所有等待线程
- Lock可以精确唤醒。
公平锁和不公平锁
公平锁和不公平锁其实很好理解,公平锁就是排队等待,不公平锁就是谁抢到就是谁的。我们在上面也有提到过,在Lock创建的时候,可以通过传入boolean值来设置是否为公平锁,代码如下:
Lock l = new ReentrantLock(true);//true代表公平锁,false代表非公平锁,如果不传入boolean值,则默认创建非公平锁,即 new ReentrantLock(false);
多线程通信
我们可以用一个简单的例子来了解,一个简单的生产和消费者问题:父亲不断依次的往盘子上放蛋糕和水果,儿子不断的从盘子上拿走蛋糕和水果吃掉。当父亲还没来得及将蛋糕和水果放到盘子上的时候,儿子是不能拿到蛋糕或水果的,同理,当儿子还来不及拿走蛋糕的时候,父亲也不能往盘子上放蛋糕。如果我们没有对这两个线程加以控制会发生什么事?我们先来看下,下面这段代码
public class Test { public static void main(String[] args) { Plate plate = new Plate(); new Father(plate).start(); new Son(plate).start(); } /** * 父亲 */ static class Father extends Thread { Plate plate; @Override public void run() { for (int n = 0; n < 10; n++) { if (n % 2 == 0) { plate.setFood("蛋糕"); } else { plate.setFood("水果"); } } } public Father(Plate plate) { this.plate = plate; } } /** * 儿子 */ static class Son extends Thread { Plate plate; @Override public void run() { for (int n = 0; n < 10; n++) { plate.getFood(); } } public Son(Plate plate) { this.plate = plate; } } /** * 盘子 */ static class Plate { // 盘子中的食物 String food = "空盘子"; public void setFood(String food) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } this.food = food; } public void getFood() { try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("儿子吃掉了" + this.food); this.food = "空盘子"; } } }
我们运行这串代码就会发现问题所在,父亲放东西的速度明显快于儿子吃东西的速度,这导致了,儿子还没把东西吃了,父亲就把东西放上去了。
这个时候我们第一时间就会想到给方法加锁,但如果单单只给方法添加synchronized会怎么样呢?答案应该是显而易见的
单单给方法添加一个synchronized关键字是无法解决这个问题的,因为synchronized是非公平锁,线程并不会按照依次交替的顺序来执行,因而在生产者消费者问题中只给方法添加synchronized是行不通的,这个时候我们就要额外进行一些操作(例如添加一个标识变量,通过判断这个标识变量,来决定让儿子线程还是父亲线程执行),让线程能按照我们预期的顺序交替执行,代码如下:
public class Test { public static void main(String[] args) { Plate plate = new Plate(); new Father(plate).start(); new Son(plate).start(); } /** * 父亲 */ static class Father extends Thread { Plate plate; @Override public void run() { for (int n = 0; n < 10; n++) { if (n % 2 == 0) { plate.setFood("蛋糕"); } else { plate.setFood("水果"); } } } public Father(Plate plate) { this.plate = plate; } } /** * 儿子 */ static class Son extends Thread { Plate plate; @Override public void run() { for (int n = 0; n < 10; n++) { plate.getFood(); } } public Son(Plate plate) { this.plate = plate; } } /** * 盘子 */ static class Plate { // 盘子中的食物 String food = "空盘子"; // 是否是空盘子 boolean flag = true; public synchronized void setFood(String food) { if (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } this.food = food; this.flag = false; // 唤醒当前在this线程下睡着的所有线程 this.notifyAll(); try { // 父亲线程进入休眠 this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void getFood() { if (!flag) { try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("儿子吃掉了" + this.food); this.food = "空盘子"; this.flag = true; // 唤醒当前在this线程下睡着的所有线程 this.notifyAll(); try { // 儿子线程进入休眠 this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
在这样修改后,让我们再次运行代码看下效果:
这个时候我们就能很直观的看到,线程按照我们给定的顺序执行了
线程的六种状态
NEW - 尚未启动的线程处于此状态
RUNNABLE - 在Java虚拟机中执行的线程处于此状态
BLOCKED - 被阻塞等待监视器锁定的线程处于此状态
WAITING - 无限期等待另一个线程执行特定操作的线程处于此状态
TIMED_WAITING - 正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态
TERMINATED - 已退出的线程处于此状态
如果只用文字表述,要理解起来会比较困难,我们可以通过画图来更好的理解这六个线程状态直接的关系
图画得比较潦草,但大概的流程就是这样。
线程池技术
如果并发的线程数量很多,而线程执行的任务时间都很短,就会造成系统的效率下降。因此我们需要用到线程池技术。接下来我们介绍下四种不同的线程池
缓存线程池(长度不限)
- 判断线程池是否存在空线程
- 存在则使用,不存在则创建一个新线程,放入线程池并使用
缓存线程池的使用
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); service.execute(new Runnable() { @Override public void run() { // 要执行的任务 } }); } }
定长线程池
- 判断线程池是否存在空线程
- 存在则使用,不存在则判断线程池是否已满
- 线程池未满,则创建一个新线程,放入线程池并使用。若线程池已满,则排队等待
定长线程池的使用:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(2);//2可以改成你想要创建的定长池大小 service.execute(new Runnable() { @Override public void run() { // 要执行的任务 } }); } }
单线程线程池
判断线程池是否为空,空则执行,忙则等待
单线程线程池的使用:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { // 要执行的任务 } }); } }
周期性任务定长线程池
执行同定长线程池一样,主要是线程会定时执行,或周期性执行
周期性任务定长线程池的使用:
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Test { public static void main(String[] args) { ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 定时执行一次 * 参数1.定时执行的任务 * 参数2.时长数字 * 参数3.时长数字的时间单位 */ service.schedule(new Runnable() { @Override public void run() { //你要创建的任务 } }, 1, TimeUnit.SECONDS); /** * 周期性执行任务 * 参数1.任务 * 参数2.延迟时长数字,即第一次执行时间 * 参数3.周期时长数字,即每隔多久执行 * 参数4.时长数字的单位 */ service.scheduleAtFixedRate(new Runnable() { @Override public void run() { //你要创建的任务 } }, 0, 5, TimeUnit.SECONDS); }
在这里要提醒下大家,在阿里代码规约中,手动创建线程池是强制的,因此推荐大家手动创建线程池。这里给大家推个手动创建线程池的文章
作者:爱_码_仕
来源:CSDN
原文:https://blog.csdn.net/sinat_36710456/article/details/107221342