JUC并发编程(深入)

从start线程说起

java线程是通过start的方法启动执行的,主要内容在native方法start0中,

openjdk的写JNI一般是一一对应的,Thread.java 就对应Thread.c,

start0就是JVM_StartThread。

在openjdk源码中jvm.h对这个对应关系进行了声明,代码如下,在jvm.cpp中才实现。

// jvm.h

static JNINativeMethod method[] = {
    {"start0",    "()V",    (void *)&JVM_StartThread},
}

在jvm.cpp中传入本地native线程,在thread.cpp中 实现了Thread::start方法。 

// jvm.cpp  2883行

Thread::start(native_thread)

在thread.cpp中,最终通过操作系统来启动一个线程 。

//thread.cpp

void Thread::start(Thread* thread){
    ....
    os::start_thread(thread);
}

CompletableFuture

Future接口理论知识

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完成。

主线程让子线程去做一项任务,主线程去忙其他,忙完找子线程要结果

总结:Future接口可以为主线程开一个分支业务。费时费力的事情让子线程干。

常用实现类FutureTask异步任务

有三个特点:多线程、有返回、异步任务

获得多线程是通过Runnable方法,但是这个方法没返回值。有一个RunnableFuture方法,这方法由Runnable和Future构成,既满足多线程,又满足了异步任务。而FutureTask又继承RunnableFuture。FutureTask既可以通过构造注入FutureTask(Callable<V> callable)满足了三个特点,也可以直接FutureTask<Runnable runnable,V value> 

package array_algorithm;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyThread());
        Thread t1 = new Thread(futureTask,"t1");
        t1.start();
        System.out.println(futureTask.get());
    }

}

class MyThread implements Callable<String>{

    @Override
    public String call() throws Exception {
        System.out.println("come in--");
        return "hello callable";
    }
}

优缺点

优点:future加线程池异步多线程任务配合,提高程序执行效率

package array_algorithm;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class FutureThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        FutureTask<String> futureTask1 = new FutureTask<String>( () -> {
            TimeUnit.MILLISECONDS.sleep(500);
            return "task1 over";
        });
        threadPool.submit(futureTask1);
        FutureTask<String> futureTask2 = new FutureTask<String>( () -> {
            TimeUnit.MILLISECONDS.sleep(300);
            return "task2 over";
        });
        threadPool.submit(futureTask2);

        TimeUnit.MILLISECONDS.sleep(500);
        threadPool.shutdown();
    }
}

缺点:get()阻塞:在Future任务完成前,会一直处于阻塞状态。

isDone()轮询:耗费cpu资源,导致cpu空转

CompletableFuture对Future的改进

CompletableFuture提供了一种观察者模式类似的机制,可以让任务完成后通知监听的一方。避免了get()阻塞和isDone()空转的问题。

CompletableFuture同时实现了Future<T>,CompletionStage<T>。

CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。

四个静态方法,创建异步任务

runAsync(无返回值):

public static CompletableFuture<Void> runAsync(Runnable runnable)

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> completableFuture =  CompletableFuture.runAsync( () -> {
            System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-1
        });
        System.out.println(completableFuture.get()); // null
    }

}

public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<Void> completableFuture =  CompletableFuture.runAsync( () -> {
            System.out.println(Thread.currentThread().getName()); // pool-1-thread-1
        },threadPool);
        System.out.println(completableFuture.get()); // null
    }

}

supplyAsync(有返回值)(常用)

public static CompletableFuture<U> supplyAsync(Supplier<U> supplier)

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> completableFuture =  CompletableFuture.supplyAsync( () -> {
            System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-1
            return "hello";
        },threadPool);
        System.out.println(completableFuture.get()); // hello
    }

}

public static CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture<String> completableFuture =  CompletableFuture.supplyAsync( () -> {
            System.out.println(Thread.currentThread().getName()); // pool-1-thread-1
            return "hello";
        },threadPool);
        System.out.println(completableFuture.get()); // hello
    }

}

Executor参数说明

如果没有指定Executor,使用默认的ForkJoinPool作为它的线程池进行异步代码

异步编程

