并发编程之线程中断&CAS

synchronized和LOCK的区别

在前面的笔记中已经记录了synchronized,这个锁是JVM内置的锁,不需要程序员手动释放锁,我们在前面已经知道了synchronized锁主要是使用monitor锁的机制来实现的,而synchronized在锁升级到重量级锁过后,是通过操作系统的mutex互斥量来实现的,这就会在用户态和内核态直接频繁切换,导致性能有一定的影响,而我们知道synchronized锁的是对象,而锁的升级过程是通过对象的对象头的markword来实现,markword在锁升级的情况下会出现不同的形态;
既然synchronized是针对的对象锁,那么我们知道java.lang.Object提供了wait和notify,锁的等待和释放,我们要注意,object的wait和notify是必须在synchronized中使用的,如果不在synchronized中使用,程序会出现错误,为什么呢?因为wait和notify底层使用的monitor机制,而monitor机制是基于对象头的markword来实现的,而monitor机制是synchronized中特有的一种机制,所以说wait和notify是基于synchronized锁对象的monitor机制来实现,简单来说wait和notify只能用于synchronized中,而Juc包下的LockSupport和ReentrantLock是基于线程的锁;比如我们来看一段代码:

public class T0925 {
    private static final Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        obj.wait();
    }
}

在这里插入图片描述
通过上面的例子就可以看出我们在直接调用object的wait方法,会出现非法的监视器状态异常;
我们知道synchronized是基于monitor实现的,而synchronized是锁的是对象,也就是阻止其他线程进入,简单来说就是让所有的线程串行执行,序列化的访问临界资源,既然锁的对象,那么对象锁的释放是通过monitor来来实现的,那么我们也可以通过object的wait来让出cpu的执行时间片,也可以使用notify来让wait的对象重新获得锁,那么我来看一个例子:

public class T0925 {


    private static final Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        T0925 t = new T0925();
        Thread thread_01 = new Thread(()->{
            t.waitTest();
        },"Thread-01");
        Thread thread_02 = new Thread(()->{
            t.notifyTest();
        },"Thread-02");
        thread_01.start();
        Thread.sleep(3000);
        thread_02.start();


    }

    public void waitTest(){
        synchronized (obj){
            System.out.println(Thread.currentThread().getName() + " wait enter ");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " wait out ");

        }
    }
    public void notifyTest(){
        synchronized (obj){
            System.out.println(Thread.currentThread().getName() + " notifyTest enter ");
            obj.notify();
            System.out.println(Thread.currentThread().getName() + " notifyTest out ");

        }
    }


}

输出:

Thread-01 wait enter 
Thread-02 notifyTest enter 
Thread-02 notifyTest out 
Thread-01 wait out 

所以wait是可以释放锁的,其他线程可以获得锁,然后进入同步代码块

线程中断

线程中断是什么?我们如果要中断一个线程该怎么来做,用stop?肯定不行,stop非常危险,为什么呢?因为我们的线程正在运行,如果你突然调用stop,那么这个线程可能正在往内存写值,那么你中断了的话,那么可能其他线程读取到的是stop前的值,就是脏数据,总之来说,stop是不建议使用的,非常危险。那么我们如何优雅的中断一个线程?通过一个标志来中断?就是运行的时候给线程发一个中断标志,线程获取到中断标志然后进行中断?
在jdk中的Thread中比较常用的一种线程中断就是interrupt,我们来看下面线程中断的3个api:

//检查当前线程的中断标记,并且设置一个中断标记为true
Thread.interrupted();
//为当前线程设置一个中断标记为true
Thread.currentThread().interrupt();
//检查当前线程的线程中断标志并且设置中断标记为false
Thread.currentThread().isInterrupted()

这三个api我们通过例子来说明,首先大家要知道,线程中断标志设置了不意味着我们的线程会终止,中断标志就是一个标志位,设置了这个标志意味着你的线程可以手动中断,停止执行,而jvm线程是不会停止当前线程的,只是当前线程会有一个标志为表示当前线程可以中断,需要程序员手动的去终止线程,比如:

public class interruptedTest {

