时隔这么久,回顾 JAVA 的多线程

纯个人笔记,一个字一个字码的,如有错误,欢迎指正。

基本概念:程序、进程、线程

程序

程序(program) 是为完成特定任务、用某种语言编写的一组指令的集合。

进程

进程(process) 是程序的一次执行过程,或是正在运行的一个程序。如任务管理器中的 PotPlayer、Typora。

线程

线程(Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。如火绒安全中的病毒查杀、防护中心。

一个进程中的多个线程共享相同的内存单元/内存地址空间。他们从同一堆中分配对象,可以访问相同的变量和对象。通俗的讲,就是一个进程中的多个线程拥有各自的虚拟机栈和程序计数器,共享方法区和堆。

并行与并发

并行

多个 CPU 同时执行多个任务。

并发

一个 CPU (采用时间片) 同时执行多个任务。

多线程的优点

  • 提高应用程序的响应。
  • 提高计算机 CPU 的利用率。
  • 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

线程的创建和使用

方式一:继承 Thread 类

1)定义子类继承 Thread 类。

2)子类中重写 Thread 类中的 run 方法。

3)创建 Thread 子类对象,即创建了线程对象。

4)调用线程对象 start 方法:启动线程,调用 run 方法。

注意:

1、如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。

2、run() 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU

调度决定。

3、想要启动多线程,必须调用 start 方法。

4、一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上

的异常 llegalThreadStateException 。要想创建多个线程就需要用 new 创建多个线程对象。

方式二:实现 Runnable 接口

1) 定义子类,实现 Runnable 接口。

2)子类中重写 Runnable 接口中的 run 方法。

3)通过 Thread 类含参构造器创建线程对象。

4)将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造器中。

5)调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法。

class MThread implements Runnable{
    @override
    public void run(){
        // do something...
    }
}

// 使用
MThread mThread = new MThread();
Thread t1 = new Thread(mThread);
t1.start();

开发中,优先选择 Runnable ,优势为:

1)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

2)避免了单继承的局限性

方式三:实现 Callable 接口

与使用 Runnable 相,Callable 功能更强大些:

  • 相比 run() 方法,可以有返回值

  • 方法可以抛出异常

  • 支持泛型的返回值

  • 需要借助 FutureTask 类,比如获取返回结果

    Future接口:

    • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
    • FutrueTask 是 Futrue 接口的唯一的实现类
    • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
class MThread implements Callable {
    @override
    public void call(){
        int sum = 0;
        // do something...
        return sum;
    }
}

// 调用
MThread mThread = new MThread();
FutrueTask futrueTask = new FutrueTask(mThread);
new Thread(futrueTask).start();

// 获取返回值
Object sum = futrueTask.get();

方式四:线程池创建(ExecutorService 和 Executors)

class MThread implements Runnable{
    @override
    public void run(){
        // do something...
    }
}

ExecutorService service = Executors.newCachedThreadPool(10);
service.execute(new MThread());
service.shutdown();
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
    • void shutdown() :关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

Thread 类的有关方法

  • void start(): 启动线程,并执行对象的 run() 方法

  • run(): 线程在被调度时执行的操作

  • String getName(): 返回线程的名称

  • void setName(String name):设置该线程名称

  • static Thread currentThread(): 返回当前线程。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类

  • static void yield():线程让步

    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程

    • 若队列中没有同优先级的线程,忽略此方法

  • join():当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止

  • 低优先级的线程也可以获得执行

  • static void sleep(long millis):(指定时间:毫秒)

    • 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后

    重排队

    • 抛出 InterruptedException 异常
  • stop(): 强制结束线程生命期,不推荐使用,已过时

  • boolean isAlive():返回boolean,判断线程是否还活着

线程的调度

调度策略

1、时间片

2、抢占式

高优先级的线程抢占 CPU。

Java 的调度方法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级

线程的优先等级

  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5

涉及的方法

  • getPriority():返回线程优先值
  • setPriority(int newPriority): 改变线程的优先级

说明:低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。

线程的生命周期

JDK 中用 Thread.State 类定义了线程的几种状态。线程的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建: 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建

状态

  • 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已

具备了运行的条件,只是没分配到 CPU 资源

  • 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线

程的操作和功能

  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中

止自己的执行,进入阻塞状态

  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程的安全问题

当多条语句在操作同一个线程共享数据(如抢票时的票数)时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。

同步代码块(Synchronized)

synchronized (同步监视器){
    // 需要被同步的代码(操作共享数据的代码)
}

同步监视器:俗称,锁。任意对象都可以作为同步锁。必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全。

继承 Thread 类的线程安全问题

class MThread extends Thread{
    // 新建对象
    private static Object obj = new Object();
    // 或 Dog dog = new Dog(); 都行,只要是一个对象
    @override
    public void run(){
        synchronized (obj){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
}

使用当前对象:

class MThread extends Thread{
    @override
    public void run(){
        synchronized (MThread.class){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
}

实现 Runnable 类的线程安全问题

class MThread implements Runnable{
    Object obj = new Object();
    // 或 Dog dog = new Dog(); 都行,只要是一个对象
    @override
    public void run(){
        synchronized (obj){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
}

下面就没有共用一把锁:

class MThread implements Runnable{
    // 或 Dog dog = new Dog(); 都行,只要是一个对象
    @override
    public void run(){
        Object obj = new Object();
        synchronized (obj){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
}

使用当前对象:

class MThread extends Thread{
    @override
    public void run(){
        synchronized (this){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
}

同步方法

如果操作共享数据的代码完成地声明在一个方法中,我们不妨将此代码声明为同步的。

继承 Thread 类的线程安全问题

class MThread implements Runnable{
    @override
    public void run(){
        synchronized (obj){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
    
    // 锁为当前类 MThread.class
    private static synchronized void show(){
        // do something...
    }
}

实现 Runnable 类的线程安全问题

class MThread implements Runnable{
    @override
    public void run(){
        synchronized (obj){
            // 需要被同步的代码(操作共享数据的代码)
        }
    }
    
    // 锁为 this
    private synchronized void show(){
        // do something...
    }
}

关于同步方法的总结:

1、同步方法仍然涉及到同步监视器,只是不需要我们显示声明。

2、非静态方法,同步监视器是 this;静态的方法,同步监视器是当前类本身。

Lock 锁方式解决线程安全问题

class MThread implements Runnable{
    private ReentrantLock lock = new ReentrantLock();
    
    @override
    public void run(){
        try{
            lock.lock();
            // do something...
        }finally{
            lock.unlock();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容