【Java并发编程 三】Java并发机制的底层实现(二)

简介: 【Java并发编程 三】Java并发机制的底层实现(二)

接下来分别举例说明同步方法和同步代码块的使用:

对于普通同步方法,锁是当前方法所属的实例对象。

package com.company;
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ; i < 5 ; i++){
            new Thread(new Runner(),"Thread_" + i).start();
        }
    }
}
class Runner implements Runnable {
    public void run(){
        runInfo();
    }
    public synchronized void runInfo(){
        for (int i = 0; i < 3; i++) {
            System.out.println("Runner Thread----- :" + Thread.currentThread().getName());
        }
    }
}

运行结果

Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_2
Runner Thread----- :Thread_0
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4

这个结果与我们预期的结果有点不同(这些线程在这里乱跑),照理来说,run方法加上synchronized关键字后,会产生同步效果,这些线程应该是一个接着一个执行run方法的。在上面提到,一个成员方法加上synchronized关键字后,实际上就是给这个成员方法加上锁,具体点就是以这个成员方法所在的对象本身作为对象锁。但是在这个实例当中我们一共new了5个ThreadTest对象,那个每个线程都会持有自己线程对象的对象锁,这必定不能产生同步的效果。所以:如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的!

package com.company;
public class ThreadTest {
    public static void main(String[] args)  {
        Runner runner=new Runner();
        for(int i = 0 ; i < 5 ; i++){
            new Thread(runner,"Thread_" + i).start();
        }
    }
}
class Runner implements Runnable {
    public void run(){
        runInfo();
    }
    public synchronized void  runInfo(){
        for (int i = 0; i < 3; i++) {
            System.out.println("Runner Thread----- :" + Thread.currentThread().getName());
        }
    }
}

运行结果

Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4

对于同步方法块,锁是Synchonized括号里配置的对象。

package com.company;
public class ThreadTest {
    private static final Object MONITOR = new Object();
    public static void main(String[] args)  {
        for(int i = 0 ; i < 5 ; i++){
           new Thread(() -> {
                synchronized (MONITOR) {
                    for (int j = 0; j < 3; j++) {
                        System.out.println("Runner Thread----- :" + Thread.currentThread().getName());
                    }
                }
            },"Thread_" + i).start();
        }
    }
}

运行结果如下:

Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1

我们创建了一个MONITOR对象 ,对象锁是唯一且共享的。线程同步!在这里synchronized锁住的就是MONITOR对象

对于静态同步方法,锁是当前对象的Class对象。

package com.company;
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ; i < 5 ; i++){
            new Thread(new Runner(),"Thread_" + i).start();
        }
    }
}
class Runner implements Runnable {
    public void run(){
        runInfo();
    }
    public static synchronized void runInfo(){
        for (int i = 0; i < 3; i++) {
            System.out.println("Runner Thread----- :" + Thread.currentThread().getName());
        }
    }
}

运行结果如下

Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_0
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_4
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_3
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_2
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1
Runner Thread----- :Thread_1

在这个实例中,run方法使用的是一个同步方法,而且是static的同步方法,那么这里synchronized锁的又是什么呢?我们知道static超脱于对象之外,它属于类级别的。所以,对象锁就是该静态放发所在的类的Class实例。由于在JVM中,所有被加载的类都有唯一的类对象,在该实例当中就是唯一的 Runner类实例。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!所以对象锁是唯一且共享的。线程同步!

锁的内存语义

了解了synchronized的实现之后,我们想知道它底层到底采取了什么机制保障了这样的实现,我们从锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

下面对锁释放和锁获取的内存语义做个总结。

  1. 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
  2. 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  3. 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息

了解了锁的内存语义后我们再来看看锁到底存在哪里,怎么优化和转换。

1. 获得同步锁;
2. 清空工作内存;
3. 从主内存拷贝对象副本到工作内存;
4. 执行代码(计算或者输出等);
5. 刷新主内存数据;
6. 释放同步锁

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键

  • 锁的释放,公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

锁释放-获取的内存语义的实现至少有下面两种方式。

  • 利用volatile变量的写-读所具有的内存语义。
  • 利用CAS所附带的volatile读和volatile写的内存语义

而在临界区执行前,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的。

实际上在指令级我们可以看到:synchronized 同步语句块的实现使⽤的是 monitorentermonitorexit 指令,其中 monitorenter指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌

Java对象头

