并发编程及JUC

并发编程及JUC

本文是对Java基础的一些个人总结,适合小白在进行基本的学习后所复习的总结文档,授人以渔不如授之以渔,在Java的学习中需要多敲多理解,所以本文中代码数量占少,所需要理解的地方居多。本文为个人理解,如有需要订正之处,敬请指出,本人邮箱1209110434@qq.com,在之后也会更新Spring系列等个人总结。最后,希望各位在Java之路越走越远。Java之路道阻且长,最后一句话:Java之路为者长存,行者长至–送给你也送给我

1.多线程的创建

继承Thread类

缺点:不利于扩展

class myThread extends  Thread{
    public static void main(String[] args) {
        Thread mythread = new Thread();
        mythread.start();
        System.out.println("主线程");
    }
    @Override
    public void run() {
        System.out.println("线程启动成功");
    }
}

实现runnable接口

  1. 定义Runnable接口实现类MyRunnable,并重写run()方法
  2. 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法
public static void main(String[] args) {
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
    System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
   Thread thread = new Thread(() ->System.out.println("ss"));
    thread.start();
}

缺点:重写的run方法没有返回值

实现 Callable 接口

  1. 创建实现Callable接口的类myCallable

  2. 以myCallable为参数创建FutureTask对象

  3. 将FutureTask作为参数创建Thread对象

  4. 调用线程对象的start()方法

  5. public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
     
        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }
    

    }

使用 Executors 工具类创建线程池

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续详细介绍这四种线程池

实现线程同步

方法一:同步代码块

锁对象只要对于同时操作的锁唯一(静态方法用类目.class,实例方法用this)

ctrl alt t

方法二:在方法上加上synchronized+ 操作共享资源的代码

方法三:lock锁

线程池:

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
            Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}

    //线程中的常用方法
    /*
    1.start --启动一个新线程,让线程进入就绪状态等待jvm的调用
    2.启动后调用run方法(不start 直接调用会在主线程中跑,不会发挥多线程的作用)
    3.join --等待线程结束 (可设置等待时间)
    4.set,getname
    5.get set priority (1-10)设置优先级
    6 getstate--获取线程运行的状态
    7.isinterrupted ---布尔值判断是否被打断
    interrupt 打断线程(如果线程正在执行其他操作如sleep wait join 则会抛出异常,正常打断会设置打断标记)
    8.isalive --判断线程是否还存活
    9.currentthread --获取当前正在运行的线程(静态)
    10.sleep 使线程休眠让出cpu 的使用权(静态)(睡眠被打断会抛出异常,睡眠结束后未必会立刻执行)
    11。 yield --提示让出cpu主要是为了测试和调试
  @Test
    void contextLoads() throws Exception {
        //线程创建方式一
        Thread thread1 = new Thread("t1") {
            @Override
            public void run() {
                System.out.println("线程一被启动");
            }
        };

        thread1.start();
        System.out.println(thread1.getState());
        //线程创建方式二
        Thread thread2 = new Thread(() -> System.out.println("线程二被执行"));
        thread2.start();
        //1.第一种睡眠
        thread2.sleep(20);
        //第二种
        SECONDS.sleep(1);
        //线程创建方式三
        FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("线程三被执行");
                return 11;
            }
        });
        Thread thread3 = new Thread(integerFutureTask);
        thread3.start();
        System.out.println(integerFutureTask.get());  //线程创建方式四:利用线程池创建线程
        /*
        1.所有线程类都直接或者间接继承ExecutorServise但是顶级接口都继承于Executor
        2.线程池有两种创建方式1.通过工具类Executors创建
        2.1newFixedthreadpool(int nThread )线程池中含有固定数目的线程,空线程会一直保留,等待队列是无限的linkedMQ
        2.2newSingleThreadExecutor--线程池中只有一个线程,它依次执行某些任务,任务排队是无限的
        2.3Executors.newCachedThreadPool();,核心线程是0有任务时才创建线程,且全部是救急线程,并可以无限创建空闲线程被保留60秒
        2.4Executors.newScheduledThreadPool();线程池能够按照计划执行任务 corepoolsize是线程池中的最小数目,任务较多时会创建更多的工作线程来执行任务
        2.5newSingleThreadScheduledExecutor--线程池中只有一个任务,而且会按照时间来执行任务
        3.手动创建线程池对象(推荐)方便自己定义参数
        int corePoolSize,--核心线程数
        int maximumPoolSize,--最大线程数
        long keepAliveTime,--救急线程存活时间
        TimeUnit unit,--救急线程时间单位
        BlockingQueue<Runnable> workQueue,--任务队列
        1.ArrayBlockingQueue基于数组结构的有界阻塞队列(先进先出)
        2.LinkedArrayBlockingQueue--基于链表的有界阻塞队列吞吐量高于第一个,先进先出---上fixedThreadpool运用了此策略
        3.priorityArrayBlockingQueue---具有优先级别的队列
        4.synchronousQueue--一个不存储元素的阻塞队列,每次插入操作必须等待另一个线程操作,可以视为只有一个元素的队
        ThreadFactory threadFactory,--用于创建线程的工厂
        RejectedExecutionHandler handler--饱和策略
        Abortpolicy:默认策略,线程池满了抛出异常
        DiscardOldestpolicy:丢弃队列里执行最久的
        CallerRunsPolicy:线程池满了会丢弃新加入的任务并且不会报异常
         */
        ExecutorService executorService1 = Executors.newFixedThreadPool(5);
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();
        ExecutorService executorService3 = Executors.newCachedThreadPool();
        ExecutorService executorService4 = Executors.newScheduledThreadPool(5);
        ExecutorService executorService5 = Executors.newSingleThreadScheduledExecutor();
        //ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 10, 10, SECONDS, 10);/*
