并发理论基础


前言

本文主要讲述线程、Synchronized、volatile、ThreadLocal内存泄露问题、cas原子操作。


一、线程

一、创建线程的方式

/**
 * 面试题:创建线程有几种方式?
 * 根据Java源码中Thread上的注释上写到两种方式创建线程,一种是继承Thread,另一种是实现Runnable
 */
public class ThreadNew {
    /**
     * 派生自Thread类
     */
    private static class UseThread extends Thread{
        @Override
        public void run() {
            System.out.println("use thread");
        }
    }
    /**
     * 实现runnable接口
     */
    private static class UseRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("use runnable");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseThread useThread=new UseThread();
        UseRunnable useRunnable=new UseRunnable();
        Thread t2 = new Thread(useRunnable);
        t2.start();
        // 再次调用t2.start方法会怎样?
        //t2.start();//报错,阻断线程,后续的代码无法执行。
        useThread.start();
        Thread.sleep(2000);
    }
}


二、线程的状态/生命周期

在这里插入图片描述

/**
* parkNanos  和  parkUntil 的区别
**/
public class LockSupport {
   //nanos 纳秒单位,3000000000L
  public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }
    //deadline 是绝对时间的时间戳 System.currentTimeMillis()+3000
  public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }
}

public final class Unsafe {
	//param1 true表示绝对时间 param2 假定延迟3秒:System.currentTimeMillis()+3000
	//param1 false表示相对时间,param2的单位为纳秒  param2 假定延迟3秒: 3000000000L 【秒 毫秒 微妙 纳秒】
   public native void park(boolean var1, long var2);
}

yield() :使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。同时执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
比如,ConcurrentHashMap#initTable 方法中就使用了这个方法, 因为ConcurrentHashMap在调用put的时候才会进行初始化内存,而分配内存速度很快,因此采用yield方法。

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin 【丢失初始化竞争;只是自旋】
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

三、等待通知机制

在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法

等待和通知的标准范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。在这里插入图片描述

通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
在这里插入图片描述
面试题
调用yield() 、sleep()、wait()、notify()、join()等方法对锁有何影响?

  • yield() 、sleep()、join()被调用后,都不会释放当前线程所持有的锁。
  • 调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
  • 调用notify()系列方法后,对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是syn同步代码的最后一行。

为什么wait和notify方法要在同步块中调用?

  • 因为wait和notify的代码块并非原子操作,因此会存在先执行notify再执行wait,这样会导致通知丢失。也就是 lost wake up 问题

如何解决lost wake up问题 ?

  • 通过加锁,让wait 代码块跟notify代码块竞争同一把锁。

为什么wait代码块要用while循环体 ?

  • 因为notifyAll会把所有的线程唤醒,如果不用while循环体,有些还未满足条件的线程就直接结束线程了。

四、java线程的中断机制

 线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
 如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
 一、一般的阻塞方法,如sleep等本身就支持中断的检查,
 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断

五、协程(又称用户线程)

java中线程池的核心线程数需要参考操作系统的内核,即线程池创建的线程跟操作系统的内核有关。
任何语言实现线程主要有三种方式:使用内核线程实现(1:1)、使用用户线程实现(1:N)、使用用户线程+轻量级进程实现(N:M)
内核线程实现
 内核线程(Kernel-Level Thread, KLT) 就是直接由操作系统内核支持的线程。线程的创建、销毁、切换和调度都由操作系统完成,因此需要消耗一定的内核资源,一个系统支持的线程数量是有限的。
纤程-java中的协程
 目前Java中比较出名的协程库是Quasar[ˈkweɪzɑː®](Loom项目的Leader就是Quasar的作者Ron Pressler), Quasar的实现原理是字节码注入,在字节码层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖Java虚拟机的现场保护虽然能够工作,但影响性能。
在这里插入图片描述

六、 进程间的通信

同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信被称为R(mote)PC,需要通过网络,并遵守共同的协议,比如大家熟悉的Dubbo就是一个RPC框架,而Http协议也经常用在RPC上,比如SpringCloud微服务。
大厂常见的面试题就是,进程间通信有几种方式?

  1. 管道,分为匿名管道(pipe)及命名管道(named pipe):匿名管道可用于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  2. 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
  3. 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
  4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  5. 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
  6. 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用Unix domain socket(比如同一机器中MySQL中的控制台mysql shell和MySQL服务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高

二、Synchronize内置锁&Volatile

一、Synchronize必须锁同一个对象,否则锁失效

public class SynInstance {

    private long  count=0;
    private Object obj =new Object();

    public void incCountBlock(){
        synchronized (this){
            count++;
        }
    }

    public void incCountObj(){
        synchronized (obj){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynInstance simplOper=new SynInstance();
       new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for(int i=0;i<10000;i++){
                            simplOper.incCountBlock();
                        }
                    }
                }
        ).start();

        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for(int i=0;i<10000;i++){
                            simplOper.incCountObj();
                        }
                    }
                }
        ).start();
        Thread.sleep(50);
        //在并发场景下,synchronized 能锁住 incCountBlock 和 incCountObj 吗????
        System.out.println(simplOper.count);//20000
       
       }
}        

