Java 基础 - 多线程基础

并发

并发在单核和多核 CPU 上都存在, 对于单核 CPU,通过轮训时间片的方式实现并发.


线程

线程对象

利用Thread对象, 有两种方式来创建并发程序:

  1. 直接创建并管理线程. 当程序要启动一个异步任务的时候, 直接创建一个线程.
  2. 将线程管理抽象出来, 把并发部分的任务交给 executor.

线程的创建

有两种方式创建线程:

  1. 提供一个实现Runnable接口的对象.
  2. 子类化Thread.

两种方法的优缺点?

Runnable 总体来说更好一点

  1. 使用Runnable 接口的方式更加灵活, 因为可以继续子类化某个类
  2. Runnable 接口的方式可以适配 concurrent包中的高级线程管理 API

线程的基本状态

线程有如下状态:

  1. NEW: 线程已经创建, 但还没有调用 start() 开始执行.
  2. RUNNABLE: 线程已经在 JVM 中开始运行, 但有可能在等待系统资源运行.
  3. BLOCKED: 线程在等待一个 monitor lock 以进入一个 synchronized 块. 也有可能是这个线程刚执行完 wait(), 其他线程又在获取 wait() 对象的 monitor lock.
  4. WAITING: 线程进入等待状态, 以下方法会使得线程进入 wait() 状态:
    • Object.wait()
    • join()
    • LockSupport.park
  5. TIMED_WAITING: 线程进入有时间限制的等待, 一下方法会使得线程进入此状态:
    • Thread.Sleep
    • Object.wait(long)
    • Thread.join(long)
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  6. TERMINATED: 线程执行结束.

注意:

当线程 A 调用某个对象的 Synchronized 方法的时候, 线程就获得了这个对象的 intrinsic lock, 线程的状态是 RUNNABLE. 其他线程假如要获取这把锁, 就会进入 BLOCKED 状态.

当线程 A 调用wait方法, 那线程A 会释放这个对象的锁(但是扔回持有其他对象的锁, 假如有的话), 然后线程转入 WAITING 状态. (若之前很多对象 pending 在这个 lock 上, 那么, 进入 wait 后会唤醒其他某个线程么?)

当其他某个线程 B 在同一个对象(也就是线程 A wait 释放的同一把锁)调用notify 或者 notifyAll时, 线程A 状态由 WAITING 转化为 BLOCKED. 此时, 线程A 并不会自动获取到锁或者状态变成 RUNNABLE, 实际上, 线程 A 也要和其他被阻塞的线程一样竞争这把锁.

WAITING 和 BLOCKED 状态都会阻止线程运行, 但是区别却很大.

WAITING 状态必须被其他线程调用notify从而显式的转化为 BLOCKED 状态. WAITING 状态从来不会直接转化为 RUNNABLE.

当一个 RUNNABLE 线程释放了锁(正常结束或者waiting), 某个被阻塞的线程会自动被唤醒.

notify 和 notifyAll 区别?

notify 唤醒被同一把锁 wait的第一个线程 notifyAll 唤醒同一把锁 wait 的所有线程, 但是优先级最高的先执行

Thread.sleep

Thread.sleep 导致当前线程暂时挂起一段时间, 其他线程可以有机会获取到 CPU 时间.

两个 API:

Thread.sleep(long ms)
Thread.sleep(long ms, long ns)

时间并不精确, 由于底层 OS 实现的限制.

Sleep 可以被打断, 当线程 A 在休眠, 而另外一个线程 B 调用 A.interrupt()时, 线程 A 就会抛出 InterruptedException

中断

中断

interrupt 会停止当前线程的正在进行的任务并且取做别的事情. 至于一个 thread 应该如何响应一个中断则是由程序员决定的.

响应中断

根据当前任务的长短, 做不同的处理:

  1. 当一个线程正在频繁的调用一个会抛出InterruptedException 的方法时, 它可以通过 try...catch 捕获, 并在catch 中做处理. 有很多方法会抛出InterruptedException, 比如 Thread.sleepsleep的中断行为被设计成为:终止当前操作并抛出异常.

    for (int i = 0; i < ary.length; i++) {
        try {
            Thread.sleep(4000);
        } caatch (InterruptedException e) {
            return;
        }
        System.out.println(importantInfo[i]);
    
    }
  2. 当一个线程在执行一个长时间任务, 并且这个任务并没有抛出 InterruptedException 的时候. 那么就需要不停地去检测当前线程有没有被中断:
    for (int i = 0; i < inputs.length; i++) {
        heavyCrunch(inputs[i]);
        if (Thread.interrupted()) {
            // or return;
            throw new InterruptedException();
        }
    }

