java多线程并发

2023-11-03

目录

一、原子性+有序性+可见性

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

1、原子性

为了直观地了解什么是原子性,我们看下下面哪些操作是原子性操作

int count = 0; // 1
count++; // 2
int a = count; // 3

除了语句1是原子操作,其它两个语句都不是原子性操作,下面我们来分析一下语句2

其实语句2在执行的时候,包含三个指令操作:

  • 指令 1:首先,需要把变量 count 从内存加载到 CPU 的高速寄存器
  • 指令 2:之后,在寄存器中执行 +1 操作
  • 指令 3:最后,将结果写入内存

对于上面的三条指令来说,如果线程 A 在指令 1 执行完后做线程切换,线程 B 执行完3个指令,又切换回线程 A ,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。这也是经典的缓存一致性问题。这个能被多个线程访问的变量count称为共享变量
在这里插入图片描述

注意:操作系统做任务切换,可以发生在任何一条CPU 指令执行完

2、有序性

为了性能优化,编译器和处理器会进行指令重排序,有时候会改变程序中语句的先后顺序,在多线程并发运行的程序中,这种优化可能会导致执行结果的变化。

例如,在单例模式中,如下:

public class Singleton {
	static Singleton instance;
	static Singleton getInstance() { // 返回实例
		if (instance == null) {
			synchronized(Singleton.class) { // 同一时间只能有一个线程持有类Singleton的class对象
				if (instance == null)
					instance = new Singleton(); // 创建实例
				}
			}
		return instance;
		}
	}

创建实例的语句 instance = new Singleton() 未被编译器优化的指令执行顺序:

  • 指令 1:分配一块内存 M
  • 指令 2:在内存 M 上初始化 Singleton 对象
  • 指令 3:将 M 的地址赋值给 instance 变量

编译器优化后:

  • 指令 1:分配一块内存 M
  • 指令 2:将 M 的地址赋值给 instance 变量
  • 指令 3:然后在内存 M 上初始化 Singleton 对象。

假如现在有A、B两个线程,线程 A 先执行 getInstance() 方法,执行完优化后的指令2时,发生了线程切换,线程B开始执行该方法,执行到第一次判断 instance==null 会发现 instance 不等于 null 了,所以直接返回 instance ,而此时的 instance 是没有初始化过的,与预期不符。现行的比较通用的做法就是采用静态内部类的方式来实现单例模式:

public class SingletonDemo {
	private SingletonDemo() {
    }
    private static class SingletonDemoHandler { // 静态内部类
		private static SingletonDemo instance = new SingletonDemo(); // 静态成员变量,在这个内部类加载的时候,就被加入到<clinit>()方法中运行
	}
	public static SingletonDemo getInstance() {
		return SingletonDemoHandler.instance; // 创建内部类实例,此时加载内部类到 JVM
	}
}

3、可见性

指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

首先看一下 java 内存的结构:
在这里插入图片描述

  • 我们定义的所有变量都储存在主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同线程之间也无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来进行

线程1对共享变量的修改要被线程2及时看到的话,要经过如下步骤:

  • 把工作内存1中更新的变量值刷新到主内存
  • 把主内存中变量的值更新到工作内存2

二、如何保证以上三性

1、硬件层面解决

回到缓存不一致问题,也就是破坏了java语句原子性的问题。如果一个变量在多个CPU(或一个CPU的不同时间段)中都存在缓存,就可能存在缓存不一致的问题。为了解决缓存不一致性问题,通常来说在硬件层面有以下 2 种解决方法:

1)在总线加 LOCK# 锁

在早期的 CPU 中,通过在总线上加 LOCK# 锁可以解决缓存不一致问题。因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线加 LOCK# 锁的话,也就能够阻塞其他 CPU 对其他计算机部件(如内存)的访问,从而使得同一时刻只能有一个 CPU 使用这个变量的内存数据。比如上面一节的例子中,如果一个线程在执行 count++,在执行这段代码的过程中,总线上发出了 LOCK# 锁的信号,那么只有等待这段代码完全执行完毕之后,其他 CPU 才能再从内存读取变量count的值,然后进行操作。这样就解决了缓存不一致的问题。

但是上面的方式导致用 LOCK# 锁住总线的期间,其他 CPU 无法访问内存,导致线程并发效率低下。

2)使用缓存一致性协议

为了保证一定的并发效率,不使用总线加锁的方式,缓存一致性协议出现了。最出名的就是英特尔的 MESI 协议,MESI协议保证了每个缓存中使用的共享变量副本是一致的。它核心的思想是:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也可能存在该变量的副本,就会发出信号通知其他 CPU 将该变量的缓存置为无效状态,因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

此种方法比锁总线的方法效率更高,当锁总线的方法更加通用。缓存一致性协议也是volatile的一部分底层实现保证。

2、软件层面解决

1)背景:JVM内存模型

即前一节可见性中介绍的内存模型,它是在软件侧面解决 java 并发问题的基础,它能够屏蔽各个硬件平台和操作系统的内存访问差异,实现让 java 程序在各种平台下都能达到一致的内存访问效果。