考虑到使用get方法会阻塞,提供了whenComplete方法,下面的代码会出现一个问题,那就是结果没返回,因为没有调用自定义的线程池时,会采用ForkJoinPool,它是守护线程,当主线程跑完,子线程还没跑完时,守护线程也结束了,所以子线程的结果也不输出。

解决方法:

1、添加自定义线程池。(推荐)

2、让主线程也睡一会,晚点结束

package array_algorithm;

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture.supplyAsync( () -> {
            System.out.println(Thread.currentThread().getName());
            try {TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
            int result = ThreadLocalRandom.current().nextInt(10);
            return result;
        }).whenComplete( (v,e) -> {
            if (e==null){
                System.out.println("值为:"+v);
            }
        }).exceptionally(e->{
            e.printStackTrace();
            System.out.println("异常情况");
            return null;
        });

        System.out.println(Thread.currentThread().getName()+"忙去了");
    }

}

优点

异步任务结束时,会自动回调某对象的方法,主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以循序执行。异步任务出错时,会自动回调某个对象的方法。

CompletableFuture常用方法

CompletableFuture同时实现了Future<T>,CompletionStage<T>。

而CompletionStage代表异步计算过程中的某一个阶段,一个阶段的计算执行可以是一个Function,Cusumer或者Runnable。一个阶段的执行可能被单个阶段的完成触发,也可能是由多个阶段一起触发。

常用方法

获得结果和触发计算

获取结果:get(),get(long timeout,TimeUnit unit),join(),getNow(T valueifAbsent)计算完返回计算完的结果,没计算完就返回valueifAbsent

主动触发计算:bool complete(T value)返回false就认为没调用complete方法,计算已经完成。返回true + value形式就是没计算完成。

对计算结果进行处理

thenApply:计算结果存在依赖关系,两个线程串行化,当前步骤错了,不走下一步


import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync( () -> {
            System.out.println("111");
            return 1;
        }).thenApply( f -> {
            // int i = 10/0
            System.out.println("222");
            return f+2;
        }).thenApply( f -> {
            System.out.println("333");
            return f+3;
        }).whenComplete( (v,e) ->{
            if (e==null){
                System.out.println("value="+v);
            }
        }).exceptionally( e -> {
            e.printStackTrace();
            return null;
        });
    }
}

handle:计算结果存在依赖关系,两个线程串行化,有异常也可以继续走,根据带的异常参数进一步处理。

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync( () -> {
            System.out.println("111");
            return 1;
        }).handle( (f,e) -> {
//            int i = 10/0
            System.out.println("222");
            return f+2;
        }).handle( (f,e) -> {
            System.out.println("333");
            return f+3;
        }).whenComplete( (v,e) ->{
            if (e==null){
                System.out.println("value="+v);
            }
        }).exceptionally( e -> {
            e.printStackTrace();
            return null;
        });
    }
}

对计算结果进行消费

thenAccept:接收任务的处理结果,并消费处理,无返回结果

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            return 1;
        }).thenApply(f -> {
            return f + 2;
        }).thenApply(f -> {
            return f + 3;
        }).thenAccept(r -> {
            System.out.println(r);
        });
    }
}

thenRun:任务A执行完执行B,B不需要A的结果

thenAccept:任务A执行完执行B,B需要A的结果,B无返回值

thenApply:任务A执行完zhixingB,B需要A的结果,B有返回值

上述三个都提供了Async异步方法。特例:

上诉例子中

1、如果第一个thenRun变成theRunAsync,如果用了线程池,只有supplyAsync的那部分会用线程池,剩下的从theRunAsync开始都是ForkJoinPool。

2、如果supplyAsync用了自定义线程池,调用thenRun方法,有可能处理太快,直接使用main线程处理。

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("1 "+Thread.currentThread().getName());
            return 1;
        },threadPool).thenRun(() -> {
            System.out.println("2 "+Thread.currentThread().getName());
        }).thenRunAsync(() -> {
            System.out.println("3 "+Thread.currentThread().getName());
        }).thenRun(() -> {
            System.out.println("4 "+Thread.currentThread().getName());
        });
        try {TimeUnit.MILLISECONDS.sleep(1000);} catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName());
    }
}

