JUC基础---跟着狂神学习系列

JUC

    • 1、JUC是什么
    • 2、进程与线程
      • 并行,并发
      • sleep与wait区别
    • 3、锁
      • 传统的Synchronized锁
      • LOCK锁
      • 传统的Synchronized锁与LOCK锁对比
      • 虚假唤醒
    • 4、生产者消费者模式
    • 5、八锁现象
    • 6、集合类不安全
      • CopyOnWriteArrayList
      • CopyOnWriteArraySet
      • ConcurrentHashMap
    • 7、Callable
    • 8、常用辅助类
      • CountDownLatch(减法计数器)
      • CyclicBarrier(加法计数器)
      • Semaphore(信号量)
    • 9、读写锁( ReadWriteLock)
    • 10、阻塞队列
      • 队列:
      • 四组API:
      • 同步队列
    • 11、线程池
      • 池化技术
      • Executors 工具类 、三大方法
      • 七大参数及自定义线程
    • 12、四大函数式接口
      • Function函数型接口
      • Predicate断定型接口
      • Consumer消费型接口
      • Supplier供给型接口
    • 13、Stream流式计算
    • 14、ForkJoin
    • 15、异步调用
    • 16、JMM
    • 17、Volatile关键字
    • 18、单例模式
      • (1)饿汉式单例
      • (2)懒汉式单例
        • DCL懒汉式 = 双重检测锁模式 + 懒汉式单例
      • (3)静态内部类
    • 19、CAS
    • 20、原子引用解决ABA问题
    • 21、各种锁的理解
      • (1)公平锁、非公平锁
      • (1)可重入锁
      • (3)自旋锁(spinlock)
      • (4)死锁排查

1、JUC是什么

java.util工具包,用处 ---->在业务中,普通线程代码 Thread 效率不高

Runnable 没有返回值,效率相对于Callable较低

业务中的写法---->降低耦合性 // 将资源和对资源的实现分开

JUC基础---跟着狂神学习系列_第1张图片

2、进程与线程

进程是一个程序,线程是程序中的任务

进程:一个程序,如 QQ.exe 一个软件(程序的集合)

一个进程往往可以包含多个线程,至少一个

java中默认有两个线程 main、GC

例:运行Typora(进程),写完字后自动保存(线程)

java实际上是开不了线程的,只能通过本地方法去调用 -----> 例: private native void start()

// 本地方法,去调用底层的c++,java无法直接操作硬件

并行,并发

并发变成:并行,并发

并发(多个线程操作同一个资源)

  • CPU单核下,模拟出多条下线程,快速交替,接近同时运行

并行(多个人一起行走)

  • CPU多核,多个线程可以同时执行

  • 提高性能 —>利用线程池

     //获得CPU核数
     //CPU密集型,IO密集型
     System.out.println(Runtime.getRuntime().availableProcessors());
    

    并发编程的本质:充分利用cpu的资源

sleep与wait区别

1、来自不同的类

sleep —>object wait ----> Thread

2、关于锁的释放

wait会释放锁,sleep抱着锁睡觉不释放

3、使用范围

wait 必须在同步代码块

sleep可以在任何地方睡

JUC里的睡眠方法----->TimeUnit.SECONDS.sleep( )

3、锁

传统的Synchronized锁

本质:排队,分配锁

  • 真正的多线程开发,公司中的开发,要降低耦合性
  • 线程就是一个单独的资源类,没有任何的附属操作 —> 不会去创建一个类去继承接口写run方法,而是写好实现方法,在main中直接创建线程调用 结合lamder

LOCK锁

interface Lock

可实现类: ReentrantLock (可重入锁—常用),ReetrantReadWriteLock.ReadLock (读锁) /WriteLock(写锁)

公平锁:公平,可以先来后到 -----> NofairSync()

非公平锁:不公平,可插队(java中默认,效率高) -----> FairSync()

Lock使用时三步骤:

  • 先建锁 Lock suo = new ReentrantLock ()
  • 上锁 suo.lock
  • finall 里解锁 suo.unlock

传统的Synchronized锁与LOCK锁对比