    public static void main(String[] args) {
          Thread thread = new Thread(()->{
              test();
          });
          thread.start();
          thread.interrupt();

    }

    private static  void test(){
        for(int i = 1; i< 1000000 ;  i++ ){
            System.out.println(i);
            if(Thread.currentThread().isInterrupted()){
                System.out.println("线程中断标志。。。。为true");
            }
            if(i > 30 ){
                break;
            }
        }
    }


}

输出:

1
线程中断标志。。。。为true
2
线程中断标志。。。。为true
3
线程中断标志。。。。为true
4
线程中断标志。。。。为true
5
线程中断标志。。。。为true
6
线程中断标志。。。。为true
7
线程中断标志。。。。为true
8
线程中断标志。。。。为true
9
线程中断标志。。。。为true
10
线程中断标志。。。。为true
11
线程中断标志。。。。为true
12
线程中断标志。。。。为true
13
线程中断标志。。。。为true
14
线程中断标志。。。。为true
15
线程中断标志。。。。为true
16
线程中断标志。。。。为true
17
线程中断标志。。。。为true
18
线程中断标志。。。。为true
19
线程中断标志。。。。为true
20
线程中断标志。。。。为true
21
线程中断标志。。。。为true
22
线程中断标志。。。。为true
23
线程中断标志。。。。为true
24
线程中断标志。。。。为true
25
线程中断标志。。。。为true
26
线程中断标志。。。。为true
27
线程中断标志。。。。为true
28
线程中断标志。。。。为true
29
线程中断标志。。。。为true
30
线程中断标志。。。。为true
31
线程中断标志。。。。为true

执行中的线程已经收到了中断标志,但是线程不会自己停止,只会设置一个标记而已
但是当我们修改下程序:

private static  void test(){
        for(int i = 1; i< 1000000 ;  i++ ){
            System.out.println(i);
            if(Thread.currentThread().isInterrupted()){
                System.out.println("线程中断标志。。。。为true");
                break;
            }
            if(i > 30 ){
                break;
            }
        }
    }


}

那么线程就会退出,而且isInterrupted是判断线程是否设置了线程中断标志,而interrupt是设置线程中断标志为true,而isInterrupted是不会修改线程中断标志的。
我们再看下另外一个api,Thread.interrupted,这个api是干什么的呢?这个api的意思就是坚持当前的线程是否设置了线程中断标志,如果设置了那么就重置为false,简单来说就是你要停止线程,我不让你停,什么意思呢?比如说我们的线程进入一个方法中或者代码块中,我们知道代码的执行时有序的进行的,比如我下面的代码需要检测你是否设置了中断标记,如果设置了我就给你停止,如果没有设置,就不停止,但是我在这个检测的前面强制检测如果设置了,再给你设置回去,那么就用于不会终止线程,但是没有这种做法,但是有些场景也会有遇到,比如设置了一个标志表示在某种场景下我不让你线程停止,在某些场景下设置了中断就可以停止,比如下面的代码:

public class interruptedTest {

    //falg=0的时候不能终止线程 1=可以终止线程
    private static final int falg = 1;
    public static void main(String[] args) {
          Thread thread = new Thread(()->{
              test();
          });
          thread.start();
          thread.interrupt();

    }

    private static  void test(){
        for(int i = 1; i< 1000000 ;  i++ ){
            System.out.println(i);
            if(falg ==0){
                //这种场景下我不让你停止线程,强制将线程中断标志设置为false
                Thread.interrupted();
            }


            if(Thread.currentThread().isInterrupted()){
                System.out.println("线程中断标志。。。。为true");
                break;
            }
            if(i > 30 ){
                break;
            }
        }
    }


}

就类似上面的情况

线程中断之唤醒

如果说我们的一个线程正在睡眠或者wait的时候,我们将其中断,那么wait和sleep的线程是否能够检测的到呢?我们来看个程序例子:

public class interruptedTest {


