3 多线程

教程 阿布都的都 ⋅ 于 2023-01-07 18:34:01 ⋅ 929 阅读

12 java多线程

11.1 进程和线程

进程:

​ 内存中运行的一个应用程序。

​ 是系统进行资源分配和调度的一个独立单位。

线程:

​ 进程中的执行流程。

​ 一个程序至少有一个进程,一个进程至少有一个线程。

多线程:

​ 在一个进程内,并发有多个线程在运行,那这个就是多线程。

​ 并发运行的本质是利用CPU时间片轮询执行。

​ 如果一个进程内运行一个线程,那这个程序就是单线程运行。

​ 如果一个进程内运行多个线程,那这个程序就是多线程运行。

file

并发和并行

并发:利用CPU时间片轮询执行多线程。(如果有1个CPU核,那只能靠CPU时间片轮询执行N个线程)

并行:同一时间,有多个线性并行执行。(如果有2个CPU核,那并行运行2个线程)

11.2 主线程和子线程

11.2.1 主线程和子线程

主线程:

​ 启动一个应用程序之后,最先启动的线程就是主线程。

​ 任务: main()
​ 默认主线程名:main

public class MainTest {
    public static void main(String[] args) {
        // main
        System.out.println(Thread.currentThread().getName());
    }
}

子线程:

​ 在主线程以外开启的线程就是子线程。

​ 子线程独立运行。

11.2.2 子线程三种方式

11.2.2.1 继承Thread类

​ 继承Thread类

​ 子线程的任务封装在run()方法中

​ 系统默认的子线程的名字: Thread-0

package com.hainiu.th1;

public class ThreadTest1 {
    public static void main(String[] args) {
        // 创建线程对象
        SubThread t1 = new SubThread();
        // 启动线程,此时就绪状态,等待CPU运行
        t1.start();
        // 当run()运行完成,线程死亡
        // 线程类对象只能启动一次,第二次就报错
        // t1.start()
    }
}
// 定义线程类
class SubThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

11.2.2.2 实现Runnable接口

定义类实现Runnable接口里的run(),run()就是线程体。

线程执行完没有返回值

file

package com.hainiu.th1;

public class ThreadTest2 {
    public static void main(String[] args) {
        // 创建Runnable实现类对象
        SubRunnable runnable = new SubRunnable();
        // 用Thread线程运行,此时进入就绪状态
        new Thread(runnable).start();
    }
}
// 定义类实现接口
class SubRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

11.2.2.3 实现Callable接口

如果想执行完线程,并获得线程的返回值,用此种方式。

file

package com.hainiu.th1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Callable实现类对象
        SubCallable callable = new SubCallable();
        // 用FutureTask包装,目的是能获取线程体返回值
        FutureTask<String> futureTask = new FutureTask<>(callable);
        // 用Thread启动线程,进入就绪状态
        new Thread(futureTask).start();
        // get()是个阻塞的方法,直到线程体执行完返回结果后,才会继续执行主线程
        String threadName = futureTask.get();
        System.out.println("main() -->" + threadName);
    }
}
// 定义Callable实现类
// Callable<String> 泛型String,代表线程体执行完会要返回String对象
class SubCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        String name = Thread.currentThread().getName();
        System.out.println(name);
        return name;
    }
}

file

11.2.2.4 三种方式对比

继承Thread类:

​ 不能共享数据(同一个对象的数据 )

​ 线程体方法不抛异常

实现Runnable接口:

​ 可以共享公共资源。

​ 没有返回值

​ 线程体方法不抛异常

实现Callable接口:

​ 可以共享公共资源。

​ 有返回值。

​ 线程体方法抛异常

不能共享数据的例子:

package com.hainiu.th1;

public class ThreadTest4 {
    public static void main(String[] args) {
        Thread2 t1= new Thread2();
        t1.start();
        Thread2 t2 = new Thread2();
        t2.start();
    }
}

class Thread2 extends Thread {
    // 算是公共资源,多个线程不能共享,只能自己用自己的
    private int count;

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + ++count);
        }

    }
}

file

能共享数据的例子:

package com.hainiu.th1;

public class ThreadTest5 {
    public static void main(String[] args) {
        Thread2Runnable runnable = new Thread2Runnable();
        new Thread(runnable).start();
        new Thread(runnable).start();

    }
}

class Thread2Runnable implements Runnable{
    // 公共资源,多个线程可以共享
    private int count;

    @Override
    public void run() {
        for(int i = 1; i <= 3; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + ++count);
        }
    }
}