1、传统的Synchronized锁会自动释放锁;LOCK锁是手动的,如果不释放,会产生死锁

2、Synchronized是内置的java关键字;Lock是一个java类

3、Synchronized无法判断获取锁的状态;Lock锁可以判断是否获取到了锁

4、Synchronized 线程1(获得锁、阻塞…),线程2(傻傻地等) ;Lock 锁不一定会等待下去,有trylock()方法会尝试获取锁

5、Synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置)

6、Synchronized适合锁少量的代码同步问题;Lock适合锁大量的同步代码

虚假唤醒

synchronized () {
   if(condition)
        this.wait(timeout);
    ... // Perform action appropriate to condition
    this.notifyAll();
} 

线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:

synchronized (obj) {
  while (<condition does not hold>)
  obj.wait(timeout);
   ... // Perform action appropriate to condition
} 

如果当前线程interrupted任何线程之前或在等待时,那么InterruptedException被抛出。 如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。

解决方法用while判断而不是if

用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后。

4、生产者消费者模式

JUC基础---跟着狂神学习系列_第2张图片

Condition因素出Object监视器方法( waitnotifynotifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock个实现。 Lock替换synchronized方法和语句的使用, Cndition取代了对象监视器方法的使用。

Lock lock = new Reentrantlock();
Condition  condition = lock.newCondition;
 condition.await( );    //等待
 condition.signalAll( ) ;   //唤醒全部

业务代码(判断 ->执行 ->通知)前要上锁,完成后释放,锁在业务代码前(try…catch前)获得

Condition实现精确通知唤醒

condition.signal( )唤醒指定的condition监视器

5、八锁现象

8锁就是关于锁的8个问题:

1、两个同步方法,一个对象调用。

synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行。

2、两个同步方法,两个对象调用。

synchronized 锁的是方法的调用者,也就是对象锁。两个对象分别调用两个方法持有的是两把把锁,一个的执行不需要等待另一个

3、一个同步方法,一个普通方法,一个对象调用

普通方法没有锁,不需要竞争锁。

4、两个同步方法,一个对象调用,一个方法执行会先睡眠但在main方法中执行顺序在前

synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行,此种情况下,执行顺序在前的会先拿到锁,与执行时是否睡眠无关。

5、两个静态同步方法,一个对象调用

static方法类一加载就执行,synchronized 锁的是Class对象即类锁,所以两个方法持有一把锁,谁先得到谁先执行。

6、 两个静态同步方法,两个对象调用

static方法类一加载就执行,synchronized 锁的是Class对象即类锁,所以两个方法持有一把锁,谁先得到谁先执行。

7、一个静态同步方法,一个普通同步方法,一个对象调用!!!

静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。

8、一个静态同步方法,一个普通同步方法,两个对象调用

静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。

小结:

普通方法 没有锁,不用等待获得锁再执行

普通同步方法 锁的是方法的调用者

static同步方法 锁的是唯一的一个class模板

6、集合类不安全

CopyOnWriteArrayList

并发下ArrayList 不安全,可能会引发ConcurrentModification 并发修改异常

解决方案:

  • 1、用Vector代替 vector是什么 实际上就是用synchronized解决,Vector方法 synchronized修饰的

  • 2、用Collections集合类,转化为synchronized方法 (工具类写法)

    List <string> list = Collctions.synchronizedlist(new Arraylist<>())
    
  • 3、调用CopyOnWriteArrayList类(JUC写法)

List <string> list = new CopyOnWriteArraylist<>()

CopyOnWrite(写入时复制):

  • 简称COW思想,是计算机程序设计领域的一种优化策略

  • 多个线程调用的时候,如果资源是唯一的,读取的时候是固定的,写入的时候可能会出现覆盖情况 ---------->使用 CopyOnWrite避免覆盖,造成数据问题

CopyOnWrite 相比于 Vector的优势:

  • Vector 的synchronized方法效率会相对低
  • CopyOnWrite 用的是LOCK锁,原理:写入会先复制一份,写完后返回

CopyOnWriteArraySet

并发下也可能抛出并发修复异常

原码: Set set = new HashSet<>()
解决方案:

  • 1、Collections写法
Set<String> set = Collctions.synchronizedlist(new HeshSet<>())
  • 2、JUC写法

    Set <String> set = new CopyOnWriteArraySet<>()
    
    //HashSet底层就是一个HashMap 
    public HashSet(){
    map = new HashMap<>();
    }
    //HashSet的add方法,本质就是  map key是无法重复的,PRESENT是个常量
    public boolean add(E e){
    return map.put(e,PRESENT)
    }
    
    

    ConcurrentHashMap

    Map在高并发中也不安全

    默认等价于 new HashMap<>(16,0.75) // 默认加载因子0.75 、初始化容量16 (底层是位运算)

    原码:Map map = new HashMap<>( )

    解决方案:

    • 1、Collections写法

       Map<String,String> map = Collctions.synchronizedMap(new HashMap<>())
      
    • 2、JUC写法

      Map<String,String> map = new ConcurrentHashMap<>()
      

7、Callable

类似于Runnable,都是为线程执行的一种接口

不同点:

  • 可以有返回值,Callable的泛型就是返回值类型
  • 可以抛出异常
  • 方法不同 , run( ) —> call( )

JUC基础---跟着狂神学习系列_第3张图片

Callable无法直接调用Thread类,需要通过Runnable,所以通过其一个适配类来启动

class Callable{
 main{   
FutureTask ft = new FutureTask(thread);  //适配类
new Thread (ft,name"A").start();
new Thread (ft,name"B").start();        
     //-------> 结果只会返回一个123
String s =(String)futureTask.get()  //获取callable 返回结果     
 	}
 }

class MyThread 	implement Callable< String>{
    
    public String call()
    {
       return "123";
    }
    
}

注意:

  • 如果call()方法是个耗时的操作,需要等待,get()方法会产生阻塞

解决方法:get( )获取方法代码最后或者通过异步通信

  • 当启动多个线程调用callable时,结果会被"缓存"以提高效率

// 对于缓存的解释: JVM第二次调用ft这个对象所持有的线程时,futuretask的线程状态已非new状态,则会直接结束对应线程,导致任务不执行,只是在第一次调用时返回结果保存了

8、常用辅助类

CountDownLatch(减法计数器)

JUC基础---跟着狂神学习系列_第4张图片

原理:

public CountDownLatch(int count)  //count代表记时总数

public void countDown()  //数量减一

public void await()

如果当前计数为零,则此方法立即返回。

如果当前计数大于零,则当前线程将被禁用以进行线程调度,并处于休眠状态,直至发生两件事情之一:

  • 由于countdown( )方法的调用,计数达到零
  • 一些其他线程interrupted当前线程

CyclicBarrier(加法计数器)

public CyclicBarrier(int parties,Runnable barrierAction) 
  • 参数

    parties - 屏障跳闸前必须调用 await()的线程数

    barrierAction - 当屏障跳闸时执行的命令,或 null如果没有动作

  • 异常

    IllegalArgumentException - 如果 parties小于1

public int await(long timeout,TimeUnit unit)  //timeout - 等待屏障的时间
unit - 超时参数的时间单位

如果当前线程不是最后一个线程,那么它被禁用以进行线程调度,并且处于休眠状态(当前线程阻塞,不执行),直到发生下列事情之一:

  • 最后一个线程到达
  • 超过指定的超时
  • 一些其他线程当前线程interrupts
  • 其他一些线程 interrupts其他等待线程
  • 一些其他线程在等待屏障时超时;
  • 其他一些线程在这个障碍上调用reset()

Semaphore(信号量)

public Semaphore(int permits)  //permits 规定指定数量的线程
public void acquire()

获得许可证,如果有可用并立即返回,则将可用许可证数量减少一个。

如果没有可用的许可证,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,直至发生两件事情之一:

  • 一些其他线程调用此[信号量的release()方法,当前线程旁边将分配一个许可证
  • 一些其他线程interrupts当前线程
public void release()			//释放许可证,将其返回到信号量
  • 发放许可证,将可用许可证的数量增加一个。 如果任何线程尝试获取许可证,那么选择一个被授予刚被释放的许可证。 (重新)线程调度用于线程调度。

作用:多个共享资源互斥的使用 , 并发限流 , 控制最大线程数


小结:

  • CountDownLatch:指定线程执行完毕,再执行操作
  • CyclicBarrier:指定个数的线程执行完毕,在进行操作
  • Semaphore :同一时间,只有指定数量个得到线程

9、读写锁( ReadWriteLock)

读写锁可以更加细粒度的控制 ----> 读的时候可以被多个线程读,写的时候只能有一个线程去写

ReadWriteLock readWriteLock = new 	ReentrantReadWriteLock();
Lock  lock = new Lock;
readWriteLock.write/readlock().lock/unlock();  //给读/写锁 ,上/开锁 
writeLock() //用于书写的锁
readLock() //用于阅读的锁

独占锁(写锁):一次只能被一个线程占有

共享锁 (读锁):多个线程可同时占有

10、阻塞队列

队列:

JUC基础---跟着狂神学习系列_第5张图片

JUC基础---跟着狂神学习系列_第6张图片

阻塞队列的应用:多线程并发处理,线程池


四组API:

方式 抛出异常 有返回值,不抛出异常 阻塞,等待 超时,等待
添加 add( ) offer( ) put( ) offer(元素 ,等待时间,等待单位)
移除 remove( ) poll( ) take( ) poll( 等待时间,等待单位)
检测队首元素 element( ) peek( ) _ _

1、抛出异常

2、不抛出异常

3、阻塞等待

4、超时等待


同步队列

  • 没有容量,不储存元素

  • 进去一个元素,必须等待取出来之后,才能往里面放一个元素 (put了一个元素,必须从里面先take取出来,否则不能再put进去值)

11、线程池

掌握:三大方法,七大参数,四种拒绝策略

池化技术

  • 程序运行的本质 : 占用系统资源,优化资源的使用 —>池化技术
  • 线程池、连接池、内存池 … //创建,销毁十分浪费资源
  • 池化技术:事先准备好一些资源,要用来我这里拿,用完之后还给我

好处:

1、降低资源消耗

2、提高响应速度

3、方便管理

线程复用、可以控制最大并发数、管理线程


Executors 工具类 、三大方法

   ExecutorService threadpool = Executors.newSingleThreadExecutor();     //创建单个线程   		
	ExecutorService threadpool = Executors.newFixedThreadPool(int num);  	//创建一个固定大小的线程,执行永远只有num个 
      ExecutorService threadpool = Executors.newCachedThreadPool();     //可伸缩的,大小可规定,不过是否能达到规定个数还要看电脑内存

        try {
            //使用线程池来创建线程
            threadpool.execute(  ()->{
                System.out.println(Thread.currentThread().getName());
            }              );
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        threadpool.shutdown();    //线程池用完,程序结束,关闭线程池
        }

七大参数及自定义线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d58fCJ8L-1628042380371)(/home/lhx/图片/8.png)]