    private final static Object object = new Object();

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            waitInterrrupt();
        }, "Thread-sleep");

        Thread waitThread = new Thread(() -> {
            waitInterrrupt();
        }, "Thread-wait");
        Thread parkThread = new Thread(() -> {
            parakInterrrupt();
        }, "Thread-lock");
        thread.start();
        waitThread.start();
        parkThread.start();
        thread.interrupt();
        waitThread.interrupt();
        parkThread.interrupt();
    }

    private static void sleepInterrrupt() {
        for (int i = 1; i < 1000000; i++) {
            System.out.println(i);
            try {
                //调用了线程中断,可以唤醒在睡眠中的线程,并且清除中断标志
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName() + " 在睡眠中被中断...");
            }
            if(Thread.currentThread().isInterrupted()){
                System.out.println("sleep....中断标志位true");
                break;
            }
        }
    }

    private static void waitInterrrupt() {
        for (int i = 1; i < 1000000; i++) {
            synchronized (object) {
                System.out.println(i);
                try {
                    //wait在等待中的线程也可以被中断线程唤醒,并且清除中断标志
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName() + " 在睡眠中被中断...");
                }
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("wait....中断标志位true");
                    break;
                }
            }

        }
    }

    private static void parakInterrrupt() {
         for (int i = 1; i < 1000000; i++) {
            System.out.println(i);
             //wait在等待中的线程也可以被中断线程唤醒,不会清除中断标志
            LockSupport.park();
            if(Thread.currentThread().isInterrupted()){
                System.out.println("parak....中断标志位true");
                break;
            }
        }
    }
}

上面有三个线程,分别是测试sleep、wait和parak在阻塞的情况下,通过线程的interrupt标志是否能唤醒线程,就是通过设置线程的中断标志是否能够唤起阻塞的线程,我们看下输入结果:

1
1
Thread-lock parak....中断标志位true
Thread-sleep 在睡眠中被中断...
2
1
Thread-wait 在睡眠中被中断...
2
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at org.bml.juc.cas.interruptedTest.waitInterrrupt(interruptedTest.java:57)
	at org.bml.juc.cas.interruptedTest.lambda$main$0(interruptedTest.java:17)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at org.bml.juc.cas.interruptedTest.waitInterrrupt(interruptedTest.java:57)
	at org.bml.juc.cas.interruptedTest.lambda$main$1(interruptedTest.java:21)
	at java.lang.Thread.run(Thread.java:748)

可以看到设置了线程中断标志过后都能被唤醒,所以线程中断标志是可以唤醒sleep、wait和park的线程的,但是他们有区别,首先sleep在睡眠期间如果设置了线程中断,那么会进入InterruptedException异常,我们可以在异常中去捕获中断线程的信息;而wait必须在同步代码块中使用,在wait期间设置线程中断标志也是能够唤醒wait的线程,但是也是要捕获InterruptedException异常,也可以在异常中获取中断信息;parak也可以接受到中断信号,其实对于park来说,用interrupt和unpark也是一样的,对于LockSupport来说,设置中断标志和发一个许可unpark也是一样的效果;但是sleep和wait与parak有两个区别:
1.park接受到线程中断不是出现异常,类似于unpark的效果一样,唤起处于阻塞的线程;
2.wait和sleep在阻塞期间,如果设置了线程的中断标志,那么会立马清除中断标志,而park是不会清除中断标志,简单来说就是,这三个在阻塞期间,如果设置了中断标志,那么只有parak这个不会清除中断标志,sleep和wait都会清除中断标志,也就是线程的中断标志由true变成了false;

CAS原理

CAS是什么呢?可能没有接触过底层的程序员来说,cas用在底层的特别多,包括synchronized,juc包下面的很多api都用了cas,包括阻塞队列等;如果说你对cas不熟悉,那么AtomicInteger大家总是用过了,大家都知道非常典型的线程安全例子i++,大家都知道,但是我们使用了AtomicInteger就能保证原子性了,大家有没有疑问,为什么它能保证原子性,而AtomicInteger底层没有使用任何的锁机制来保证原子性,那么它是如何保证原子性的呢?这边我们来揭开它的面纱,它底层就是使用的CAS自旋锁来保证原子性,简单来说就是AtomicInteger的值是vloatile的,值是在内存中是可见的,然后通过cas来进行比较,cas就是比较和替换的操作。

什么是CAS

CAS可以看做是乐观锁的一种实现方式,Java原子类中的递增操作就通过CAS自旋实现的。
CAS全称Compare And Swap (比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
LOCK_IF_MP(%4) "cmpxchgl %1,(%3)“ lock cmpxchgl:汇编指令(CAS操作的汇编指令)
unsafe.compareAndSwapInt(this, valueOffset, expect, update)
this:需要改变属性值的对象,也是在内存中获取属性偏移量的对象;
valueOffset:value变量在内存中的偏移量的内存地址;
expect:期望更新的值
update:比较成功过后要更新的值
如果原子变量中的value,也就是从内存中获取的最新值等于expect,则使用update更新最新的值,然后返回true,否则返回false.

AtomicInteger(CAS典型使用案例)

我们就用AtomicInteger来简单了解下CAS的原理;

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

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;
    

private static final Unsafe unsafe = Unsafe.getUnsafe():申明一个全局静态的唯一Unsafe静态对象,cas操作必须要借助于Unsafe对象,Unsafe 是可以直接操作内存,操作内存做什么呢?我们都知道我们的java对象都存放在内存中,所有通过Unsafe 可以直接拿到我们要比较的值在内存中的最新值;
private static final long valueOffset:这个叫类的属性在内存中的偏移量,也就是可以拿到我们在内存中的最新的值;
private volatile int value:内存中的值为什么要加volatile ?因为AtomicInteger是在多线程环境下保证数据安全的的操作,所以首先要保证整个值在内存中对所有的线程可见,所以是volatile修饰的,这个值是配合对象使用的。
这边用一个自增长的方法做说明:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    //最后我们要返回的值
     int var5;
    do {
        //var5就是获取我们要比较的值在内存中的偏移量的最新的值,它是通过
        //var1和var2进行获取的,如果不好理解,你就理解下比如有个类Demo,Demo中
        //有个属性a,然后如果你要获取整个a的值,那么你可以通过Demo的对象.getA()
        //来获取,下面的这个方法类似这种效果,就是通过var1(value值所指的对象this)
        //和var(value)来获取,getIntVolatile是本地方法,在c++中实现的,java中没有实现
        var5 = this.getIntVolatile(var1, var2);
        //使用do while循环一直去cas,直到成功为true过后,方可成功
        //这个过程中:var1和var2主要是用来获取value在内存中的最新值
        //var5是上面的第一次从内存中获取的最新值,那么为什么还要把var1和var2传过去呢?
        //因为cas是在c++中实现的,你真正去调用的时候,可能其他线程已经把值该了
        //这个时候cas是不能成功的,需要从新从内存中获取最新的值再次进行比较
        //var5就是每次需要比较的值,var5+var4就是cas成功过后要更新的值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

compareAndSwapInt jvm中的实现

openjdk的路径\hotspot\src\share\vm\prims\unsafe.cpp

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;
  

上面的这个cas的代码就是我们在上面的方法中调用compareAndSwapInt本地方法执行
我们知道我们的对象在c++中就是一个oop,在jvm笔记中已经详细介绍了,那么
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
这两行代码就是获取我们要比较的值在内存中的最新值
p=var1
offset=var2
最后调用Atomic::cmpxchg进行cas
在这里插入图片描述

CAS缺点

1.只能保证一个变量的原子操作;
2.如果竞争激励,可能长期更新不成功,自旋长期占用CPU的时间片;
3.ABA问题。

什么是ABA问题

比如说,一个值等于1,线程1要更新成2,线程2这个时候把这个值更新为3后,在线程2更新之前,又把这个值更新成了1,那么线程2发现这个是是1,cas是成功的,所以这就是aba问题。
举个例子:某某公司财务职工,从公司挪用了100w去投资,一个月后,赚了150w,然后在把100万还回去了,自己赚了50w,而原来的100w其实已经不是之前的100w了。

public class ABATest {

    public final AtomicInteger sum = new AtomicInteger(0);


    public static void main(String[] args) {

        ABATest aba = new ABATest();
        Thread thread1 = new Thread(() -> {
            aba.thread1();
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            aba.thread2();
        }, "Thread-2");
        thread1.start();
        thread2.start();
    }


    private void thread1() {

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 开始执行...");

        if (sum.compareAndSet(0, 1)) {
            System.out.println(Thread.currentThread().getName() + " update sum 0 to 1 success");
        } else {
            System.out.println(Thread.currentThread().getName() + " update sum 0 to 1 failure");

        }


    }

    private void thread2() {

        System.out.println(Thread.currentThread().getName() + " 开始执行...");

        sum.compareAndSet(0, 1);

        System.out.println(Thread.currentThread().getName() + "update sum 0 to 1");

        sum.compareAndSet(1, 0);
        System.out.println(Thread.currentThread().getName() + "update sum 1 to 0");


    }
}

上面的代码就是启动2个线程,cas 的value初始值为0,线程1要把0修改为1,线程1由于某些原因需要等待2s后执行,这个时候线程2启动了,线程2将0修改为1过后,又把值修改为0,那么这个时候线程1开始运行了,线程1发现value还是0,就更新了,那么这就是ABA问题,类似上面举个例子
输出如下:

Thread-2 开始执行...
Thread-2update sum 0 to 1
Thread-2update sum 1 to 0
Thread-1 开始执行...
Thread-1 update sum 0 to 1 success

如何解决ABA问题

那么我们知道了cas的ABA问题,那么我们如何解决呢?在jdk中Doug Lea大神已经给我们想到了,在jdk中解决ABA问题的一个原子类操作AtomicStampedReference,这个类在cas的时候api有4个参数
expectedReference:期望更新的值(就是期望比较的值)
newReference:cas成功过后的更新的值
expectedStamp:期望的版本号(也就是cas前的版本号)
newStamp:cas成功过后的版本号
废话不多说,来看实例:

public class ResoveAbaTest {


    //初始的值为aa,版本号为0
    private final AtomicStampedReference<String> atomicStampedReference =
            new AtomicStampedReference<>("aa", 1);

    public static void main(String[] args) {
        ResoveAbaTest aba = new ResoveAbaTest();
        Thread thread1 = new Thread(() -> {
            aba.thread1();
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            aba.thread2();
        }, "Thread-2");
        thread1.start();
        thread2.start();
    }


    private void thread1() {

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 开始执行...");

        if (atomicStampedReference.compareAndSet("aa", "bb", 1, 2)) {
            System.out.println(Thread.currentThread().getName() + " update atomicStampedReference 'aa' to 'bb' success");
        } else {
            System.out.println(Thread.currentThread().getName() + " update atomicStampedReference 'aa' to 'bb' failure");
        }
    }

    private void thread2() {
        System.out.println(Thread.currentThread().getName() + " 开始执行...");

        atomicStampedReference.compareAndSet("aa", "bb", 1, 2);

        System.out.println(Thread.currentThread().getName() + "update atomicStampedReference 'aa' to 'bb'");

        atomicStampedReference.compareAndSet("bb", "aa", 2, 3);
        System.out.println(Thread.currentThread().getName() + "update atomicStampedReference 'bb' to 'aa'");
    }
}

代码逻辑比较简单,和ABATest逻辑一样,看结果

Thread-2 开始执行...
Thread-2update atomicStampedReference 'aa' to 'bb'
Thread-2update atomicStampedReference 'bb' to 'aa'
Thread-1 开始执行...
Thread-1 update atomicStampedReference 'aa' to 'bb' failure

这个时候失败了,线程2将值修改成了bb,由修改到aa,线程1开始启动,修改,但是这个时候修改失败了,因为线程1发现你已经不是原来的你了(o( ̄︶ ̄)o),你已经变了,我不能接受你了。所以就是这个道理,这个过程中其实就是用了stamp来控制的,我们把它理解成版本号,反正就是一个标志。在实际的工作开发过程中,我们要特别注意的就是如果使用上面的场景的话,那么每次cas过后的版本号不能手动设置,需要设置一个只能增长,不能回退的版本号来控制,当然了这个得具体看的业务场景,因为如果这个版本号可以随便设置,那么还是可能会出现,你把expect的值又给你修改回来了,版本号也是给你改回来了,那么其实还是存在ABA问题的。所以这个就的看你的业务场景和实现的考量。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值