实习日记 08/23 day33 理解JVM---Java核心卷中的并发

概览 Part 1

首先是Java基础中关于并发的只是
在这里插入图片描述

理解:顶层的并发

马德邦吃着火锅唱着歌,火锅没有阻塞他唱歌,同时唱歌也没有阻塞他吃火锅,可是马县长只有一张嘴他是怎么做到的呢?
或许你的计算机有多个CPU,又或许你的CPU有多个核心,但是进程的数目并不是由CPU数目限定的,操作系统将CPU的时间片分配给每一个线程,给人并行处理的错觉。
啥是并行,啥又是并发?
并行是汤师爷边在张麻子手下当师爷,又在黄四郎那里当内应,两个同时进行有不当误 ,并发是看似马邦德边吃火锅边唱歌,但其实嘴只有一个,只是切换的快,造成了并行的错觉,称为并发

线程的六种状态

  1. New 新创建
  2. Runnable 可运行
  3. Blocked 被阻塞
  4. Waiting 等待
  5. Timed waiting 计时等待
  6. Terminated 终止

新创建线程

当用New操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行,这意味着他的状态是new,当一个线程处于新创建状态时,程序还没有运行线程中的代码。
在Java中有三种主要的新建线程的方式:

  1. 继承Thread类,并实现其中的Runable方法,Runable顾名思义就是另开线程的主要运行。
  2. 引用Runable接口,但是想跑起来还是需要new Thread对象来带着。
  3. 引用Callabe和Future来实现预期式的线程。

通过继承Thread类或者实现Runnable接口。Callable接口都可以实现多线程,实现Runnabel接口与实现Callable接口里定义的方法返回值,可以声明抛出异常而已。因此将实现Runnable接口和实现Callable解耦可以归为一种方式。

可运行线程

如果调用了start方法,线程处于runnable状态。一个可运行的线程可能正在运行,也可能没有运行,这取决于操作系统给线程提供的运行时间。

被阻塞线程和等待线程

当线程处于阻塞或等待状态时,他暂时不活动,不运行任何代码且消耗最少的资源,直到线程调度器重新激活它。

  1. 当一个线程尝试获取一个内部对象锁(不是concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它时,该线程变为非阻塞状态。
  2. 当线程等待另一个线程通知调度器一个条件,他自己饿进入等待状态。
  3. 有几个方法有一个超时参数。调用它们导致线程进入计时等待状态

阻塞于终止,沉睡与等待:
Sleep和Wait的区别:
对于sleelp,它是属于thread类的,wait方法则是属于Object类中的,sleep方法导致了程序暂停执行执行指定的时间,期间只让出cpu但是其占有的锁不会释放,也就是它的监控状态依然保持,当指定的的时间结束又可以继续执行,在调用sleep方法的过程中,线程不会释放对象锁,当调用wait方法时,线程又会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象的调用notify方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
抱着枕头睡觉和放下枕头等待是有区别的。就像枕头大战,虽然人家不参与战斗,但是不干活还抱着枕头睡觉不让你参与进来,你只能wait,看人家sleep。
Wait和Notify的搭配:
PV原语的java实现中,当sempher信号量不达标则会使用wait和notify来暂停消费者或者生产者进程,但是在java中wait和notify都是对象级的锁而不是线程级的,每个对象都有锁,通过线程获得,如果线程需要等待某些锁那么调用对象就有意义,如果wait方法定义在Thread类,线程正在等待的是哪个锁就不明显了,所以wait和notift不能在thread类中定义。
Notify与NotifyAll的区别:
Notify可能会导致死锁,而NotifyAll不会,任何时候只有一个线程可以获得锁,也就是说一个线程可以运行synchronized中的代码。这保证了不会有多个线程同时更改数据导致混乱。NotifyAll可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列,而Notify只能唤醒一个。wait应搭配while使用,并且前后都需要验证检查条件,避免造成死锁。使用while的原因也很简单:notify是对notifyall的一个优化,它又很精明的应用场景,并且要求正确使用。不然可能导致死锁,使用while时如果唤醒的线程无法被正确的执行,则可以进行唤醒下一个线程。

被终止的线程

  1. 因为run方法正常退出而自然死亡。
  2. 因为一个没有捕获的异常而中run方法而意外死亡。

停止一个正在运行的线程:
3. 使用退出标志,使线程退出,也就是当run方法完成后线程终止
4. 使用stop方法强行终止
5. 使用interrrupt方法中断

线程属性

线程优先级

在Java语言中,每一个线程有一个优先级。默认情况下,线程继承其父线程的优先级,可以使用setPriority的方式提高或者降低任何一个线程的优先级。可以将优先级设为在MIN_PRIORITY与MAX_PRIORITY之间的任何值,NORM_PRIORITY被定义为5.
每当线程调度器有机会选择新线程时,它首先会选择有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数或许会更多,也可能会更少,SO,不要依赖优先级来确保功能的正常性。

守护线程

将线程转为守护线程。这样一个线程唯一的作用及时为其他线程提供服务。例如计时器线程,定时的发送滴答滴答给其他线程或者清空过时的高速缓存项线程。

未捕获异常处理器

线程的run方法不能抛出任何受查异常,但是非受查异常会导致线程终止。

同步

在大多数的多线程应用中,两个或者两个以上的线程需要共享同一个数据的存储。则需要控制线程同步。此时就需要使用到线程同步工具。
关于锁的讲解可以很深入,再下一篇在进一步深入,本篇日记只是从表面理解线程。
当两个线程不约而同的去处理同一个数据的时候,就不可避免地会出现竞争,如果竞争结果不确定,那也就意味着结果不确定,结果的不确定会导致程序的不可预测,违背了图灵机的定义(线程是计算机运行调度的单位,一个多线程不可预测的任务就让机器成为随机进行的程序)所以要防止代码出现并发访问的干扰,有两种主要的解决方法:

  1. synchronized关键字
  2. ReentrantLock类(Lock类是一个大类,但是ReentrantLock比较有名)
/**
*我写的有问题,但个人水平有限没法排查,请大家斧正!
**/
public final class Tets {
    Integer test = 10;
    Integer count=0;
    Thread A=new Thread(new Runnable() {
       @Override
       public void run() {
           while (true) {
               System.out.println("线程A获取值:" + test);
               while (test < 10) {
                   test++;
                   System.out.println("线程A结束工作:" + test);
               }
               try {
                   Thread.sleep(200);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }

       }
   });
       Thread B=new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println("线程B获取值:" + test);
                while (test >= 10) {
                    test--;
                    System.out.println("线程B结束工作:" + test);
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });

    public static void main(String[] args) throws InterruptedException {
            Tets tets=new Tets();
            synchronized (tets.test) {
                tets.A.start();
                tets.B.start();
            }
    }
}

不加锁
不加锁的情况下,发生了A居然干活了,理论上是不应该干活的可是A居然获取B刚刚改变的9,并加1,这就乱了。
加锁
加锁的情况下A获取值但不处理,B获取10减1.A在获取9处理加一有序进行。

同步与锁

相似点:
这两种同步方式都是加锁方式的同步,而且都是阻塞式的同步方式,也就是说如果一个线程获取了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行阻塞和唤醒饿代价是非常高的。
区别:
两种方式最大的区别是synchronized,他是java语言的关键字,在原生语法上面的互斥,它是基于monitor,也就是操作系统的管程,也可成为监视器,和mutex一样,需要jvm去实现,也就是说他是操作系统原理层面的互斥锁,而ReentrantLock是API层面上的互斥锁,需要lock和unlock方法配合try/finally语句块来实现。
synchronized关键字是解决多个线程之间访问资源的同步性,synchronied关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程在执行。synchronized属于重量级锁,效率低下,因为监视器是依赖于底层的操作系统的Mutex Lock来实现的,Java线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户状态转换到核心状态,那么这就是时间与性能的浪费,关于synchronized效率低的问题,1.6之后对锁进行了大量优化,比如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,首先需要尝试获取对象锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,就把锁+1,exit则相反会把锁-1,当计算器为0时,就意味着获取对象失败,当前线程就要阻塞,直到对象被另一个线程释放。
ReentrantLock是JUC下的一个包
在这里插入图片描述

当然还有其他的
在这里插入图片描述

有一些高级功能:

  1. 等待可中断,持有锁长期不释放的情况下,正在等待的线程可以选择放弃等待,避免死锁的发生。
  2. 公平锁,多个线程等待同一个锁,必须按照申请锁的时间顺序来获取锁,Synchronized锁是非公平的,但是性能表现不是很好。
  3. 锁可以绑定多个条件,一个ReentrantLock可以绑定多个对象。
    Lock为程序设计人员提供了高精度锁定控制,synchronozed则没有。

啥是线程安全

线程安全就是当多程序访问同一段代码,不会产生不确定的结果。就像图灵定义的那样,一个计算机应当是经过有序的特定的步骤给出一份正确的完整的答案,如果多线程导致计算机每次计算结果都不一样,那么就说明多线程是失败的,代码在多线程执行下和在单线程执行永远都能获得一个一样的结果,那么你的代码就是线程安全的。

  1. 不可变
    像是String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它的值,只能新建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
  2. 绝对线程安全
    不管运行环境如何,调用者都不需要额外的同步措施。要做到这一点付出许多额外的代价,Java中有自己的线程安全类,绝对安全的有:CopyOnWriteArrayList,CopyOnWriteArraySet。
  3. 相对线程安全
    相对线程安全就是通常意义上的线程安全,像Vector这种,add和remove方法都是原子操作不会被打断,多个线程同时进行add某个Vector会报错ConcurrentModificationException。
  4. 线程非安全
    ArrayList、LinkedList、HashMap都是非线程安全的,还有StringBuild。

Volatile域

同步和锁都是Rpg,如果是打苍蝇则不需要使用这么劳师动众,仅仅为了读写一个或者两个实例域就使用同步,开销太大了,毕竟出错的地方只是处理器和编译器,为何不对症下药呢,所以就有了Volatile域。
问题:

  1. 多处理器的计算机能够暂时在寄存器或者本店内存缓冲区中保存内存中的值。结果是运行在不同处理器上的线程可能在同一个内存位置取到不同的值
  2. 编译器可以改变指令执行顺序来使吞吐量最大化,虽然不会改变语义,但是内存的值如果再此期间被其他线程修改了会怎样呢?

这时可以采用Volatile关键字来使变量对于线程处于可见状态和禁止指令重排序。

总结

  1. 锁用来保护代码片段,任何时候只能有一个线程执行被保护的代码
  2. 锁可以管理试图进入被保护代码片段的线程
  3. 所可以拥有一个或者多个条件相关的条件对象
  4. 每个条件对象管理那些已经进入被保护的代码段,但是还不能运行的线程

阻塞队列

阻塞队列是一个支持两个附加操作的队列。
操作为:在队列为空的时候,获取元素的线程会等待队列变为非空,当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用与生产者和消费者的场景,生产者就是往队列中添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费这也只能从容器里拿元素。
其中阻塞队列:
ArrayBlokingQueue:一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的无界阻塞队列
SynchronousQueue:一个不存储元素的阻塞队列
LinkedTransferQueue:一个由链表组成的无界阻塞队列
LinkedBlockingDeque:一个由链表组成的双向阻塞队列。
在这里插入图片描述

在JUC中有阻塞队列的几个变种。默认情况下,LinkedBlockingQueue的容量是没有上边界的,也可选择指定最大容量,LinkedBlockingDeque是一个双端的版本,ArrayBlockingQueue在构造时需要制定容量,并可以设置是否公平。PriorityBlcokingQueue是一个带优先级的队列,而不是先入先出队列,元素按照他们的优先级顺序被移出,该容量是没上线的,如果队列为空,则取元素的操作会被阻塞。
这与他们的底层结构设计有关,有兴趣的可以去JUC中查看源码。

线程池

讲到阻塞队列下一步就是线程池,这点我认为Java核心卷这本书排序有问题,写的很混乱,阻塞队列是线程池的重要组成部分,也是阻塞队列的具体实现,所以我的学习路线是:阻塞队列-》线程池-》Asynctask,依次递增的认识线程。
构建一个新的线程是需要代价的,因为涉及到与操作系统的交互,如果程序中创建了大量的生命期很短的线程,则应该使用线程池,一个线程池中包含许多准备运行的空闲线程,将Runnable对象交给线程池,就会有一个线程调用run方法,当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至引起虚拟机崩溃。
newCachedThreadPool 必要时创建新线程;空闲时会被保留60s;
newFixedThreadPool该池包含固定数量的线程,空闲时线程会一直被保留
newSingleThreadExecutor该线程只有一个池,该线程顺序执行每一个提交的任务
newSingleThreadScheduledExecutor用于预订执行而构建的单线程池。

同步器

JUC工具包包含了几个能帮人管理互相合作线程集的类
CyclicBarrier
允许线程集等待直到其中预订数目的线程到达一个公共障栅,然后选择执行一个处理障栅的动作。比如有大量的线程需要在它们的结果可用之前完成时。
Phaser
类似于循环障栅,不过有一个可变的计数
CountDownLatch
允许线程集等待直至计数器减为0,当一个或者多个线程需要等待直到指定数目的事件发生
Exchanger
允许两个线程在要交换的对象准备好时交换对象,当两个线程工作在同一数据结构的两个实例上时,一个向实例添加数据,一个从实例删除数据
Semaphore
允许线程集等待直到被允许继续运行为止,限制访问资源的线程总数。如果许可数是1,尝尝阻塞线程直到另一个线程给出许可为止
SynchronousQueue
允许一个线程把对象交给另一个线程,在没有显示同步的情况下,当两个下次讷航准备好将一个对象从另一个线程传递到另一个。
在这里插入图片描述

这些机制具有为线程之间的共用集结点模式提供预置功能。如果有一个相互合作的线程集满足这些行为模式之一,那就应该直接重用合适的类库,而不需要提供手工的锁。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值