欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java基础-多线程基础概念 (多线程的好处;创建线程的三种方式;多线程常用方法:sleep(long),wait(),notifyAll())

程序员文章站 2022-05-05 09:36:14
...

Java基础-多线程基础概念

前言:多线程系列拖到现在,也该复习下
这个东西怎么说呢,也是非常常见。而且面试基本上是必问的(好吧,这是我复习的主要目的)
一共预计会有6篇,这是第一篇

推荐书籍:通过博客来理解多线程总会缺失点什么,毕竟每个人写的博客水平有不同。而且有些信息被加工后可能并不适合你
《Java核心技术 卷Ⅰ》 这本书不止是线程,其实每章内容都不错。而且对Java水平有比较大的提升
《Java编程思想》 这本书对Java水平提升帮助很大,但是老实说,感觉多线程那一篇内容很全,但是文章很乱
《Java并发编程的艺术》 多线程相关鼎力推荐

还有种好的方式就是用IDEA,通过ctrl+B快捷键直接追溯源码。不需要一字一句的去理解源码(太费时间)。看输出删除,返回值。或者其他想知道的东西。而且能看到继承,实现关系。这种方式收益也挺大
多线程相关类或接口 :Thread Runnable Callable FutureTask

大纲脑图
Java基础-多线程基础概念 (多线程的好处;创建线程的三种方式;多线程常用方法:sleep(long),wait(),notifyAll())

1. 线程简介

本段简单介绍什么是线程,进程。线程的状态,状态转换,以及线程优先级

1.1 线程与进程

进程 是资源分配的最小单元,一个进程可以有多个线程
线程 是资源调度的最小单元,同一个进程下,各个线程的内存质押共享,所以线程间的通信代价小。线程也称为轻量进程

1.2 为什么使用多线程

  • 对于多CPU,多核的来说,多线程能提高程序响应速度。相当几个人做同时开工,肯定比一个人快。(但是这个也不一定,因为线程调度,分配资源都需要开销。就像一个项目组5-10个人效率,可能比20-100个人效率更高,因为开会,信息共享都需要大量时间)
  • 对于单CPU单核机器(这种情况可能更常见)来说,因为有阻塞的存在,多线程效率能够更好。主要是磁盘太慢,如果是单线程,可能磁盘在007,CPU却大多数时间在喝茶。如果运行在内存中,单线程可能会更好,没有上下文切换的额外开销(例如 Redis)
  • 有些场景,多线程实现起来简单。例如游戏,不同的角色同时在做不同的事情,用单线程就很难处理这种情况

单线程相当于顺序结构,一次做一件事,这件事情做完了(即使这件事中途可能要等待好久)才能做另外一件事情。
多线程相当于在一个时间段内,做几件事情

有些时候多线程好:一般是有些事情等待时间长,可以中途做其他事情
有些时候单线程好:例如我想8个小时写3篇博客,肯定是单线程好,如果中途切换着写,可能思路就没了

1.3 线程状态

Java的线程状态和操作系统的不一样
Java基础-多线程基础概念 (多线程的好处;创建线程的三种方式;多线程常用方法:sleep(long),wait(),notifyAll())
Java线程转换,其实我觉得理解其每个状态的意思就自然知道装换规则了。不用可以去记忆,没必要

  • New 新建状态
  • Runnable 可运行,代表线程可以被运行,可能正在运行,也可能等待时间片的过程中
  • Waiting 等待 因为调用了wait而暂停。注意与Blocked区分
  • Time_Waiting 计时等待,调用了sleep和wait后暂停,等待固定的时间后,进入可运行状态
  • Blocked 阻塞 因为缺少某些资源*不能运行
  • Terminated 终止状态 线程完成,或者说某些原因线程挂了(功成身退与中途陨落)

1.4 线程优先级

