聊聊Java线程是个啥东西-Java多线程(1)

简介: 聊聊Java线程是个啥东西-Java多线程(1)


为什么要有线程

在这个效率和质量并存的时代,首先, "并发编程" 成为 "刚需".

单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU

资源. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编

程.

就比如一个人为了节约出时间来看手机, 那么他肯定会在吃饭的时候去看或者是休息的时候去看会手机等等.

初识线程

上次我们所讲的进程, 是一个比较重量级别的的概念, 因为进程消耗的资源多, 执行的速度慢, 无论是创建一个进程还是销毁, 调度一个进程, 其成本都比较高. 多进程编程可以解决高并发的问题, 但不是一个高效的选择.

为什么进程这么"重量"? 主要是体现在 资源的分配上, 资源分配是需要花时间的, 它是一个很耗时的操作, 比如系统要给一个进程分配内存,系统就需要遍历自己的空闲内存的表(一种特定的数据结构),找到一个空间大小合适的内存来进行分配. 其二, 有很多个进程在向系统申请内存. 多进程编程的成本高就是体现在资源这块上.

相比于进程, 线程是一个轻量级别的概念, 他是如何解决这种资源分配的问题呢? 其实就是把申请资源的过程简化. 一个进程中可以包含多个线程, 多个线程每个线程都是独立可以调度的"执行流", 这些执行流之间本身就是并发的, 这些线程共用同一份进程的内存资源, 这就意味着对于线程而言, 内存资源是分配好的, 创建线程就省下了分配资源的开销.

多线程之间, 可能是在多个CPU上执行, 也可能是一个CPU核心上, 通过调度来进行运行. 操作系统在调度的时候, 其实在调度的线程, 而不是进程. 线程是操作系统 调度运行的基本单位.

一个进程中的多个线程之间, 共用同一份系统资源, 也就是 :

  1. 内存空间
  2. 文件描述符表

只有在进程启动,创建第一个线程的时候, 需要花成本去申请系统资源, 一旦第一个线程创建完毕, 后续在创建的线程将会申请更少的空间, 于是创建/销毁的效率就提高了许多.

线程和进程的区别联系

  1. 进程包含线程
  2. 进程有自己独立的内存空间和文件描述符表, 同一个进程中的多个线程之间, 共享同一份地址空间和文件描述符表
  3. 进程是操作系统资源分配的基本单位, 线程操作系统调度执行的基本单位
  4. 进程之间具有独立性, 一个进程挂了, 不会影响到另外一个进程, 同一个进程里的多个线程之间, 一个线程挂了有可能会把整个进程挂掉, 同时也会影响到其他进程\
  5. 进程和线程都可以并发和并行

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

Java标准库提供了一个类Thread可以用来表示一个线程

Thread类

首先来介绍如何创建一个Thread对象?

创建thread的子类

class MyThread extends Thread {
    public void run() {
        System.out.println("hello world!");
    }
}

其中Thread为MyThread类的父类, 其中run方法被重写了, Thread类中的run方法如下:

实例化(向上转型)

先创建MyThread的实例, 然后赋值给他的父类Thead向上转型, 然后使用变量名+点号+start()的形式启动线程.

class MyThread extends Thread {
    public void run() {
        System.out.println("hello world!");
    }
}
public class threadingDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread(); // 向上转型
        t.start();  // 启动线程
    }
}

t.start() : 启动线程, 在进程中另外创建了一个流水线, 开始并发执行新的逻辑

上述代码Java创建了两个线程

  1. main方法所对应的线程(一个进程里面至少有一个线程, 这个main方法相当于是这个程序自带的一个线程), 也可以称之为主线程
  2. t.start()所创建的线程

独立执行流

java程序每一个线程都是一个独立的执行流

第一个多线程java程序

接下来的代码, 来感受java多线程的和普通程序的区别:

class MyTread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello libo");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread tem = new MyTread();
        tem.start();
        while(true) {
            System.out.println("i love you libo!");
            Thread.sleep(1000);
        }
    }
}