三大方法实质:

public ThreadPoolExecutor(
						  int corePoolSize,			//核心线程池大小
                          int maximumPoolSize,		//最大核心线程池大小
                          long keepAliveTime,		//超时了没有人调用就会释放
                          TimeUnit unit,			//超时单位
                          BlockingQueue<Runnable> workQueue,	//阻塞队列
                          ThreadFactory threadFactory,			//线程工厂:创建线程的,一般不用动
                          RejectedExecutionHandler handler		//拒绝策略
                          )

JUC基础---跟着狂神学习系列_第7张图片

 //自定义线程举例:
ExecutorService threadpool = new ThreadPoolExecutor
        (
                corePoolSize  2,
                maximumPoolSize  5,
                keepAliveTime 3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()

        );
// 最大承载 = 阻塞队列大小 + 最大核心线程池大小

AbortPolicy() //银行满了,还有人进来,不处理这个人,抛出异常

CallerRunsPolicy() //哪里来的去哪里,例:本来应该用main线程处理,就返回到main线程

DiscardOldestPolicy() //队列慢了,丢掉任务,不抛出异常

DiscardPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常

拓展:

最大线程如何定义(调优)

1、CPU密集型 CPU是几核就是几,可以保持CPU效率最高

 System.out.println(Runtime.getRuntime().availableProcessors());	//获取CPU内核数