中断的标志位

中断机制是由内部标识中断状态的一个标志控制的:

  • 当调用 Thread.interrupt时, 这个标志会被设置.
  • 当调用 Thread.interrupted时, 标志会被清理.
  • 当调用 Thread.isInterrupted 查询中断状态时, 标志不变.
  • 任何方法假如因为 InterruptedException 而退出, 那么中断标志会被清理(但是有可能立即被设置).

Join

join 方法让一个线程可以等待另外一个线程执行结束后再往下执行. 和 sleep一样, join 响应中断的方式也是退出并且抛出InterruptedException.


线程同步

线程间通讯主要靠开放字段的访问或者字段引用的对象. 会带来两个问题:

  1. 线程干扰(thread interference)
  2. 内存一致性错误(memory consistency)

防止这两种错误的机制就是 线程同步. 然而, 线程同步会带来线程间竞争(thread contention). 饥饿和活锁都是 线程竞争 的表现.

线程干扰 - Thread Interference

指的是, 一个语句可能被虚拟机拆分成很多步执行, 然而当两个线程交叉执行时, 线程 A 的执行导致线程 B 的运行结果是不准确的. 比如, 线程 A 执行 c++, 线程 B 执行 c--, 开始两个线程读到 c 的值是0, 假如线程 B 在 A 之后执行完, 那么结果是 -1 而不是 0.

内存一致性错误 - Memory Consistency Error

Memory Consistency Error 指的是线程对同一份数据却有不一致的值. 防止这种错误的关键是保证 happens-before 关系. 比如:

    int count = 0;

线程 A 执行:

    count++;

线程 B 打印 count的值:

    System.out.println(count);

那么线程 B 打印出的结果可能是 0, 因为线程 A 的自增操作并没有和 B 的打印语句建立一个 happens-before 关系_.

happens-before 关系 保证的是, 某个语句所导致的内存写入动作会对另外的语句可见.

如何建立 happens-before 关系?

  1. 同一个线程, 前面的指令总比后面的指令先执行
  2. 释放 monitor lock(离开 synchronized 代码块或者方法), 总是发生在获取同一个 monitor lock(进入 synchronized 代码块或者方法)之前. 由于 happens-before 关系的传播性, 释放锁的方法或者代码块, 总是比获取锁或者代码块之前执行.
  3. 写入volatetile 变量总是在读取前执行.
  4. 调用 Thread.start 建立两种 happens-before 关系:
    • Thread.start前面的语句会在 Thread.start 之前执行
    • Thread.start前面的语句会在新线程语句之前执行
  5. 线程 A 结束并导致线程 B Thread.join返回, 线程 A 中的所有语句会在线程 B Thread.join后面的语句之前执行

线程同步方法

Java 语言级别提供了两种方法:

  1. synchronized method
  2. synchronzied statements

注意:

  • 构造函数不能用 synchronized 修饰, 否则会发生编译错误.
  • 对象的构造一定是在同一个线程中完成的.
  • 不要提早的泄露字段的引用. 比如在构造函数添加下列的语句:
    instances.add(this) //就会导致构造函数并未完成, 却将它通过 instances 暴露出去了.

Intrinsic Lock / Monitor Lock

同步的机制是建立在 intrinsic lock 又称 monitor lock 之上的. Intrinsic Lock 作用是

  • 强制独占对象的状态(只要一个线程有一个 intrinsic lock, 其他线程是获取不到这个锁的)
  • 建立一种 happens-before 关系
  • 保证了状态更改的可见性.

synchronized method 中的锁

当线程调用一个 synchronized method 的时候, 线程会自动获得对象的 intrinsic lock. 并在下列情况释放:

  • 方法正常返回
  • 有未捕获异常发生

当线程调用一个 static synchronized method 的时候, 线程会获取与对象关联的 Class 对象的锁. 所以静态同步方法的锁和实例锁是不同的.