打印结果为hello libo i love you libo!

交替打印, 因为输出窗口的打印函数, 每次只能显示一个打印信息, 所以在打印的时候, 会无序的打印两个字符串:

创建多线程程序的方法

  1. 继承Thread来创建一个线程.

首先实现一个继承了Thread类的类, 然后再重写其run方法. 随后实例化一个实例对象, 然后调用里面重写的run方法, 再使用start方法启动线程即可.

class MyTread extends Thread { // 继承了Thread类的类
    @Override // 重写run方法
    public void run() {
        System.out.println(" this is your code! ");
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyTread tem = new MyTread(); // 实例化
        tem.start(); // 启动线程
    }
}

2. 实现Runable接口

class MyThread implements Runnable {
    public void run() {
        System.out.println("hello world!");
    }
}
 
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread());
        t.start();
    }
}

结果输出 "hello world!"

3. 匿名内部类

匿名内部创建Thread的子类

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println("hello world!");
            }
        };
        t.start();
    }
}

结果通过线程调用输出"hello world!"

4.匿名创建Runnable子类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("runnable 子类!");
            }
        });
    }
}

5. lambda表达式创建Runnable子类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("使用匿名类创建 Thread 子类对象");
        });
    }
}

使用JDK提供的工具来查看Java进程里的线程详细

  1. 在java目录下找到JDK工具包:

  1. 到bin目录下找打jconsole.exe程序

  1. 运行一个多线程进程
 
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true) {
                System.out.println("xxxxx!!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        while(true) {
            System.out.println("hello world!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

交替运行

4. 运行jconsole

jconsole只能分析java进程, 不能识别非java进程, 其中:

Main为我们运行的代码的进程, 其次 jconsole也有自己的进程, jconsole也是java实现的,

双击Main进入这个java进程, 可以看到这个界面:

然后进入到线程页签:

可以看到, 一个java进程里面不止一个线程, 里面还有很多其他的线程,

这个main为main方法的进程, 因为main方法里面有一个while(true)循环, 所以main方法线程一直没有结束, Thread-0同样如此,Thread-0线程是我们的Thread对象执行的线程.

这个堆栈跟踪线程中的代码执行到哪了.

其中这个Thread-0就是我们创建的第一个线程, 同理第二个线程为Thread-1, 以此类推.

Thread类及其常见方法

Thread类是Jvm用来管理线程的一个类, 换句话来说, 每个线程都有一个唯一的Thread对象与之相关联, 用我们上面的例子来说, 每个执行流, 也就是需要一个Thread对象来描述, 而Thread类的对象就是用来描述一个线程执行流的, JVM会将这些Thread对象组织起来, 用于线程调度, 线程管理.

例如:

Thread常见构造方法

方法

说明

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用 Runnable 对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用 Runnable 对象创建线程对象,并命名

Thread()和Thread(Runnable target) 我们上面都有举例过, 下面这个Thread(String name) 只是给这个线程取了个小名, 便于我们后面去调试这个线程, 例如下面的例子:

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread("MyThread-0") {
            @Override
            public void run() {
                System.out.println("hello world!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        t.start();
    }
}

这里传入了name = MyThread-0, 然后去jconsole去查看, 如下:

Thread(Runnable target, String name) 同理, 这个name不会影响程序执行, 只是方便调试的时候快速找到所需要的线程.

Thread常见属性

每个进程都有自己的状态, 上下文, 记账信息, 等属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

对其解释:

  • ID : 是线程的唯一标识, 不同的线程不会重复使用同一个ID
  • 名称: 是各种调试工具所要用到的, 便于寻找对应的线程
  • 状态 : 标识一个线程当前所处的情况
  • 优先级 : 优先级高的线程在理论上会更先辈调度到
  • 后台线程: JVM会在一个进程的所有非后台线程结束后, 才会结束运行, 后台进程不会阻止java进程结束, 哪怕后台线程没有执行完, java进程该结束的就会结束, true表示是 后台线程, false表示为前台进程. 必须进程中所有的前台线程都执行完成java进程才会结束.
  • 是否存活: run方法是否运行结束了
  • 线程中断:

例如

 
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread("MyThread-0") {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello world!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
        System.out.println("t->ID = " + t.getId()); // ID
        System.out.println("t->名称 = " + t.getName()); // 名称
        System.out.println("t->状态 = " + t.getState()); // 状态
        System.out.println("t->优先级 = " + t.getPriority()); // 优先级
        System.out.println("t->是否为后台线程 = " + t.isDaemon()); // 是否为后台线程
        System.out.println("t->是否存活 = " + t.isAlive()); // 是否存活
        System.out.println("t->是否被中断 = " + t.isInterrupted()); // 是否被中断
 
    }
}

启动一个线程:start()方法

我们之前覆盖写了run方法, 来创建了一个线程对象, 但是线程对象被创建出来了, 并不代表线程就开始运行了.

例如:

public class Main {
    public static void main(String[] args) {
       Thread t = new Thread(() -> {
           System.out.println("hello MyThread!!!");
       });
    }
}

使用lambda表达式来创建一个Thread匿名内部类. 单击运行, 可以发现系统没有任何提示输出:

这个时候就需要使用start方法来启动线程:

public class Main {
    public static void main(String[] args) {
       Thread t = new Thread(() -> {
           System.out.println("hello MyThread!!!");
       });
       t.start();
    }
}

中断一个线程

如果线程执行到一半, 我们需要让他停下来, 该怎么做?

目前有两种办法来实现:

  1. 通过共享标记来进行实现
  2. 调用interrupt()方法来实现

标志位

看一个例子:

public class Main {
    public static void main(String[] args) {
       Thread t = new Thread(() -> {
          while (true) {
              System.out.println("hello MyThread!");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
       });
       t.start();
    }
}

这个例子中, 线程t对象中使用了while(true)的死循环, 导致入口方法无法执行完毕, 自然线程就不能结束, 但是如果把这个变量换成全局变量来控制, 是否能达到我们所需要的效果?

public class Main {
    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
          while (!flag) {
              System.out.println("hello MyThread!");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
           System.out.println("线程终止!!");
       });
       t.start();
       Thread.sleep(3000);
 
       flag = true;
    }
}

结果出现:

同我们的预想相同.

注意, 这里不能将flag设置为main方法中的局部变量:

下面是错误的写法:

public class Main {
 
    public static void main(String[] args) throws InterruptedException {
        boolean flag = false;
       Thread t = new Thread(() -> {
          while (!flag) {
              System.out.println("hello MyThread!");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
           System.out.println("线程终止!!");
       });
       t.start();
       Thread.sleep(3000);
 
       flag = true;
    }
}

运行结果:

原因: lambda变量捕获, 我们实现的是lambda表达式Thread子类, lambda捕获的变量必须是final修饰的, 或者是实际上没有final的, 也就是没有被final修饰, 但是也没有被修改的.

解决方法: 将这个标志位设置为成员变量, 而非局部变量

Thread.interrupt() / Thread.currentThread().isInterrupted()

Thread.isInterrupted() : java里面的每个Thread类对象都有自带的一个标志位, 也就是Thread.isInterrupted(), isInterrupted()的值默认是false的.

Thread.currentThread() : 这个方法在哪个线程对象里面使用, 返回的就是哪个线程对象的引用

Thread.interrupt() : 将这个自带的标志位设置为true.

一个简单的例子 :

 
 
public class Main {
 
    public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
          while (!Thread.currentThread().isInterrupted()) {
              System.out.println("hello MyThread!");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
           System.out.println("线程终止!!");
       });
       t.start();
       Thread.sleep(3000);
        t.interrupt();
    }
}

我们将控制条件设置为自带的标志位, 然后使用t.interrupt()方法来将标志位值为true来结束循环. 点击运行, 结果如下:

为什么会抛出异常?

而interrupt的作用是: 将标志位设置为true, 如果线程正在阻塞态(例如sleep等), 此时就会把阻塞态立马唤醒.然后通过抛出异常的方法让sleep结束.

当sleep方法被唤醒的时候, sleep会自动把自带的标志位isInterrupted()清空(true -> false) 这下子导致循环任然可以继续执行.

所以在后面使用interrupt方法的时候, 程序正在sleep阻塞态, 立马被唤醒然后抛出上面的异常, 然后由于标志位被设为了false, 此时线程任然继续进行.

等待一个线程-join()

线程之间是并发执行的, 操作系统对线程的调度是无序的, 无法判断两个线程谁先执行结束.

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作. 可以在另外一个线程里面使用join()方法:

例如 : 这里有线程A 和线程B, 我们在线程A里面使用 B.join(), 也就意味着, A线程执行到B.join()之后, 就必须等B执行完才能继续执行A.

看下面这个案例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(()->{
           System.out.println("hello t");
       });
       t.start();
       t.join();
        System.out.println("hello main!");
    }
}

