多线程
1. 概念
1.1 并发与并行
并发:多个任务在同一个时刻点同时执行。效率高;CPU一核,模拟多条线程,快速交替。
并行:多个任务在同一段时间内分时执行。效率低,宏观上同时执行,微观上分时执行。CPU多核,多个线程同时执行,线程池。
注意:并发编程的本质是更加充分的运用CPU的资源。
1.2 进程与线程
- 进程:内存中正在运行的应用程序,是系统进行资源分配和调度的基本单位。每一个进程有自己独立的运行空间,相互之间不影响。进程就是程序的一次执行过程,即是一个进程从加载到内存到从内存中释放消亡的过程。
- 线程:进程内部的独立运行单元,是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
就是说:处于运行状态的程序可称为进程,每个进程运行在自己的空间中,空间相对独立,受操作系统保护,在每个进程空间中,一般都会有一个或者多个线程在运行。
- 多线程:在一个进程中,可以开启多个线程,多个线程同时去执行功能。
- 主线程:任何一个程序的运行,都有一个独立的运行入口。而负责这个入口的线程称为程序运行的主线程。Java程序的主线程即main线程。
1.3 线程的调度
- Java 程序的进程里面至少包含两个线程,一个是主进程即 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
- 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
- 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
线程的调度方式分两类: 分时调度 , 抢占式调度(时间片轮转)。
分时调度:多个任务平均分配执行时间。
抢占式调度:线程之间抢夺CUP的执行权,谁抢到谁执行(随机性)。
2. 创建线程
2.1 【继承Thread类,重写run方法】
好兄弟,你首先要创建一个类并继承【Thread】类
public class MyThread extends Thread{
for(int i = 0; i <10; i++) {
System.out.println(i + "-----------"+ Thread.currentThread().getName());
}
}
创建一个测试类,首先创建线程对象,再开启线程
public class Test {
public static void main(String[] args) throws InterruptedException {
//1.创建线程对象
MyThread myThread = new MyThread();
//为线程起名
myThread.setName("线程A");
//2.开启线程-----当获取cpu时间片,那么此线程就会执行run()方法的代码
myThread.start();
for (int j = 0; j < 10; j++) {
System.out.println(j + "=============线程名:" + Thread.currentThread().getName());
}
}
}
最后,运行能看到
示例:用线程Thread类实现4个窗口各卖票20张(模拟线程不安全)
public class ThreadTicket extends Thread{
//票数
private int ticket = 20;
//构造函数
public ThreadTicket (String name) {
super(name);
}
//重写run方法
@Override
public void run() {
while (ticket>0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了:" + ticket + "张");
}
}
}
class Test01{
public static void main(String[] args) {
ThreadTicket a = new ThreadTicket("线程A");
ThreadTicket b = new ThreadTicket("线程B");
ThreadTicket c = new ThreadTicket("线程C");
a.start();
b.start();
c.start();
}
}
2.2 【实现Runnable接口】
嘿嘿,这个你要先来一个类来实现【Runnable】接口,并重写run()方法
//它是一个线程任务类对象
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+">>>>>>"+i);
}
}
}
再整一个测试类,
public class Test {
public static void main(String[] args) {
//1.创建线程任务类对象
MyRunnable myRunnable = new MyRunnable();
//2.创建线程对象
Thread t1 = new Thread(myRunnable,"线程A");
//3.开启线程
t1.start();
//此处for循环代表主线程
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"<<<<<<<"+i);
}
}
}
最后,运行看效果
2.3 实现Callable接口
创建一个实现Callable的实现类,设置泛型,指定call方法返回的类型
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
sum += i;
}
}
return sum;
}
}
创建Callable接口实现类的对象
public class Test {
public static void main(String[] args) {
//创建Callable接口实现类的对象
MyCallable callable = new MyCallable();
//将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//获取Callable中call方法的返回值(因为会等待线程结束后再获取,所以可以当作闭关锁使用)
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
try {
Integer i = futureTask.get();
System.out.println("i = " + i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
3. 线程状态
- New(新创建): 线程被 New出来,但还没调用 start 方法时,就处于这种状态。一旦调用了 start 方法也就进入了 Runnable状态。
- Runnable(可运行): 处于 Runnable的线程,比较特殊。它还分两种状态:Running 和 Ready。也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
- 注意: 一个处于 Runnable 状态的线程,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,则该线程暂时不运行。但是,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
- Blocked(被阻塞): 从 Runnable 进入 Blocked 只有一种可能:就是进入 synchronized 关键字保护的代码,但是没有获取到 monitor 锁。
- Waiting(等待): 进入该状态表示当前线程需要等待其他线程做出一些的特定的动作(通知或中断)。Waiting 是在等待某个条件,比如 join 的线程执行完毕,或者是 notify ()/notifyAll ()。
- Timed Waiting(计时等待): 这种状态与 Waiting 状态的区别在于:有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。
- Terminated(被终止): 最后一种,想要进入终止状态就比较简单了,有三种情况:
– 任务执行完毕,线程正常退出。
–出现一个没有捕获的异常(比如直接调用 interrupt () 方法)。
总结:线程的生命周期
- 新建:创建线程对象; > .start() >进入就绪状态。
- 就绪:有执行资格(有资格去抢时间片),没有执行权(还没有抢到不能执行代码),这个阶段就是不停的抢CPU;> 抢到CPU的执行权 > 进入运行状态。
- 运行:有执行资格,有执行权,这个阶段就是运行代码;但是CPU的执行权有可能被其他线程抢走,抢走后就会回到就绪状态; > run()执行完毕后 > 进入死亡状态。
- 死亡:线程死亡,变成垃圾。
- 阻塞:线程运行时,除了进入死亡状态,还有可能遇到sleep()或者其他阻塞方式,这时就会进入阻塞状态,此时线程没有执行资格,也没有执行权,等sleep()时间或其他阻塞方式结束,就会回到就绪状态
常见方法
名称 | 方法 |
---|---|
休眠 | .sleep(long millis)当前线程主动休眠millis毫秒 |
加入 | .join()允许其他线程加入到当前线程中,知道其他线程执行完后,当前线程才会执行 |
放弃 | .yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片 |
优先级 | .setPriority()线程优先级1-10,默认为5,越高获取cpu概率越高 |
守护线程 | .setDaemon(true)设置为守护线程 |
补充
线程有两类:
- 用户线程(前台线程)
- 守护线程(后台线程)
如果程序中所有的用户线程都执行完毕,守护线程也会自动结束。垃圾回收线程属于守护线程。
4. 线程安全
案例1:买票引发的安全问题
public class MyThread extends Thread{
//static表示这个属性所有的对象共享
static int ticket = 0;
@Override
public void run() {
while (true) {
if(ticket < 100){
//休眠
try {
Thread.sleep(500);
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
MyThread my3 = new MyThread();
//设置线程名
my1.setName("窗口1");
my2.setName("窗口2");
my3.setName("窗口3");
//开启线程
my1.start();
my2.start();
my3.start();
}
}
运行后发现有的窗口卖的票都一样或是卖多了,这样是不行的,怎么解决呢?
4.1 synchronized 同步代码块
如上案例所说,三条线程,毫无规矩的去操作票数,产生误差,现在我们拥有了synchronized同步代码块,将操作票数的代码给放进去,三条线程谁先进去先买票,卖完后出去了下一条线程才能操作买票。
格式:
synchronized (锁) {
操作共享数据的代码
}
特点:
- 锁默认是打开的,有一个线程进去了,锁自动关闭。
- 里面的代码全部执行完毕后,线程出来,锁自动打开。
示例:
public class MyThread extends Thread{
//static表示这个属性所有的对象共享
static int ticket = 0;
//锁对象,一定要是唯一的
static final Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if(ticket < 100){
//休眠
try {
Thread.sleep(200);
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
break;
}
}
}
}
}
注意:
- synchronized同步代码块不能写在循环的外边,否则会出现一点小问题;如果写在循环外边,第一个抢到CPU执行权的线程,它要把代码走完,怎么走完,票卖完了就ok了!那么就是说,其他线程就算抢到了执行权也没用,一切都结束了。
- synchronized后面的锁对象一定要是唯一的;假设有俩条线程,现在锁对象不是唯一的,那么两条线程看到的锁就是不一样的,这样你的同步代码块还有什么意义呢?一般我们可以把当前类的字**节码文件(当前类名.class)**当作锁对象。
4.2 同步方法
所谓的同步方法就是把synchronized关键字添加到方法上。
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
特点:
- 同步方法是锁住方法里面的所有代码。
- 锁对象不能自己指定,是java已经指定好的;如果当前方法是非静态的,锁对象为this,如果当前方法是静态的,锁对象为当前类的字节码文件。
示例:
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
while (true) {
//2.同步代码块(最后提取为同步方法)
if (MyMethod()) break;
}
}
private synchronized boolean MyMethod() {
if (ticket == 100) {
//3.判断共享数据是否到了末尾,如果到了
return true;
} else {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//4.判断共享数据是否到了末尾,如果没到
ticket++;
System.out.println(Thread.currentThread().getName()+ "正在卖第" + ticket + "张票");
}
return false;
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable, "窗口1");
Thread t2 = new Thread(myRunnable, "窗口2");
Thread t3 = new Thread(myRunnable, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
上面的两种方式都可以理解为自动锁,例如同步代码块,当你执行玩之后,锁会自动打开。下面我们来了解一下手动的加锁与开锁。
4.3 lock锁
- JDK5以后提供了一个新的锁对象Lock,它实现提供比synchronized方法和语句跟广泛的锁定操作。
- Lock中提供了获得锁和释放锁的方法:void lock():获得锁 / void unlock():释放锁.
- Lock是接口不能直接实例化,采用它的实现类ReentrantLock来实例化 。
示例:
public class MyRunnable implements Runnable {
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
//循环
while (true) {
//手动加锁
lock.lock();
try {
if (ticket == 100) {
//3.判断共享数据是否到了末尾,如果到了
break;
} else {
Thread.sleep(30);
//4.判断共享数据是否到了末尾,如果没到
ticket++;
System.out.println(Thread.currentThread().getName()+ "正在卖第" + ticket + "张票");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//解锁
lock.unlock();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable, "窗口1");
Thread t2 = new Thread(myRunnable, "窗口2");
Thread t3 = new Thread(myRunnable, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
4.4 死锁
死锁?所谓死锁就是在我们程序中出现了锁的嵌套(一个锁套装一个锁)。这可不是一个知识点,这是一个错误。
示例:
一男一女,一双筷子,每人各一只筷子,当吃饭需要一双,男女双方都各自拿个一只筷子,等对方放下筷子,这就形成了僵局。
public class MyRunnable implements Runnable {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
//循环
while (true) {
if("线程A".equals(Thread.currentThread().getName())){
synchronized (objA) {
System.out.println("线程A拿到了A锁,准备拿B锁");
synchronized (objB) {
System.out.println("线程A拿到了B锁,ok!");
}
}
} else if ("线程B".equals(Thread.currentThread().getName())) {
synchronized (objB) {
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (objA) {
System.out.println("线程B拿到了A锁,ok!");
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable, "线程A");
Thread t2 = new Thread(myRunnable, "线程B");
t1.start();
t2.start();
}
}
运行结果:程序卡死。
4.5 等待唤醒机制(生产者消费者机制)
这是一个十分经典的多线程协作模式。
常用方法
方法名称 | 说明 |
---|---|
wait() | 当前线程等待,直到被其他线程唤醒 |
notify() | 随即唤醒单个线程 |
notifyAll() | 唤醒所有线程 |
示例:
/**
* {@code @ProjectPath} : xianCheng
* {@code @Author} : MrLiu
* {@code @Date} : 2023/6/6 17:08
* <
* 生产者:厨师
* >
*/
public class Cook extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
//判断平台是否有食物
if (Desk.foodFlag == 1) {
//如果有,就等待
try {
Desk.lock.wait();//让当前线程和锁绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//如果没有,就制作食物
System.out.println("厨师制作了一碗面条");
//修改桌子上食物的状态
Desk.foodFlag = 1;
//叫醒等待的消费者开始吃
Desk.lock.notifyAll();
}
}
}
}
}
}
/**
* {@code @ProjectPath} : xianCheng
* {@code @Author} : MrLiu
* {@code @Date} : 2023/6/6 17:09
* <
* 消费者:吃货
* >
*/
public class Foodie extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if(Desk.count == 0){
break;
} else {
//先判断平台是否有食物
if (Desk.foodFlag == 0) {
//如果没有,就等待
try {
Desk.lock.wait();//让当前线程和锁绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//把吃的总数-1
Desk.count--;
//如果有就,开吃
System.out.println("吃货正在吃面条,还能再吃"+ Desk.count + "碗");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子上食物的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
/**
* {@code @ProjectPath} : xianCheng
* {@code @Author} : MrLiu
* {@code @Date} : 2023/6/6 17:10
* <
* 平台:桌子
* 作用:控制生产者和消费者的执行
* >
*/
public class Desk {
//是否有食物--->0:没有 1:有
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
/**
* {@code @ProjectPath} : xianCheng
* {@code @Author} : MrLiu
* {@code @Date} : 2023/6/6 19:09
* <>
*/
public class Test {
public static void main(String[] args) {
//创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
//设置线程名
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
等待唤醒机制(阻塞队列实现方式)
**注意:**生产者和消费者必须使用同一个阻塞队列。
阻塞队列的继承结构
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
String take;
try {
//不断从阻塞队列中获取面条
take = queue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("吃了一碗"+take);
}
}
}
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
queue.put("面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("厨师做了一碗面条");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//1.创建阻塞队列对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2.创建线程对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//3.开启线程
c.start();
f.start();
}
}
5. 线程池
以前写多线程,用到的时候就创建线程,就完之后线程就消失,这样是不对的,它会浪费操作系统的资源。怎么办呢?我们准备一个容器,用来存放线程,就是线程池。
刚开始的时候,线程池里面是空的,当我们给线程池提交一个任务的时候,线程池本身就会自动的创建一个线程,我们拿个这个线程去执行任务,执行完后将线程还回线程池,下次再提交任务的时候,线程池会将之前还回去的线程给我们用。
特殊情况:如果第一个线程正在被使用,又来了第二任务,这时线程池会自动创建第二个线程,供我们使用。同时,线程池是有上限的,这个上限我们可以自己设定。
核心原理
- 创建一个空池子。
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程不死亡而是还给池子,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可。
- 但如果提交任务时,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。
线程池代码实现
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static ExcutorsService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExcutorsService newFixedThreadPool(int nThreads) | 创建一个有上限的线程池 |
示例:
public class MyThreadPoolDemo {
public static void main(String[] args) {
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
ExecutorService pool2 = Executors.newFixedThreadPool(3);
//2.提交任务
// pool1.submit(new MyRunnable());
// pool1.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
//3.销毁线程池(等待所有线程执行完毕后)
// pool1.shutdown();
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + ": >---->" + i );
}
}
}
自定义线程池
这里给大家说一下自定义线程池的七个元素:
- 核心线程数(不能小于0)
- 最大线程数(最大数量>=核心线程数量)
- 空闲时间(值)(不能小于0)
- 空闲时间(单位)(用TimeUnit指定)
- 任务队列(不能为null)
- 创建线程的方式(不能为null)
- 要执行的任务过多时的解决方案(不能为null)
任务拒绝策略
方法名 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
示例:
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数(不能小于0)
6,//最大线程数(最大数量>=核心线程数量)
60,//空闲时间(值)(不能小于0)
TimeUnit.SECONDS,//空闲时间(单位)(用TimeUnit指定)
new ArrayBlockingQueue<>(3),//任务队列(不能为null)
Executors.defaultThreadFactory(),//创建线程的方式(不能为null)
new ThreadPoolExecutor.AbortPolicy()//要执行的任务过多时的解决方案(不能为null)
);
}
}
总结· 如果不断的有任务提交,会有以下三个临界点:
- 当核心线程满时,再提交任务就要排队。
- 当核心线程满时,队伍满时,会创建临时线程。
- 当核心线程满时,队伍满时,临时线程满时,会触发任务拒绝策略。