结果没锁住,输出的count != 20000 ,原因是synchronized (this) 锁的是SynInstance.class 而 synchronized (obj) 锁的是Object obj 这个变量,两个锁的对象不一样。

public class SynInstance {

    // static 必须要加,不然new的对象会分配新的空间给count变量。
    private static long count=0;
    private Object obj =new Object();

    public void incCountBlock(){
        synchronized (this){
            count++;
        }
    }

    public void incCountObj(){
        synchronized (obj){
            count++;
        }
    }

    private static class RunnableSyn implements Runnable{

        private SynInstance simplOper;

        public RunnableSyn(SynInstance simplOper){
            this.simplOper=simplOper;
        }

        @Override
        public void run() {
            for(int i=0;i<10000;i++){
                simplOper.incCountBlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynInstance s1 = new SynInstance();
        SynInstance s2 = new SynInstance();
        new Thread(new RunnableSyn(s1)).start();
        new Thread(new RunnableSyn(s2)).start();
        Thread.sleep(50);
        System.out.println(s1.count);
        System.out.println(s2.count);
        //s1.count 和 s2.count 输出的结果是多少???
        
    }
}

结果:s1.count 和 s2.count 输出的结果是一致的,然而并没有达到20000

public class SynStatic {

    private static long count =0;
    private static Object obj = new Object();//静态变量

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    /*用在同步块上*/
    public void incCountBlock(){
        synchronized (this){
            count++;
        }
    }

    /*用在同步块上,但是锁的是单独的对象实例*/
    public void incCountObj(){
        synchronized (obj){
            count++;
        }
    }
    /*syn直接放在方法上,静态方法*/
    public synchronized static void incCountMethod(){
        count++;
    }

    //线程
    private static class Count extends Thread{
        private SynStatic simplOper;
        public Count(SynStatic simplOper) {
            this.simplOper = simplOper;
        }

        @Override
        public void run() {
            for(int i=0;i<10000;i++){
                simplOper.incCountMethod();
            }
        }
    }

    private static class Count2 extends Thread{
        private SynStatic simplOper;
        public Count2(SynStatic simplOper) {
            this.simplOper = simplOper;
        }

        @Override
        public void run() {
            for(int i=0;i<10000;i++){
                simplOper.incCountObj();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynStatic simplOper = new SynStatic();
        //启动两个线程
        SynStatic.Count count1 = new SynStatic.Count(simplOper);
        SynStatic.Count2 count2 = new SynStatic.Count2(simplOper);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(simplOper.count);//20000
    }
}

结果:没锁住,count !=20000,原因是object虽然加了static,然而incCountObj方法锁住的对象是object.class ,incCountMethod方法锁住的是SynStatic.class ,锁的对象不统一。

二、Volatile是最轻量的通信/同步机制

volatile保证了不同线程对这个变量进行操作时的可见性,但是volatile不能保证数据在多个线程下同时写时的线程安全,volatile最适用的场景:一个线程写,多个线程读。


三、ThreadLocal内存泄露问题

一、什么是内存泄露?

应该被回收的内存,实际没被回收,就是内存泄露。

二、ThreadLocal的使用

  • public void set(T value)
    将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写这个方法,只依赖initialValue方法来设置线程局部变量的值。
    参数:
    Value -要存储在当前线程的这个线程本地副本中的值。
    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  • public T get()
    返回此线程局部变量的当前线程副本中的值。如果变量对当前线程没有值,则首先将其初始化为initialValue方法调用返回的值。
    返回:
    线程本地当前线程的值
    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • public void remove()
    移除此线程局部变量的当前线程值。如果这个线程局部变量随后被当前线程读取,它的值将通过调用它的initialValue方法重新初始化,除非它的值是由当前线程在过渡期间设置的。这可能导致在当前线程中多次调用initialValue方法。
    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
  • protected T initialValue()
    返回当前线程的这个线程局部变量的“初始值”。该方法将在线程第一次使用get方法访问变量时调用,除非该线程先前调用了set方法,在这种情况下,将不会为该线程调用initialValue方法。通常,每个线程最多调用此方法一次,但在随后调用remove和get的情况下,可能会再次调用该方法。
    这个实现简单地返回null;如果程序员希望线程局部变量具有非null的初始值,则必须将ThreadLocal子类化,并重写此方法。通常,将使用匿名内部类。
    返回:
    这个线程本地的初始值
    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

三、以下代码ThreadLocal产生内存泄露的原因?

public class ThreadLocalMemory {
    private static final int SIZE =500;

    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5, 1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    static class LocalVariable{
        private byte[] a=new byte[1024*1024*5];
    }

    ThreadLocal<LocalVariable> threadLocalLV;