在main方法中加入t.join(), 也就是如果线程t还没有结束, 那么main线程就会阻塞(Blocking)等待, 代码走到这个t.join()就会停下来(main线程, 别的线程不受影响),

也就是: 如果main线程调用到t.join()的时候, 如果t线程没有结束, 那么main线程就会阻塞, 直到t线程结束, main线程才会解除阻塞, 然后继续执行下面的部分.

但是如果main线程调用t.join()时, t线程已经结束了, 那么main线程就会继续往下执行

获取当前线程的引用

这个在前面已经介绍到了,

使用Thread.currentThread(), 放回当前线程的引用, 例如:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
    }
}

输出结果:

休眠当前进程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的:

方法

说明

public static void sleep(long millis) throws InterruptedException

休眠当前线程 millis毫秒

public static void sleep(long millis, int nanos) throws InterruptedException

更高精度的休眠

线程状态

线程状态是一个枚举类型(Thread.State)

public class Main {
    public static void main(String[] args) throws InterruptedException {
        for (Thread.State state :
                Thread.State.values()) {
            System.out.println(state );
        }
        // Thread.State state 是单个枚举实例,
        // Thread.State.values()是这个Thread类所有状态类型 的枚举实例数组的引用
    }
}

输出:

类型

解释

NEW

安排了工作, 还未开始行动