对计算速度进行选用

谁快用谁

applyToEither:

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "A";
        });
        CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "B";
        });
        CompletableFuture<String> result = playA.applyToEither(playB,f->{
            return f + " is winner";
        });
        System.out.println(result.join());
    }
}

对计算结果进行合并

两个CompletionStage任务都完成后,最终将两个任务的结果一起交给thenCombine来处理,先完成的等待其他分支任务。

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "A";
        });
        CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "B";
        });

        CompletableFuture<String> result = playA.thenCombine(playB, (x, y) -> {
            return x + y;
        });
        System.out.println(result.join());
    }
}

JAVA锁事

乐观锁和悲观锁

悲观锁:synchronized关键字和Lock的实现类都是悲观锁。认为自己在使用数据的时候一定有别的进程来修改数据,因此获取数据前先加锁。

适合写操作多的场景,先加锁可以保证写操作时数据正确。

乐观锁:认为自己使用数据时别人不会来,所以不添加锁,只有更新数据的时候去判断,之前有没有别的线程更新了这个数据。如果没有,就将自己修改的数据写入。如果有,就会采用其他方式,例如放弃修改,重新抢锁等。判断规则:版本号机制、CAS算法

适合读操作多的场景,两种实现方式:1、Version版本号机制。2、CAS(Compare-and-Swap)比较替换算法实现

synchronized底层源码分析(字节码角度)

通过javap  -c ***.class文件反编译,也可以通过javap -v ***.class反编译,可以输出更多信息。

同步代码块

通过monitorenter和monitorexit来控制锁。一般情况下是一个enter对应两个exit,以防遇到特殊情况锁没释放出去

普通同步方法(对象锁)

检查方法的ACC_SYNCHRONIZED标志是否被设置,设置了线程将会先持有monitor锁,然后再执行方法,最后释放monitor

静态同步方法(类锁)

检查方法的ACC_STATIC,ACC_SYNCHRONIZED标志是否被设置,设置了线程将会先持有monitor锁,然后再执行方法,最后释放monitor

为什么任意一个对象都可以成为锁

每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来。

在HotSpot虚拟机中,monitor采用ObjectMonitor实现,在ObjectMonitor中,有一个owner属性,即持有ObjectMonitor对象的线程。 

公平锁和非公平锁

公平锁:先来先得,排队

非公平锁:不按照申请锁的顺序来分配。后来的也可能优先获得锁。有可能造成优先级翻转和饥饿的状态

为什么会有这样的设计,为什么默认非公平

1、恢复挂起的线程到真正获取还是有时间差的,非公平锁可以更充分的利用CPU的时间片,减少空闲状态时间。

2、多线程需要考虑到线程切换的开销,采用非公平锁时,当一个线程刚释放锁再获取锁的概率就比较大,因为减少了线程的开销。

可重入锁(递归锁)

同一个线程外层的锁被获取后,如果持有同一把锁,内层的锁就不会再阻塞它。例如开了家门,就不会被卧室门阻挡。

种类

隐式:

synchronized关键字使用的锁。一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁的。

synchronized的重入原理

每个锁对象拥有一个锁计数器和一个执行该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为0,说明没被其他线程持有,然后将锁对象的持有线程设置为当前线程,计数器加1;

如果当前技数器不为0,判断是否是当前线程,是就加1。否就等待那个线程释放锁。

执行到monitorexit时,计数器减1,当为0时表示锁已经释放。

显示:

Lock也有ReentrantLock

死锁和排查

死锁:两个或两个以上的线程,因争夺资源而造成的一种互相等待的现象,若无外力干涉他们都将无法推进下去。(jstack查看)

产生原因:系统资源不足、进程推进顺序不合、资源分配不当

写锁(独占锁)、读锁(共享锁)

自旋锁SpinLock

与后文的CAS相关,自旋锁借鉴了CAS思想。

舱室获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,直到获取。好处是减少了线程上下文切换的消耗。缺点是循环会消耗CPU。