file

11.3 线程状态

线程分成5种状态:

1)new(新建)

2)Runnable(就绪)

3)Running(运行)

4)Blacked(阻塞)

5)Dead(死亡)

file

新建:

​ 当程序使用new关键字创建了一个线程后,该线程就处于新建状态。

就绪:

​ 启动线程(调用start()) ,处于了 就绪状态,等待cpu的调用执行;

​ 失去CPU执行权。

运行:

​ 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

阻塞:

​ 暂停一个线程的执行以等待某个条件发生(如某资源就绪)。

​ 当前正在执行的线程阻塞后,其他线程就可以获得执行机会。被阻塞的线程会合适的时候重新进入就绪状态,而不是运行状态。

何时进入阻塞状态:

​ 1)线程执行了Thread.sleep(n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行;(不会释放锁)

​ 2)线程执行了一个对象的wait()方法,进入阻塞状态;(会释放锁)

​ 3)线程执行一个线程的join()方法,进入阻塞状态,当前线程等待这个线程执行完成后,才能执行;

​ 4)线程要执行一段同步代码,没有获取到同步锁时,进入阻塞状态;

何时解除阻塞状态:

​ 1)Thread.sleep(n),休眠N毫秒后进入就绪状态;

​ 2)wait() 需要其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒;

​ 3)join() 需要等到那个线程执行完后,当前线程进入就绪状态

​ 4)获取到同步锁后,进入就绪状态;

死亡:

​ 线程体执行完成。

​ 线程体执行过程中抛异常。

11.4 线程同步

11.4.1 什么是线程同步?

同步:就是“排队”,排队的意思就是不让一起同时进行。
线程同步:几个线程之间进行排队,一个一个对共享资源进行操作。线程同步的目的就是避免线程同时执行。

11.4.2 线程安全问题

由于多个线程执行的不确定性引起执行结果的不稳定

package com.hainiu.th1;

public class BankTest1 {
    public static void main(String[] args) {
        Bank1Runnable runnable = new Bank1Runnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank1Runnable implements Runnable{
    private int money;

    @Override
    public void run() {
        // 当两个线程启动后,可能会存在线程安全问题
        // 有可能两个线程同一时间同时存入100
        for(int i = 1; i <= 3; i++){
            this.money += 100;
            System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
        }
    }
}

file

11.4.3 线程锁

为了解决可能存在的线程安全问题,可通过线程锁实现线程排队(一个一个的访问共享资源)

争抢使用公共资源权限 → 上锁 → 访问公共资源 → 释放锁

file

​ 就好比,有一个房间,房间与外面连接的是个门,门里面有个锁,门外面有5个猴子排队进入吃香蕉。这个规则是一只猴子吃一根香蕉,无论多少只猴子同时进入,房间只提供一根香蕉。每次只有一只猴子进来,进来后从里面把门锁上(加锁),吃一根香蕉(抢的这个公共资源),吃完后把里面的锁打开(释放锁)出去,房间再提供一只香蕉,然后再放一只猴子进来吃香蕉……

​ 上面的例子中,里面的猴子把门锁上后,外面的猴子打不开,只能等里面的猴子把锁打开(锁等待)。

​ 如果猴子进房间后不锁门,后果可能就是剩下的4个猴子都进来抢同一根香蕉。这就好多个线程同时抢这个公共资源,可能出现安全问题。

11.4.3.1 synchronized

1)synchronized代码块

语法:

// 同步代码块锁谁取决于代码块里的逻辑
synchronized(obj){
    ...
    //此处的代码就是同步代码块
}

代码示例:

package com.hainiu.th1;

public class BankTest2 {
    public static void main(String[] args) {
        Bank2Runnable runnable = new Bank2Runnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank2Runnable implements Runnable{
    private int money;

    @Override
    public void run() {

        for(int i = 1; i <= 3; i++){
            // 通过同步代码块解决了线程安全问题
            // 同步代码块锁谁取决于代码块里的逻辑
            // 本例是对当前对象的属性进行操作,所以锁的是当前对象
            synchronized (this){
                this.money += 100;
                System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);

            }
        }
    }
}

执行多次没有发现错误,都是排队往里存100

file

2)synchronied方法

语法:

// 谁调用了该同步方法,就锁谁
public synchronized 修饰符 方法名([方法参数]){
    //方法体...
}

代码示例:

package com.hainiu.th1;

public class BankTest3 {
    public static void main(String[] args) {
        Bank3Runnable runnable = new Bank3Runnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}
// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank3Runnable implements Runnable{
    private int money;

