线程冲突、volatile、synchronized、Lock、ThreadLocal



多线程操作同一个变量的问题

Java 的变量默认是所有线程共享的,存在共享内存中

线程从共享内存读取变量到自己的工作内存,执行后再写回共享内存,什么时候读,什么时候写,由系统决定,是不确定的

这样会导致变量值的不确定性

比如 T1 和 T2 两个线程

假设有下面步骤

  1. T1 读取 counter 到自己的工作内存,修改值为 10,再写回共享内存
  2. T2 读取 counter 到自己的工作内存,修改值为 20,再写回共享内存
  3. T1 读取 counter 到自己的工作内存,这时值为 20,从 T1 自己的角度出发,counter 的值不完全由自己控制,会受到其他线程的干扰而变化

再比如这两个线程都对 counter 做 counter++ 操作,执行 10 次

顺利的话可能是这样执行

  1. T1 读取 counter 到自己的工作内存,当前值为 0,自增 5 次变成 5,再写回共享内存
  2. T2 读取 counter 到自己的工作内存,当前值为 5,自增 5 次变成 10,再写回共享内存
  3. T1 读取 counter 到自己的工作内存,当前值为 10,自增 5 次变成 15,再写回共享内存
  4. T2 读取 counter 到自己的工作内存,当前值为 15,自增 5 次变成 20,再写回共享内存

最终变成 20 结果正确,尽管从每个线程自己的角度出发其实是受到了干扰,但不影响最终结果

但也可能这样执行

  1. T1 读取 counter 到自己的工作内存,当前值为 0,做了 5 次自增变成 5,再写回共享内存
  2. T2 读取 counter 到自己的工作内存,当前值为 5,做了 3 次自增变成 8,但还没写回共享内存
  3. T1 读取 counter 到自己的工作内存,当前值为 5,做了 5 次自增变成 10,再写回共享内存
  4. T2 继续使用工作内存的 counter 为 8,做了 2 次自增变成 10,再写回共享内存

可以看到最终结果是 10 这是不正确的,原因在于无法保证读取、修改、写回这三个动作作为一个原子操作被执行

volatile

volatile 修饰的变量会保证每次读写该变量都需要去共享内存

volatile int counter;

注意 volatile 只是保证必然到共享内存读写数据,但同样无法保证原子性,无法保证数据的同步,比如

  1. T1 读取 counter 到自己的工作内存,当前值为 0,自增 1 次变成 1,再写回共享内存
  2. T2 读取 counter 到自己的工作内存,当前值为 1,自增 1 次变成 2,再写回共享内存
  3. T1 读取 counter 到自己的工作内存,当前值为 2
  4. T2 读取 counter 到自己的工作内存,当前值为 2,自增 1 次变成 3,再写回共享内存
  5. T1 继续使用工作内存的 counter 为 2,自增 1 次变成 3,再写回共享内存

可以看到这里少自增了一次,volatile 只能保证每次读和写都必须操作共享内存,防止读进来后,后面的读和写都在自己的工作内存执行(比如做了 5 次 counter++ 再写回共享内存)的情况,但实际上无法保证数据同步,除非操作是原子性的,比如如果 counter++ 是原子性的,即读共享内存,加1,写回共享内存,这一系列操作不会被其他线程打断,那就没问题,但 java 的 int 变量的 ++ 操作不是原子性的

AtomicInteger

java 也有提供支持原子操作的类,比如 AtomicInteger,它包含一个 volatile int 变量,并提供 getAndIncrement 函数实现原子性的自增

synchronized

要实现数据的同步和互斥操作,可以使用 synchronized 关键字,保证由 synchronized 修饰的函数或代码块,同一时间只能被一个线程执行,即 synchronized 修饰的函数或代码块有互斥性,在前面的例子中,如果把全部 10 次 counter++ 放到 synchronized,或是每次 counter++ 放到 synchronized 下同时 counter++ 是 volatile 变量,就不会出错

synchronized 用于函数

public synchronized void syncMethod() {
}

表示该函数每次只能被一个线程调用,其他线程需要等待

synchronized 用于代码块,括号里是对象

synchronized (this) {
}

表示这个代码块每次只能被一个线程调用,其他线程需要等待

更进一步讲,synchronized (this) 是获得了对象锁,该对象的所有其他 synchronized 代码都不能被其他线程调用,比如

public class Test {
    public void func_a() { 
        synchronized (this) {
            // 代码块 A
        }
    }

