Java多线程基础

线程状态

Java一共有六种状态

  • NEW: 通过new Thread() 创建出来的线程, 此时它仅仅算是一个Java对象而已, 还不算真正的线程, 此时可以设置线程的优先级, 线程是否守护线程等
  • RUNNABLE: 调用start() 方法之后, Java虚拟机为线程分配虚拟机栈,本地方法栈和程序计数器等, 线程处于RUNNABLE状态, 处于RUNNABLE 状态的线程可能在运行, 也可能只是就绪状态,取决于cpu 调度器由有没有分配时间片
  • BLOCKED: 线程因为synchronized, 而进入阻塞状态
  • WAITING: 线程执行 Thread.sleep, t.join, obj.wait 方法而进入等待状态(sleep是Thread的静态方法, join 是Thread的成员方法, wait 则是Object的成员方法)
  • TIMED WAITING: 执行带超时时间的上述方法会进入该状态
  • TERMINATED: 线程终止

线程的创建方式

本质上只有一种方式创建线程, 就是 new Thread.

不管是new Thread, 然后重写它的run() 方法, 还是 创建一个Runnable 对象, 然后传递给Thread, 本质上都是新建一个线程, 然后交给它一个任务去执行. Thread 负责线程本身相关的职责和控制, Runnable 负责逻辑执行单元的部分.

Thread 构造函数

Thread的构造函数中存在以下这些参数:

  • group: ThreadGroup类型, 表示线程的分组, 如果不指定,默认采用它的父线程的分组. 每个线程的父线程是创建它的线程. 我们知道 main线程Java虚拟机创建的, 然后如果我们在main线程中创建线程, 那么新建出来的线程的父线程是main线程, 一个线程默认继承父线程的分组,线程优先级, 是否守护线程, 上下文加载器
  • target: Runnable类型, 表示该线程的任务,即Runnable 对象
  • name: 线程的名称, 如果不指定默认是Thread-x, x 是从0开始递增的整数. 一般建议给线程命名,这样方便日志查看和问题定位
  • stackSize: 线程的虚拟机栈大小, 默认是设置为0, 即不会产生影响. 一般通过虚拟机参数 -Xss 设置虚拟机栈大小

虚拟机栈是线程私有的, 在调用start方法后,Java虚拟机为该线程分配的方法执行内存空间. 在线程中, 方法在执行时,会创建一个栈帧, 用来存放局部变量表, 操作数栈,动态链接和方法出口等信息, 方法的调用就对应着一个栈帧在虚拟机栈中的压栈和弹出的过程.
所以, 这个虚拟机栈的大小就控制着一个线程的最大调用深度.
但是虚拟机栈也不是越大越好, 如果虚拟机栈越大,可创建的线程就越少.

毕竟一个进程可用的地址空间是有限的, 比如在32位的windows 操作系统中, 进程的最大进程大小是2G
Java进程的内存大小 = jvm堆内存 + 系统保留内存 + 线程数 * stackSize, 系统保留内存在 136M 左右.
其中, jvm 堆内存可以使用 -Xmx 和 -Xms 分别设置堆的最大内存和最小内存

守护线程

守护线程是一些后台线程,比如垃圾收集线程等. 如果Java进程中除了守护线程, 没有其他线程时, 进程会结束.
可以通过 t.setDeamon(true) 设置, 但是只有在调用start 方法之前设置才会生效.

Thread 的API

sleep() 方法

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

静态方法, 可中断, 在哪个线程中执行, 哪个线程就进入休眠状态
[注意] 如果是在持有了对象锁,然后进入sleep, 线程不会释放对象锁

yield() 方法

public static native void yield();

静态方法, 表示线程让出当前cpu 的执行机会, 它只是给调度器一个提示, cpu 调度器不保证每次都会满足yield 提示

线程优先级

public final void setPriority(int newPriority);
public final int getPriority();

设置和获取线程优先级, 理论上优先级高的线程会获得更多的执行机会, 但实际上无法保证总是这样, 所以不能让业务严重依赖线程的优先级
可设置的优先级是 1-10, 默认是5. 设置的线程优先级,不能高于它所在group的最高优先级

获取当前线程

public static native Thread currentThread()

静态方法, 返回当前线程的引用

线程上下文加载器

public void setContextClassLoader(ClassLoader cl) ;
public ClassLoader getContextClassLoader();

中断线程

与线程中断有关的方法有三个

public void interrupt();
public static boolean interrupted();
public boolean isInterrupted();

在线程内部有一个中断标志,interrupt flag. 如果调用一个线程的 interrupt() 方法, 这个中断标志就会设置为true.
此时如果调用成员方法 isInterrupted(), 则会返回true.
调用静态方法 interrupted(),也会返回true, 但是这个静态方法会清除打断标志. 成员方法 isInterrupted() 则不会.