synchronzied statements

写法:

 public void addName(String name) {
        synchronized(this) {
                lastName = name;
                nameCount++;
        }
        nameList.add(name);
 }

注意: 在 synchronized method 或者 synchronzied statements 中要避免其他对象的同步代码(方法或代码块).

同步代码重入

  • 一个线程不可以获得其他线程拥有的锁
  • 一个线程可以获取自身已经拥有的锁

原子访问

意思是: 不可打断的操作

Java 中的原子操作:

  • 读取或改变引用类型的引用
  • 读取或改变基本类型(long 和 double 除外)
  • 读取或改变所有volatile类型的变量(包括引用, longdouble)

原子操作不会被拆分, 所以不用担心线程干扰(Thread Interference)的问题, 但是却仍然要注意内存一致性(Memory Consistency)的问题.

使用volatile 可以避免内存一致性错误, 因为写入volatile 变量建立了一种 happen-before 的关系: 写入总比后续读先.

synchronized method 和 synchronized statements 会保证原子操作.


Liveness

Deadlock

死锁描述了这样一种情况: 两个或者多个线程进入永远的阻塞, 互相等待.

避免方法: 上锁的顺序相同.

如何排查? 通过 JStack 可以查看:

jstack <pid>

Java stack information for the threads listed above:
===================================================
"Thread-1":
    at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)
    - waiting to lock <0x0000000795706590> (a basic.DeadlockBower$Friend)
    at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)
    - locked <0x00000007957065d8> (a basic.DeadlockBower$Friend)
    at basic.DeadlockBower$2.run(DeadlockBower.java:49)
    at java.lang.Thread.run(Thread.java:745)
"Thread-0":
    at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)
    - waiting to lock <0x00000007957065d8> (a basic.DeadlockBower$Friend)
    at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)
    - locked <0x0000000795706590> (a basic.DeadlockBower$Friend)
    at basic.DeadlockBower$1.run(DeadlockBower.java:43)
    at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock. 

Starvation

饥饿描述了这样的情况: 线程无法长时间访问不到共享资源, 从而无法取得进展

Livelock

描述的是: 两个线程互相根据对方的行为做出响应, 导致各自没有实质性的进展

Guarded Blocks

线程之间经常协调他们的行为, 其中最常用的协调方法是 guarded block:

public void guardedJoy() {
    // Simple loop guard. Wastes processor time. Don‘t do this!
    while (!joy) { }

    System.out.println("Joy has been achieved");
}

上面的代码通过不停 检测 joy 的状态来决定是否往下执行. 这样非常的耗费 CPU 时间.

更好的应该用 Object.wait() 来挂起当前线程.

public synchronized void guardedJoy() {

    // This guard only loops once for each special event, which may not be
    // the event we‘re waiting for.

   while (!joy) {

        try {
            wait();
        } catch (InterruptedException e) {}
   }
   System.out.println("Joy and efficiency have been achieved!");

}

值得注意的是, 确保 wait() 在一个循环中, 因为你不能保证:

  1. 是 InterruptedExcpetion 还是正常唤醒导致 wait() 结束
  2. 唤醒的线程是否将 joy 的值改变(尤其是在 notifyAll中)

消费者和生产者模式

解决了, 解耦了生产者和消费者, 并且更合理的运用了 CPU 时间.

不可变对象

不可变对象是那些构造后状态无法被改变的对象. 由于它的不可变性, 他不会有 Thread interference 和 Memory inconsistent 等问题.

不可变对象的特征:

  1. 不要设置 setter 方法.
  2. 所有的成员都设置成 final + private
  3. 不允许子类重写方法. 简单的方法是将类声明前 + final
  4. 如果实例变量中有引用类型, 别让他们被更改:
    • 不要提供更改他们的方法
    • 不要共享他们的引用, 包括
      • 不要在构造函数中直接引用参数
      • 不要将实例引用变量返回, 如果不得不, 则, 返回拷贝!
时间: 2024-10-14 08:58:39

Java 基础 - 多线程基础的相关文章

java笔记--多线程基础