    public void func_b() { 
        synchronized (this) {
            // 代码块 B
        }
    }

    public synchronized void func_c() {
    } 

    public synchronized void func_d() {
    } 
}

当代码块 A 被某个线程调用时,其他线程不仅不能调用代码块 A,同时也不能调用代码块 B,也不能调用函数 func_c,同样的,如果函数 func_c 被某个线程调用,其他线程不仅不能调用函数 func_c,也不能调用代码块 A 或 B,也不能调用函数 func_d

实际上这四个函数会获取同一个对象锁,执行需要获取对象锁的代码就会受影响,如果执行的是不需要获取对象锁的代码就不影响

为了避免锁住整个对象,可以创建专门用于锁住小段代码的对象,比如

    Object lock1 = new Object();
    Object lock2 = new Object();

    public void func_a() { 
        synchronized (lock1) {
            // 代码块 A
        }
    }

    public void func_b() { 
        synchronized (lock2) {
            // 代码块 B
        }
    }

此外还可以锁住类,比如

public class SyncTest {
    public void func_a() { 
        synchronized (this) {
        }
    }

    public synchronized static void func_b() {
    }

    public void func_c() {
        synchronized(SyncTest.class) {
        }
    }
}

这里 func_b 和 func_c 互斥,但 func_a 和 func_b、func_c 互不影响

也就是类锁和对象锁,是不一样的

Lock

synchronized 是 java 的内置关键字,使用方便,但有一些不足,比如

  1. 无法中断,如果线程 A 没释放锁(比如执行太久),线程 B 需要一直等待,不很适合大量代码
  2. 无法判断是否获取锁的状态
  3. 非公平,即锁释放后,等待的线程需要竞争

为了提供更多功能就引入了 Lock 和 ReadWriteLock 接口,优点有

  1. 可以中断,可以设置超时,能应付大量代码
  2. 可以判断锁的状态,可以在无法获取锁的时候阻塞或立刻返回
  3. 可以是公平锁,也可以是非公平锁
  4. 支持读写锁

Lock 的实现是 ReentrantLock,而 ReadWriteLock 的实现是 ReentrantReadWriteLock

这里的 Reentrant 是指可重入性,就是当前线程成功获取锁后,如果再获取锁,不会被 block

synchronized 同样是可重入锁

ReentrantLock 的基本使用

ReentrantLock lock = new ReentrantLock();

lock.lock();
// 其他操作
lock.unlock();

最好是在 try...catch 的 finally 里面做 unlock 防止没有释放

其他功能比如

new  ReentrantLock(true);    // 创建公平锁,就是等待锁是先到先得机制,默认是非公平,即释放锁后需要竞争
lock.isLocked()              // 是否被当前线程锁住
lock.getHoldCount()          // 当前线程每 lock 一次就 +1,每 unlock 一次就 -1
lock.getQueueLength()        // 有多少个线程在等这个锁
lock.tryLock()               // 尝试获取锁,成功返回 true,失败返回 false,不阻塞
lock.tryLock(10, TimeUnit.SECONDS)   // 尝试获取锁,一定时间内无法获取才返回 false
lock.lockInterruptibly();    // 等待锁的时候可以被 Thread.interrupt 中断并抛异常,而 lock() 无法被中断

// 等待和唤醒机制
Condition c = lock.newCondition();
c.await();       // 线程 1 等待
c.signal();      // 线程 2 唤醒线程 1

ReentrantReadWriteLock 的基本应用

ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

ReadLock readLock = readWriteLock.readLock();
readLock.lock();
readLock.unlock();
        
WriteLock writeLock = readWriteLock.writeLock();
writeLock.lock();
writeLock.unlock();

读锁被获取后,其他线程还是可以获取读锁
读锁被获取后,其他线程不可以获取写锁
写锁被获取后,其他线程不可以获取写锁或读锁

悲观锁、乐观锁、锁的选择

synchronized 和 Lock 属于悲观锁,会真正锁住资源,比较耗性能,悲观锁适合于写多读少的场景

乐观锁实际上不加锁,如果在最后更新数据时发现冲突了再进行处理(依据不同算法,或者重试,或者抛出异常),由于不加锁,如果很少冲突的话,性能就大大提升,冲突频发就不行,乐观锁适合于写少读多的场景

比如前面提到的 AtomicInteger 用的就是乐观锁,采用 CAS (Compare And Swap) 方法

java.util.concurrent 包中的原子类就是通过 CAS 实现乐观锁