借鉴CAS实现自旋锁。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+ " come in ----");
        while (!atomicReference.compareAndSet(null, thread)) {
//                如果有人用锁,我就在这等,等到没人用
        }
    }

    public void unlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName() + " come out---");
    }
    
    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread( () ->{
            spinLockDemo.lock();
            try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
            spinLockDemo.unlock();
        },"A").start();

        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread( () ->{
            spinLockDemo.lock();
            spinLockDemo.unlock();
        },"B").start();
    }
}

无锁—>独占锁—>读写锁—>邮戳锁

无锁—>偏向锁—>轻量锁—>重量锁

LockSupport与线程中断

LockSupport

用于创建锁和其他同步类的基本线程阻塞原语

park():除非有许可证,否则阻塞线程

unpark():解除阻塞线程,如果没有许可证,我给你许可证

等待唤醒机制

1、Object中的wait()方法让线程等待,notify()方法唤醒线程。这两个方法需要放在synchronized同步块或者方法里,且成对出现。notify在wait前面先执行,程序无法正常唤醒。

2、JUC包中Condition的await()方法让线程等待,使用signal()方法叫醒线程

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread( () -> {
    lock.lock()
    try{
        condition.await();
    }catch{....
    }finally{
        lock.unlock;
    }
},"t1").start();
 

new Thread( () -> {
    lock.lock()
    try{
        condition.signal();
    }catch{....
    }finally{
        lock.unlock;
    }
},"t2").start();
 

Condition中的线程等待和唤醒方法,需要先获取锁,一定要先await后signal

3、LockSupport类中park()和unpark()阻塞和唤醒指定线程

Thread t1 = new Thread( () -> {
    LockSupport.park();
},"t1");
t1.start();
 
new Thread( () -> {
    LockSupport.unpark(t1);
},"t2").start();

 

解决了不用锁块和同步关键字的问题,也避免了先await后signal的问题

重点:

许可证不会累积,只有一个。线程阻塞需要消耗premit,而凭证最多只有一个,如果有两个park(),会卡在第二个park(),哪怕t2线程写了多个unpark()。

线程中断

Java提供了一种停止线程的协商机制----中断,也叫中断标识协商机制。

中断只是一种协作协商机制,Java没提供任何语法,中断的过程完全由程序员自己实现。若要中断一个线程,需要手动调用吸纳成的interrupt方法,该方法也仅仅是将线程对象的中断标识设为true。代码需要不停检查当前线程的标识位,由线程自己来决定要不要停止运行。interrupt可以在别的线程中调用,也可以在自己的线程中调用。

void interrupt:中断此线程。(设置中断标志位为true,发起一个协商而不是立刻停止线程)

static boolean interrupted:测试当前线程是否已被中断。(判断线程是否被中断并清除当前中断状态)1、返回当前中断状态,测试是否已经中断。2、将当前线程的中断清零并重新设置为false,清除线程的中断状态。

boolean isInterrupted:测试此线程是否已被中断。(检查中断标志位)

内存模型之JMM

因为cpu和主存的速度不一致,CPU与主存之间不是直接操作的关系,而是通过缓存间接操作,但内存的读和写操作会造成不一致的问题。JVM试图通过一种Java内存模型JMM来屏蔽掉各种硬件和操作系统的内存访问差异,使得Java程序在各平台下都能达到一致的内存访问效果。

JMM本身是一种抽象的概念,它仅仅描述的是一组约定或规范,定义了程序中各个变量的读写访问方式并决定一个线程对共享变量的写入如何变成对另一个线程可见。关键技术都是围绕多线程的原子性、可见性和有序性展开。

作用:

1、通过JMM实现线程和主内存之间的抽象关系。

2、屏蔽各个硬件平台和操作系统的内存访问差异,让Java程序在各个平台下都能达到一致的内存访问效果。

三大特性

可见性

当一个线程改变了某个共享变量的值,其他线程能否即刻知道该改变。

JMM规定所有的变量都存储在主内存中。线程不可以直接操作主内存,通过将数据拉取到本地内存,在本地内存中进行操作。不同线程无法直接访问其他线程中的工作内存(本地内存),只能通过传递给主内存,其他线程拉取主内存数据来完成数据交互。