线程运行原理:
线程在启动后栈区会分配内存给线程
线程的上下切换,1.使用cpu的时间片用完了2.垃圾回收暂停所有线程3.有更高级的线程需要运行4.线程自己调用了sleep, yield,join,park ,synchronize,lock 等方法
利用程序计数器记录下一条JVM指令的执行地址,并且是线程私有的

线程的生命周期
1.new 初始状态--值、指线程的创建,创建线程后jvm 会为其分配空间
2.runnable可运行状态--对象调用strat方法后进入就绪状态,虚拟机会为其创建程序计数器和本地方法栈
3.running 运行状态--处于执行状态指该线程获得了cpu开始执行run方法,该线程则处于运行状态
4.block:阻塞状态 指因为某种原因放弃了cpu使用,让出cpu 暂停运行
阻塞分为三种
4.1.等待阻塞--运行执行线程中的o.wait方法 ,jvm会把该线程放在等待队列
4.2.同步阻塞
5.deda 死亡状态 线程已经执行完毕,不会再转为其他状态
*/

多线程进阶:

1.多个线程去读取共享资源,一个代码块内对多个共享资源多线程进行读写操作叫做临界区

2.由于代码的执行序列不同导致结果无法预测,叫做境界条件

解决多线程临界区的竞态条件产生的方案

阻塞式方案 1 .synchronize保护临界区

1.1 即使锁内的第一个线程时间片用完了被踢出去但是们还是锁住的2线程同样进不来,等到了1线程的时间片才继续运行执行完后锁才会让出来

1.2关键字加在成员方法上 锁住的是this 加在静态方法上锁住的是类对象

2.lock

线程安全分析:

成员变量和静态变量安全性:

  • 没有被共享的变量是线程安全的线程安全
  • 基本类型的访问是线程安全的,对象在创建时会创造栈帧,
  • 引用类型的是不安全的,因为运行中访问的是同一个引用对象

被共享了变量只有读操作线程安全,如果有读写操作则需要考虑线程安全性问题

局部变量: 局部变量是线程安全的,引用类型的对象局部变量是安全的(对象没有逃离方法范围,Return 后线程不安全)

继承后线程共享资源局部变量引用会造成多线程问题(可以用final ,priviate 增强线程的安全性)

常见的线程安全类

线程安全多个线程调用他们同一个实例的某个方法时是线程安全的