java 内存模型定义了程序中变量的访问规则,往大一点说,定义了程序执行的次序。注意,为了获得较好的执行性能, Java 内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在 java 内存模型中,也会存在缓存一致性问题和指令重排序的问题。所以,Java内存模型的设计并没有直接解决三性问题,而是提供了解决的基础。

2)原子性

在java中,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

注意:在 32 位平台下,对 64 位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的 JDK 中,JVM 已经保证对 64 位数据的读取和赋值也是原子性操作了。

java 内存模型只保证了基本读取和常量赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过 synchronized、Lock或CAS机制来实现。

3)可见性

对于可见性,Java提供了 volatile 关键字来保证。

当一个共享变量被 volatile 修饰时,它的值一旦被修改,就会被立即更新到主存,当其他线程要读取该值时,会直接去内存中读取新值,而不是在自己的私有工作空间读取旧值。未被volatile修饰的共享变量不能保证可见性,因为被修改之后,最新的值什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值。

另外,synchronized 和 Lock 也能够保证可见性,因为synchronized 和 Lock 能保证同一时刻只有一个线程获取锁,然后执行同步代码,所以不存在共享变量,也就不会有别的线程来修改已经被 synchronized 和 Lock 锁定的变量。在释放锁之前会将对变量的修改刷新到主存当中。

4)有序性

Volatile能够禁止指令重排序,但synchronized 和 Lock不会禁止指令的重排序,因为它们将线程编程了串行执行的,即时重排序也不会影响串行执行线程的执行结果,所以也就不禁止了。

另外,Java 内存模型具备一些先天的有序性,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从 happens-before 原则推导出来,那么就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
  • volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作C
  • 线程启动规则:Thread 对象的 start() 方法先行发生于此线程内的每个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

前 4 条规则是比较重要的,后 4 条规则都是显而易见的,总结下来就是:

  1. 没有依赖关系的指令的顺序可以随意调整
  2. 同步区的锁必须被释放后,下一个线程才能获取这把锁
  3. 写先于读
  4. 传递性,A先于B没问题,B先于C也没问题,那么A先于C也没问题

三、java对象的内存布局

此部分内容来自马士兵教育在 b 站的 jvm 公开课,此部分和锁的实现较为相关

java对象的内存布局,即java对象在堆中存储时的内存布局。在maven中引入工具JOL(java object layout)后,可以使用System.out.println(ClassLayout.parseInstance(new Object()).toPrintable();输出java对象的内存布局:
在这里插入图片描述
new一个对象,此对象的内存布局如下:
在这里插入图片描述
每个对象都拥有锁,都可以被独占,对象头中的 markword 是用来保存锁状态的,非常重要,会在锁升级部分中讲解markword具体保存什么数据

1、markword:8字节,记录锁这个对象的信息

2、class pointer:指向该对象的Class对象,markword和class pointer合起来就是对象头。默认情况下,JVM开启了指针压缩,此部分占4字节,若不开启指针压缩,此部分在64位机器上就是8字节。通过jvm参数-XX:+UseCompressedClassPointers可以设置。另一个参数叫做-XX:+UseCompressedOops意为普通对象指针压缩,如指向String常量的String类型引用变量就是这种指针,被压缩为4字节

3、instance data:成员变量的引用或基础变量直接存储

4、padding:使得整个对象占有的内存达到8字节的倍数,因为总线读内存的时候,按照8字节的倍数读,如果分开存储就会变慢,所以padding是用来提高效率用的

数组的内存布局:
在这里插入图片描述

四、synchronized

1、介绍

此关键字用来锁定一个对象。纠正一个错误:代码是不会被锁定的,只能锁定对象

Object o = new Object();
synchronized(o){
	// 执行这段代码的时候,对象o被此线程独占
}

synchronized void methodName() {
	// 此方法执行过程中,this对象被锁定
}

static synchronized void methodName() {
	// 此方法执行过程中,this对象的class对象被锁定
	// 相当于写成:
	// synchronized(Main.class)
}

具体实现过程:
1、代码层面:synchronized
2、字节码层面:在互斥代码区前面加 monitor enter ,在互斥代码区最后加 monitor exit
3、JVM层面:进行锁升级
4、汇编指令:lock cmpxchg,利用计算机系统的mutex Lock实现。每一个可重入锁都会关联一个线程ID和一个锁状态status,所以很好做重入操作的判断。

2、锁升级(无锁、偏向锁、轻量级锁、重量级锁)

JDK1.6 为了提升性能,减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁(new)、偏向锁、轻量级锁(自旋锁、无锁CAS)和重量级锁,它会随着多线程竞争变得激励而逐渐升级。锁降级是gc中的一个过程,就是没有任何线程访问这个对象,只有gc线程访问它,所以说降级没有任何意义,所以可以认为锁降级不存在。

  1. 无锁
    无锁状态其实就是乐观锁
  2. 偏向锁
    Java偏向锁(Biased Locking)是指它会偏向于第一个访问这个锁的线程。如果在运行过程中,只有一个线程访问某对象,不存在多线程竞争的情况,那么线程是不需要重复获取这个锁的,这种情况下,就会给这个对象加一个偏向锁,当下次这个线程又需要锁定此对象时,直接使用这个临界资源即可,无需重新上锁。某对象为偏向锁状态时,它的Mark Word包含了指向这个线程的指针。
  3. 轻量级锁
    一旦有别的线程来竞争锁了,就立刻升级为轻量级锁。具体升级过程如下:首先撤掉 markword 里面指向第一个线程的那个指针,然后正在竞争的几个线程都在自己的线程栈内创建一个 lock record 对象,一旦某个线程抢到了这个锁,就在markword里存:指向抢到锁的线程对应lock record对象的指针。抢的过程是自旋的过程,即CAS。
  4. 重量级锁
    如果线程并发进一步加剧,线程的自旋超过了一定次数,或者CPU有一半的核都在执行自旋,轻量级锁就会升级为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞(不自旋,也就不消耗CPU资源)。升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。实际上重量级锁是OS内核分配的,内核中有一定数量mutex(互斥锁),当要升级为重量锁时,内核会为某对象分配一个mutex,markword内部就会存有指向这个mutex的指针。

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。synchronized默认是一个非公平锁,所有挂起的线程会被放在等待队列中,这个队列默认是无序的。在hotspot中,不同的锁状态对应markword为:
在这里插入图片描述
图源:马士兵教育在b站的jvm公开课视频

在实际运行中,要获得锁状态,会首先检查最右两位“锁标志位”,然后检查偏向锁位,即可分出5种状态来。

上图中的分代年龄即gc回收需要使用的对象年龄记录。ps/po中的默认值为15,CMS中的默认值为6,分代年龄用4位表示,最大就是15,不能再更大了。

五、volatile

1、Volatile可以保证单个共享变量可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程(其他cpu)来说是立即可见的
  • 禁止进行指令重排序(后面再说)

先看一段代码,假如线程 1 先执行,线程 2 后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

这是一段很典型的代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,线程1不一定会被成功中断。在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

因为线程1和2在运行的时候,会将 stop 变量的值拷贝一份放在自己的工作内存当中。当线程 2 更改了 stop 变量的值之后,有可能在还没来得及写入主存中的时候,就发生了线程切换,线程 2 被迫阻塞,而线程1不知道线程 2 对 stop 变量的更改,会一直循环下去(假设是多核CPU)。

但是用 volatile 修饰之后,在工作内存中被修改的值会立即写入主存,且当线程 2 修改stop时,线程 1 的工作内存中缓存变量 stop 的缓存行被置为无效状态,线程 1 只能再次从内存读取变量 stop 的值,才能接着使用。

2、Volatile不保证原子性

看例子:

public class Main {
    volatile int inc = 0;

    void increase() {
        inc++;
    }

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 10; i++) {
            new Thread(){
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        main.increase();
                    }
                }
            }.start();
        }

        while(Thread.activeCount()>1){ // 保证前面的进程都执行完了
            Thread.yield();
        }
        System.out.println(main.inc);
    }
}

