Java中的锁

一、并发编程

1.并发编程三大特性

可见性、有序性、原子性。
有序性:实际执行顺序是否与代码编写顺序一致。

2.线程基本概念

线程是一个程序中不同的执行路径。
当线程切换时,需要将旧线程的现场保存到cache中。
系统中分为用户线程和内核线程

3.Java线程模型与Go的线程模型对比

(1)Go语言单独起一个执行路径,叫协程,只需一个关键字就可以启动。Go语言天生支持高并发。
在Java中需要new一个thread,再start。
(2)Go语言中是一个m:n的关系,而且m远远大于n。GPM模型。

Go 调度器模型我们通常叫做G-P-M 模型,他包括 4 个重要结构,分别是G、P、M、Sched:
G:Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
P: Processor,表示逻辑处理器,对 G 来说,P 相当于 CPU 核,G 只有绑定到 P 才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等。P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量)。P 的数量由用户设置的 GoMAXPROCS 决定,但是不论 GoMAXPROCS 设置为多大,P 的数量最大为 256。
M: Machine,OS 内核线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取。M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。
Sched:Go 调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等。
调度器循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复。

Java中,一个用户线程对应一个内核线程,1:1。
(3)当启动一个Go语言程序时,会自动化启动一堆内核线程。当在Go语言中起一个协程,会放到一个队列中,一共有很多个队列。交给下面的内核线程去执行。
Go协程更像是一个任务,任务创建处理放到队列中,等待内核线程去处理。
Java中不能写特别多的线程,否则大部分资源都花在线程切换上。但是Go语言中,协程数量可以比Java大很多,因为它真正执行程序的时候,实际上就几十个内核线程,任务(用户线程)都在队列中等着。
go routine非常类似Java中的线程池。Java的线程池,启动一堆线程,从队列中取任务,取一个执行一个。Java中的ForkJoinPool和GPM模型非常类似,每一个线程都有自己的任务队列。
区别是:Java的线程池任务和任务之间是不能做同步的。但是Go可以,两个任务可以交替执行,也可以串行执行。
Go线程同步的方法:在用户空间模拟了CPU执行的原理,一个线程执行到一半如果阻塞了,保存现场,执行另外一个线程。
在这里插入图片描述

4. 锁

(1)锁可以自己指定,如synchronized(o){} 对象o是锁
(2)如果不持有锁的线程要访问锁,有两种等待方式:①忙等待(自旋等)。叫自旋锁,也叫轻量级锁,消耗CPU ②进队列等待,必须等待操作系统的调度。叫重量级,不消耗CPU。
(3)锁消除

public void add(String str1, String str2){
    StringBuffer sbr = new StringBuffer();
    sbr.append(str1).append(str2);
}

StringBuffer是线程安全的,因为它的关键方法都被synchronized修饰过。但是上面的代码中的sbr变量,只会在add方法中使用,不可能被其他线程引用(因为是局部变量,栈私有),因此sbr是不可能共享的资源,此时JVM就会自动消除StringBuffer对象内部的锁。
(4)锁粗化

public string test(string str){
    int i = 0;
    StringBuffer sbr = new StringBuffer();
    while(i < 100){
        sbr.append(str);
        i++;    
    }
    return sbr.totring();
}

JVM会检测到这样一连串的操作都对同一个对象加锁(sbr.append(str)要执行100次加锁解锁),此时JVM会将加锁的范围粗化到这一连串的操作的外部(如while循环外),使得这一系列的操作只加一次锁即可。
(5)锁在内存中的实现
参考另一篇博客

5.JIT

just in time compiler即时编译器,JVM的一些优化,把一些热点代码直接翻译成机器语言。

二、Java中的锁

1.synchronized锁

JVM 1.0等早期版本,synchronized一开始就是重量级锁,效率很低,后来JVM对其进行了优化,经过自旋锁升级成重量锁等操作。
自旋锁不需要经过操作系统。线程数量比较少时,这种方法会非常方便。所以针对用户空间的锁,JVM1.5之后推出了JUC下的各种锁。
例子:

private static /*volatile*/ int m = 0;//加volatile也没用
public static void main(String[] args) throws Exception{
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);

    for(int i = 0; i < threads.length;i++){
        threads[i] = new Thread(()->{
            for(int j = 0; j < 10000;j++){
                m++;
            }
            latch.countDown();
        });
    }

    Arrays.stream(threads).forEach((t) -> t.start());
    latch.await();
    System.out.println(m);//结果只有3万多
}

注:latch是门栓,是一种新型的锁,用来等待线程结束。
最终理想结果本来应该是100万,但是执行结果只有三万多。加volatile(1.线程可见性 2.指令重排序)也不行。因为只是保证了线程可见性。只能加锁。

private static /*volatile*/ int m = 0;//加volatile也没用
public static void main(String[] args) throws Exception{
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);

    for(int i = 0; i < threads.length;i++){
        threads[i] = new Thread(()->{
            synchronized(o){
                 for(int j = 0; j < 10000;j++){
                    m++;
                 }
                latch.countDown();           
            }
        });
    }

    Arrays.stream(threads).forEach((t) -> t.start());
    latch.await();
    System.out.println(m);//结果为100万
}

2.AtomicInteger类——CAS锁

