【Java基础】多线程

Java 线程在运行的生命周期中6 种状态

  • NEW: 初始状态,线程被创建出来但没有被调用start() 。
  • RUNNABLE: 运行状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作 (通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等
    待 。77777- TERMINATED:终止状态,表示该线程已经运行完毕
    在这里插入图片描述

几个锁的概念

  公平锁:

  多个线程按照申请锁的顺序来获取锁,先来后到,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程等待队列的第一个,就占有锁,否则就会加入到等待队列,以后会按照FIFO先进先出的规则从队列中取。

  非公平锁:

  多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程获取锁。在高并发的情况下,哟可能会造成优先级反转或者饥饿现象。

 悲观锁

   悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

 乐观锁

   乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

 CAS

  CAS 全名 compare and swap (比较并交换)是一种基于 Java 实现的 计算机代数系统,用于多线程并发编程时数据在无锁的情况下保证线程安全安全运行。
  CAS机制 主要用于对一个变量(操作)进行原子性的操作,它包含三个参数值:需要进行操作的变量A、变量的旧值B、即将要更改的新值C。会对当前内存中的 A 进行判断看是否等同于 B ,如果相等则把 A 值更改为 C 。(CAS 的原理是期望的值和原本的一个值作比较,如果相同则更新成新的值。
  Atomic 原子类
    Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。Atomic 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

 AQS,AbstractQueuedSynchronizer

  AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS能简 单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock, SynchronousQueue等等皆是基于 AQS 的。
  AQS 核心思想是,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制是用 CLH 队列 实现的,即将获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten) 是一个双向队列。AQS是将每条线程封装成一个 CLH 队列的一个结点来实现锁的分配。 在 CLH 队列中,一个节点表示一个线程,它保存着线程的引用、 当前节点在队列中的状态、前驱节点、 后继节点
在这里插入图片描述
  AQS 使用 int 成员变量 state 表示同步状态,通过内置的线程等待队列来完成获取资源线程的排队工作。
state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。
  // 共享变量,使用volatile修饰保证线程可⻅性
  private volatile int state;
  另外,状态信息 state 可以通过 protected 类型的getState()、setState()和 compareAndSetState() 进行操作。并且,这几个方法都是 final 修饰的,在子类中无法被重写

 可重入锁,也叫递归锁

  指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
  JDK 提供的所有现成的 Lock 实现类,包括 synchronized

 Volatile

  Volatile标记的变量就是指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 的意义就是禁用 CPU 缓存。volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  volatile 关键字除了可以保证变量的可⻅性,还有一个重要的作用是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序

 Synchronized

  synchronized 的实现使用的是 monitorentermonitorexit 指令。
  当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。每个对象中都内置了一个monitor对象。 wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块中才能调用wait/notify等方法。 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
  synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 CC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否为同步方法。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率

  synchronized 和 volatile 有什么区别?

  synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。
  volatile 关键字是线程同步的轻量级实现, volatile性能肯定比 synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
  volatile关键字主要用于解决变量在多个线程之间的可⻅性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性

 synchronized 和 ReentrantLock 有什么区别

  1 两者都是可重入锁。
  2 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API:
     synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的。
    ReentrantLock 是 API 层面的,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成
  3 ReentrantLock 比 synchronized 增加了一些高级功:
    可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lockInterruptibly 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而 synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的。
    选择性通知: synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口。

Condition具有很好的灵活性,可以在一个Lock对象中可以创建多个Condition实例,线程可以注册在指定的Condition中,从而可以有选择的进行线程通知。在使用notify()/ notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,ReentrantLock类结合Condition实例可以实现“选择性通知” 。而 synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。本质是一个分组

 Semaphore 信号量

   synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而 Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 Semaphore 中的共享资源,下面的代码表示同一时刻N 个线程中只有 5 个 线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。


   // 初始共享资源数量
   final Semaphore semaphore = new Semaphore(5);
   // 获取1个许可
   semaphore.acquire();
   // 释放1个许可
   semaphore.release();
   

   当初始的资源个数为 1 的时候,Semaphore 退化为排他锁

 CountDownLatch

   CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
   CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch 使用完毕后,它不能再次被使用。
   CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。 当线程使用 countDown() 方法时,其实以CAS 的 操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。

  CyclicBarrier

   字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续干活

 happens-before 规则

		int userNum = getUserNum(); // 1
		int teacherNum = getTeacherNum(); // 2
		int totalNum = userNum + teacherNum; // 3

  •1 happens-before 2
  •2 happens-before 3
  •1 happens-before 3
  虽然 1 happens-before 2,但对 1 和 2 进行重排序不会影响代码的执行结果, 所以 JMM 是允许编译器和处理器执行这种重排序的。但 1 和 2 必须是在 3 执行之前,也就是说 1,2 happens-before 3 。
  happens-before 表达的意义并不是一个操作发生在另外一个操作的前面,它想表达的意义是前一个操作的结果对于后一个操作是可⻅的,无论这两个操作是否在同一个线程里。
  常⻅规则
    1. 程序顺序规则 :一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作
    2. volatile 规则 :对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可⻅的
    3. 传递规则 :如果 A happens-before B,且 B happens-before C,那么 A happens-before C

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值