2、IO密集型

判断你程序中十分耗IO的线程,大于其数量

12、四大函数式接口

新时代程序员需掌握:lambda表达式,链式编程,函数式接口,stream流式计算

函数式接口:只有一个方法的接口 ----> @FunctionalInterface

Runnable接口

public interface Runnable{

public abstract void run ( ) ;

}

// 超级多的Functional Interface

// 简化编程模型,在新版本的框架底层 大量应用

// foreach(消费者类的函数式接口)

四大函数式接口: Function ,Consumer , Predicate , Supplier

函数式接口要和Lambda表达式结合起来


Function函数型接口

public interface Function<T, R> {			//传入参数类型为T,返回参数类型为R
R apply(T t);				---->           // 需要重写的方法
public static void main(String[] args) {
    Function<String,String> function = new Function<String,String>() {
        @Override
        public String apply(String str) {
            return "OK";
        }
    };

    System.out.println(function.apply("ok"));

// lambda 写法

    Function<String,String>  function1  = (str)  ->  { return  str ;  };
    System.out.println(function1.apply("ok"));
    
}

Predicate断定型接口

public interface Predicate<T> {   			//传入参数类型为T
    boolean test(T t);						//重写的返回参数类型为布尔型    ----> 可用于判断字符串是否为空
public static void main(String[] args) {
    Predicate<String> predicate = new Predicate<String>() {
        @Override
        public boolean test(String s) {
            return s.isEmpty();  //判断字符串是否为空
        }
    };

// lambda 写法

    Predicate<String> predicate1 = (str)  ->{  return  true ;    };
    System.out.println(predicate1.test("OK"));

}

Consumer消费型接口

public interface Consumer<T> {			//只有输入,没有返回值
    void accept(T t);
public static void main(String[] args) {
        Consumer<String> consumer =new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        consumer.accept("OK");

        Consumer <String> consumer1 =(s)  ->{
            System.out.println(s);
        };
			consumer1.accept("ok");

   	 }

Supplier供给型接口

public interface Supplier<T> {			//只有返回值,没有输入
    T get();
public static void main(String[] args) {
    Supplier<String> supplier = new Supplier<String>() {
        @Override
        public String get() {
            return "OK";
        }
    };
    System.out.println(supplier.get());

    Supplier<String> supplier1 = () -> { return  "ok";   };
    System.out.println(supplier.get());


}

13、Stream流式计算

大数据:储存+计算

集合、MySQL本质就是储存东西的

计算都应该交给流来操作

例:

// list.stream.方法 ----> 链式编程

**// 双引号::是一种新的语法,被称为“方法引用” **