多次运行发现,上面的运行结果都不一样,都小于我们的预期10000

因为inc++这个语句包括3个指令:1)读入工作内存;2)+1;3)写入内存。

假如线程1刚刚把inc读入工作内存,就被阻塞,然后线程2开始执行inc++,由于线程1尚未进行+1,也就不会导致线程2的inc对应缓存行无效,所以线程2会直接执行+1指令,然后写入内存,这时线程1回来了,继续执行+1和写入内存,结果就是内存中的inc只+1,而不是+2

注意:Volatile的起效时刻是线程读入工作内存的时刻,在这一时刻,线程会判断inc是否已经在别的线程的工作内存被改变了,但我们的例子中,线程1读完inc以后,inc才被线程2改变,所以就出现错误啦

改的方法有3个(前两个是直接用重量级锁,最后一个是使用无锁的CAS):

1.用synchronized

public class Main {
    int inc = 0; // 变量不用管,因为方法是同步的,一次必然只有一个线程操作inc

    synchronized void increase() { // 方法改成同步的
        inc++;
    }

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 10; i++) {
            new Thread(){
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        main.increase();
                    }
                }
            }.start();
        }

        while(Thread.activeCount()>1){ // 保证前面的进程都执行完了
            Thread.yield();
        }
        System.out.println(main.inc);
    }
}

2.使用Reentrant锁

public class Main {
    int inc = 0;
    Lock lock = new ReentrantLock(); // 使用Reentrant锁

    void increase() {
        lock.lock(); // 锁定
        try {
            inc++;
        } finally {
            lock.unlock(); // 保证锁一定被解开
        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        main.increase();
                    }
                }
            }.start();
        }

        while (Thread.activeCount() > 1) { // 保证前面的进程都执行完了
            Thread.yield();
        }
        System.out.println(main.inc);
    }
}

3.使用AtomicInteger

import java.util.concurrent.atomic;
public class Main {
    AtomicInteger inc = new AtomicInteger(); // 一种保证自增原子性的类

    void increase() {
        inc.getAndIncrement();
    }

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        main.increase();
                    }
                }
            }.start();
        }

        while (Thread.activeCount() > 1) { // 保证前面的进程都执行完了
            Thread.yield();
        }
        System.out.println(main.inc);
    }
}