Java线程可以通过 getPriority()与setPriority() 获取和设置线程优先级
Java线程有10个优先级。默认为5。优先级是有操作系统觉得的,其实在Java程序设置了优先级,也没什么卵用,因为操作系统不认(3月份HB的绿码,BJ也不认啊)
windows有7个优先级

1.5 守护线程

守护线程,在《Java编程思想》里叫后台线程,为其他线程服务的线程,例如垃圾回收线程
可以通过 setDaemon()将线程设置为守护线程

2. Java线程相关操作

2.1 Java创建线程的三种方式

方式1: 继承Thread类,重写run()方法来创建线程

   // 创建线程方式一:继承Thread类,重写Run方法
    class ThreadChild extends Thread{
        @Override
        public void run() {
            super.run();
            int length = 100;
            Thread.currentThread().setName("方式一:继承Thread方式实现线程");
            for (int i = 0; i < length; i++){
                System.out.println("这是以方式壹实现的线程,循环体i的值为" + i);
            }
        }
    }

方式2: 实现Runnable接口,重写run()方法

// 创建线程方式二:实现Runnable接口,重写Run方法
    class ThreadRunnable implements Runnable{
        @Override
        public void run() {
            int length = 300;
            Thread.currentThread().setName("方式二:实现Runnable接口方式实现线程");
            for (int i = 200; i < length; i++){
                System.out.println("这是以方式二实现的线程,循环体i的值为" + i);
            }
        }
    }

方式3: 实现Callable接口,重写call()方法

// 创建线程方式三:实现Callable接口,重写Call方法
    class ThreadCallable implements Callable{

        @Override
        public Object call() throws Exception {
            int length = 900;
            Thread.currentThread().setName("方式3:实现Callable接口方式实现线程");
            for (int i = 800; i < length; i++){
                System.out.println("这是以方式三实现的线程,循环体i的值为" + i);
            }
            // 我没有什么需要返回的,有点尴尬
            return null;
        }
    }

三种方式对比

方式一-继承Thread类的方式尽量少用,一般来说继承太过于重量级(继承子类用于所有父类公有的域,方法),实现接口的方式比继承好。
一般用方式二-实现Runnable接口的方式。如果有返回值就用方式三实现Callable接口

Runable和Callable接口都只有一个方法run()。实现Callable接口的方式有返回值。而且需要和FutureTask 配合使用

需要注意的是,启动线程用的start(),不是调用run方法 如果调用run就是调用普通方法

全部类代码

package com.java.basic.multithreading;

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

/**
 * @author dengtiantian
 */
public class ThreadTest {

    // 创建线程方式一:继承Thread类,重写Run方法
    class ThreadChild extends Thread{
        @Override
        public void run() {
            super.run();
            int length = 100;
            Thread.currentThread().setName("方式一:继承Thread方式实现线程");
            for (int i = 0; i < length; i++){
                System.out.println("这是以方式壹实现的线程,循环体i的值为" + i);
            }
        }
    }

    // 创建线程方式二:实现Runnable接口,重写Run方法
    class ThreadRunnable implements Runnable{
        @Override
        public void run() {
            int length = 300;
            Thread.currentThread().setName("方式二:实现Runnable接口方式实现线程");
            for (int i = 200; i < length; i++){
                System.out.println("这是以方式二实现的线程,循环体i的值为" + i);
            }
        }
    }

    // 创建线程方式三:实现Callable接口,重写Call方法
    class ThreadCallable implements Callable{

        @Override
        public Object call() throws Exception {
            int length = 900;
            Thread.currentThread().setName("方式3:实现Callable接口方式实现线程");
            for (int i = 800; i < length; i++){
                System.out.println("这是以方式三实现的线程,循环体i的值为" + i);
            }
            // 我没有什么需要返回的,有点尴尬
            return null;
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.testThread();
    }