  • **Lambda写法: s -> system.out .println (s ) **
  • 方法引用写法:system.out ::println

JUC基础---跟着狂神学习系列_第8张图片

JUC基础---跟着狂神学习系列_第9张图片

14、ForkJoin

forkjoin类似大数据中的 Map Reduce (把大任务拆分为小任务) -------并行执行任务,提高效率,适用于大数据量

JUC基础---跟着狂神学习系列_第10张图片

工作特点:

工作窃取 -------> 假设有两个线程A、B,如果A已经完成了自己的任务,B还没有完成,这时A会窃取B的未完成的任务去完成,从而提高工作效率

原因: 这个里买维护的都是双端队列 (两边都可以操作)

JUC基础---跟着狂神学习系列_第11张图片

如何使用ForkJoin:

1 、通过ForkJoinPool执行

2、计算任务ForkJoinPool.execute(ForkJoinPool task)

3、计算类需要继承ForkJoinTask


例: 计算1到一亿的和(使用ForkJoin)

JUC基础---跟着狂神学习系列_第12张图片

三种方式的比较: 简单的For循环 < ForkJoin < Stream

JUC基础---跟着狂神学习系列_第13张图片

15、异步调用

JUC基础---跟着狂神学习系列_第14张图片

举例:

异步调用 --> 你喊朋友去吃饭,朋友在忙,说待会忙完去找你,你就先去做别的,朋友忙完了找你,你们一起去

同步调用 --> 你喊朋友去吃饭,朋友在忙,你就一直在等,等朋友忙完了,你们一起去

回归到线程上,就是一个任务发起请求后,不会占用程序的时间,但最终会加载回结果,降低了时间成本

与同步处理相比,异步处理不用阻塞当前线程来等待处理完成

在生活中常用于抢票、支付等


异步调用:CompletableFuture(类似与ajax)