java 1.5的 java.util.concurrent.atomic 包下提供了一些原子操作类,即对基本数据类型的自增、自减、以及加法操作、减法操作进行了封装,保证这些操作是原子性操作。atomic 包是利用 CAS 来实现原子性操作的(Compare And Swap),CAS 实际上是利用处理器提供的 CMPXCHG 指令实现的,而处理器执行 CMPXCHG 指令是一个原子性操作。

3、Volatile能保证有序性

编译器的优化行为——重排序,会导致指令乱序执行。比如2条指令:1、去内存读取数据;2、与第一条指令无关。指令1必然较慢,编译器会使得cpu等待指令1结束的时间内,首先执行完成指令2。此即发生了乱序执行,也叫流水线执行,多线程并发环境下会出现一些问题。

volatile会禁止重排序,但并不是真的不让编译器优化,而是只可以优化部分指令顺序。
举个例子:

//x、y为非volatile变量
//flag为volatile变量
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

由于 flag 变量为 volatile 变量,那么重排序的时候,就不会将3放到1和2前面,也不会将3放到4和5后面。但是要注意1和2之间的顺序、4和5之间的顺序是不作任何保证的。并且 volatile 关键字能保证,执行到 3 时,1 和 2 必定是执行完毕了的,且1和2的执行结果对345都是可见的。

怎么实现的呢?

答:当我们在源码中的某变量前加了volatile修饰,字节码中就会多一条字节码ACC_VOLATILE,JVM执行这条字节码的时候,就会添加不同的内存屏障。可以把内存屏障看成一堵墙,编译器优化指令顺序时,不允许将屏障两边的指令顺序搞乱。jvm规范(JSR)中要求jvm的实现必须提供以下类型的内存屏障:LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障。例如LoadLoad屏障前后的2条读指令不可重排序,其他屏障同理。

再详细一点看,在volatile变量的写操作之前,需要加SS屏障,后面要加SL屏障,为什么呢?SS屏障前面的读操作和其后面的读操作不能换顺序,白话就是:必须写完了,才能写。同理,SL屏障前面的写操作和后面的读操作不能换顺序,白话就是:必须写完了,才能读。

在volatile变量的操作之前,需要加LL屏障,后面要加LS屏障。

所以说,内存屏障其实是jvm来加的,至于怎么实现,去看c++源码吧,其实跟系统底层的fence等原语无关,而是用的lock指令,把总线锁了。为啥不用cpu支持的原语,非要自己实现呢?因为不是所有cpu的这些原语都支持一致性,但lock指令是几乎所有cpu都支持的,所以jvm偷懒用了这条指令。

4、cache line缓存行

cpu和主存之间的缓存是分级的,它们之间读取是以chache line为基本单位的,一个cache line大小为64字节,也可称为按块读取。按块读取的设计主要是考虑了磁盘上数据的局部性质,例如我们需要读取x的值,那么与它相邻的y通常就是下一个要读取的变量。下图就是缓存的一个示意:
在这里插入图片描述
L3由多个核共享。现在假设一种场景:

线程1主要利用cpu1修改上图中的x,线程2主要利用cpu2修改上图中的y,已知x和y在一个cache line中,并且它们都被volatile关键字所修饰。那么每次x或y的值被修改,都会通知另一个线程x或y的值发生变化,下次要使用此变量的值,请重新去主存中读取,另一个线程就会读取相应的缓存行到自己的缓存中去。因为在cpu层面的数据一致性保证是以cache line为单位的,所以每次都会读整个缓存行,而不是单独的一个x或y,虽然可能只有x或y被volatile修饰。此即缓存一致性协议,英特尔的cpu用的是MESI协议,别的就不一定了。MESI缓存一致性协议对缓存行进行状态标记:modified、exclusive、shared、invalid。当某些数据过大,一个缓存行无法存下,就会只用锁总线的方式进行缓存一致性的实现(锁总线是一个实现缓存一致性的万能方式)。

代码实现以上场景:

public class T1 {
    static class T {
        public volatile long x = 0L; // 8字节
    }