原子性

如果没有原子性,会出现线程脏读情况。

有序性

一般认为从上到下有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序,规定JVM线程内部维持循顺序话语义。只要程序的最终结果与它顺序化执行的结果相等,指令的执行顺序可以和代码顺序不一致。此过程也叫指令的重排序。

优点:可以根据CPU的特性,充分发挥CPU的性能

缺点:串行语义可以保证一致,但是多线程间的语义不一定能一致。

读取过程

前面讲过线程将主内存的数据读取到自己的本地内存(副本/拷贝),在本地内存中进行操作。在本地本地内存和主内存之间,读取受到缓存一致性协议or总线锁机制==》(JMM控制)。

先行发生原则之happens-before

如果一个操作执行的结果需要对另一个操作可见或者代码重排序。那么这两个操作之间必须存在happens-before原则,即逻辑上的先后关系。

happens-before==》

1、如果一个操作先行发生于另一个操作,那么第一个操作的结果将对第二个操作可见,且第一个操作顺序排在第二个操作之前。

2、两个操作之间存在happens-before关系,并不意味着一定按照happens-before规定的顺序来执行,如果重排序之后的执行结果与happens-before关系执行的结果一致,那么这种重排序就可以。

happens-before八条规则

次序规则

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作

锁定规则

AB两个线程都想用同一把锁,A先用了锁,那么A的unLock操作一定先行发生于B的lock操作

volatile变量规则

对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。前面的写对后面的读是可见的。

1、当第一个操作是volatile读,不管第二个操作是什么,都不能重排序。为了保证volatile读之后的操作不会排序到读之前

2、当第二个操作是volatile写,无论第一个操作是什么,也不能重排序。为了保证volatile写之前的操作不会排序到写之后

3、当第一个操作为volatile写,第二个操作为volatile读,也不能重排序。

传递规则

A先于B,B又先于C。那么A一定先于C。

线程启动规则

Thread对象的start()方法先行于此线程的每一个动作

线程中断规则

对线程interrupt()方法的调用 先行于线程的检查中断事件代码的发生。

== 先调用了interrupt()方法设置了中断标志位,我才能检测到中断发送

线程终止规则

线程中的所有操作都先行于此线程的终止检测,通过isAlive()检测线程是否已经终止执行。

对象终结规则

对象的初始化完成(构造方法)先行于它的finalize()方法的开始。

volatile与JMM

volatile特点:可见性、有序性(可以禁止重排)

volatile内存语义:通过内存屏障控制

1、写volatile变量时,JMM会把本地内存的共享变量值立即刷新回主内存中

2、读volatile变量时,JMM会把本地内存设置为无效,重新回到主内存读取最新共享变量

内存屏障(重点)

也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器对内存随机访问的操作中的一个同步点,使得同步点前的所有操作都执行完后,才能执行此点之后的操作。

避免代码重排序,内存屏障其实就是一种JVM指令,重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些指令,volatile实现了可见性和有序性(禁重排),但无法保证原子性。

写屏障(Store Memory Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回主内存中。看到Store屏障指令,就必须把指令前所有写操作写回主内存。

读屏障(Load Memory Barrier):在读指令之前插入读屏障,让工作内存或CPU缓存中的数据都失效,重新回到主内存中获取最新的数据。在Load屏障指令之后,一定能保证后面读取的数据一定是最新的数据。

因此重排序时,不允许把内存屏障之后的指令排序到屏障之前。对volatile变量的写操作也先行于任意后续对这个变量的读操作,也叫做写后读。

CAS

原子类:java.util.concurrent.atomic

什么是CAS:compare and swap,比较并交换。它包含三个操作数----内存位置、预期原值及更新值。执行CAS操作的时候,将内存位置的值与预期原值比较;匹配:处理器自动将该位置的值更新为新值;不匹配:处理器不操作,多个线程执行的CAS操作只有一个会成功。

CAS是JDK提供的非阻塞原子性操作,提供硬件保证了比较-更新的原子性。CAS是一条CPU的原子指令(cmpxchg指令)执行这个指令的时候,会判断是否是多核系统,如果是就给总线加锁,只有一个线程可以成功对总线加锁,成功后才执行cas操作。本质上是通过CPU独占实现的原子性。

AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5,2024); // 如果5和主内存的数据相同,就改成2024
// sout(atomicInteger.get())

