java多线程释放锁顺序,java基础知识总结——多线程与锁

1、线程与进程的区别?

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

进程是资源分配的最小单位,线程是程序执行的最小单位。

进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

2、线程的创建方式

2.1继承Thread类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

package Thread;

import java.util.concurrent.*;

public class TestThread {

public static void main(String[] args) throws Exception {

testExtends();

}

public static void testExtends() throws Exception {

Thread t1 = new MyThreadExtends();

Thread t2 = new MyThreadExtends();

t1.start();

t2.start();

}

}

class MyThreadExtends extends Thread {

@Override

public void run() {

System.out.println("通过继承Thread,线程号:" + currentThread().getName());

}

}

2.2实现Runnable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程。

package Thread;

import java.util.concurrent.*;

//测试类

public class TestThread {

public static void main(String[] args) throws Exception {

testImplents();

}

public static void testImplents() throws Exception {

MyThreadImplements myThreadImplements = new MyThreadImplements();

Thread t1 = new Thread(myThreadImplements);

Thread t2 = new Thread(myThreadImplements, "my thread -2");

t1.start();

t2.start();

}

}

//线程类

class MyThreadImplements implements Runnable {

@Override

public void run() {

System.out.println("通过实现Runable,线程号:" + Thread.currentThread().getName());

}

}

2.3实现Callable接口

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

package Thread;

import java.util.concurrent.*;

public class TestThread {

public static void main(String[] args) throws Exception {

testCallable();

}

public static void testCallable() throws Exception {

Callable callable = new MyThreadCallable();

FutureTask task = new FutureTask(callable);

new Thread(task).start();

System.out.println(task.get());

Thread.sleep(10);//等待线程执行结束

//task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值

//get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待

System.out.println(task.get(100L, TimeUnit.MILLISECONDS));

}

}

class MyThreadCallable implements Callable {

@Override

public Object call() throws Exception {

System.out.println("通过实现Callable,线程号:" + Thread.currentThread().getName());

return 10;

}

}

2.4三种方式的优缺点

采用继承Thread类方式:

(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。   (2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

采用实现Runnable接口方式:

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

Runnable和Callable的区别:

(1)Callable规定的方法是call(),Runnable规定的方法是run().   (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的   (3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常   (4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

start()和run()的区别:

start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行

run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

3、线程的生命周期

Java 线程 的 状态 可以 使用 监控 工具 查看, 也可以 通过 Thread. getState() 调用 来 获取。 Thread. getState() 的 返回 值 类型 Thread. State 是一 个 枚举 类型( Enum)。 Thread. State 所 定义 的 线程 状态 包括 以下 几种。

NEW: 一个 已 创建 而未 启动 的 线程 处于 该 状态。 由于 一个 线程 实例 只 能够 被 启动 一次, 因此 一个 线程 只可 能有 一次 处于 该状态。

RUNNABLE: 该 状态 可以 被 看成 一个 复合 状态。 它 包括 两个 子 状态: READY 和 RUNNING。 前者 表示 处于 该 状态 的 线程 可以 被 线程 调度 器( Scheduler) 进行 调度 而使 之 处于 RUNNING 状态。 后者 表示 处于 该 状态 的 线程 正在 运行, 即 相应 线程 对象 的 run 方法 所 对应 的 指令 正在 由 处理器 执行。 执行 Thread. yield() 的 线程, 其 状态 可能 会 由 RUNNING 转换 为 READY。 处于 READY 子 状态 的 线程 也 被称为 活跃 线程。

BLOCKED: 一个 线程 发起 一个 阻塞 式 I/ O( Blocking I/ O) 操作 后, 或者申请 一个 由其 他 线程 持有 的 独占 资源( 比如 锁) 时, 相应 的 线程 会 处于 该 状态。 处于 BLOCKED 状态 的 线程 并不 会 占用 处理器 资源。 当 阻塞 式 I/ O 操作 完成 后, 或者 线程 获 得了 其 申请 的 资源, 该 线程 的 状态 又可 以 转换 为 RUNNABLE。

WAITING: 一个 线程 执行 了 某些 特定 方法 之后 就会 处于 这种 等待 其他 线程 执行 另外 一些 特定 操作 的 状态。 能够 使其 执行 线程 变更 为 WAITING 状态 的 方法 包括: Object. wait()、 Thread. join() 和 LockSupport. park( Object)。 能够使 相应 线程 从 WAITING 变更 为 RUNNABLE 的 相应 方法 包括: Object. notify()/ notifyAll() 和 LockSupport. unpark( Object))。

TIMED_ WAITING: 该 状态 和 WAITING 类似, 差别 在于 处于 该 状态 的 线程 并非 无限制 地 等待 其他 线程 执行 特定 操作, 而是 处于 带有 时间 限制 的 等待 状态。 当 其他 线程 没有 在 指定 时间 内 执行 该 线程 所 期望 的 特定 操作 时, 该 线程 的 状态 自动 转换 为 RUNNABLE。

TERMINATED: 已经 执行 结束 的 线程 处于 该 状态。 由于 一个 线程 实例 只 能够 被 启动 一次, 因此 一个 线程 也 只可 能有 一次 处于 该 状态。 Thread. run() 正常 返回 或者 由于 抛出 异常 而 提前 终止 都会 导致 相应 线程 处于 该 状态。 一个 线程 在 其 整个 生命 周期 中, 只可 能有 一次 处于 NEW 状态 和 TERMINATED 状态。

4、锁

线程 同步 机制 是一 套用 于 协调 线程 间的 数据 访问( Data access) 及 活动( Activity) 的 机制, 该 机制 用于 保障 线程 安全 以及 实现 这些 线程 的 共同 目标。 如果 把 线程 比作 在 公路 上 行驶 的 车辆, 那么 线程 同步 机制 就 好比 是 任何 车辆 都 需要 遵循 的 交通规则。 公路 上 行驶 的 车辆 只有 遵守 交通规则 才能 够 达到 其 目的—— 安全 地 到达 目的地。从 广义 上 来说, Java 平台提供 的线程同步机制包括锁、volatile 关键字、 final 关键字、 static 关键字 以及 一些 相关 的 API, 如 Object. wait()/ Object. notify() 等。

线程 安全 问题 的 产生 前提 是 多个 线程 并发 访问 共享 变量、 共享 资源( 以下 统称 为 共享 数据)。 于是, 我们 很容易 想到 一种 保障 线程 安全 的 方法—— 将 多个 线程 对 共享 数据 的 并发访问转换 为 串行 访问, 即 一个 共享 数据 一次 只能 被 一个 线程 访问, 该 线程 访问 结束 后 其他 线程 才能 对其 进行 访问。 锁( Lock) 就是 利用 这种 思路 以 保障 线程 安全 的 线程 同步 机制。

一个 线程 在 访问 共享 数据 前 必须 申请 相应 的 锁( 许可证), 线程 的 这个 动作 被称为 锁 的 获得( Acquire),一个 锁 一次 只能 被 一个 线程 持有。 锁 的 持有 线程 可 以对 该 锁 所 保护 的 共享 数据 进行 访问, 访问 结束 后 该 线程 必须 释放( Release) 相应 的 锁。 锁 的 持有 线程 在 其 获得 锁 之后 和 释放 锁 之前 这段 时 间内 所 执行 的 代码 被称为 临界区( Critical Section)。 因此, 共享 数据 只 允许 在 临界 区内 进行 访问, 临界 区 一次 只能 被 一个 线程 执行。如果 有多 个 线程 访问 同一个 锁 所 保护 的 共享 数据, 那么 我们 就 称 这些 线程 同步 在这 个 锁上。

4.1与锁相关的概念

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

可 重入 性( Reentrancy): 一个 线程 在 其 持有 一个 锁 的 时候 能否 再次( 或者 多次) 申请 该 锁。 如果 一个 线程 持有 一个 锁 的 时候 还能 够 继续 成功 申请 该 锁, 那么 我们 就 称 该 锁 是 可 重入 的( Reentrant), 否则 我们 就 称 该 锁 为 非可 重入 的( Non- reentrant)。

资源 的 争用、 调度:锁 可以 被 看作 多 线程 程序 访问 共享 数据 时 所需 持有 的 一种 排他性 资源。 锁 的 调度基本上 是由 Java 虚拟 机 来决定的, Java 平 台中 锁 的 调度 策略包括 公平策略 和 非公平 策略, 相应 的 锁 就被 称为 公平 锁 和 非 公平 锁。 内部 锁 属于非 公平 锁, 而 显 式 锁 则 既 支持 公平 锁 又 支持 非 公平 锁。

锁 的 粒度:一个 锁 实例 可以 保护 一个 或者 多个 共享 数据。 一个 锁 实例 所 保护 的 共享 数据 的 数量 大小 就被 称为 该 锁 的 粒度( Granularity)。 一个 锁 实例 保护 的 共享 数据 的 数量 大, 我们 就 称 该 锁 的 粒度 粗, 否则 就 称 该 锁 的 粒度 细,锁 粒度 的 粗细 是 相对 的。 锁 的 粒 度过 粗 会 导致 线程 在 申请 锁 的 时候 需要 进行 不必 要的 等待。 这 好比 我们 去 银行 柜台 办理 业务 的 情形: 假如 一个 柜台 同时 能够 办理 多种 业务, 那么 就可能 出现 这样 的 场景—— 办理 客户 资料 变更 的 客户 需要 等待 前面 要 办理 定期 存款 的 客户。 而 如果 一个 柜台 只 办理 一种 业务, 比如 将 开户、 销 户 和 客户 资料 变更 归为 一种 业务 放在 一个 柜台 办理, 那么 办理 客户 资料 变更 的 客户 需要 等待 的 时间 就会 相对 减少。 不过, 锁 的 粒度 过细 会 增加 锁 调度 的 开销。

4.2内部锁(sychronized关键字)

内部 锁 是一 种 排他 锁, 它 能够保障 原子 性、 可见 性 和 有序 性。 内部 锁 是 通过 synchronized 关键字 实现 的。

synchronized 实现 的 锁 就被称为 内部 锁,线程 对 内部 锁 的 申请 与 释放 的 动作 由 Java 虚拟 机 负责 代为 实施。内部 锁 的 使用 并不 会 导致 锁 泄漏。 这是 因为 Java 编译器( javac) 在 将 同步 块 代码 编译 为 字节 码 的 时候, 对 临界 区 中 可能 抛出 的 而 程序 代码 中 又 未 捕获 的 异常 进行 了 特殊( 代为) 处理, 这使 得 临界 区 的 代码 即使 抛出 异常 也不 会 妨碍 内部 锁 的 释放。

内部锁的调度:Java 虚拟 机会 为 每个 内部 锁 分配 一个 入口 集( Entry Set), 用于 记录 等待 获得 相应 内部 锁 的 线程。 多个 线程 申请 同一个 锁 的 时候, 只有 一个 申请 者 能够 成为 该 锁 的 持有 线程( 即 申请 锁 的 操作 成功), 而 其他 申 请者 的 申请 操作 会 失败。 这些 申请 失败 的 线程 并不 会 抛出 异常, 而是 会被 暂停( 生命 周期 状态变为 BLOCKED) 并被 存入 相应 锁 的 入口 集中 等待 再次 申请 锁 的 机会。 入口 集中 的 线程 就被 称为 相应 内部 锁 的 等待 线程。 当 这些 线程 申请 的 锁 被 其 持有 线程 释放 的 时候, 该 锁 的 入口 集中 的 一个 任意 线程 会被 Java 虚拟 机 唤醒, 从而 得到 再次 申请 锁 的 机会。 由于 Java 虚拟 机 对 内部 锁 的 调度 仅 支持 非 公平 调度, 被 唤醒 的 等待 线程 占用 处理器 运行时 可能 还有 其他 新的 活跃 线程( 处于 RUNNABLE 状态, 且 未 进入 过 入口 集) 与 该 线程 抢占 这个 被 释放 锁, 因此 被 唤醒 的 线程 不一定 就能 成为 该 锁 的 持有 线程。 另外, Java 虚拟 机 如何 从 一个 锁 的 入口 集中 选择 一个 等待 线程, 作为 下一个 可以 参与 再次 申请 相应 锁 的 线程, 这个 细节 与 Java 虚拟 机 的 具体 实现 有关: 这个 被选 中的 线程 有可能 是 入口 集中 等待 时间 最长 的 线程, 也可能 是 等待 时间 最短 的 线程, 或者 完全 是 随机 的 一个 线程。 因此, 我们 不能 依赖 这个 具体 的 选择 算法。

4.3显示锁:Lock接口

显 式 锁 是 自 JDK 1. 5 开始 引入 的 排他 锁。 作为 一种 线程 同步 机制, 其 作用 与 内部 锁 相同。 它 提供 了 一些 内部 锁 所不 具备 的 特性, 但 并不是 内部 锁 的 替代品。

显 式 锁( Explicit Lock) 是 java. util. concurrent. lcoks. Lock 接口 的 实例, 该 接口 对 显 式 锁 进行 了 抽象。

显 式 锁 的 使用 方法如下所示。

private final Lock LOCK=....//创建一个Lock接口实例

......

lock.lock();//申请锁lock

try{

//在此对共享数据进行访问

......

}finally{

//总是在finally块中释放锁,以避免锁泄露

lock.unlock();//释放锁lock

}

虽然 释放 锁 的操作通过 调用 Lock. unlock() 即可 实现, 但是 为了 避免 锁 泄漏, 我们 必须 将 这个 调用 放在 finally 块 中 执行。 这样, 无论是 临界 区 代码 执行 正常 结束 还是 由于 其 抛出 异常 而 提前 退出, 相应 锁 的 unlock 方法 总是 可以 被 执行, 从而 避免 了 锁 泄漏。 可见, 显 式 锁 不像 内部 锁 那样 可以由 编译器 代为 规避 锁 泄漏 问题。

公平 锁 适合于 锁 被 持有 的 时间 相对 长 或者 线程 申请 锁 的 平均 间隔 时间 相对 长的 情形。遇到此情形时选用显示锁更为合理。总的来说公平锁的开销要比非公平锁大,因此,显示锁默认是采用非公平调度策略的。

注意:内部锁是基于代码块的锁,而显示锁是基于对象的锁(充分发挥面向对象编程的灵活性)。

4.4轻量级锁(volatile关键字)

volatile 关键字 用于 修饰 共享 可变 变量, 即 没有 使用 final 关键字 修饰 的 实例 变量 或 静态 变量, 相应 的 变量 就被 称为 volatile 变量,

private volatile int num;

volatile 关键字 表示 被 修饰 的 变量 的 值 容易 变化( 即被 其他 线程 更改), 因而 不稳定。 volatile 变量 的 不稳定 性 意味着 对这 种 变量 的 读 和 写 操作 都 必须 从高 速 缓存 或者 主 内存( 也是 通过 高速 缓存 读取) 中 读取, 以 读取 变量 的 相对 新 值。 因此, volatile 变量 不 会被 编译器 分配 到 寄存器 进行 存储, 对 volatile 变量 的 读写 操作 都是 内存 访问( 访问 高速 缓存 相当于主内存) 操作。即volatile关键字保证了共享变量的可见性,只要共享变量一发生改变,就会使得其他线程工作内存中的变量无效,并将改变后的值更新到主存中,其他线程会到主存中去更新该共享变量的值。

volatile 关键字 常被 称为 轻量级 锁, 其 作用 与 锁 的 作用 有 相同 的 地方: 保证 可见 性 和 有序 性。 所 不同 的 是, 在 原子 性 方面 它 仅能 保障 写 volatile 变量 操作 的 原子 性, 但 没有 锁 的 排他性; 其次, volatile 关键字 的 使用 不会 引起 上下文 切换( 这是 volatile 被 冠以“ 轻量级” 的 原因)。 因此, volatile 更 像是 一个 轻量级简易( 功能 比 锁 有限) 锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值