    static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

上面的代码中,arr[0]和arr[1]大概率会在同一个缓存块中,每次修改arr[0]或arr[1]中的x都会通知另一个线程重新读取主存中的缓存行,运行结果为200多毫秒。如果做如下修改:

public class T2 {
    static class Padding {
        public volatile long p1, p2, p3, p4, p5, p6, p7;
    }
    static class T extends Padding {
        public volatile long x = 0L;
    }
    static T[] arr = new T[2];
    static {
        arr[0] = new T();
        arr[1] = new T();
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

上面的代码将两个x弄进了两个缓存行内,运行结果为90毫秒左右,提升效率,这就叫做缓存行对齐技术。消息队列框架Disruptor就采用了这种技术,部分源码:

public long p1, p2, p3, p4, p5, p6, p7; // padding
private volatile long cursor = INITIAL_CURSOR_VALUE; // 关键
public long p8, p9, p10, p11, p12, p13, p14; // padding

关键值cursor的前后都加了padding,使得cursor不会和其他变量进入同一缓存行内。

超线程:如果一个核内有多组寄存器(指令寄存器+数据寄存器),那么不同的线程就可以在一个核内同时运行,所谓的四核八线程、二核十六线程就是这个概念。这样的设计是因为逻辑计算单元ALU速度太快了,多套寄存器可以更加充分地利用ALU的算力。

六、CAS机制

1、介绍

CAS(Compare And Swap),即比较并交换。使用Synchronized和Lock类,可以实现原子性,但它们是悲观锁,有时会导致频繁的上下文切换,消耗太大。CAS算法可以实现无锁的原子性保证,同时消耗不像上述重量级锁那么大。

CAS 操作包含三个操作数——内存位置V、预期原值A和新值B。Volatile修饰的数据原值A被线程从主存拿到后,计算得到最新的值B,比较 A 和主存当前存储的值V,若 V 与 A 相等,则更新B到主存中,此变量值修改成功;若不相等,说明这个原值被别的线程改了,修改就失败了,之后就会一直循环重试,直到V与A相等。
在这里插入图片描述

2、CAS的问题

1)ABA问题

一个变量 count 被线程1修改为值 count1,后来又被线程2修改回值 count,此时CAS操作就会成功,但是此时的 count 已经不是之前的 count 了。解决这个问题的方法是:给每个值前面加一个版本号,每次更新值的同时也会更新版本号,就可以知道此时的count 是不是未被修改过的 count 。

从 Java 1.5 开始 JDK 的atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法会首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的新值。

2)原子性问题

例如线程1检查V==A完毕后,尚未将V赋值为B,另一个线程2过来将V改成了C,然后又切换回线程1,线程1不会再检查V是否等于A,便直接赋值为B,就出错啦!所以要保证CAS在多线程并发时的正确性,就必须保证它的原子性。在Unsafe的各种本地方法中,在jvm层面c++实现了汇编级的原子性。指令就是:lock cmpxchg

3)循环时间长,开销大

利用CAS可以实现自旋锁,自旋锁就是请求锁不成功时,线程会一直循环地等待,直到锁释放。这种无锁方式,适用于线程较少、任务执行快且频繁的场景下,因为如果执行太慢、更新不频繁,自旋的设计会导致CPU一直在那个自旋的循环内空转,浪费资源。相反,线程数少、执行时间比较长时,用重量级锁synchronized。

3、应用

在 Java 中,sun.misc.Unsafe类提供了硬件级别的原子操作来实现这个 CAS,java.util.concurrent包下的大量类都使用了这个Unsafe类的 CAS 操作。

java.util.concurrent.atomic包下的类大多是使用 CAS 操作来实现原子性操作的,如AtomicInteger、AtomicBoolean和AtomicLong等。下面以AtomicInteger的部分实现来大致讲解下这些原子类的实现。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 为了使用Unsafe类中的CAS机制,先get一个UnSafe对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    private volatile int value;// 初始数值
    // 省略了部分代码...

    // 有参构造函数,可设置初始数值
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    // 无参构造函数,初始数值为0
    public AtomicInteger() {
    }

    // 获取当前值
    public final int get() {
        return value;
    }

    // 设置新值
    public final void set(int newValue) {
        value = newValue;
    }

    //返回原值并设置新值
    public final int getAndSet(int newValue) {
        // 使用for循环不断通过CAS操作来设置新值
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

    // 利用unSafe的方法,原子地设置新值为update, expect为期望的原值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update); // native方法
    }

    // 获取当前值current,并使current+1
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next)) // 自增
                return current;
        }
    }

    // 此处省略部分代码,余下的代码大致实现原理都是类似的
}

一般来说,在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用synchronized关键字的方式高效的多。可以从getAndSet()方法看出,如果资源竞争十分激烈的话,这个for循环可能换持续很久都不能成功跳出。在这种情况下,我们可能需要考虑如何降低对资源的竞争。

以上的原子类地一个典型应用就是计数,在多线程的情况下需要考虑线程安全问题,示例代码如下:

public class Counter {
    private int count;
    public Counter(){}
    public int getCount(){
        return count;
    }
    public void increase(){
        count++;
    }
}

上面这个类在多线程环境下会有线程安全问题,要解决这个问题最简单的方式可能就是加锁,优化代码如下:

public class Counter {
    private int count;
    public Counter(){}
    public synchronized int getCount(){
        return count;
    }
    public synchronized void increase(){
        count++;
    }
}

使用原子类提供的乐观锁实现,大多竞争没那么激烈的情况下,效率更高:

public class Counter {
    private AtomicInteger count = new AtomicInteger();
    public Counter(){}
    public int getCount(){
        return count.get();
    }
    public void increase(){
        count.getAndIncrement();
    }
}

七、ReentrantLock

在另一篇文章里:ReentrantLock源码解析

八、各类锁总结

1、乐观锁和悲观锁

悲观锁

一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。

在 Java 语言中 synchronized 和 ReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。

适用于读少写多场景,写的时候不能让其他线程参与进来,使用乐观锁会导致线程不断进行重试,这样可能还降低了性能

乐观锁

乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。

适用于写少读多场景,尽量减少阻塞,节省开销

2、独占锁和共享锁

独占锁

独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

共享锁

共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

3、互斥锁和读写锁

互斥锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

读写锁

读写锁是共享锁的一种具体实现。读写锁管理一个只读的锁,和一个写锁。

读锁可以在没有写锁的时候被多个线程同时持有,是共享锁,写锁则是独占锁。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容(及时更新)。

读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