多线程技术 在java中实现多线程技术有两种方式: 1.继承Thread类: 2.实现Runnable接口 这两种方法都需要重写run()方法:通常将一个新线程要运行的代码放在run()方法中(这是创建没有返回值线程的方法)由于java只支持单继承,当类已经继承有其他类时,只能选择实现Runnable接口在启动线程时需要使用Thread类的start()方法,而不是直接使用run()方法: 如: public static void function() { for (int i = 0; i

java核心技术-多线程基础

进程.线程 ? 进程(Process) 是程序的运行实例.例如,一个运行的 Eclipse 就是一个进程.进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位.线程(Thread)是进程中可独立执行的最小单位.一个进程可以包含多个线程.进程和线程的关系,好比一个营业中的饭店与其正在工作的员工之间的关系. 1.1 线程的创建.启动与运行 在 Java 中实现多线程主要用两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口.(当然还有Callable和线程池).下

java 基础--多线程基础练习

1.随便选择两个城市作为预选旅游目标.实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市.分别用Runnable接口和Thread类实现. import java.util.Random; //多线程类 //1.是Thread的子类 //2.重写run方法 public class Test01 extends Thread{ @Override public void run() { for(int i=0;i<10;i++

Java 复习 —— 多线程基础

1.基本概念 1)进程:运行当中的程序,程序是静止的概念,进程的是动态的概念,进程与进程之间互不运影响 2)线程:指程序中单独顺序的流控制,线程依附于进程中,他是最小的执行单位!一个任务一个线程. 3)多线程:指的是单个程序中可以同时运行多个不同的线程,执行不同的任务.(本身就要把线程理解为为不同的任务而服务的) 4)二者关系:一个进程当中可以有一个或多个线程,但是至少有一个线程. 2.作用与关系 1)多线程的目的:最大限度利用CPU资源 2)主线程:Java程序默认启动一个线程就是main线程

[转]Java多线程干货系列—(一)Java多线程基础

Java多线程干货系列—(一)Java多线程基础 字数7618 阅读1875 评论21 喜欢86 前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 正文 线程与进程 1 线程:进程中负责程序执行的执行单元线程本身依靠程序进行运行线程是程序中的顺序控制流,只能使用分配给程序的资源和环境 2 进程:执行中的程序一个进程至少包含一个线程 3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程 4

Java多线程基础(一)

线程与进程 1 线程:进程中负责程序执行的执行单元线程本身依靠程序进行运行线程是程序中的顺序控制流,只能使用分配给程序的资源和环境 2 进程:执行中的程序一个进程至少包含一个线程 3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程 4 多线程:在一个程序中运行多个任务目的是更好地使用CPU资源 5  在Java语言中,引入对象互斥锁的概念,保证共享数据操作的完整性. 每个对象都对应于一个可称为"互斥锁"的标记,这个标记保证在任一时刻,只能有一个线程访问对象用关键字synchr

Java多线程基础

1. 前言 这篇文章,是对Java多线程编程的基础性介绍. 文章将介绍Java语言为支持多线程编程提供的一些特性.通过这篇文章,您将了解到如何通过Java语言创建一个线程,如何通过内置的锁来实现线程间的同步,如何在线程间进行通信以及线程的中断机制. 2. 什么是线程 线程是操作系统调度的最小单位,在一个进程中,一般至少有一个线程在运行.一个进程中包含的多个线程,在多核处理器中,操作系统可以将多个线程调度到不同的CPU核心上运行,多个线程可以并行运行. 在同一个进程中的多个线程,共享同一个进程空间

Java多线程基础(四)Java传统线程同步通信技术

Java多线程基础(四)Java传统线程同步通信技术 编写代码实现以下功能 子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次. 分析 1)子线程循环10次与主线程循环100次必须是互斥的执行,不能出现交叉,下面代码中通过synchronized关键字实现此要求: 2)子线程与主线程必须交替出现,可以通过线程同步通信技术实现,下面代码中通过bShouldSub变量实现此要求: 其他需要注意的地方 1)其中business变量必须声

黑马程序员——java基础——多线程

 黑马程序员--java基础--多线程 ------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 进程:是一个正在执行中的程序.每一个进程执行都有一个执行顺序.该顺序是一个执行路径,或者叫一个控制单元. 线程:就是进程中的一个独立的控制单元.线程在控制着进程的执行.一个进程中至少有一个线程. 一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区.自己的变量.