  • 异步执行
  • 成功回调
  • 失败回调

Future的设计初衷:对将来某个事件的结果进行建模,但里面有延时,所以一般使用异步回调用CompletableFuture

JUC基础---跟着狂神学习系列_第15张图片

JUC基础---跟着狂神学习系列_第16张图片

回调都是函数式接口,可以结合lambda

public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
   //返回与此阶段相同结果或异常的新CompletionStage,当此阶段完成时,将使用结果(或 null如果没有))和此阶段的异常(或 null如果没有))执行给定操作。 
action - 要执行的动作
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
    //返回一个新的CompletableFuture,当CompletableFuture完成时完成,结果是异常触发此CompletableFuture的完成特殊功能的给定功能; 否则,如果此CompletableFuture正常完成,则返回的CompletableFuture也会以相同的值正常完成。
fn - 用于计算返回的CompletableFuture的值的函数,如果此CompletableFuture异常完成    

16、JMM

JMM:java内存模型,不存在的东西。是个概念,约定。

关于JMM的一些同步约定:

1、线程解锁前,必须把自己的工作内存(自己的共享变量)立刻刷回主存

2、线程加锁前,必须读取主存中的最新值到工作内存中

3、加锁和解锁是同一把锁

JUC基础---跟着狂神学习系列_第17张图片

存在问题:线程B修改了值,但是线程A不能及时可见 --------> 通过volatile解决

例A:

private  static  int num =0;					//定义了静态变量,线程1开始会一直运行
    public static void main(String[] args) {

        new Thread(		//线程1
                 () ->{
                     while (num == 0){}
                 }).start();

        num=1;							//按道理说主线程执行到这,num=1后,线程1会停止,但却没有,因为线程1不知道主内存中num值的改变
         }
    }

八种内存交互操作:

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。

操作规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。

  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中。

  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。

  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。

  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。


17、Volatile关键字

Volatile是java虚拟机提供的轻量级的同步机制

特点:

  • 1、保证可见性

    在例A中,由于num值的改变线程A无法获得的问题解决:

private  volatile  static  int num =0 ;				//通过volatile使num变为可见性的变量