CAS 要用到三个值

  1. 内存位置(V)
  2. 预期原值(A)
  3. 新值(B)

初始化时获取内存位置 V

执行操作时,先获取 V 的当前值到 A,修改并形成新的值 B

然后调用 CPU 的一条原子性指令,实现功能:如果位置 V 的值是 A,将其替换为 B,否则返回位置 V 的当前值

如果这个原子性命令执行失败,也就是 V 的值发生了变化,意味着和其他线程冲突了,那就循环执行操作,直到成功

java 代码如下

AtomicInteger atomicInteger = new AtomicInteger();
int counter = atomicInteger.getAndIncrement();

AtomicInteger 的定义和初始化代码

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;   // value 的位置
    private volatile int value;    // 要操作的值

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

通过乐观锁实现原子性的自增

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);  // 获取当前 valueOffset 位置的值 (也就是 value 的值)
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  // 执行原子性 CAS,失败就重试

        return var5;
    }

    // 这个函数实现原子性的 CAS 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这里 native 表示这个函数是 java 的 JNI (Java Native Interface) 实现,也就是说这个函数不是 java 实现的,是其他语言实现的,比如 C 语言,因为 java 本身无法操作 CPU 指令,而这个 CAS 需要指定特殊的 CPU 指令,所以要用 JNI

这样的 CAS 算法存在 ABA 问题,就是位置 V 的值原来是 A 然后变成 B 又再变回 A,这样误以为 V 的值没变,但实际上发生过变化,一种解决方法是在值前面添加版本号,每次更新都把版本号 +1,这样就变成 1A -2B-3A,避免判断失误

顺便说一下,数据库也有类似的乐观锁实现,比如通过执行

select version, value_column from [table] where key_column=[key]

-- 其他操作形成新的值

update [table] set version=[操作前的值+1], value_column=[new_value] where key_column=[key] and version=[操作前的值]

这样执行成功就是没有冲突,失败就重试或报错

一些其他概念
无锁:遇到冲突不退出不阻塞,而是循环尝试直到成功,比如 CAS
自旋锁:不阻塞而循环尝试也叫自旋,自旋锁有自旋次数的限定,防止一直循环浪费 CPU
偏向锁:多数情况下锁总是由同一线程多次获得,偏向锁设置线程 ID,下次该线程只需要做一次 CAS 操作判断线程 ID,偏向锁只有遇到竞争才释放
轻量级锁:锁的获取和释放比偏向锁操作多,通过自旋等待,不阻塞
重量级锁:需要阻塞

选择锁的考虑

  1. 需不需要锁住资源(乐观,悲观)
  2. 需不需要阻塞(乐观,悲观)
  3. 乐观锁性能(无锁、偏向锁、轻量锁)
  4. 竞争锁需不需要排队(公平锁,非公平锁)
  5. 同一个线程能不能获取同一把锁(可重入锁,不可重入锁)
  6. 多个线程能不能共享同一把锁 (共享锁、读锁,互斥锁、写锁)

synchronized 经过优化,能判断并从无锁 - 偏向锁 - 轻量级锁 - 重量级锁做升级,优化性能

ThreadLocal

如果希望同一个变量名,在不同的线程有不同的值,并且不会冲突,那么可以使用 ThreadLocal

        ThreadLocal<String> localVar = new ThreadLocal<>();

        localVar.set("value");
        localVar.get();
        localVar.remove();

实现

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以简单理解为一个两层的字典 (线程ID,(ThreadLocal 变量,值))

当然实际情况更复杂些

类的加载和初始化

有三个阶段

  • 装载
  • 连接
  • 初始化

其中装载阶段有三个动作

  • 通过类型的完全限定名,产生一个代表该类型的二进制数据流
  • 解析这个二进制数据流为方法区内的内部数据结
  • 构创建一个表示该类型的 java.lang.Class 类的实例

连接阶段又分为三部分

  • 验证,确认类型符合 Java 语言的语义
  • 准备,分配内存,设置默认初始值
  • 解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。

只有当类被主动使用时,才会做初始化,执行 static 变量,执行 static 代码块

  • 当创建类的新实例时
  • 当调用类的静态方法时
  • 当使用类的静态字段时
  • 当调用 Java API 中的某些反射方法时
  • 当初始化某个子类时
  • 当虚拟机启动某个被标明为启动类的类(即包含 main 方法的那个类)

Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit



posted @ 2022-02-20 17:03  moon~light  阅读(122)  评论(0编辑  收藏  举报