RUNNABLE

可以工作-> (正在工作 / 即将工作)

BLOCKING

表示排队等着其他线程先结束

WAITING

表示正在等待其他线程

TIMED_WAITING

表示正在等待其他线程

TERMINATED

工作完成了


目录
相关文章
|
2天前
|
监控 Java API
Java 程序设计 第八章 线程
Java 程序设计 第八章 线程
|
2天前
|
存储 安全 Java
Java多线程编程--JUC
Java多线程编程
|
2天前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
12 0
|
3天前
|
Java API
详细探究Java多线程的线程状态变化
Java多线程的线程状态主要有六种:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)和终止(TERMINATED)。线程创建后处于NEW状态,调用start()后进入RUNNABLE状态,表示准备好运行。当线程获得CPU资源,开始执行run()方法时,它处于运行状态。线程可以因等待锁或调用sleep()等方法进入BLOCKED或等待状态。线程完成任务或发生异常后,会进入TERMINATED状态。
|
3天前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。
|
10天前
|
缓存 Java 测试技术
Java性能优化(八)-多线程调优-线程池大小设置
Java性能优化(八)-多线程调优-线程池大小设置
11 0
|
12天前
|
安全 Java 容器
多线程(进阶四:线程安全的集合类)
多线程(进阶四:线程安全的集合类)
15 0
|
12天前
|
开发框架 监控 Java
【.NET Core】多线程之线程池(ThreadPool)详解(二)
【.NET Core】多线程之线程池(ThreadPool)详解(二)
30 3
|
12天前
|
SQL 开发框架 Java
【.NET Core】多线程之线程池(ThreadPool)详解(一)
【.NET Core】多线程之线程池(ThreadPool)详解(一)
22 2
|
18天前
|
安全 Linux 编译器
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
22 0