[java] java并发

前言

各种知识多而且容易遗忘,还不容易复习。最好的方法当然是自己给自己提问,不断补缺查漏,缺什么补什么。本文将各类知识归类,并将全文知识点浓缩在自问自查中,并且都写好目录,自问自查时可以随时跳转过去,方便大家系统的学习复习知识。 水平有限,有错误敬请指正

食用方法
自问自查—阅读原文—自问自查–阅读原文…
无限循环


自查自问

1. 线程六状态 
2. lock&Synchronized  区别
3. AQS&CAS  实现原理 优缺点
4. 线程池  运行原理优缺点
5. 线程池拒绝策略
6. Threadlocal 实现原理
7. JUC包下的类 
8. volatile原理  内存屏障
9. java各种锁  锁升级
10.  BlockingQueue 的原理 实现
11. 线程如何同时启动
12. Interrupted和stop


线程六状态

在这里插入图片描述
在这里插入图片描述
就绪态不能直接到 等待 阻塞

等待 阻塞 到 就绪

lock&Synchronized

https://www.cnblogs.com/handsomeye/p/5999362.html

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。readLock()和writeLock()用来获取读锁和写锁

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。 tryLock()

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象 lock.unlock(); //释放锁
3)ReentrantLock 提供可以中断锁的一个方法lock.lockInterruptibly()方法。

ReentranceLock(重入锁)
同一时刻只能有一条线程拥有重入锁,但此线程可以重复获得锁。
其余需求获得此锁的线程被阻塞。

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

Syncronized 非公平,可重入,独享

ReentrantLock 可公平也可非公平,默认非公平,独享,可重入
抢占通过 CAS抢占 如果state=0就可以抢占 state=1CAS失败
在这里插入图片描述

https://blog.csdn.net/qq_35190492/article/details/104943579 公平与非公平

lock 实现原理

继承自AbstractQueuedSynchronizer 简称AQS

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。 就是通过CAS(乐观锁)去修改state的值(锁状态值)
在这里插入图片描述

https://blog.csdn.net/qq_29373285/article/details/85964460

ReenTrantLock的实现是一种自旋锁

synchronized 进入后不能中断

AQS&CAS

https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
CAS
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的 适合于竞争不是很激烈的
在这里插入图片描述
在这里插入图片描述

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列
state较多的时候 不等于0 state-- 获得资源 释放资源state++

在这里插入图片描述

AQS双向队列 入队比较容易 直接从队尾加入即可

CAS的缺点:

1.CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

ABA问题
产生ABA问题的原因
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。
有可能地址改变了
如何规避ABA问题
常用的办法是在更新数据的时候加入版本号,以版本号来控制更新。

循环时间长开销大问题。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

线程池

好处:
1、降低资源消耗
可以重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

ExecutorService service = Executors.newFixedThreadPool(3);
Executors 工厂类
service.execute(atomic_ABC.new RunnableA());
service.execute(atomic_ABC.new RunnableB());
service.execute(atomic_ABC.new RunnableC());
service.shutdown();
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.如果核心线程还没满,则直接起线程;
2. 如果核心线程已满而队列没满则直接入队;
3. 如果队列满了但最大线程不够则再起线程达到最大线程;
4. 如果队列多了则按抛弃策略来抛弃;

当我们在使用线程池时,希望的是先达到最大线程数,然后再进入队列排队,这样当突然有个流量高峰的时候,能够快速的达到最大线程数,尽量把任务处理完,处理不完才入队列;但目前却是先达到核心线程数,如果队列没满则入队,等队列满了再增加线程数到最大线程数

线程池拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
在这里插入图片描述
默认AbortPolicy

Threadlocal

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
在这里插入图片描述
在这里插入图片描述
其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里是remove之前。

set源码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

ThreadLocalMap底层结构
在这里插入图片描述
ThreadLocalMap是ThreadLocal类的一个静态内部类
在这里插入图片描述
用数组(开放地址法)是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

https://mp.weixin.qq.com/s/LzkZXPtLW2dqPoz3kh3pBQ

JUC包下的类

java.util.concurrent

atomic包
AutomicInteger
lock包
ReentranceLock,
并发容器
ConcurrentHashMap
线程池
excutor
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

volatile原理