JDK中有一个读写锁接口

public interface ReadWriteLock {
    // 获取读锁
    Lock readLock();
    // 获取写锁
    Lock writeLock();
}

ReentrantReadWriteLock实现了这个接口。

4、公平锁和非公平锁

公平锁

多个线程按照申请锁的顺序来获取锁。

非公平锁

不按照申请锁的顺序分配锁,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。通过传入boolean值给构造函数,可以创建公平的ReentrantLock

// 创建一个公平锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
Lock lock = new ReentrantLock(true);

5、可重入锁ReentrantLock与Synchronized

又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。从ReentrantLock的名字就可以看出它是一个可重入锁。Synchronized也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。以 synchronized 为例,看一下下面的代码:

public synchronized void mehtodA() throws Exception{
	 // Do some magic tings
	 mehtodB();
}
public synchronized void mehtodB() throws Exception{
	 // Do some magic tings
}

methodA 调用 methodB,如果一个线程调用methodA时,已经获取了锁,再去调用 methodB 时,就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。

与synchronized的区别:

synchronized基于JVM实现,ReentrantLock在JDK1.6时加入,可查看源码。两者都是可重入锁。ReentrantLock需要手动声明、加锁、释放锁,为了避免忘记释放锁,需要将释放的代码放入finally块中。ReentrantLock的功能更高级一些,当竞争激烈时,使用ReentrantLock更好。

6、自旋锁

指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋,当锁被其他进程占用的时间较短时,这个线程就不会被挂起,而是在一段时间的忙循环以后,直接获得锁。

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的情况。

AtomicInteger类有自旋的操作:

public final int getAndAddInt(Object o, long offset, int delta {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

CAS 操作如果失败就会一直循环获取当前 value 值然后重试。

在JDK1.6又引入了自适应自旋锁,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会持续较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。

7、分段锁

一种锁的设计,并不是具体的一种锁。分段锁设计目的是将锁的粒度进一步细化,例如当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。CurrentHashMap 底层就用了分段锁,在保证隔离性的同时,可以进行高效的并发使用。

8、锁优化技术(锁粗化、锁消除)

1)锁粗化

锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求,只需加锁和解锁一次即可。

举个例子,一个循环体中有一个同步代码块,每次循环都会执行加锁解锁操作。

private static final Object LOCK = new Object();

for(int i = 0;i < 100; i++) {
    synchronized(LOCK){
        // do some magic things
    }
}

经过锁粗化后就变成下面这个样子了,只需一次上锁:

synchronized(LOCK){
     for(int i = 0;i < 100; i++) {
        // do some magic things
    }
}

2)锁消除

锁消除是指虚拟机在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。举个例子:

public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1).append(s2);
    return stringBuffer.toString();
}

上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。test 方法中三个变量s1, s2, stringBuffer,它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。

我们都知道StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除。

StringBuffer.class
// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