  • 2、不保证原子性(不可分割) ----> 线程A在执行任务的时候,不能被打扰的,也不能被分割,。要么同时成功,要么同时失败
public class Test01  {
    private  static  int num =0;
    private static void add(){
        num++;
    };
    public static void main(String[] args) {
            for (int i = 0; i <20 ;i++) {
              
                new  Thread(  () ->{
                  for (int i = 0; i <20 ;i++) { add();	}     //理论上应该num最后结果为400,但却因为	num++ 并非原子性操作,所以运行时20个线程不会“排队”,得到的结果每次都不同
               }).start();
        }						
        

Lock锁和Synchronized关键字会保证原子性,可以解决这个问题

另外可以通过原子类Atomic解决

JUC基础---跟着狂神学习系列_第18张图片

原子类为什么这么高级 -----> 方法的底层都是调用的C,直接与操作系统挂钩,在内存中修改值


  • 3、禁止指令重排

指令重排:你写的程序,计算机并不是按照你写的那样去执行

源代码 —> 编译器优化的重排 —> 指令并行也可能重排 ----->内存系统也会重排 -----> 执行

==处理器在进行指令重排的时候,考虑:数据之间的依赖性

例1:

int x = 1 ; //1
int y = 2;  //2
x = x + 5;  //3
y = x * x;  //4

我们所期望的 : 1234执行			但可能执行的时候变回 2134  1324
但不可能是 4123    

例2:

a , b , x , y 默认值为0;

线程A 线程B
x=a y=b
b=1 a=2

正常的结果 : x=0 ;y=0

由于指令重排导致的问题:

线程A 线程B
b=1 a=2
x=a y=b

导致结果 : x=2;y=2

通过Volatile避免指令重排:

内存屏障(常用于单例模式) —>CPU指令

作用:

1、保证特定的操作的执行顺序

2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

JUC基础---跟着狂神学习系列_第19张图片

18、单例模式

(1)饿汉式单例

JUC基础---跟着狂神学习系列_第20张图片

未启用就会占用所有的资源,浪费内存空间


(2)懒汉式单例

  • 用的时候再加载资源,占用对应空间
  • 单线程下可以,多线程并发下存在问题

JUC基础---跟着狂神学习系列_第21张图片

DCL懒汉式 = 双重检测锁模式 + 懒汉式单例

JUC基础---跟着狂神学习系列_第22张图片


(3)静态内部类

JUC基础---跟着狂神学习系列_第23张图片


利用反射对枚举尝试破坏:

JUC基础---跟着狂神学习系列_第24张图片

如何破坏:

  • 可以动态创建类的对象
  • 可以获得类加载器
  • 可以修改private数据权限 …

总结:

单例不安全,反射会破坏单例,但反射不能破坏枚举

在命令行,学会用 ==Javap - p + class 文件名 ----------> 反编译得到 反编译源码


19、CAS

CAS:CompareAndSet ------> 比较并交换

CAS 是CPU的并发原语JUC基础---跟着狂神学习系列_第25张图片

Unsafe类

JUC基础---跟着狂神学习系列_第26张图片


​ 底层的实现 --> 自选锁的使用

JUC基础---跟着狂神学习系列_第27张图片


小结:

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行操作,如果不是就一直循环

缺点:

1、循环会耗时

2、一次性只能保证一个共享变量的原子性

3、ABA问题:会造成资源提前挪用


20、原子引用解决ABA问题

ABA举例:你和你的女朋友分手了,过了一段时间复合了,他还是你的女朋友,但你并不知道她有没有交过其他男朋友(你以为数据没改过,实际是改过以后的)

JUC基础---跟着狂神学习系列_第28张图片


解决思想:乐观锁,CAS就是乐观锁的一种,还有一种就是版本号

**原子引用: **就是带版本号的原子操作 // AtomicReference <> ---------> 实现类: AtomicStampReference

注意: **如果泛型是个包装类(Integer…),注意对象引用问题 **

正常业务中,这里面比较的一般是一个个对象,比较他们是否相等

JUC基础---跟着狂神学习系列_第29张图片

AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(期望值,版本号stamp);	// 期望值要在上述范围内 		
int stamp = atomicStampedReference.getStamp();		//获得版本号
atomicStampedReference.compareAndSet(1,1,stamp,stamp+1);
   public boolean compareAndSet(V   expectedReference,		//期望值
                                 V   newReference,			//新值	
                                 int expectedStamp,			//版本号	
                                 int newStamp) 				//新的版本号

21、各种锁的理解

(1)公平锁、非公平锁

公平锁:非常公平,不能插队,必须先来后

非公平锁:不公平,可以插队 (默认的,防止一个耗时长的长时间占用资源)

(1)可重入锁

可重入锁:递归锁,所有的锁都是可重入锁

JUC基础---跟着狂神学习系列_第30张图片

Synchronized版

JUC基础---跟着狂神学习系列_第31张图片

Lock版

JUC基础---跟着狂神学习系列_第32张图片

注意LOCK的配对问题:,lock( )和unlock( )必须数量同时对应,否则会死锁


(3)自旋锁(spinlock)

自旋锁:不断的去循环尝试,直到成功为止

自定义自旋锁测试:

JUC基础---跟着狂神学习系列_第33张图片

JUC基础---跟着狂神学习系列_第34张图片

结果 :

T1 lock

T2 lock

T1 unlock

T2 unlock


(4)死锁排查

在idea终端,利用jsp命令:

1、使用 jsp -L定位进程号

2、使用jstack +进程号找到死锁问题

你可能感兴趣的:(笔记,juc,java)