假设synchronized是重量级锁,效率很低,所以JDK1.5之后诞生了JUC包,在JUC中增加了一个AtomicInteger类。

private static AtomicInteger m = new AtomicInteger(0);//轻量级锁,无锁,自旋锁
public static void main(String[] args) throws Exception{
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);

    for(int i = 0; i < threads.length;i++){
        threads[i] = new Thread(()->{
            for(int j = 0; j < 10000;j++){
                 m.incrementAndGet();//m++
            }
            latch.countDown();                       
        });
    }

    Arrays.stream(threads).forEach((t) -> t.start());
    latch.await();
    System.out.println(m);//结果仍为100万
}

去掉synchronized锁,m自增时调用m.incrementAndGet(). 不用上锁,最后结果仍然是100万。这里面用到的就是自旋锁,也称之为CAS。
CAS的实现分析见下一部分。

三、CAS

1.CAS(compare and swap/exchange) :自旋 / 自旋锁 / 无锁 (无重量锁)
因为经常配合循环操作,直到完成为止,所以泛指一类操作。
cas(v, preValue, value) ,变量v,期待值preValue, 修改值value
在这里插入图片描述
2.CAS是乐观锁的一种方式,是轻量级锁,不需要操作系统调度,只需要在用户空间执行。

3.ABA问题:其他线程改成其他值又改回来。解决方法:版本号(数字类型,时间戳)、AtomicStampedReference。gdk中的其他办法:加boolean类型表示是否改动过。

4.CAS底层实现本质上和synchornize和volatile完全一样。
读底层源码可以发现CAS最终保障原子性的是一条汇编指令 lock cmpxchg。当执行这条指令时,其他线程不允许对该值进行修改。(synchornize和volatile的底层实现跟lock这个指令都有关系)
(JVM的底层源码,最常用的是oracle的JVM Hotspot)

5.CAS修改值时候的原子性问题
CAS是比较后修改,如果比较和修改操作之间有另外一个线程修改了访问的变量,那么就无法保证CAS执行的正确性。所以要保证CAS的原子操作。
Java1.5之后诞生了JUC包,JUC中全是CAS实现的。如JUC中的AtomicInteger可以实现原子递增。
(1) CAS的应用和AtomicInteger源码分析:

AtomicInteger i = new AtomicInteger();
i.incrementAndGet();

JDK5之后添加了一些原子类,如AtomicInteger,可以实现自旋锁,使用incrementAndGet方法递增,本质上这些递增操作用的都是CAS操作。

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

unsafe的getAndAddInt调用了compareAndSwapInt方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

compareAndSwapInt方法如果返回false就循环。这个方法就是CAS操作。CAS方法的实现是native的,hotspot的C++的代码。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

在hotspot源码中,可以看到该方法的实现:
unsafe_CompareAndSwapInt

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

在这里插入图片描述
在unsafe_CompareAndSwapInt中调用了Atomic类的cmpxchg方法。
在atom_linux_x86头文件中,有原子类在linux中x86的实现Atomic::cmpxchg方法

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

在这里插入图片描述
_asm_表示汇编语言的实现
在Atomic::cmpxchg方法中有两条指令LOCK_IF_MP和cmpxchgl,LOCK_IF_MP:MP为multi processors,多核。意思是,如果是多核就加lock。

static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

最终实现:cmpxchg = CAS修改变量值。CAS是由cmpxchg这条汇编指令执行的。

lock cmpxchg 指令

lock cmpxchg. 即lock cas,因为cas本身不是原子的,所以要加lock,保证只有一个线程能够通过这一点执行下面的语句。

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

lock是几乎所有CPU都支持的一条指令,意思是锁总线。

四、synchronized

1.使用方法

如果是在非静态方法中加synchronized关键字,锁定的是this对象。如:public synchronized void m(){}
静态方法 public static synchronized void m(){}此时锁是当前类的class对象。
如果是代码块:synchronized(o){} 对象o是锁

2.synchronized的锁升级

(1)锁升级的过程,mark word对其进行了清晰的记录

在这里插入图片描述
在这里插入图片描述
偏向锁可以关掉。
synchronized底层自旋锁的时候是CAS自旋,重量级锁时是操作系统互斥量。

(2)偏向锁

偏向锁,严格来将不是锁。比轻量级锁还轻,所谓的占有锁的方式是第一个线程到来的时候贴上一个标签线程ID,不用抢。偏向锁的意义是,在只有一个线程访问锁的情境下,减少没必要的锁竞争消耗。
当有其他线程要访问锁,就升级位轻量级锁,CAS自旋。——第一次锁升级(偏向锁——>轻量级锁)。

(3)四种锁状态

锁的四种状态:①刚刚new出来,没有锁的状态。②偏向锁的状态。 ③轻量级锁的状态。④重量级锁
偏向锁——>轻量级锁:只要有竞争,就升级。
轻量级——>重量级锁:等待情况到达一定阈值,升级。线程进队列。

3.Synchronized实现过程的四个层级

(1)代码

加 synchonized关键字

(2)字节码

a.代码块的实现:加了monitorenter,加锁结束的部分moniterexit
b.加synchonized关键字方法的实现是加了关键字ACC_SYNCHRONIZED

(3)执行

自动升级

(4)编译

lock comxchg

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值