那么锁到底存在哪里呢?锁里面会存储什么信息呢?synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,在JVM系列的Blog中我们介绍过Java对象头的组成形式:HotSpot虚拟机的对象头包括两部分信息,分别是Mark Word和类型指针(klass pointer),锁就存储在Mark Word中:

各部分的含义如下:

  • 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
  • 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

当然还有32位虚拟机的布局,该布局组成元素同64位相同,只是占用大小略有不同

锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁轻量级锁,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

设置锁的操作

我们给对象设置锁时,使用的方式是CAS(Compare and Swap)比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。传入两个参数,旧值(期望操作前的值)和新值,执行时会比较旧值是否和给定的数值一致,如果一致则修改为新值,不一致则不修改新值

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁加锁

偏向锁的加锁流程如下,主要就是检测对象头中的偏向锁信息。

  1. 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
  2. 以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
  3. 如果测试成功,表示线程已经获得了锁。
  4. 如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁)
  1. 如果已设置,则尝试使用CAS将对象头的偏向锁指向当前线程
  2. 如果未设置,则使用CAS竞争锁;

以上就是偏向锁的加锁流程。

偏向锁撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)才会执行

  1. 它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着
  2. 如果线程不处于活动状态,则将对象头设置成无锁状态,然后重新偏向其它线程
  3. 如果线程仍然活动着,检查该对象的使用情况
    1. 如果仍然需要持有偏向锁,也就是产生了竞争,则偏向锁升级为轻量级锁
    2. 如果不需要持有偏向锁,则重新变为无锁状态,然后重新偏向新的线程,本线程偏向锁撤销。
  4. 最后唤醒暂停的线程。

偏向锁比轻量锁更容易被终结,轻量锁是在有锁竞争出现时升级为重量锁,而一般偏向锁是在有不同线程申请锁时升级为轻量锁,这也就意味着假如一个对象先被线程1加锁解锁,再被线程2加锁解锁,这过程中没有锁冲突,也一样会发生偏向锁失效,不同的是这回要先退化为无锁的状态,再加轻量锁,如果是线程1持有锁,且2也要争夺偏向锁,则直接到轻量级锁状态

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word

  1. 轻量级加锁时,线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
  2. 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

解锁失败会导致膨胀

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争

三种锁对比

以下是偏向锁、轻量级锁以及重量级锁三者之间的优缺点和使用场景。

我们也可以按照时间线的顺序来看待这三个锁的状态变化

  • 成为偏向锁 ,一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
  • 升级为轻量级锁,一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。假如一个对象先被线程1加锁解锁,再被线程2加锁解锁,这过程中没有锁冲突,也一样会发生偏向锁失效,不同的是这回要先退化为无锁的状态,再加轻量锁,如果是线程1持有锁,且2也要争夺偏向锁,则直接到轻量级锁状态
  • 膨胀为重量级锁,轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

以上就是锁状态的切换过程

原子操作

原子操作(atomic operation)意为不可被中断的一个或一系列操作,在多线程中实现原子操作较为复杂。处理器使用基于对缓存加锁总线加锁的方式来实现多处理器之间的原子操作。

  • 首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
  • Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问

但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性

处理器如何保证操作原子性

处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性

使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2

原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存

使用缓存锁保证原子性

第二个机制是通过缓存锁定来保证原子性。在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,

  • 在Pentium 6和目前的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

当CPU1修改缓存行中的i时使用了缓存锁定,那么CPU2就不能同时缓存i的缓存行,但是有两种情况下处理器不会使用缓存锁定。

  • 第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
  • 第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

针对以上两个机制,我们通过Intel处理器提供了很多Lock前缀的指令来实现

Java如何保证操作原子性

在Java中可以通过锁和循环CAS的方式来实现原子操作

使用循环CAS实现原子操作

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count

package com.company;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadTest {
    private AtomicInteger atomicI = new AtomicInteger(0);  //安全计数器共享变量atomicI初始化值0
    private int a = 0;     //安全计数器共享变量a初始化值0
    public static void main(String[] args) {
        final ThreadTest cas = new ThreadTest();
        List<Thread> ts = new ArrayList<>(600);
        //开启100个线程,每个线程执行10000次,总计执行一百万次
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }
        // 所有线程开始执行
        for (Thread t : ts) {
            t.start();
        }
        // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("count "+cas.a);
        System.out.println("safecount "+cas.atomicI.get());
    }
    /** * 使用CAS实现线程安全计数器 */
    private void safeCount() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }
    /**
     * 非线程安全计数器
     */
    private void count() {
        a++;
    }
}