    // 测试
    public void testThread(){
        ThreadChild threadChild = new ThreadChild();
        ThreadRunnable threadRunnable = new ThreadRunnable();
        ThreadCallable threadCallable = new ThreadCallable();
        //方式一开始运行
        threadChild.start();
        //方式二开始运行
        (new Thread(threadRunnable)).start();
        //方式三运行
        FutureTask<Object> task = new FutureTask<>(threadCallable);
        (new Thread(task)).start();
    }

}

2.2 线程相关常用方法

去看源码或者API最自在,我这个也是从里面复制的

2.2.1 Object线程相关方法

  • public final void wait() throws InterruptedException 暂停线程 会释放锁
  • public final native void notify();随机唤醒一个线程
  • public final native void notifyAll() 唤醒所有线程

这三个方法都是Object的,我记得有个很常见的问题是:wait和sleep的区别
这个是利用每个对象本身自带的锁实现的。都是本地方法
notify()这个方法其实用的少,因为是随机唤醒一个线程,他自己也不知道唤醒的谁

2.2.2Thread相关常用方法

  • getXXX系列方法
  • setXXX系列方法
  • public void interrupt() 中断 试图停止线程
  • public static boolean interrupted()测试线程是否被中断
  • public synchronized void start() 启动线程
  • public static native void sleep(long millis) throws InterruptedException 休眠,不会释放锁
  • public final void stop() 停止线程,已被放弃
  • public final void suspend()暂停线程,已被放弃
  • public final void resume() 唤醒线程,已被放弃
  • public static native void yield() 让出锁和时间片,如果他的优先级还是最高,那么他还会抢到锁

3. 线程间协作

其实这个标题当时其实是线程间的通信,后面改成同步。最后是这个
反正这节 目的是为了表达线程之间工作关系
重复:线程是共享内存,所以通信代价很低,但是这也有问题。
举个例子:父亲剥柚子,儿子吃柚子。这两个动作是可以同时进行的(在已有剥好的前提下。) 儿子吃柚子得在有柚子可以吃的前提下,就是父亲有剥好的柚子。就是得剥好才能吃(你可不要说不剥儿子就吃,我敬你是条汉子)。本节就是说明线程的协作关系。
以及还有父亲,儿子都吃瓜子。两种是竞争关系,得有瓜子才行

3.1 Synchronized和Volatile机制

Synchronized 是通过对某个共享的资源加锁,是这个资源一次只能被一个资源消耗者使用(利用对象锁,下章节详细写)
Volatile相当于,对某个资源的操作一旦发生,马上所有关注这个资源的用户都知晓(内存模型里讲最好)

3.2 等待通知机制

利用wait和notifyAll方法,对于有一定先后关系的操作,一旦 后者先执行了,马上wait,然后先者,执行后notifyAll唤醒后者消费

3.3 使用管道通信

我不记得Java管道知识点了,有点尴尬。后续补上

聊下Redis的管道和操作系统的管道缓解下尴尬
Redis的管道有点类似于JDBC里面的addBatch()和累积发送(数据攒到一定量在传输)。就是说Redis的管道为了提高效率,将多条命令攒到一起提交给服务器执行
Linux的管道相当于提供一个中间文件,线程或进程将数据放到管道,然后另外的线程或进程从管道获取数据。其实也是表明了一种先后的顺序,虚拟机里面happen-before那味儿。

3.4 ThreadLocal的使用

这个其实线程有些数据不想共享给别的线程,就相当于局部变量。知道这个我觉得就好了,其实没什么特殊的,具体用法用的时候查看具体的API或者看源码(ctrl+b)就好了。我不明白为什么很多有很多文章特意提这个

4. 小结

  1. 什么是线程,线程与进程的区别
  2. 多线程的好处。多线程一定快吗?(需要知道上下文切换是有消耗的)
  3. 创建线程的三种方式
  4. 线程相关的方法

总目录:Java进阶之路-目录

                天高地迥,觉宇宙之无穷。兴尽悲来,识盈虚之有数
                《滕王阁序》 王勃
                博主:五更依旧朝花落
                首次发布时间:2020年4月18日17:24:53
                末次更新时间: