java并发编程-基础概念、volatile、synchronized、CAS、ABA、线程、线程基本机制

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_40918961/article/details/96607888

目录

基础概念   

关键字volatile

关键字synchronized

cas:不加锁实现原子操作(乐观锁)

cas中的ABA问题

线程

进程与线程的区别

java线程的实现方式

线程优先级

线程状态

Daemon线程

线程基本机制

中断Interrupted

等待/通知机制

 管道输入/输出流

Thread对象的join()方法

线程局部变量ThreadLocal

辅助类CountDownLatch


基础概念   

         Java代码在javac编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。

        现代操作系统采用时分的方式调度运行的程序,系统为每个线程分配时间片,当时间片用完了就发生线程的调度。多核处理器当然可以同时处理多个任务,但总内存还是只有那么一块。因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中数据读到处理器缓存里。

       在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存 ,LOCK#信号一般不锁总线,而是锁缓存。即关键字上锁->指令信号锁缓存。同时,一个处理器的缓存回写到内存会导致其他处理器的缓存无效。毕竟指令还是按照顺序执行的,因此java依据基本底层指令提供上锁的关键字用于保证内存可见性/锁住资源,使我们能在更高层次上进行并发编程。

关键字volatile

        volatile用于声明变量(字段)使其成为共享变量。用关键字volatile声明的字段每次被使用时系统都会将该字段写回到内存,同时使缓存中的该字段失效(即该字段可能保存在缓存中,系统将该字段写回到内存并使缓存失效),这样,每当线程需要使用该字段时都需要从内存中读取该字段,保证了该字段的唯一可见性。注意,volatile只保证了内存可见性,并不保证原子性,volatile适用于get/set,并不适用于getAndOperate。我猜可能有这样一种情况,如果两个核同时拿到了字段,对字段同时进行写回,相当于只执行一次操作而不是两次。

关键字synchronized

synchronized可用于声明方法,同时可以锁住对象。

  • 对于普通同步方法,锁住当前实例对象。
  • 对于静态同步方法,锁住当前类的Class对象。
  • 对于同步方法块,锁住Synchonized括号里配置的对象。

synchronized基于指令monitorenter和monitorexit实现,是java提供的基本上锁机制。

每一个对象都有自己的监视器(monitor),获取锁就相当于拿到该对象的监视器进入同步方法/方法块,否则进入同步队列,陷入BLOCKED(阻塞)状态,当监视器退出后重新竞争锁。