Unsafe

是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地native方法来访问,unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。Unsafe类中的所有方法都是native修饰的,所有unsafe类中的方法都是直接调用操作系统底层资源执行任务的。

AtomicReference

自定义原子类型

AtomicReference<Book> atomicReference = new AtomicReference<>();
Book b1 = new Book("haha",128);
atomicReference.set(b1);  // 主内存数据,设置成b1
Book b2 = new Book("nana",88);
atomicReference.compareAndSet(b1,b2);  // 比较b1和主内存数据是否相同,相同就改成b2,这里相同,会改变。

ABA问题

比如1线程从内存位置V取出了数据A,此时线程2也从内存位置V取出了A,同时线程2将数据改成了B写入内存位置V,然后又将这个内存位置V的数据改成了A。当线程1回来进行CAS操作时,发现内存中的数据还是A,和预期一样,所以线程1也操作成功了。

虽然线程1,2的CAS操作都成功了,但是不代表这个过程是没问题的。

解决方法

版本号时间戳原子引用:AtomicStampedReference

简单案例:单线程

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicStampedReference;


@NoArgsConstructor
@AllArgsConstructor
@Data
class Book{
    private int id;
    private String name;
}

public class AtomicStampedDemo {

    public static void main(String[] args) {
        Book b1 = new Book(1,"java");
        Book b2 = new Book(2,"python");
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(b1,1);

        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());

        stampedReference.compareAndSet(b1,b2, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());

        stampedReference.compareAndSet(b2,b1, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
    }
}

案例2:多线程

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedDemo {
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        new Thread( () ->{
            System.out.println(Thread.currentThread().getName()+"首次版本号:"+stampedReference.getStamp());
            try {TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) {e.printStackTrace();}

            stampedReference.compareAndSet(100,101, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" 2次版本号:"+stampedReference.getStamp());

            stampedReference.compareAndSet(101,100, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" 3次版本号:"+stampedReference.getStamp());
        },"A").start();

        new Thread( () ->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"首次版本号:"+stamp);
            try {TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) {e.printStackTrace();}
            boolean b = stampedReference.compareAndSet(100,2024, stamp, stamp+1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
        },"B").start();
    }
}

原子类

基本类型原子类

AtomicInteger、AtomicBoolean、AtomicLong

countDownLatch(避免主线程跑太快,子线程计算没完成)

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

class MyNumber{
    AtomicInteger atomicInteger = new AtomicInteger();
    public void add(){
        atomicInteger.getAndIncrement();
    }
}
public class AtomicStampedDemo {
    public static final int SIZE = 50;
    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 0; i < SIZE; i++) {
            new Thread( () ->{
                try {
                    for (int j = 0; j < 1000; j++) {
                        myNumber.add();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t"+myNumber.atomicInteger.get());
    }
}

数组类型原子类

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

引用类型原子类

AtomicReference:自旋锁

AtomicStampedReference:携带版本号(+1),解决修改过几次、状态戳原子引用

AtomicMarkableReference:一次性。false,true。解决是否修改过。

对象的属性修改原子类

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

原子更新对象中XX类型字段的值。可以以线程安全的方式操作非线程安全对象内的某些字段。

更新的对象必须使用public volatile修饰符、每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类型。

案例1 AtomicIntegerFieldUpdater

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

class MyNumber{
    String name = "BBA";
    public volatile int number = 0;
    AtomicIntegerFieldUpdater<MyNumber> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(MyNumber.class,"number");
    public void add(MyNumber myNumber){
        fieldUpdater.getAndIncrement(myNumber);
    }
}

public class AtomicIntegerFieldUpdaterDemo {
    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 0; i < SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        myNumber.add(myNumber);
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);   // main	50000

    }
}

案例2 AtomicReferenceFieldUpdater

多线程并发调用类的初始化方法,但只有一个线程进行初始化,其他处理失败。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

class MyNumber{
    String name = "BBA";
    public volatile Boolean is_init = Boolean.FALSE;
    AtomicReferenceFieldUpdater<MyNumber,Boolean> fieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(MyNumber.class,Boolean.class,"is_init");
    public void init(MyNumber myNumber) {
        if (fieldUpdater.compareAndSet(myNumber,Boolean.FALSE,Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName()+"\t"+"start init");
            try {TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"finished init");
        }else {
            System.out.println(Thread.currentThread().getName()+"\t"+"其他线程在init");
        }
    }
}
public class AtomicReferenceFieldUpdaterDemo {
    public static void main(String[] args){
        MyNumber myNumber = new MyNumber();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                myNumber.init(myNumber);
            }, String.valueOf(i)).start();
        }
    }
}