八、线程安全的数据类型

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)。
1、Java标准库的java.lang.StringBuffer是线程安全的。
2、还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
3、还有,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。
4、Vector、HashTable、Properties是线程安全的;ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
5、Collection接口提供了几种方法,可以返回指定数组类型的线程安全类型对象,例如static <T> List<T> synchronizedList(List<T> list);可以返回一个线程安全的List对象,static <T> Set<T> synchronizedSet(Set<T> s); 返回一个线程安全的Set对象

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java多线程并发 的相关文章

  • 如何查看Pocketsphinx词典中是否存在该单词?

    我只是想看看字典文件中是否存在字符串 字典文件位于问题底部 我想检查语音识别器是否可以识别单词 例如 识别器将无法识别字符串ahdfojakdlfafiop 因为字典中没有定义 所以 我可以检查某个单词是否在 pocktsphinx 词典中
  • HTTP 状态 404 - 请求的资源不可用

    在使用 MyEclipse IDE 中的 Tomcat 服务器和 Struts 2 框架时 我遇到了反复出现的问题 我将我的程序作为服务器应用程序运行 当它运行时 默认的index jsp 文件将成功打开 但应用程序的其他过去都不起作用 当
  • 如何在 Eclipse 中用阿拉伯语读写

    我在 eclipse 中编写了这段代码来获取一些阿拉伯语单词 然后打印它们 public class getString public static void main String args throws Exception PrintS
  • Java 创建浮雕(红/蓝图像)

    我正在编写一个 Java 游戏引擎 http victoryengine org http victoryengine org 并且我一直在尝试生成具有深度的 3D 图像 您可以使用那些红色 蓝色眼镜看到 我正在使用 Java2D 进行图形
  • 使用 Checkstyle Plugin 时从插件调用代码时出现问题:“org.eclipse.jface”

    我正在尝试在 Rational Software Architect 7 0 0 4 上使用 eclipse cs 插件 我最近卸载了旧的 beta2 版本并安装了 beta3 插件本身按照之前的配置工作 但是每当我尝试通过 Windows
  • JTree 节点不会被直观地选择

    不知何故 我无法为我的 JTree 节点启用 选择突出显示 我正在我的项目中使用自定义单元格渲染器 这很可能导致此问题 这是完整的渲染器类代码 protected class ProfessionTreeCellRenderer exten
  • 有没有好的方法来解析用户代理字符串?

    我有一个Java接收模块User Agent来自最终用户浏览器的字符串的行为需要略有不同 具体取决于浏览器类型 浏览器版本甚至操作系统 例如 FireFox 7 0 Win7 Safari 3 2 iOS9 我明白了User Agent由于
  • Java 泛型/类型调度问题

    考虑以下程序 import java util List import java util ArrayList public class TypeTest public static class TypeTestA extends Type
  • MI设备中即使应用程序被杀死,如何运行后台服务

    您好 我正在使用 alaram 管理器运行后台服务 它工作正常 但对于某些 mi 设备 后台服务无法工作 我使用了服务 但它无法工作 如何在 mi 中运行我的后台服务 MI UI有自己的安全选项 所以你需要的不仅仅是上面提到的粘性服务 你需
  • Google Inbox 类似 RecyclerView 项目打开动画

    目前 我正在尝试实现 Google Inbox 例如RecyclerView行为 我对电子邮件打开动画很好奇 我的问题是 该怎么做 我的意思是 他们使用了哪种方法 他们用过吗ItemAnimator dispatchChangeStarti
  • 方法断点可能会大大减慢调试速度

    每当向方法声明行添加断点 在 Intellij IDEA 或 Android Studio 中 时 都会出现一个弹出窗口 方法断点可能会大大减慢调试速度 为什么会这样戏剧性地减慢调试速度 是我的问题吗 将断点放在函数的第一行有什么不同 Th
  • Java:从元素创建 DOM 元素,而不是文档

    如您所知 在 Java 中创建 Dom 元素的正确方法是执行以下操作 import org w3c dom Document import org w3c dom Element Document d Element e e d creat
  • 打印包含 JBIG2 图像的 PDF

    请推荐一些库 帮助我打印包含 JBIG2 编码图像的 PDF 文件 PDFRenderer PDFBox别帮我 这些库可以打印简单的 PDF 但不能打印包含 JBIG2 图像的 PDF PDFRenderer尝试修复它 根据 PDFRedn
  • 在 AKKA 中,对主管调用 shutdown 是否会停止其监督的所有参与者?

    假设我有一位主管连接了 2 位演员 当我的应用程序关闭时 我想优雅地关闭这些参与者 调用supervisor shutdown 是否会停止所有参与者 还是我仍然需要手动停止我的参与者 gracias 阻止主管 https github co
  • OpenJDK 版本控制

    上下文 我想确保我们系统上安装的 Java 不受 CVE 2022 21449 的影响 java version 给出 openjdk version 11 0 7 2020 04 14 LTS OpenJDK Runtime Enviro
  • struts 教程或示例

    我正在尝试在 Struts 中制作一个登录页面 这个想法是验证用户是否存在等 然后如果有错误 则返回到登录页面 错误显示为红色 典型的登录或任何表单页面验证 我想知道是否有人知道 Struts 中的错误管理教程 我正在专门寻找有关的教程 或
  • Spock模拟inputStream导致无限循环

    我有一个代码 gridFSFile inputStream bytes 当我尝试这样测试时 given def inputStream Mock InputStream def gridFSDBFile Mock GridFSDBFile
  • 重写Object类的finalize()方法有什么用?

    据我所知 在java中如果我们想手动调用垃圾收集器 我们可以执行System gc 1 我们在重写的finalize 方法中做了哪些操作 2 如果我们想手动调用JVM垃圾收集器 是否需要重写finalize 方法 我们在重写的 Finali
  • 从一个文本文件中获取数据并将其移动到新的文本文件

    我有一个文件 里面有数据 在我的主要方法中 我读入文件并关闭文件 我调用另一种方法 在原始文件的同一文件夹内创建一个新文件 所以现在我有两个文件 原始文件和通过我调用的方法生成的文件 我需要另一种方法 从原始文件中获取数据并将其写入创建的新
  • 尝试使用带有有效购买令牌的 Java Google Play Developer API v3 检索应用内购买信息时出现错误请求(无效值)

    当使用 Java Google Play Developer API 版本 3 并请求有效购买令牌的购买信息时 我收到以下异常 API 调用返回 400 Bad Request 响应以及以下消息 code 400 errors domain