除此还有一个需要注意的, 当调用线程的interrupt() 方法时, 如果线程正阻塞在以下方法时, 阻塞会中断,并抛出InterruptedException异常, 并且, 中断标志也会擦除

  • Object 的wait 方法,不管是否带超时参数
  • Thread的sleep 方法, 不管是否带超时参数
  • Thread的join 方法, 不管是否带超时参数
  • InterruptibleChannel 的io 操作
  • Selector的wakeup 操作
  • 其他可中断方法

此外, 如果在一个线程调用可中断方法之前, 就调用了它的中断方法, 之后它进入可中断方法会怎么呢?

结果是, 如果一个线程设置了中断标志, 那么接下来的可中断方法会立即中断

join 方法

join 方法也是可中断方法, 可以利用join方法做一些线程同步操作.
如果A线程调用B线程的 join() 方法, 那么A线程会阻塞. 直到线程B执行结束. 但如果在A线程阻塞期间,别的线程再调用了A线程的interrupt() 方法, 那么A线程会中断, 并抛出InterruptedException异常.

关闭线程

JDK提供了一个方法stop() 方法来关闭线程, 但是已经不推荐使用, 因为它关闭线程时可能不会释放monitor锁, 所以我们要采用别的方法来关闭线程

  1. 线程任务执行完, 正常结束
    这种方式不需要做额外的操作, 线程执行完任务, 自然结束生命周期. 但是一般都不会只让线程执行一个任务, 就让它结束生命周期, 毕竟申请线程是需要一定开销的, 所以一般会将线程放进线程池中,然后不断从任务队列获取任务来执行, 所以我们还需要别的线程关闭方法
  2. 调用interrupt 方法中断线程
    如果一个线程正在执行可中断方法, 调用它的interrupt方法, 线程会抛出 InterruptedException异常,此时,可以捕获这个异常, 然后让线程结束任务的执行.
    但是在捕获异常的方法中结束线程的方式不总是可行, 因为当线程不是在执行可中断方法时, 调用interrupt 方法, 仅仅会设置线程的中断标志. 虽然可以通过检查中断标志来判断是否要结束线程, 但是, 中断标志是可以被擦除, 所以通常还会配合一个变量来识别
  3. volatile变量 + 中断标志

volatile 变量保证了所有的线程对该变量的读取都是最新的

在关闭方法中, 还调用了interrupt 方法是为了保证如果线程正执行可中断方法时, 也能进行中断
而在while循环的条件中, 还加 !isInterrupted() 的判断,则是为了如果线程在正常执行任务的时候, 别的线程调用了它的interrupt方法, 它之后也能感知, 从而不再执行其他任务,进行线程关闭

private volatile boolean closed = false;

    @Override
    public void run() {
   
        while(!closed && !isInterrupted()){
   
            // 执行任务
        }
    }
    public void close(){
   
        closed = true;
        this.interrupt();
    }

线程同步

如果一个可变的变量, 不同的线程共享, 那么会存在线程安全问题.

为了避免这个问题, 可以在对该变量的访问之前使用synchronized进行加锁.

synchronized 包含两条JVM 指令, monitor enter 和 monitor exit.
JVM保证任何时候 任何线程执行到 monitor enter成功后, 都必须从主存中获取数据, 而不是从线程的缓存中获取
另外, 也保证在执行monitor exit之后, 共享变量更新后的值会 刷入到主存

synchronized 也称为对象锁, 它是与对象关联的, 可以是一个普通Java对象, 也可以是一个类对象.

它的原理是这样的:
每个对象都与一个monitor 关联, 一个monitor 在同一时刻只能被一个线程持有, 当一个线程尝试获得一个对象关联的monitor的所有权时, 会出现以下几种情况:

  1. 如果monitor的计数器为0, 那么该线程称为该monitor的owner, 计数器加一
  2. 如果monitor的计数器不为0, 但是该线程发现monitor的owner时自己, 那么重入,计数器加一
  3. 如果一个线程发现monitor计数器不为0, owner 也不是自己, 那么进入BLOCKED 状态, 直到monitor的计数器为0

当一个线程执行monitor exit 指令时, monitor的计数器就减一,当计数器为0, 那么就相当于释放了锁.

使用 synchronized 进行线程同步时, 要注意是否对同一个对象加锁, 以及注意加锁的粒度, 如果粒度太大,则会丧失了并发的本意

使用 synchronized 加锁有两个弊端, 一个是无法控制阻塞时长, 一个是无法中断阻塞
无法控制阻塞时长指的是, 一个线程阻塞加锁过程, 它无法知道它要阻塞多久, 只有等占有锁的线程释放了锁,它才有机会去竞争锁
无法中断阻塞指的是, 一个线程一旦开始阻塞在加锁过程, 没有方法可以中断这个阻塞状态, 进程意外终止除外

线程间通信

我们假定一个这样的场景, 有一个盘子最大可以放10个苹果, 父母负责往盘子放苹果, 孩子们则从苹果上拿苹果吃. 如果盘子满了,父母就要等到盘子有空间了才能放; 如果盘子空了,孩子也必须要等到有苹果才能吃.
那么该怎么实现呢?

// 设计一个盘子	
public class Plate<T> {
   
    private static final int MAX = 10;
    private  Deque<
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值