可见性,有序性
volatile通常被比喻成"轻量级的synchronized",也是Java并发编程中比较重要的一个关键字。和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
在这里插入图片描述
内存一致性:每条线程都有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,相似的,Java虚拟机也定义了一套内存访问协议来保证内存一致性;
指令重排序:对应于处理器乱序执行,Java虚拟机的即时编译器中也有着类似的优化,同样只保证最终结果的一致性

内存屏障:
在这里插入图片描述
在这里插入图片描述

java各种锁

https://tech.meituan.com/2018/11/15/java-lock.html

JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

本文中的源码来自JDK 8和Netty 3.10.6

  1. 乐观锁 VS 悲观锁
    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。
    先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
    而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
    乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

  2. 自旋锁 VS 适应性自旋锁
    在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。
    阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
    在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
    而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
    在这里插入图片描述
    自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
    自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
    自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

  3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
    这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。
    首先为什么Synchronized能实现线程同步?
    在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。
    java对象头见 java对象头
    Monitor
    Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
    Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
    现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
    如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
    所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
在这里插入图片描述
4. 公平锁 VS 非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
在这里插入图片描述
再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
5. 可重入锁 VS 非可重入锁
可重入锁又名递归锁, 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

BlockingQueue

//使用锁lock,并且创建两个condition,相当于两个阻塞队列
lock=new ReentrantLock();
notFull=lock.newCondition();
notEmpty=lock.newCondition();

notFull.await();//阻塞生产线程
notEmpty.signalAll();//唤醒消费线程
notEmpty.await();//阻塞消费线程
notFull.signalAll();//唤醒生产线程

而使用lock/condition,可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程。

https://blog.csdn.net/chenchaofuck1/article/details/51592429
https://blog.csdn.net/bigtree_3721/article/details/83309364
https://blog.csdn.net/Cy_LightBule/article/details/103145222
在这里插入图片描述
在这里插入图片描述

线程同时启动

CountDownLatch是计数器,只能使用一次,而CyclicBarrier的计数器提供reset功能
等待其他线程完成后 再统一执行

java.util.concurrent.CyclicBarrier 让其他线程等待然后一起开始

Cyclic 循环
Barrier 屏障
在这里插入图片描述

public class TestCyclicBarrier {

    class Worker implements Runnable{

        CyclicBarrier cyclicBarrier;

        public Worker(CyclicBarrier cyclicBarrier){
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                cyclicBarrier.await(); // 等待其它线程
                System.out.println(Thread.currentThread().getName() + "启动@" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public void doTest() throws InterruptedException {
        final int N = 5; // 线程数
        CyclicBarrier cyclicBarrier = new CyclicBarrier(N);
        for(int i=0;i<N;i++){
            new Thread(new Worker(cyclicBarrier)).start();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestCyclicBarrier testCyclicBarrier = new TestCyclicBarrier();
        testCyclicBarrier.doTest();
    }
}

java.util.concurrent.CountDownLatch

CountDown 倒数
Latch 锁
在这里插入图片描述

public class TestCountDownLatch {

    class Worker implements Runnable{

        CountDownLatch countDownLatch;

        Worker(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                countDownLatch.await(); // 等待其它线程
                System.out.println(Thread.currentThread().getName() + "启动@" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doTest() throws InterruptedException {
        final int N = 5; // 线程数
        CountDownLatch countDownLatch = new CountDownLatch(N);
        for(int i=0;i<N;i++){
            new Thread(new Worker(countDownLatch)).start();
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestCountDownLatch testCountDownLatch = new TestCountDownLatch();
        testCountDownLatch.doTest();
    }
}

https://blog.csdn.net/wb_zjp283121/java/article/details/88127786

Interrupted和stop

public void interrupt()中断线程,将线程标记为中断状态,不会强求被中断线程一定要在某个点进行处理。实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理。
public boolean isInterrupted()测试线程是否已经中断。线程的中断状态不受该方法的影响。
静态方法interrupted可以将当前线程的中断状态清除

stop 会强行把执行到一半的线程终止
一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。 所以你看到Thread.suspend, Thread.stop等方法都被Deprecated了


自查自问

1. 线程六状态 
2. lock&Synchronized  区别
3. AQS&CAS  实现原理 优缺点
4. 线程池  运行原理优缺点
5. 线程池拒绝策略
6. Threadlocal 实现原理
7. JUC包下的类 
8. volatile原理  内存屏障
9. java各种锁  锁升级
10.  BlockingQueue 的原理 实现
11. 线程如何同时启动
12. Interrupted和stop
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值