String ,包装类,Stringbuffer,random,vector,hashtable ,java concurrent 下的包,他们的每个方法都是原子的,但是组合在一起 不一定是线程安全的(只是指方法内的执行是线程安全的)

String 底层是对原有的对象进行一层复制再进行其他的操作,只是创建了新的字符串对象,再赋值给原字符串对象

实现线程安全成员变量条件:成员变量不可变则是线程安全的

synchronize底层:

Java对象底层结构:synchronize关键字绑定锁的对象结构

synchronize(lock)//lock结构包括klassword(指类对象)和markword

在这里插入图片描述

关键字使用

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

Monitor

Moniter又名监视器和管程 ,加锁时该对象会关联一个moniter对象通过markword创建指针指向monitor

markword格式

在这里插入图片描述

状态

01 默认值,没有关联Monitor对象

00 轻量级锁被启用

10 重量级锁 对象markword通过指针关联moniter后 存储锁住对象的地址

Moniter结构

在这里插入图片描述

获得锁进入ownner ,没有获得到锁是进入entrylist进入阻塞状态后等获得锁线程释放锁时进行竞争,对象总是与锁关联

注意:synchronize 必须是进入同一个对象才有以上效果,加synchronize才会关联监视器

弊端:这种锁耗费资源过大

synchronize优化:

轻量级锁:适用访问时间错开没有竞争,在加入synchronize修饰时初始锁位轻量级锁

轻量级锁原理:

线程在运行栈帧时会加载锁记录,锁记录里lockrecord会与锁对象mark word进行一次(CAS操作)

在这里插入图片描述

缺点:每次重入都需要进行一次CAS 操作)

  • CAS交换成功则设置对象头中记录为00
  • CAS交换失败则判断是否有锁竞争,有竞争则膨胀为重量级锁
  • CAS交换失败如果是锁重入则增加一条lock record

解锁时利用CAS 将mark word依次值恢复给对象头

锁膨胀:

CAS操作无法成功会进行锁膨胀,将轻量级变为重量级1.申请moniter 锁后两位变成10后同上

自旋优化:

重量级锁让线程2多进行几次自循循环避免线程进行阻塞(适合多核CPU)

自旋失败则会进入阻塞

偏向锁

只有第一次将线程ID设置到mark word头,同意一个线程多次获取相同的锁,且访问时不用重新CAS该对象归该线程所有即为偏向锁

偏向锁:如果开启了偏向锁,对象创建后三位为101(biased_lock为1),它的thread,epoch,age都是0,加锁时会加入进去偏向锁默认时是延迟的(可以对vm配置进行修改)

撤销可偏向:对象竞争或有第二个线程尝试获取锁

禁用偏向级锁,在测试代码机上VM参数 –usebiaselocking

可偏向状态调用hashcode后会撤撤销偏向状态

批量重偏向:偏向锁的偏向可以改变,当撤销偏向锁阀超过20次后,会重新进行偏向

批量撤销:当撤销偏向锁阀值超过40次时,会撤销偏向状态变成不可偏向

在这里插入图片描述

设计模式:保护性暂停:

在这里插入图片描述

public class guardobject {
 private  Object response;
 public Object getResponse(long timeout)  {
     long begin= System.currentTimeMillis();
     long passtime = 0;
     synchronized (this){
         while (response==null){
             if (passtime >= timeout){
                 break;}
             try {
                 this.wait(timeout - passtime);//避免虚假唤醒时等待的时长
     } catch (InterruptedException e) {
                 e.printStackTrace();} }
         passtime= System.currentTimeMillis()-begin;
         return response;} }
 public void compeletget(Object response){
     synchronized (this){
         this.response = response;
         this.notify();}}}

wait -notify原理:

wait&notify是Object类的方法且调用wait 方法时会释放锁

wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用,必须先获得锁对象,再使用锁对象调用

  • 获得owner(锁)对象的线程如果发现条件不满足则调用Wait方法则会进入Monitor中的waitset变为waiting状态,不占用CPU时间片
  • Monitor中的entrylist为阻塞队列为BLOCK状态且不占用CPU时间片
  • BOLCK会在OWNER线程释放锁后惊醒竞争唤醒
  • waitting状态的线程需要等待notify唤醒后不会立即获得锁会重新进入entrylist等待并重新竞争
synchronize(lock){
wait()
wait(long time)
lock.notify//(随机唤醒一个正在等待的线程)
lock.notifyAll//(唤醒所有在waitset的线程重新进入entrylist)
}

注意:使用wait方法需要避免错误警报和虚假唤醒,可利用死循环判断条件是否成立

synchronized (lock) {
    //  判断条件谓词是否得到满足
    while(!false) {
        //  等待唤醒
        monitor.wait();
    }
    //  处理其他的业务逻辑
}

sleep和wait区别

  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

join 原理:

指一个线程等待一个线程的结束 ,应用保护性暂停设计模式。(等待谁就调用谁)

生产者消费者模式(异步(会有一定的延时))

在这里插入图片描述

class mesageque {
    private  LinkedList<message> list = new LinkedList();
    private int capacity;
    public message take (){
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            message message = list.removeFirst();
            list.notifyAll();
            return message;
        }

    }
    //存入消息
    public  void  put(message msg){
        synchronized (list){
            while (list.size() == capacity){
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(msg);
            list.notify();
        }

    }

}
 final class  message{
    private  int id;
    private Object value ;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    @Override
    public String toString() {
        return "message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

park&unpark

是使用locksupport类中的方法locksupport.park暂停线程,unpark恢复暂停线程对象

特点:

  • park和unpark不需要获得锁
  • park和unpark是以线程为单位阻塞和唤醒线程
  • unpark可以再park前用,也可以在park 后用

底层原理:每个线程都有自己的一个parker对象由_counter,_cond,_mute组成

counter状态有:0和1,0代表线程不可park,1代表线程可park

调用park对象:

  • 如果counter为0则线程进入cond
  • 如果counter为1则线程不会被阻断

调用unpark

  • 如果线程正在cond中则被唤醒
  • 如果线程正在运行则会补充counter为1,且多次调用只会补充一次

线程状态以及状态切换:

线程状态:

  1. 新建(new):新创建了一个线程对象。

  2. 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。

  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

    阻塞的情况分三种:

    • 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    • 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程状态切换

  • new==>runnable

线程调用start方法时

  • Runnable<===>waiting

线程用synchronized(obj) 获取了对象锁后

  1. 调用obj.wait()方法时,t线程从RUINABLE ===> WAITING
  2. 调用obj.notify(), obj.notifyA11(), t.interrupt()时会WAITING ==>RUINABLE
  3. 竞争锁成功,t线程从WAITING ==> RUINABLE
  4. 竞争锁失败,t线程从WAITING ===> BLOCKED
  • RUNNABLE <====> WAITING当
  1. 当前前线程调用t.join()方法时,当前线程从RUNNABLE ===> WAITING注意是当前线程在线程对象的监视器上等待
  2. t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING ==> RUINNABLE
  3. 线程调用LockSuport.park会让当前线程从RUNNABLE ===> WAITING
  4. 线程调用LockSuport.unpark,或调用了当前线程的interrupt()时,当前线程从WAITING ==> RUINNABLE
  • RUNNABLE <===> TIMED_ WAITING

t线程用synchronized(obj) 获取了对象锁后

  • 调用obj.wait(1ong n)方法时,t 线程从RUNNABLE ==> TIMED_ WAITING

  • t线程等待时间超过了n毫秒,或调用obj .notify(), obj. notifyA1l(), t. interrupt()时

  1. 竞争锁成功,t线程从TIMED_ WAITING==>RUNNABLE
  2. 竞争锁失败,t 线程从TIMED_ WAITING --> BLOCKED
  • 当前线程调用t.join(1ong n)方法时,当前线程从RUNNABLE ==> TIMED_ WAITING注意是当前线程在线程对象的监视器上等待

  • 当前线程等待时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_ WAITING ==> RUNNABLE

  • 当前线程调用Thread.sleep(1ong n),当前线程从RUNNABLE => TIMED_ WAITING

  • 当前线程等待时间超过了n毫秒,当前线程从TIMED_ WAITING --> RUNNABLE

  • 线程调用了LockSupport.parknaos(long naos)或LockSupport.parkuntil(long millis)t 线程从RUNNABLE ==> TIMED_ WAITING

  • 调用unpark或者线程的interrupt方法或者超时会使线程变成TIMED_ WAITING --> RUNNABLE

线程死锁

在并发中可以设置多把锁来提高并发量,但是容易发生死锁

**死锁概念:**当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

产生死锁的条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

防止死锁:

  • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
  • 尽量不要几个功能用同一把锁。
  • 尽量减少同步的代码块。

补充:

定位死锁–利用jconsole ,利用jps, jstack进程ID

活锁:两个线程互相改变对方的结束条件,导致循环得不到结束,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

饥饿:线程优先级太低,始终得不到CPU调度执行

交替输出实验:

  public class park {
    static  Thread t1;static  Thread t2;static  Thread t3;
    public static void main(String[] args) {
        parkunpark p = new parkunpark(5);
        t1 = new Thread(()->{
            p.print("a",t2); });
        t2 = new Thread(()->{
            p.print("b",t3); });
        t3 = new Thread(()->{
            p.print("c",t1); });
        t1.start();t2.start();t3.start();
        LockSupport.unpark(t1); }}
class parkunpark{
    private  int loopnumber;
    public parkunpark(int loopnumber){
     this.loopnumber = loopnumber; }
    public void   print(String str, Thread next){
        for (int i = 0; i < loopnumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(next); }}}

运用reentred

public class test12 {
    public static void main(String[] args) throws InterruptedException {
        reentred re = new reentred(5);
        Condition condition1 = re.newCondition();
        Condition condition2 = re.newCondition();
        Condition condition3 = re.newCondition();

        new Thread(() -> {
            re.print("a", condition1, condition2);
        }).start();
        new Thread(() -> {
            re.print("b", condition2, condition3);
        }).start();
       new Thread(() -> {
            re.print("c", condition3, condition3);
        }).start();
       Thread.sleep(1000);
       re.lock();
       try {
          condition1.signal();
       }finally {
           re.unlock();
       }
    }
}
class reentred extends ReentrantLock {
    private int numbers;
    public  void  print( String str,Condition conn ,Condition nextcondition){
        for (int i = 0; i < numbers; i++) {
            try {
                conn.await();
                System.out.print(str);
                nextcondition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                unlock();
            }
        }
        lock();
    }

    public reentred(int numbers) {
      this.numbers = numbers;
    }
}

volatile

  • volatile可以修饰成员变量和静态成员变量,他可以避免线程从自己工作的缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile 都是直接操作主存。synchronize 也可以保证此效果。

  • volatile不能保证原子性,适合一个或多个线程是读取变量,另一个线程是修改变量,不适合于指令交错的场景。

  • jvm会在不影响正确性的前提下,可以调整语句的执行顺序会自动进行指令重排序,加volatile关键字会避免在多线程下的指令重排序,可以保证在之前的变量不会重排序

底层原理:volatile 底层原理是内存屏障

对volatile变量的写指令会加入写屏障–保证在该屏障之前,对共享变量的改动,都同步到主内存中–保证可见性,保证之间的代码不会进行指令重排–保证有序性

对volatile 变量的读指令会加入读屏障,在读取操作之后的代码,都会同步更新到主内存–保证可见性,在读屏障之后的情况都不会读到屏障之前。

dcl(double-check-locking)问题分析:

最初始:两次加锁 解决:–在变量上加volatile

happens- before规则

synchronize–在区域范围内保证原子性,可见性,有序性

volatile–被修饰的变量写会同步到主存中,读操作才可见

线程start之前操作的写,对该线程操作之后的读可见

线程结束前对变量的写入,对其他线程得知它结束后的读可见

线程一打断线程二前对变量的写,对于其他线程得知线程二被打断后的变量的的读可见。

对变量默认值的写,对其他线程对该变量的读可见

共享模型之无锁—乐观锁

  • 乐观指不怕别人修改,改了再重试

  • CAS(compare and set//compare and swap)–保证原子性

  • 每次操作线程时都会进行比较,比较成功后才会进行设置

  • 底层为了保证原子性和可见性底层运用了volatile

性能分析:synchronize于无锁性能分析:synchronize容易进行上下文切换,无锁会减少上下文切换,锁效率会高(线程数小于cpu时)

CAS 工具包

原子整数:AutomaticInteger(加减法)–getAndadd,inctrmentandget

updateandget(x->x*10)–更新同时设置

原子引用:

AtomicReference

AtomicMarkbleReference(判断是否被更改过)

AtomicStampeReference(判断版本号可以解决ABA问题)

解决ABA问题

(线程修改没有被线程察觉到)–AtomicStampeReference(根据版本号判断是否被修改过)AtomicMarkbleReference–只关注有没有被更改过

原子数组:

atomicIntegerarray

atomiclongarray

atomicreferencearray;(保护数组的元素)

字段更新器:

是基于反射的工具类,用来将指定类型的指定的volatile引用字段进行原子更新,对应的原子引用字段不能是private的。通常一个类volatile成员属性获取值、设定为某个值两个操作时非原子的,若想将其变为原子的,则可通过AtomicReferenceFieldUpdater来实现

atomicreferencefieldupdate,

atomicintegerfieldupdate,

atomiclongfieldupdate保护对象里的属性,保护成员变量的安全性(属性必须是volatile)

public class AtomicReferTest {
    public static void main(String[] args) throws Exception  
    {  
        AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dry.class,String.class,"name");  
        Dry dry=new Dry();  
        System.out.println(updater.compareAndSet(dry,"dry","compareAndSet"));        
        System.out.println(dry.name);  
        System.out.println(updater.getAndSet(dry, "getAndSet"));
        System.out.println(dry1.name);
    }  
}
class Dog  
{  
     volatile  String name="dry1";  
  
}

原子累加器:

jdk1.8 longadder(notremmber)–重要

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。–获取最终结果通过 sum 方法,将各个累加单元的值加起来就得到了总的结果

不可变类设计

不可变类设计:

成员变量都是不可变的,类对象也需要加上final,保证不可以被继承。String等不可变类都是保护性拷贝(底层赋值)。

tips:单个方法是不可变的

享元设计模式(适用重用数量有限的同一类对象)包装类

在这里插入图片描述

final变量原理:final在设置之后会加入写屏障

tomcat线程池:

在这里插入图片描述
在这里插入图片描述

Fork/Join线程池

ForkJoin是JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进-步提升了运算效率
Fork/Join默认会创建与cpu核心数大小相同的线程池

使用:需要继承Recursivetask(有结果),RecursiveAction

1.创建线程池对象(forkjoin线程池)

2.执行任务

在这里插入图片描述

AQS原理

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

在这里插入图片描述

特点:
用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何
获取锁和释放锁 getState -获取state状态 setState .设置state状态
compareAndSetState - cas机制设置state状态
独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源提供了基于FIFO的等待队列,类似于Monitor的EntyList条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet

  • 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何
  • 获取锁和释放锁
  • getState -获取state状态
  • etState .设置state状态
  • ”compareAndSetState - cas机制设置state状态
  • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于FIFO的等待队列,类似于Monitor的EntyList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet

AQS 对资源的共享方式

AQS定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

Reentrentlock

跟synchronize相比:

  • 可重入
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronize一样,都支持可重入(可重入是指同一个线程可对同一个对象反复加锁)

用法: 创建对象

可打断:其他线程可以用interrupt打断该线程(指打断无止境的等待锁)

锁超时:获取锁的线程等待不到超过了时间则放弃锁等待trylock

公平性:reentrantlock默认时不公平锁(可设置为公平锁)设置fair性 先入先得

条件变量:支持多个条件变量,有多个休息室

   Condition condition1 = reentrantLock.newCondition();
        Condition condition2 = reentrantLock.newCondition();
        reentrantLock.lockInterruptibly();
        try{
condition1.await();//进入休息室
condition1.signal();//唤醒
condition1.signalAll();//唤醒所有
        }
        finally {
            reentrantLock.unlock();
        }
/*await前需要获得锁
await执行后, 会释放锁,进入conditionObject等待
await的线程被唤醒(或打断、或超时)取重新竞争lock锁
竞争lock锁成功后,从await后继续执行*/

原理:

在这里插入图片描述

加锁

通过CAS操作将state从0改到1并获得线程对象(exclusiveOwnerThread)

竞争出现时

  • 先通过CAS获取资源失败后尝试
  • tryAcquire获取连接,如果state已经是1,则会失败
  • 进入add waiter逻辑,构造node(在header后)队列
  • 图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态
    Node的创建是懒惰的
  • 其中第一个Node称为Dummy (哑元)或哨兵,用来占位,并不关联线程
  • 请求失败后进入acquireQueued逻辑
  • acquireQueued会在一个死循环中不断尝试获得锁, 失败后进入park阻塞
  • 如果自己是紧邻着head (排第二位),那么再次tryAcquire尝试获取锁,当然这时state仍为1,失败
  • 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为1,这次返回false
  • shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire
    尝试获取锁,当然这时state仍为1,失败
  • 当再次进入shouldParkAferFailedAcquire 时,这时因为其前驱node 的
    waitStatus已经是-1,这次返回true
  • 进入parkAndCheckInterupt,Thread-park (灰色表示)

释放锁:

释放锁进入进入TryReleaser如果成功

  • 设置exclusiveOwnerThread为null,state=0
  • 找到离队列中head最近的一个node(没被取消的),unpark恢复运行
  • 唤醒node线程的acquirequeque流程,如果加锁成功则会设置,如果有其他锁竞争则重新进入阻塞

可重入原理:同步器会判断该线程是否是已获得锁的线程

可重入后释放:多次释放

可打断原理(默认不可被打断)

  • 打断是指会用interrupt标记自己被打断过,但是不会立即响应,等获得到锁时再进行打断

非公平锁:不会去检查AQS队列,直接抢夺线程

条件变量原理:

await原理

持有锁线程调用await,进入ConditionObject的addConditionwait的双向链表

创建新的Node并设置为-2

进入fullyrelease释放同步器上的锁

unpark AQS队列中的下一个节点,竞争锁

singal

  • 判断该线程是否是锁的持有者
  • 进入ConditionObject的dosingnal
  • 执行transferforsignal流程,将该Node加入AQS队列尾部,并将原线程的waitstatus设置为-2

ReentrantReadandWritelock

读读不互斥可并发,读写,写写互斥

注意事项:

  • 读写不支持条件变量
  • 重入时不支持升级,及有读锁的情况下去获取写锁,会导致写锁永久等待
  • 重入时降级支持

原理(后看)

stampedlock

进一步优化性能特点是在使用读写锁是都必须配合戳使用

乐观读(tryOptimistcRead)在读取完毕之后进行一次戳校验(校验是否有写操作,失败后锁会升级)
在这里插入图片描述

semaphore

信号量,用来限制能够同时访问共享资源的线程数

原理:(tryRelease)

  • 将参数设置给AQS的state
  • 获得Acquire后利用CAS更行state
  • state为0时执行CAS流程创建Node

释放:(tryrelease)

  • 利用CAS将state由0改到1
  • 后用AQS方法进行唤醒

原理:

Countdownlatch:

用来进行线程同步协作,等待其他 线程完成倒计时(等待线程运行结束)

await用来等待计数归零,countDown()用来让计数减一

cyclicbarrier

循环栅栏,用完后会立即恢复。线程池线程数要跟cyclicbarrier

JUC

Blocking类:大部分实现基于锁,并提供用来阻塞的方法

Copyonwrite:采用拷贝方式(底层拷贝)适用读多写少的形式

concurrent:内部应用CAS优化,提高吞吐量

遍历时弱一致性

ConCurrentHashMap

为什么使用CurrentHashMap。

  • 在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMap形成环形链表,即链表的一个节点的next节点永不为null,就会产生死循环。这时,CPU的利用率接近100%,所以并发情况下不能使用HashMap。

  • HashTable通过使用synchronized保证线程安全,但在线程竞争激烈的情况下效率低下。因为当一个线程访问HashTable的同步方法时,其他线程只能阻塞等待占用线程操作完毕。

  • ConcurrentHashMap使用分段锁的思想,对于不同的数据段使用不同的锁,可以支持多个线程同时访问不同的数据段,这样线程之间就不存在锁竞争,从而提高了并发效率。

原理:

sizeCtl:默认值为0,初始化时值为-1,扩容时为-(1+扩容数)

Node:本质是一个map,是哈希链表结构的最小单元,包括键值,哈希值,next

ForwardingNode:表示在扩容中表示该链表被处理过即处理完毕的节点

TreeBIn:作为红黑树的头节点储存root和first(如果哈希表长度大于64时)

TreeNode:作为红黑树的节点(储存parent,left,right)

构造器:

jdk8时实现懒惰初始化,等到真正用时才去创建

get:

  • spread方法保证哈希值为正整数
  • TabAt方法哈希值与数组长度减一进行按位与运算确定下标值
  • 判断哈希值与equals方法
    • 头节点hash值为负数表示正在扩容
    • 负数表示为红黑树结构,调用find方法寻找

put:

  • 调用put方法
  • 判断键和值是否为空 ConcurrentHashMap不能存储空的键值
  • 求哈希码进入死循环判断是否有Nodetable(懒惰利用CAS操作创造避免线程安全问题)
    • 判断是否有头节点
      • 利用CAS创建头节点
    • 判断头节点是否是负数
    • 对链表加锁(synchronize)
    • 找到相同的Key覆盖旧值
    • 没有相同Key创建新节点
    • 若为红黑树则调用红黑树Putval方法
  • 判断bincount是否需要转换为红黑树

扩容 计数addcount(利用Long adder)

jdk7:

  • 它维护了一个segment数组,每个segment对应一把锁
  • 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与jdlk8中是类似的
  • 缺点: Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

移位计算segement(继承rentrantlock)下表

在这里插入图片描述

put:

  • 计算hash码和segement数组(懒惰初始化)
  • segment继承reentrantlock,尝试加锁(trylock)
  • 计算索引后同上(头插法)

**get:**用unsafe方法保证可见性

  • 先定位segment 方法

CopyOnWriteArrayList

CopyOnWriteArrayList 是一个并发容器,非复合场景下操作它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。

在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。合适读多写少的场景。

缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。

CopyOnWriteArrayList 的设计思想

读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突

结语

感谢在学习途中乐于分享的每一位码员,有了你们的分享才让学习者变得更轻松

断是否有头节点
- 利用CAS创建头节点

  • 判断头节点是否是负数
  • 对链表加锁(synchronize)
  • 找到相同的Key覆盖旧值
  • 没有相同Key创建新节点
  • 若为红黑树则调用红黑树Putval方法
  • 判断bincount是否需要转换为红黑树

扩容 计数addcount(利用Long adder)

jdk7:

  • 它维护了一个segment数组,每个segment对应一把锁
  • 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与jdlk8中是类似的
  • 缺点: Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

移位计算segement(继承rentrantlock)下表

[外链图片转存中…(img-eZlodthb-1647538724745)]

put:

  • 计算hash码和segement数组(懒惰初始化)
  • segment继承reentrantlock,尝试加锁(trylock)
  • 计算索引后同上(头插法)

**get:**用unsafe方法保证可见性

  • 先定位segment 方法

CopyOnWriteArrayList

CopyOnWriteArrayList 是一个并发容器,非复合场景下操作它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。

在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。合适读多写少的场景。

缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。

CopyOnWriteArrayList 的设计思想

读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突

结语

感谢在学习途中乐于分享的每一位码员,有了你们的分享才让学习者变得更轻松

作者:Yang11😊

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值