    // 通过同步方法解决了线程安全问题
    // 谁调用了该同步方法,就锁谁
    public synchronized void setMoneyAndPrint(){
        this.money += 100;
        System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
    }

    @Override
    public void run() {

        for(int i = 1; i <= 3; i++){
            setMoneyAndPrint();
        }
    }
}

file

3)两者区别

​ 同步代码块能设置锁定的对象,而同步方法不能(谁调用同步方法锁谁)。

4)释放锁

​ 同步代码块或同步方法执行完成。

​ 报错抛异常

11.4.3.2 lock

​ 通过显示定义同步锁对象来同步,在这种机制下,同步锁使用Lock 对象充当。

​ Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作。Lock 是控制多个线程对共享资源进行访问的工具。每次只能有一个线程对 Lock 对象进行加锁,线程开始访问共享资源之前应先获得 Lock 对象。

​ 采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。

​ 比较常用的是ReentrantLock(可重入锁)

可重入锁:

​ 一个线程获取锁后,在没释放这个锁之前,还可以再次获取锁,这样防止死锁。

不可重入锁:

​ 一个线程获取锁后,在没释放这个锁之前,不能再次获取锁。

​ 比如:

// 一个普通锁
lock.lock();  // 第一次获取锁
lock.lock();  // 当第二次获取锁时,由于和上一个lock()操作同一个锁对象,那么这里就永远等待了
...
lock.unlock();
lock.unlock();

死锁:

​ 两个线程,都等待对方释放锁,但都释放不了。

​ 你给我offer我就告诉你。

代码格式:

public class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //...
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //需要保证相线程安全的代码
        }
        //使用finally块来保证释放锁
        finally{
            lock.unlock();
        }
    }

}

代码示例:

package com.hainiu.th1;

import java.util.concurrent.locks.ReentrantLock;

public class BankTest4 {
    public static void main(String[] args) {
        Bank4Runnable runnable = new Bank4Runnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

// 定义线程类实现每个线程连续存3次100,看累计情况
class Bank4Runnable implements Runnable{
    private int money;

    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        for(int i = 1; i <= 3; i++){
            // 加锁
            lock.lock();
            try{
                this.money += 100;
                System.out.println(Thread.currentThread().getName() + "存100,累计:" + this.money);
            }finally {
                // 释放锁
                lock.unlock();
            }
        }
    }
}

11.5 线程池

11.5.1 概念

线程池:

​ 装线程的池子。

线程池技术:

​ 1)创建线程池时,会初始化指定个数的线程。

​ 2)当要使用线程执行任务时,从线程池里获取线程,执行任务。

​ 3)执行完任务后,还给线程池。

​ 这样做到线程池里线程的复用。

线程池好处:

​ 线程和任务分离,不需要使用线程时自己new线程对象,提升线程重用性;

​ 提升系统响应速度。

​ 创建线程用的时间为T1;

​ 执行任务用的时间为T2;

​ 销毁线程用的时间为T3;

​ 那么使用线程池就免去了T1和T3的时间;

11.5.2 技术说明

线程池类图:

file

创建线程池的参数说明:

// new线程池的构造器
public ThreadPoolExecutor(
    int corePoolSize,     // 核心线程数
    int maximumPoolSize,  // 最大线程数
    long keepAliveTime,   // 销毁线程的时间, 如果等于0不销毁
    TimeUnit unit,        // 时间单位
    BlockingQueue<Runnable> workQueue) // 线程安全的队列,可以保证每个线程获取一个任务,没有多个线程争抢一个任务的问题

Executors 类的静态方法 :

file

11.5.3 执行不带返回值的线程

执行不带有返回值的代码示例:

package com.hainiu.th1;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest1 {
    public static void main(String[] args) {
        // 创建只有1个线程的线程池
        ExecutorService pool = Executors.newSingleThreadExecutor();
        // 用1个线程串行运行10个任务
        for(int i = 1; i <= 10; i++){
            pool.submit(new TaskRunnable("任务" + i));
        }
        // 关闭线程池
        pool.shutdown();

//        // 创建有3个线程的线程池
//        ExecutorService pool = Executors.newFixedThreadPool(3);
//        for(int i = 1; i <= 10; i++){
//            // 用3个线程运行10个任务
//            pool.submit(new TaskRunnable("任务" + i));
//        }
//        // 关闭线程池
//        pool.shutdown();

    }
}