原子操作增强类深度剖析

DoubleAccumulator、DoubleAdder

LongAccumulator、LongAdder

LongAdder只能用来计算加法,初始值为0。性能比AtomicLong好。

它的基本思路是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己操中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果想获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心思想就是将之前AtomicLong一个value的更新分散到多个value中去,从而降级更新热点。

内部一个base变量和一个Cell[]数组。低并发直接累加到base变量;高并发,累加到各个线程自己的Cell[i]中。Value=Base + ΣCell[i]

LongAdder longAdder = new LongAdder(); //0
longAdder.increment(); //1
longAdder.increment(); //2
longAdder.increment(); //3

LongAccumulator可以自定义计算。

LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0); //0是这里的初始值,相当于x
longAccumulator.accumulate(1); // 1可以看作是y,0+1=1
longAccumulator.accumulate(3); // 3可以看作是y,1+3=4
longAccumulator.get()

ThreadLocal

它实现了每一个线程都有自己专属的本地变量副本,解决了让每个线程绑定自己的值,通过get、set方法,获取默认值从而避免了线程安全问题。

JVM内部维护了一个线程版的Map<ThreadLocal,Value>。通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作key,放入ThreadLocalMap中,每个线程要用这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量。

为什么采用弱引用

当方法执行完毕后,如果线程的ThreadLocalMap里的某个entry的key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏。使用弱引用就会大概率减少内存泄漏的问题,可以使得ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null。

内存泄漏问题

不会再被使用的对象或者变量,它们占用的内存不能被回收,就是内存泄漏。

强引用

内存不足时,JVM开始垃圾回收,但是对于强引用的对象,就算出现了OOM也不会对该对象进行回收,即使该对象以后永远都不会被用到,也不会回收。因此强引用是造成Java内存泄露的主要原因之一。

软引用

相对强引用弱化了一些的引用,需要java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。当系统内存充足时不会被回收,当系统内存不足时会被回收。通常用在对内存敏感的程序中,比如高速缓存中就用到了软引用,内存够就保留,不够就回收。

弱引用

需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短。只要垃圾回收机制一运行,不管JVM内存空间是否足够,都会回收该对象所占的内存。

虚引用

1、虚引用必须和引用队列联合使用

虚引用需要java.lang.ref.PhantomReference类来实现,形同虚设。虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。他不能单独使用,又不能通过它访问对象,虚引用必须和原因队列联合使用。

2、PhantomReference的get方法总是返回null

虚引用的作用是跟踪对象被垃圾回收的状态。仅仅提供了一种确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

3、处理监控通知使用

设置虚引用关联对象的唯一目的,就是这个对象被回收的时候收到一个通知,或者后续添加进一步的处理。用来实现比finalize机制更灵活的回收操作。

Java对象内存布局和对象头

内存布局

对象在堆内的布局分布可以分为三部分:对象头、实例数据、对齐填充

对象头(64位系统中)

对象标记 Mark Word (8字节)

里面包括:哈希码、GC标记、GC次数、同步锁标记、偏向锁持有者

这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储更多的数据。它会根据对象的状态复用自己的存储空间,也就是说运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。  

类元信息(类型指针 8字节)

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对齐填充

对象头+数据不足8字节的整数倍时,通过填充补到8字节的倍数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值