    public static void main(String[] args) {
        for (int i = 0; i < SIZE; i++) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocalMemory oom = new ThreadLocalMemory();
                    oom.threadLocalLV = new ThreadLocal<>();
                    oom.threadLocalLV.set(new LocalVariable());
                    //解决内存泄露问题
                   oom.threadLocalLV.remove();
                }
            });
            SleepTools.ms(100);
        }
        System.out.println("pool execute over");
    }

}

根据上述代码可得如下流程图,Entry.key弱引用ThreadLocal,在GC的时候会回收ThreadLocal对象,而Entry.value 需要ThreadLocal.get才能获取,现在ThreadLocal被回收后,Entry.value就访问不到了,如果线程一直不结束,则就出现内存泄露。
解决方案:移除Entry.value.
在这里插入图片描述

ThreadLocalMap 跟 HashMap的区别
ThreadLocalMap 解决Hash冲突采用开放定址法中的线性探测算法。
HashMap 解决Hash冲突采用链地址法。

四、CAS

一、什么是原子操作

  • 通过操作系统的CAS指令,操作过程先比较,再交互。但是在业务操作上,不一定一次就完成,所以会循环CAS操作,直到成功为止。但是CAS实现原子操作还有三个问题,分别是ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。

一、什么是ABA问题

  • 就是A值修改后又修改回来,而CAS在比较的时候没发现。

该如何解决ABA问题呢?

  • 使用版本号,比如AtomicStampReference 与AtomicMarkableReference【这个是比较值的同时还比较mark(Boolean),如果中间变更后,值跟mark跟初始值一样,还是一样能执行成功】
public class AMRRefer {
    AtomicMarkableReference<Integer> atomicReference =new AtomicMarkableReference<>(100,false);

    public static void main(String[] args) {
        AMRRefer refer=new AMRRefer();
        new Thread(()->{
            refer.atomicReference.compareAndSet(100,101,refer.atomicReference.isMarked(),!refer.atomicReference.isMarked());
            System.out.println("变成101:"+refer.atomicReference.isMarked());
            refer.atomicReference.compareAndSet(101,103,refer.atomicReference.isMarked(),!refer.atomicReference.isMarked());
            System.out.println("变成103:"+refer.atomicReference.isMarked());
            refer.atomicReference.compareAndSet(103,100,refer.atomicReference.isMarked(),!refer.atomicReference.isMarked());
            System.out.println("变成100:"+refer.atomicReference.isMarked());
        },"Thread1").start();

        new Thread(()->{
            SleepTools.ms(1000);
            refer.atomicReference.compareAndSet(100,111,false,true);
            System.out.println(refer.atomicReference.getReference());//100
            System.out.println("是否变成111:"+!refer.atomicReference.isMarked());
        },"Thread2").start();//false
        SleepTools.ms(100);
    }
}

AtomicStampReference

public class ASRefer {
    AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,0);

    public static void main(String[] args) {
        ASRefer refer=new ASRefer();
        new Thread(()-> {
            refer.atomicStampedReference.compareAndSet(100, 103, refer.atomicStampedReference.getStamp(), refer.atomicStampedReference.getStamp() + 1);
            System.out.println(refer.atomicStampedReference.getStamp());//1
            refer.atomicStampedReference.compareAndSet(103, 100, refer.atomicStampedReference.getStamp(), refer.atomicStampedReference.getStamp() + 1);
        },"1").start();

        new Thread(()-> {
            refer.atomicStampedReference.compareAndSet(100, 111, 0, refer.atomicStampedReference.getStamp() + 1);
            System.out.println(refer.atomicStampedReference.getReference());//100
        },"2").start();
    }
}

二、循环时间长开销大怎么解决

  • 因为在高并发场景下,失败并不断自旋会一直占用cpu,所以多任务快速处理的场景不适合CAS,单个任务处理够快且任务量大, 使用CAS会带来很好地效果 。为了解决高并发场景下的失败不断自旋问题,jdk1.8引入了LongAdder,该类的基本思路是分散热点,对于LongAdder来说,内部有一个base变量,一个Cell[]数组。
    base变量:非竞态条件下,直接累加到该变量上。
    Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
    /**
     * Table of cells. When non-null, size is a power of 2.
     */
    transient volatile Cell[] cells;

    /**
     * Base value, used mainly when there is no contention, but also as
     * a fallback during table initialization races. Updated via CAS.
     */
    transient volatile long base;
        /**
     * Returns the current sum.  The returned value is <em>NOT</em> an
     * atomic snapshot; invocation in the absence of concurrent
     * updates returns an accurate result, but concurrent updates that
     * occur while the sum is being calculated might not be
     * incorporated.
     *
     * @return the sum
     */
    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

三、只能保证一个共享变量的原子操作

  • 用锁的方式
  • 取巧,将多个共享变量合并,比如a=1,b=2 合并为ab=12;

总结

简单介绍并发编程中,线程、进程、协程的概念,线程安全问题的解决方案之一Synchronized,解释ThreadLocal为什么会有内存泄露的风险,解释CAS的原子操作以及三大问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值