随机推荐

  • 径向基函数

    注意核函数是一回事 径向基函数是另一回事 核函数表示的是高维空间里由于向量内积而计算出来的一个函数表达式 后面将见到 而径向基函数是一类函数 径向基函数是一个它的值 y 只依赖于变量 x 距原点距离的函数 即 也可以是距其他某个中心点的距离
  • 我使用的Vim插件

    2023年9月5日 周二下午 为了方便以后还原自己的Vim插件配置 于是写这篇博客来记录一下 不定期更新 目录 语法检查Syntastic 文件树The NERD tree 自动补全括号auto pairs 超轻量级自动补全vim auto
  • 交换机端口安全实验

    文章目录 一 实验的背景与目的 二 实验拓扑 三 实验需求 四 实验解法 1 PC配置IP地址部分 2 在SW1上开启802 1X身份验证 3 创建一个用户身份验证的用户 用户名为wangdaye 密码为123456 4 创建一个端口隔离组
  • 死锁,死锁必要条件及处理策略

    大自然的搬运工 完美分割线 多线程中 常见的一种问题除了竞态条件外就是死锁 那什么是死锁呢 死锁就是 是指两个或两个以上的进程在执行过程中 因争夺资源而造成的一种互相等待的现象 若无外力作用 它们都将无法推进下去
  • 图片在div中居中

    一 方法1 html代码 div class content img src img 举手yeah png div css代码 content display flex justify content center align items
  • 微服务网关实战——Spring Cloud Gateway

    导读 作为Netflix Zuul的替代者 Spring Cloud Gateway是一款非常实用的微服务网关 在Spring Cloud微服务架构体系中发挥非常大的作用 本文对Spring Cloud Gateway常见使用场景进行了梳理
  • 带你一周刷完操作系统视频笔记(3)

    本片笔记将会从基本概念 进程结构 区分进程线程这三个方面解释什么是进程 process 概念 process是一个具有一定独立功能的程序关于某个数据集合的一次运行活动 是系统进行资源分配和调度的独立单位 是资源分配的最小单位 要点 进程是程
  • SD卡通信协议那些事

    SD卡通信 SD卡通信协议主要包括物理层 数据传输层和应用层三个部分 物理层 SD卡使用SPI或SDIO两种物理层协议进行通信 SPI是一种同步串行通信协议 使用4根信号线进行通信 SDIO是一种异步串行通信协议 使用9根信号线进行通信 数
  • Jenkins用户权限控制插件——Role-based Authorization Strategy

    看了下公司的jenkins权限配置 发现公司用的是Role based Authorization Strategy插件做的权限控制 之前公司一直用的是安全矩阵的方式控制的 这里就大概记录一下吧 使用Role based Authoriza
  • Mybatis 笔记(1)- 搭建最基础的springboot+mybatis结构

    不讲mybatis的历史 mybatis和springmvc的历史 这里只列举springboot和mybatis整合需要做哪些工作 1 添加依赖 版本可以根据你的实际情况自行调整 2 配置dao mapper interface 和 Ma
  • selenium-server

    Selenium grid for selenium1 and webdriver Introduction Grid allows you to scale by distributing tests on several machine
  • 最便宜的云服务器

    阿里云和腾讯云都有推广活动 所以价格比较低 但直接从首页下单还是原价 必须从他们的推广链接 看下文 进去下单才便宜 顺便比较了一下网易云 华为云 都没有这么便宜 阿里云 点击选购最便宜服务器 阿里云福利 阿里云1888元优惠券红包
  • linux查看各进程占用cpu/内存情况

    目录 一 ps top命令 1 ps命令 1 1 语法 1 2 使用场景 2 top命令 1 语法 2 top命令内容说明 3 top命令使用过程中的交互的命令 二 排序进程 1 复杂方法 2 简单方法 很多时候 你可能遇见过系统消耗了过多
  • 【底层驱动不含main】XPT2046 制作一个电位器AD转换装置

    简介 XPT2046是一款四线制电阻触摸屏控制芯片 内含12位分辨率125KHz转换速率逐步逼近型A D转换器 支持从1 5V到5 25V的低电压I O接口 所谓逐步逼近型 就是输入一个模拟量 其与1000 0000 0000 对应的模拟量
  • ubuntu20安装ros noetic

    记录下自己在虚拟机上安装ros Noetic的过程 也供大家参考 我使用的虚拟机是vmware 默认已经安装好ubuntu20了 ubuntu20对应的ros版本是noetic 这里只记录了最少的命令 大家可根据自己需要安装其它所需的包 1
  • shell while true

    7 while循环注意为方括号 且注意空格 min 1 max 100 while min le max do echo min min expr min 1 done 8 双括号形式 内部结构有点像C的语法 注意赋值 i i 1 i 1
  • android 使用 onnxruntime 部署 scrfd 人脸检测

    下面是使用 opencv camera 实时处理区域内人脸检测 android 推理 demo 首先是整合 opcv camera 进去 为了方便直接将整个 opencv android sdk 全部导入 然后在原来的项目模块app中添加
  • java知识点之数据类型总结【全】

    java数据类型总结 目录 java数据类型总结 一 java数据类型知识点总结 1 Java变量 1 1 什么是变量 1 2 变量声明的方式 1 3 变量的分类 1 4 变量的命名规范 扩展 包 类 方法等命名规范 2 Java常量 2
  • 定义一个一维数组,其元素个数从键盘中输入,元素的值为[100,200]的随机整数。 (1)输出数组的每个数 (2)对数组的数进行升序排序,输出排序后的数组元素 (3)从键盘上输入一个整数,查找该整数是

    定义一个一维数组 其元素个数从键盘中输入 元素的值为 100 200 的随机整数 1 输出数组的每个数 2 对数组的数进行升序排序 输出排序后的数组元素 3 从键盘上输入一个整数 查找该整数是否存在 若存在输出其所在的下标 若不存在给出提示
  • java多线程并发

    目录 一 原子性 有序性 可见性 1 原子性 2 有序性 3 可见性 二 如何保证以上三性 1 硬件层面解决 1 在总线加 LOCK 锁 2 使用缓存一致性协议 2 软件层面解决 1 背景 JVM内存模型 2 原子性 3 可见性 4 有序性