cas:不加锁实现原子操作(乐观锁)

        cas即compare and swap,一种乐观锁策略,对于变量x,它的实现过程是,有3个操作数,内存值V,旧的预期值E,要修改的新值U,当且仅当预期值E和内存值V相同时,才将内存值V修改为U,否则什么都不做。

        原子操作是并发基本操作,保证字段的唯一修改性。cas常用于并发包中,对于共享字段x,假如要执行x++这一操作,x++必须保证原子性。一种办法是,x++写入方法中,对该方法使用synchronized上锁,肯定能保证x++的原子性,显而易见这种方式会比较耗时,另一种办法就是使用cas。

        cas同样基于指令实现,依靠内存改变的唯一顺序性保证原子操作,抽象出来就是cas(旧值,新值)。拿上面那个例子说明一下,有这样一种情况,如果两个核同时拿到了字段x=56,对字段同时进行x++写回,相当于只执行一次操作(57)而不是两次(58)。现在使用cas,两个核同时访问字段x=56,核1使用cas(56,57),意为旧值为56,新值为57,核2同样像这样干,执行cas(56,57),现在,因为指令执行仍是顺序的,指令级别上必定有先后顺序,且字段x的内存值的修改是瞬间的有前后关系的(无法实现同时改变内存值吧,那就太魔幻了),因此,假设核1先执行到了,x的内存值变为了57,核2的cas(56,57)也到了,但显然旧值56和57不同啊,因此操作失败,什么也不干,下面有一张图(来源:https://www.cnblogs.com/Mainz/p/3546347.html)

ConcurrencyCAS

        在程序层面上,因为核2的cas(56,57)操作失败,那么核2继续拿新值(可以使用volatile保证能从内存拿),继续执行cas(57,58),因此,x的操作就变为了原子操作,显而易见,必须对x进行包装,内部提供cas(旧值,新值)操作,返回一个boolean判断是否执行成功,如果不成功,一直循环执行。

AtomicInteger atomicI = new AtomicInteger(0);//包装原子类
for (;;) {
    int i = atomicI.get();//获取当前内存值
    boolean success = atomicI.compareAndSet(i, ++i);//尝试cas
    if (success) {
        break;
    }
}

cas中的ABA问题

        ABA问题比较抽象,线程A,B同时拿到了字段x,A执行cas(x,x+1),右执行cas(x,x-1),此时B以为x没变化,但实际上x变化了,如果单纯的是换值,这么做当然没问题,但是如果操控的是带有状态的结构,就可能引发问题。

下面这篇文章写得非常好,引用于:https://cloud.tencent.com/developer/article/1098132

        先描述ABA。假设两个线程T1和T2访问同一个变量V,当T1访问变量V时,读取到V的值为A;此时线程T1被抢占了,T2开始执行,T2先将变量V的值从A变成了B,然后又将变量V从B变回了A;此时T1又抢占了主动权,继续执行,它发现变量V的值还是A,以为没有发生变化,所以就继续执行了。这个过程中,变量V从A变为B,再由B变为A就被形象地称为ABA问题了。

        上面的描述看上去并不会导致什么问题。T1中的判断V的值是A就不应该有问题的,无论是开始的A,还是ABA后面的A,判断的结果应该是一样的才对。

        不容易看出问题的主要还是因为:“值是一样的”等同于“没有发生变化”(就算被改回去了,那也是变化)的认知。毕竟在大多数程序代码中,我们只需要知道值是不是一样的,并不关心它在之前的过程中有没有发生变化;所以,当我需要知道之前的过程中“有没有发生变化”的时候,ABA就是问题了。

现实ABA问题

         警匪剧看多了人应该可以快速反应到发生了什么。应用到ABA问题,首先,这里的A和B并不表示被掉的包这个实物,而是掉包过程中的状态的变化。假设一个装有10000W箱子(别管它有多大)放在一个房间里,10分钟后再进去拿出来赎人去。但是,有个贼在这10分钟内进去(别管他是怎么进去的)用一个同样大小的空箱子,把我的箱子掉包了。当我再进去看的时候,发现箱子还在,自然也就以为没有问题了的,就继续拿着桌子上的箱子去赎人了(别管重量对不对)。现在只要知道这里有问题就行了,拿着没钱的箱子去赎人还没有问题么?

        这里的变量V就是桌子上是否有箱子的状态。A,是桌子上有箱子的状态;B是箱子在掉包过程中,离开桌子,桌子上没有箱子的状态;最后一个A也是桌子上有箱子的状态。但是箱子里面的东西是什么就不不知道了。

程序世界的ABA问题

在运用CAS做Lock-Free操作中有一个经典的ABA问题:

         线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题

        当然,一般来说使用Actomic原子包即可,如果出现问题,可能是ABA,那么就使用AtomicStampedReference<E>,实现方式是内部增加一个版本号(实现起来应该比较复杂了)。

public boolean compareAndSet(
    V expectedReference, // 预期引用
    V newReference, // 更新后的引用
    int expectedStamp, // 预期标志
    int newStamp // 更新后的标志
)

线程

进程与线程的区别

进程是操作系统资源分配(包括上下文资源,cpu处理时间资源)的最小单元

线程是操作系统调度的最小单元

java线程的实现方式

线程的两种实现方式:继承Thread,实现接口Runnable

本质是要实现run()方法,该方法相当于线程的执行入口。启动线程时调用start()方法,Java虚拟机调用此线程的run方法。

线程优先级

java线程通过priority(内置int变量)控制优先级,通过方法setPriority(int)修改,但操作系统可以完全不理会优先级的设定,因此不能通过控制priority来控制java线程的执行顺序与执行时间。

线程状态

  • NEW:线程被构建,但未调用start()方法
  • RUNNABLE:运行状态
  • BLOCKED:阻塞,需要拿到锁
  • WAITING:等待,需要被唤醒,调用wait()方法即进入
  • TIME_WAITING:超时等待,可在超时时自动返回,即正在等待执行中
  • TERMINATED:终止,当前线程执行完毕

通过调用getState()返回线程当前状态,返回值为Thread.State,一个枚举类,继承于java.lang.Enum<Thread.State>。

Daemon线程

        支持性线程,也可看作是守护线程,即只是为了支持其他线程工作而启动的线程,调用setDaemon(boolean)方法设置,当进程中没有除Daemon线程外的其它任何线程时,jvm将退出,Daemon线程也立即终止,因此Daemon不能依靠finally确保执行关闭。

线程基本机制

中断Interrupted

中断用于终止当前线程,或通过控制终止其它线程

       调用interrupt()设置中断(当前线程中断),调用isInterrupted()判断当前线程是否中断,如果中断,终止或抛出InterruptedException则返回假 ,Thread.interrupted()用于测试当前线程是否中断。

可通过设置interrupt()或内部boolean变量安全的终止线程。

package chapter4_threads;

public class Shutdown {
    public static void main(String[] args){
        Runner a=new Runner("A");
        Runner b=new Runner("B");

        a.start();
        b.start();
        SleepUtils.second(2);
        //通过设置interrupt()终止线程
        a.interrupt();
        SleepUtils.second(2);
        //通过内置boolean变量终止线程
        b.shutdown();
    }

}
class Runner extends Thread{
    private long i=0;//计数
    private volatile boolean on=true;//用于控制

    public Runner(String name) {
        super(name);
    }

    @Override
    public void run(){
        while (on&&!Thread.currentThread().isInterrupted()){
            i++;
        }
        System.out.println(Thread.currentThread().getName()+":"+i);
    }

    public void shutdown(){
        on=false;
    }
}

等待/通知机制

        线程A,B通过对象O进行通信。A的执行需要某些条件,执行到某一步,条件满足就继续执行,否则先暂停直到条件满足,如何感受到条件变化?通过中间条件O进行控制,A拿到O的锁,条件不满足则调用wait()方法进入等待队列同时释放O的锁,B拿到锁后进行执行,到某一步调用notify()/notifyAll(),到释放锁后就通知等待队列中的某个线程/所有线程从等待队列进入到同步队列,重新竞争锁并从wait()方法之后继续执行。

典型的经典范式为消费者/生产者模式

消费者:                                             生产者:

synchronized(O){                                       synchronized(O){

    while(条件不满足){                                     改变条件

        O.wait();                                                  O.notify()/O.notifyAll();           

    }                                                               }       

    statement...

}

package chapter4_threads;

public class WaitNotify {
    public static Object o=new Object();
    public static boolean control=false;
    public static void main(String[] args){
        Thread a=new Thread(new A(),"A");
        Thread b=new Thread(new B(),"B");

        a.start();
        SleepUtils.second(2);
        b.start();
    }

    static class A implements Runnable{
        private int i=0;
        @Override
        public void run(){
            //拿到o的锁,A等待,条件满足时通知
            synchronized (o){
                try {
                    i++;
                    //A线程拿到o的锁并等待通知,感受到通知就返回
                    while(!control){
                        o.wait();//达不到条件就等待,调用完wait()方法就会释放锁,进入等待队列,感受到通知进入同步队列,重新竞争锁,拿到锁之后从wait()向下继续执行
                        i++;
                    }
                }catch (InterruptedException e){
                    System.out.println(Thread.currentThread().getName()+"-中断");
                }
            }
            System.out.println("A-"+i);
        }
    }

    static class B implements Runnable{
        @Override
        public void run(){
            synchronized (o){
                //B拿到o的锁,进行通知
                o.notify();
                control=true;
            }
        }
    }
}

 管道输入/输出流

线程间通过管道PipedWriter/PipedReader传递信息,媒介为内存,使用时必须进行连接

out.connect(in);即PipedWriter对象必须调用connect()方法连接PipedReader对象

线程A中使用PipedWriter对象向管道中写入,线程B中使用PipedReader对象从管道中读取数据,完成线程间的通信

package chapter4_threads;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
 * 线程间通过管道PipedWriter/PipedReader传递信息,媒介为内存,使用时必须进行连接
 * out.connect(in);
 */
public class Piped {

    public static void main(String[] args){
        PipedReader in=new PipedReader();
        PipedWriter out=new PipedWriter();

        try{
            //管道必须进行连接,否则抛出异常
            out.connect(in);
            Thread printThread=new Thread(new Print(in),"printThread");
            printThread.start();

            int receive=0;
            while((receive=System.in.read())!=-1){
                out.write(receive);//写入管道
            }

            out.close();//关闭
        }catch (IOException e){
            System.out.println(Thread.currentThread().getName()+"IO异常");
        }
    }
    
    static class Print implements Runnable{
        private PipedReader in;
        public Print(PipedReader in){
            this.in=in;
        }

        public void run(){
            int receive=0;
            try{
                while((receive=in.read())!=-1){//从管道中接收
                    System.out.println((char)receive);
                }
            }catch (IOException e){
                System.out.println(Thread.currentThread().getName()+"IO异常");
            }

        }
    }
}

Thread对象的join()方法

join()方法,在线程A中使用B.join()则A先等待,直到B线程结束才返回从A继续开始执行

线程局部变量ThreadLocal<T>

ThreadLocal,一个泛型类,提供线程局部变量 ,ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。

package chapter4_threads;

import java.util.concurrent.TimeUnit;

/**
 * ThreadLocal,一个泛型类,提供线程局部变量
 * ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。
 */
public class Threadlocal {
    static ThreadLocal<Long> TIME_THREADLOCAL=new ThreadLocal<>();
    public static void main(String[] args){
        begin();
        try{
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){}

        System.out.println("经过时间:"+end());
    }

    //设置开始时间
    private static void begin(){
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
    //返回经过的时间差
    private static long end(){
        return System.currentTimeMillis()-TIME_THREADLOCAL.get();
    }
}

辅助类CountDownLatch

位于java本身提供的并发包concurrent中

package MyStudy;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * CountDownLatch,允许一个或多个线程等待直到在其他线程中执行完成并返回继续执行的同步辅助类。
 * 其内部维护一个计数器,构造器初始传值,当前线程使用CountDownLatch对象的await()方法时会进入阻塞状态直到计数器变为0
 * 内部只有五个方法
 * void await():使当前线程等到锁存器计数到零,除非线程是 interrupted 。
 * boolean await(long timeout, TimeUnit unit):使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。
 * void countDown():减少锁存器的计数,如果计数达到零,释放所有等待的线程。
 * long getCount():返回当前计数。
 * String toString():返回一个标识此锁存器的字符串及其状态。
 * 该类可用于控制线程间的执行顺序(如几个线程同时开始执行,或先后执行)
 */
public class CountDownLatchPractice {
    private static CountDownLatch start=new CountDownLatch(1);//计数

    public static void main(String[] args){
        Thread t=new Thread(new Test(),"Test");
        t.start();

        try{
            System.out.println(System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(1);
            start.countDown();//计数器减一,main先执行,Test才能执行
        }catch (InterruptedException e){
            System.out.println(Thread.currentThread().getName()+"-中断");
        }
    }


    static class Test implements Runnable{
        @Override
        public void run(){
            try{
                start.await();//当前线程会被阻塞直到内部计数器为0
                System.out.println(System.currentTimeMillis());
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+"-中断");
            }
        }
    }
}

参考:

《java并发编程的艺术》--方腾飞

https://www.cnblogs.com/Mainz/p/3546347.html

https://cloud.tencent.com/developer/article/1098132

猜你喜欢

转载自blog.csdn.net/qq_40918961/article/details/96607888