您的位置 首页 java

Java并发编程基础

并发与并行

并行 :是说在单位时间内多个任务同时在执行。 如下图所示,双 CPU配置,线程A和线程B各自在自己的 CPU上执行任务,实现了真正的并行运行。

Java并发编程基础

并发 :同一个时间段内多个任务同时都在执行,并且都没有执行结束。

如下图所示,在单个 CPU 上运行两个 线程 ,线程 A 和线程 B 是轮流使用 CPU 进行任务处理的,也就是在某个时间内单个 CPU 只执行一个线程上面的任务。 当线程 A 的时间片用完后会进行线程上下文切换,也就是保存当前线程 A 的执行上下文,然后切换到线程B 来占用 CPU 运行任务。

Java并发编程基础

多线程处理共享变量时 Java 的内存模型

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。如下图所示

Java 中的 synchronized 关键字

synchronized 块是 Java 提供的一种原子性内置锁, Java 中的每个对象都可以把它当作 一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫监视器锁。

线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的 wait 系列方法时释放该内置锁。内置锁是排它锁, 也就是当一个线程获取这个锁后, 其他线程必须等待该线程释放锁后才能获取该锁。

另外,由于 Java 中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized 的使用就会导致上下文切换。

Java 中的 volatile 关键字

使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。 对于解决内存可见性问题, Java 还提供了一种弱形式的同步,也就是使用 volatile 关键字。 该关键字可以确保对一个变量的更新对其他线程马上可见。 当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。 volatile 的内存语义和 synchronized 有 相似之处,具体来说就是,当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取 volatile 变量值时就相当于进入同 步块 (先清空本地内存变量值,再从主内存获取最新值)。

Java 中的 CAS 操作

在 Java 中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。 Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是 volatile 只能保证共享变量的可见性,不能解决读-改-写等的原子性问题。

CAS 即 Compare and Swap,是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较–更新操作的原子性。一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

1) 比较 A 与 V 是否相等。(比较)

2) 如果比较相等,将 B 写入 V。(交换)

3) 返回操作是否成功。

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

关于 CAS 操作有个经典的 ABA 问题, 具体如下: 假如线程 I 使用 CAS 修改初始值为 A 的变量 X, 那么线程 I 会首先去获取当前变量 X 的值(为 A), 然后使用 CAS 操作尝试修改 X 的值为 B, 如果使用 CAS 操作成功了 , 那么程序运行一定是正确的吗?其实未必, 这是因为有可能在线程 I 获取变量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改了变量 X 的值为 B,然后又使用 CAS 修改了变量 X 的值为 A。 所以虽然线程 I 执行 CAS 时 X 的值是 A,但是这个 A 己经不是线程 I 获取时的 A 了。 这就是 ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B, 然后再从 B 到 A。如果变量的值只能朝着一个方向转换,比如 A 到 B , B 到 C, 不构成环形,就不会存在问题。 JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

文章来源:智云一二三科技

文章标题:Java并发编程基础

文章地址:https://www.zhihuclub.com/199735.shtml

关于作者: 智云科技

热门文章

网站地图