class TaskRunnable implements Runnable{
    private String taskName;
    public TaskRunnable(String taskName){
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 执行 " + this.taskName);
    }
}

用1个线程串行运行10个任务:

file

用3个线程运行10个任务:

file

11.5.4 执行带返回值的线程

执行带有返回值的代码示例:

package com.hainiu.th1;

import java.util.concurrent.*;

public class ThreadPoolTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建有3个线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        for(int i = 1; i <= 10; i++){
            // 用这3个线程运行10个任务
            Future<String> future = pool.submit(new TaskCallable("任务" + i));
            String res = future.get();
            System.out.println(res);
        }
        // 关闭线程池
        pool.shutdown();

    }
}

class TaskCallable implements Callable<String> {
    private String taskName;
    public TaskCallable(String taskName){
        this.taskName = taskName;
    }

    @Override
    public String call() {
        // 将哪个线程执行哪个任务的信息返回
        String result = Thread.currentThread().getName() + " 执行 " + this.taskName;
        return result;
    }
}

file

12 python多线程

12.1 概念

​ Python 代码的执行有Python 虚拟机来控制。对Python 虚拟机的访问由全局解释器锁(global interpreter lock,GIL)来控制

GIL规定:在同一时刻,只有一个线程在运行。

Python虚拟机按照以下方式执行:

​ 1)设置GIL;

​ 2)切换到一个线程去执行;

​ 3)运行:

​ a. 运行指定数量的字节码指令;

​ b. 或者线程主动让出控制;

​ 4)把线程设置为睡眠状态;

​ 5)解锁GIL;

​ 6)再次重复以上所有步骤;

java:在同一时刻,可以有多个线程并行运行,取决于CPU核数,比如:CPU核是2, 那就能并行运行两个线程

python: 在同一时刻,只有一个线程在运行, 跟CPU核数没关系。

12.2 使用threading模块实现多线程

说明:

1)创建线程
        导入模块threading,通过threading.Thread创建线程。
2)启动线程
        通过线程对象调用start() 启动线程。
3)执行线程
        通过run(),线程逻辑在run() 里实现。

代码示例:

#-*- encoding: utf-8 -*-
"""
thread_demo.py
Created on 21-9-11 下午2:46
Copyright (c) 21-9-11, 海牛学院版权所有.
@author: 野牛
"""
import threading

class ThreadDemo(threading.Thread):

    def __init__(self, name):
        # 因为父类有,所有需要主动调用
        super(self.__class__, self).__init__()
        self.name = name

    # 线程体方法
    def run(self):
        print('ThreadDemo # run() ...')

thread_demo = ThreadDemo("hehe")
# 开启线程运行
thread_demo.start()

线程锁

import threading
import time

def task(n):
    # 休眠1秒
    time.sleep(1)
    # 加锁
    r_lock.acquire()
    try:
        for i in range(1, n + 1):
            print(i)
    finally:
        # 释放锁
        r_lock.release()

# 创建线程锁对象
r_lock = threading.RLock()
for i in range(1, 6):
    t = threading.Thread(target=task,args=(3,))
    t.start()
# -*- encoding: utf-8 -*-
"""
thread_demo.py
Created on 2022/7/21 15:47
Copyright (c) 2022/7/21, 海牛学院版权所有.
@author: 野牛
"""

import threading
import time
class Thread1(threading.Thread):
    def __init__(self, name, n):
        # 主动调用父类初始化
        super(self.__class__, self).__init__()
        self.name = name
        self.n = n

    # run()是线程体
    def run(self):
        # 休眠1秒
        time.sleep(1)
        # print(f'线程名:{threading.currentThread().getName()}')
        r_lock.acquire()
        try:
            for i in range(1, self.n + 1):
                print(i)
        finally:
            r_lock.release()

r_lock = threading.RLock()
for i in range(1, 6):
    # 创建对象
    th1 = Thread1(f'thread{i}', 3)

    # 调用start()
    th1.start()
版权声明:原创作品,允许转载,转载时务必以超链接的形式表明出处和作者信息。否则将追究法律责任。来自海汼部落-阿布都的都,http://hainiubl.com/topics/76121
成为第一个点赞的人吧 :bowtie:
回复数量: 0
    暂无评论~~
    • 请注意单词拼写,以及中英文排版,参考此页
    • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
    • 支持表情,可用Emoji的自动补全, 在输入的时候只需要 ":" 就可以自动提示了 :metal: :point_right: 表情列表 :star: :sparkles:
    • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif,教程
    • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
    Ctrl+Enter