执行结果如下,可以看到安全执行的原子操作刚好符合预期。

count 987753
safecount 1000000

可以看看该方法操作的源码:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

继续向下钻取查看:

//预期引用
 private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

这里还有个比较有意思的是继续钻取查看,发现该类定义的变量为volatile ,就是为了满足共享变量的可见性。

private volatile int value;

CAS的问题

在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,CAS虽然很高效地解决了原子操作,但是CAS仍然存在三大问题。ABA问题循环时间长开销大,以及只能保证一个共享变量的原子操作

  • ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
  • 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用

虽然有折中解决的办法,例如循环开销大可以使用处理器的指令pause,只能保证一个共享变量原子操作可以考虑把多个共享变量合并成一个共享变量来操作。但最好的解决方式还是使用锁

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

总结

本篇Blog从并发编程三大特性出发,探索了Java底层对于该三个原则的满足方式,分别讨论了volatile关键字和synchronized关键字从底层对Java并发的支持。这里只是做了一个大概的理解,很多概念在深入理解了JMM底层的内存机制后再进行详细阐述。

相关文章
|
1天前
|
数据采集 安全 Java
Java并发编程学习12-任务取消(上)
【5月更文挑战第6天】本篇介绍了取消策略、线程中断、中断策略 和 响应中断的内容
21 4
Java并发编程学习12-任务取消(上)
|
1天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第15天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将通过实例分析,理解线程安全的重要性,并学习如何通过各种技术和策略来实现它。同时,我们也将探讨如何在保证线程安全的同时,提高程序的性能。
|
1天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第15天】 在Java的多线程编程中,锁机制是实现线程同步的关键。然而,不当的锁使用往往导致性能瓶颈甚至死锁。本文深入探讨了Java并发编程中针对锁的优化策略,包括锁粗化、锁消除、锁分离以及读写锁的应用。通过具体实例和性能分析,我们将展示如何有效避免竞争条件,减少锁开销,并提升应用程序的整体性能。
|
1天前
|
Java 开发者
深入理解Java并发编程:从基础到高级
【5月更文挑战第13天】本文将深入探讨Java并发编程的各个方面,从基础知识到高级概念。我们将首先介绍线程的基本概念,然后深入讨论Java中的多线程编程,包括线程的创建和控制,以及线程间的通信。接下来,我们将探讨并发编程中的关键问题,如同步、死锁和资源竞争,并展示如何使用Java的内置工具来解决这些问题。最后,我们将讨论更高级的并发编程主题,如Fork/Join框架、并发集合和并行流。无论你是Java新手还是有经验的开发者,这篇文章都将帮助你更好地理解和掌握Java并发编程。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】本文将深入探讨Java并发编程中的一个重要主题——线程池。我们将从线程池的基本概念入手,了解其工作原理和优势,然后详细介绍如何使用Java的Executor框架创建和管理线程池。最后,我们将讨论一些高级主题,如自定义线程工厂和拒绝策略。通过本文的学习,你将能够更好地理解和使用Java的线程池,提高你的并发编程能力。
|
1天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
1天前
|
缓存 Java
Java并发编程:深入理解线程池
【5月更文挑战第7天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将了解线程池的基本概念,以及如何使用Java的Executor框架来创建和管理线程池。此外,我们还将讨论线程池的优点和缺点,以及如何选择合适的线程池大小。最后,我们将通过一个示例来演示如何使用线程池来提高程序的性能。
|
1天前
|
缓存 Java 调度
Java并发编程:深入理解线程池
【4月更文挑战第30天】 在Java并发编程中,线程池是一种重要的工具,它可以帮助我们有效地管理线程,提高系统性能。本文将深入探讨Java线程池的工作原理,如何使用它,以及如何根据实际需求选择合适的线程池策略。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】 本文将深入探讨Java中的线程池,解析其原理、使用场景以及如何合理地利用线程池提高程序性能。我们将从线程池的基本概念出发,介绍其内部工作机制,然后通过实例演示如何创建和使用线程池。最后,我们将讨论线程池的优缺点以及在实际应用中需要注意的问题。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第29天】在Java中,线程池是一种管理线程的强大工具,它可以提高系统性能,减少资源消耗。本文将深入探讨Java线程池的工作原理,如何使用它,以及在使用线程池时需要注意的问题。