Java锁(持续更新)

  先来看一张图,大致按照不同类型特性,将锁进行了分类。
Java锁(持续更新)_第1张图片
1、悲观锁和乐观锁
  悲观锁,顾名思义,很悲观,总觉得自己在使用数据的时候,很可能有其他线程在做该数据的修改,所以使用数据的时候,会将该数据加锁,其他想要操作该数据的线程都会被阻塞,等待释放锁,synchronize和ReentrantLock这些排它锁都是这种锁。传统RDB的行锁、表锁、读写锁也是悲观锁。线程释放锁后,CPU唤醒其他线程来获取锁。
  而乐观锁呢,跟它恰好相反,乐观锁认为自己在使用数据的时候,不会有其他线程在操作该数据,所以不会像悲观锁那样给数据加锁,只会在自己要去更新数据的时候,判断从自己读取到更新这段时间内,有没有其他线程操作过这个数据,如果没有,就成功写入,如果有,就报错或者自动重试什么的,具体要看你的实现方式。可见乐观锁更适合读操作比较多的数据,版本号机制和CAS实现都是乐观锁的应用,java.util.concurrent.atomic就是用CAS实现。
  那么问题来了,乐观锁不加锁,怎么确保线程同步呢?ok,fine,follow me。
  乐观锁主要是用了CAS,那我们就来看看CAS是怎么一回事。
  Compare And Swap,也就是比较与交换,试着理解一下,比较一下两个东西,根据比较结果进行交换,就是一个更新的操作。
  涉及到3个操作数:内存值V旧的预期值A要修改的新值B
  更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
  举个例子:两个线程1、2,一个变量i=6,现在线程1想把变量i的值加1,比如i++,此刻对于线程A来说,内存值V=x01,预期值A=6,要修改的新值B=7,然而,在线程1准备更新时,被线程2抢先了,也就说,变量i的值变成7了,这时候线程1去更新的时候,发现内存值V中存放的值不等于线程1的预期值A=6了,那线程1的更新操作就是失败的,那它只能又重新从内存值V这个地址中获取新的预期值A=7,假设没线程跟它抢,这时候,它去查内存值V中存放的值,发现跟它的预期值A相等,那它就会用要修改的新值B=8替换内存值V存放的,这个重新尝试的过程就是自旋啦。

  ABA问题
  ABA,可以看到A有重复,但中间穿插了一个B,是这个意思,某线程持有值A,其他线程将其改为B,而后改回A,这时该线程尝试修改时,得到的值依然是A,认为是正确的,然后修改成功,但其实线程已经经过其他线程修改到B,才改回A的。
  这个问题,有时候确实没什么影响,但是那个值,终究不是原来那个数据,有些场景就有问题了,来看个栈的例子。
由上至下栈元素依次为:A、B、C、A、E。
首先线程们并发取得A,某线程准备将A改为A2,但此时其他线程把B、C取出来了,想改A的那个线程准备更新的时候,发现栈顶是A,就认为没问题,直接更新了,但其实它更新的是第二个A,也就说第四个元素。
如何避免ABA?
  加版本号、加时间戳,确保唯一标识每一个值持有的状态,如java的Atomic就是通过AtomicStampedReference解决ABA问题。


2、自旋锁和自适应自旋锁
  唤醒或者阻塞线程,都要等OS切换CPU状态来触发,这种状态切换是耗时的,线程挂起和恢复可能是种浪费。假设是多核CPU,能让线程并行处理,在等待的线程不放弃CPU时间片,而是通过自旋等待,而占用资源的线程也很快释放锁,等待线程自旋完事儿,锁也释放了,直接使用该资源,岂不快哉。
  但是,凡事有利有弊,不总是这么理想,确实避免了线程切换的开销,但是如果当前持有锁的线程耗时,下一个线程一直在等待,反倒浪费了处理器的时间,所以也控制了自旋的次数,默认10次,可以通过设置 -XX:PreBlockSpin 参数来修改,超过这个次数,就让等待线程直接挂起。
自旋锁其实也是用了CAS,java.util.concurrent.atomic.AtomicInteger类里有一个方法,用户自动设定给定值newValue并返回旧值,即进行自增操作:

 	public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
	}

再来看调用的这个getAndSetInt()方法,

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

        return var5;
    }

  其中这个do-while就是个自旋操作,我们看到这个代码的意思就是,如果修改失败,就一直循环(自旋),直到修改成功,跳出循环(结束自旋)。
  JDK 1.6引入自适应自旋锁,自适应主要体现在不固定自旋的时间(次数),而是由前一个在该锁的自旋时间及锁拥有者状态来决定。如果同一个锁对象,刚刚自旋等待成功获取到过锁,且持有锁线程处于运行中,那么JVM会认为这次也可能成功,就会让它自旋等待,且相对更长时间。若某个锁,自旋很少成功,那么之后会直接忽略,而让其直接阻塞线程,避免浪费处理器资源。
  还有TicketLock、CLHlock和MCSlock这三个,也是自旋锁,有兴趣的可以自行深入了解学习。


3、无锁、偏向锁、轻量级锁、重量级锁
都是针对synchronize,表示锁状态。


4、公平锁和非公平锁
公平锁就是大家都按照自己申请的顺序来获取锁,进入队列中排队,队列里的第一个线程才能获取到锁,

你可能感兴趣的:(Java,多线程)