怀化物流网站建设报价,wordpress评论选择头像,wordpress如何汉化主题,网站开发使用的语言有哪些由一个问题引发的思考
线程的合理使用能够提升程序的处理性能#xff0c;主要有两个方面#xff0c;第一个是能够利用多核cpu以及超线程技术来实现线程的并行执行#xff1b;第二个是线程的异步化执行相比于同步执行来说#xff0c;异步执行能够很好的优化程序的处理性能提…由一个问题引发的思考
线程的合理使用能够提升程序的处理性能主要有两个方面第一个是能够利用多核cpu以及超线程技术来实现线程的并行执行第二个是线程的异步化执行相比于同步执行来说异步执行能够很好的优化程序的处理性能提升并发吞吐量 同时也带来了很多麻烦举个简单的例子 多线程对于共享变量访问带来的安全性问题 一个变量i. 假如一个线程去访问这个变量进行修改这个时候对于数据的修改和访问没有任何问题。但是如果多个线程对于这同一个变量进行修改就会存在一个数据安全性问题 。对于线程安全性本质上是管理对于数据状态的访问而且这个这个状态通常是共享的、可变的。共享是指这个数据变量可以被多个线程访问可变指这个变量的值在它的生命周期内是可以改变的。
一个对象是否是线程安全的取决于它是否会被多个线程访问以及程序中是如何去使用这个对象的。所以如果多个线程访问同一个共享对象在不需额外的同步以及调用端代码不用做其他协调的情况下这个共享对象的状态依然是正确的正确性意味着这个对象的结果与我们预期规定的结果保持一致那说明这个对象是线程安全的。package com.sp.demo;/*** author : lssffy* Description :* date : 2024/2/16 18:02*/
public class Demo {private static int count 0;public static void inc(){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count;}public static void main(String[] args) throws InterruptedException {for (int i 0; i 1000;i){new Thread(()-Demo.inc()).start();}Thread.sleep(3000);System.out.println(运行结果 count);}
}
思考如何保证线程并行的数据安全性
问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使得线程的并行变成串行那是不是就不存在这个问题呢 按照大家已有的知识最先想到的应该就是锁吧。 毕竟这个场景并不模式我们在和数据库打交道的时候就了解过悲观锁、乐观锁的概念。 什么是锁它是处理并发的一种同步手段而如果需要达到前面我们说的一个目的那么这个锁一定需要实现互斥的特性。 Java 提供的加锁方法就是Synchroinzed关键字。synchronized 的基本认识
在多线程并发编程中synchronized一直是元老级角色很多人都会称呼它为重量级锁。但是随着 Java SE 1.6 对synchronized 进行了各种优化之后有些情况下它就并不那么重Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续我们会慢慢展开.synchronized 的基本语法
synchronized 有三种方式来加锁分别是
1. 修饰实例方法作用于当前实例加锁进入同步代码前要获得当前实例的锁 获得当前类对象的锁
2. 静态方法作用于当前类对象加锁进入同步代码前要
3. 修饰代码块指定加锁对象对给定对象加锁进入同步代码库前要获得给定对象的锁。 不同的修饰类型代表锁的控制粒度
synchronized 的应用
修改前面的案例使用synchronized关键字后可以达到数据安全的效果 package com.sp.demo;/*** author : lssffy* Description :* date : 2024/2/16 18:02*/
public class Demo {private static int count 0;public static void inc(){**synchronized** (Demo.class) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count;}}public static void main(String[] args) throws InterruptedException {for (int i 0; i 1000;i){new Thread(()-Demo.inc()).start();}Thread.sleep(3000);System.out.println(运行结果 count);}
}
思考锁是如何存储的
可以思考一下要实现多线程的互斥特性那这把锁需要哪些因素
1. 锁需要有一个东西来表示比如获得锁是什么状态、无锁状态是什么状态
2. 这个状态需要对多个线程共享 那么我们来分析synchronized锁是如何存储的呢观察synchronized 的整个语法发现synchronized(lock)是基于存储和这个lock对象有关系呢
对象在内存中的布局
lock 这个对象的生命周期来控制锁粒度的那是不是锁的于是我们以对象在jvm内存中是如何存储作为切入点去看看对象里面有什么特性能够实现锁 在Hotspot 虚拟机中对象在内存中的存储布局可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding) 探究Jvm源码实现
当我们在Java 代码中使用new 创建一个对象实例的时候hotspot 虚拟机JVM 层面实际上会创建一个 instanceOopDesc 对象。
Hotspot 虚拟机采用 OOP-Klass 模型来描述 Java 对象实例OOP(Ordinary Object Point)指的是普通对象指针Klass 用来描述对象实例的具体类型。Hotspot 采用instanceOopDesc 和 arrayOopDesc 来描述对象头arrayOopDesc 对象用来描述数组类型 instanceOopDesc 的定义在 Hotspot 源 码 中 的 instanceOop.hpp 文件中另外arrayOopDesc 的定义对应 arrayOop.hpp.从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自oopDescoopDesc 的定义载 Hotspot 源码中的 oop.hpp 文件中 是 _mark和 _metadata 在普通实例对象中oopDesc的定义包含两个成员分别_mark 表示对象标记、属于markOop类型也就是接下来要讲解的Mark World它记录了对象和锁有关的信息 _metadata 表示类元信息类元信息存储的是对象指向它的类元数据(Klass)的首地址其中 Klass 表示普通指针、 _compressed_klass 表示压缩类指针
MarkWord
在Hotspot 中markOop 的定义在 markOop.hpp 文件中代码如下Mark word 记录了对象和锁有关的信息当某个对象被synchronized 关键字当成同步锁时那么围绕这个锁的一系列操作都和Mark word有关系。Mark Word在32位虚拟机的长度是32bit、在64位虚拟机的长度是64bit。 Mark Word里面存储的数据会随着锁标志位的变化而变化Mark Word 可能变化为存储以下5中情况. 为什么任何对象都可以实现锁 1. 首先Java 中的每个对象都派生自 Object 类而每个Java Object 在 JVM 内部都有一个 native 的 C对象 oop/oopDesc 进行对应。 2. 线程在获取锁的时候实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象所有的Java 对象是天生携带 monitor。在 hotspot 源码的 markOop.hpp 文件中可以看到下面这段代码。 多个线程访问同步代码块时相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系 .
synchronized 锁的升级
在分析markword 时提到了偏向锁、轻量级锁、重量级锁。在分析这几种锁的区别时我们先来思考一个问题 使用锁能够实现数据的安全性但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。 hotspot 虚拟机的作者经过调查发现大部分情况下加锁的代码不仅仅不存在多线程竞争而且总是由同一个线程多次获得。所以基于这样一个概率是的synchronized在JDK1.6 之后做了一些优化为了减少获得锁和释放锁带来的性能开销引入了偏向锁、轻量级锁的概念。因此大家会发现在synchronized中锁存在四种状态 分别是无锁、偏向锁、轻量级锁、重量级锁 锁的状态根据竞争激烈的程度从低到高不断升级。 偏向锁的基本原理
前面说过大部分情况下锁不仅仅不存在多线程竞争而是总是由同一个线程多次获得为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢 当一个线程访问加了同步锁的代码块时会在对象头中存储当前线程的ID后续这个线程进入和退出这段加了同步锁的代码块时不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的就不需要再尝试获得锁了。偏向锁的获取和撤销逻辑
1. 首先获取锁 对象的Markword判断是否处于可偏向状态。 biased_lock1、且 ThreadId 为空。
2. 如果是可偏向状态则通过CAS操作把当前线程的ID写入到MarkWord
a) 如果 cas 成功那么markword 就会变成这样。表示已经获得了锁对象的偏向锁接着执行同步代码块
b) 如果 cas 失败说明有其他线程已经获得了偏向锁这种情况说明当前锁存在竞争需要撤销已获得偏向锁的线程并且把它持有的锁升级为轻量级锁这个操作需要等到全局安全点也就是没有线程在执行字节码才能执行
3. 如果是已偏向状态需要检查 markword 中存储的ThreadID 是否等于当前线程的ThreadID 块 偏向锁的撤销
a) 如果相等不需要再次获得锁可直接执行同步代码
b) 如果不相等说明当前锁偏向于其他线程需要撤销偏向锁并升级到轻量级锁 偏向锁的撤销并不是把对象恢复到无锁可偏向状态因为偏向锁并不存在锁释放的概念而是在获取偏向锁的过程中发现cas 失败也就是存在线程竞争时直接把被偏向的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时原获得偏向锁的线程有两种情况
1. 原获得偏向锁的线程如果已经退出了临界区也就是同步代码块执行完了那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程
2. 如果原获得偏向锁的线程的同步代码块还没执行完处于临界区之内这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块 在我们的应用开发中绝大部分情况下一定会存在2个以的资源消耗。所以可以通过jvm参数 上的线程竞争那么如果开启偏向锁反而会提升获取锁UseBiasedLocking 来设置开启或关闭偏向锁。
**流程图分析**轻量级锁的基本原理
轻量级锁的加锁和解锁逻辑
锁升级为轻量级锁之后对象的Markword也会进行相应的的变化。升级为轻量级锁的过程
1. 线程在自己的栈桢中创建锁记录 LockRecord。
2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
3. 将锁记录中的Owner指针指向锁对象。
4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。 自旋锁
轻量级锁在加锁过程中用到了自旋锁 所谓自旋就是指当有另外一个线程来竞争锁时这个线程会在原地循环等待而不是把该线程给阻塞直到那个获得锁的线程释放锁之后这个线程就可以马上获得锁的。 注意锁在原地循环的时候是会消耗cpu的就相当于在执行一个啥也没有的for循环。
所以轻量级锁适用于那些同步代码块执行的很快的场景这样线程原地等待很短的时间就能够获得锁了。 自旋锁的使用其实也是有一定的概率背景在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制否则如果一个线程执行同步代码块的时间很长那么这个线程不断的循环反而会消耗CPU资源。默认情况下自旋的次数是10次 可以通过 preBlockSpin来修改在JDK1.6 之后引入了自适应自旋锁自适应意味着自旋的次数不是固定不变的而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上自旋等待刚刚成功获得过锁并且持有锁的线程正在运行中那么虚拟机就会认为这次自旋也是很有可能再次成功进而它将允许自旋等待持续相对更长的时间。如果对于某个锁自旋很少成功获得过那在以后尝试获取这个锁时将可能省略掉自旋过程直接阻塞线程避免浪费处理器资源 轻量级锁的解锁
轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑通过CAS 操作把线程栈帧中的LockRecord 替换回到锁对象的MarkWord 中如果成功表示没有竞争。如果失败表示当前锁存在竞争那么轻量级锁就会膨胀成为重量级锁
**流程图分析** 重量级锁的基本原理
当轻量级锁膨胀到重量级锁之后意味着线程只能被挂起阻塞来等待被唤醒了。
**重量级锁的monitor**
加了同步代码块以后在字节码中会看到一个monitorenter 和 monitorexit。
每一个JAVA 对象都会与一个监视器monitor 关联我们可以把它理解成为一把锁当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时该线程得先获取到synchronized修饰的对象对应的monitor。 monitorenter 表示去获得一个对象监视器。monitorexit表示释放monitor 监视器的所有权使得其他被阻塞的线程可以尝试去获得这个监视器
monitor 依赖操作系统的MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核Linux调度状态这个会导致系统在用户态与内核态之间来回切换严重影响锁的性能 。重量级锁的加锁的基本流程 任意线程对ObjectObject由synchronized保护的访问首先要获得Object的监视器。如果获取失败线程进入同步队列线程状态变为BLOCKED。当访问Object的前驱获得了锁的线程释放了锁则该释放操作唤醒阻塞在同步队列中的线程使其重新尝试对监视器的获取。 回顾线程的竞争机制
再来回顾一下线程的竞争机制对于锁升级这块的一些基本流程。方便大家更好的理解 加入有这样一个同步代码块存在Thread#1、Thread#2等多个线程 synchronized (lock) { // do something
}情况一只有Thread#1会进入临界区
情况二Thread#1和Thread#2交替进入临界区,竞争不激烈
情况三Thread#1/Thread#2/Thread3… 同时进入临界区竞争激烈偏向锁 此时当Thread#1进入临界区时JVM会将lockObject的对象头Mark Word的锁标志位设为“01”同时会用CAS操作把Thread#1 的线程 ID记录到Mark Word 中此时进入偏向模式。所谓“偏向”指的是这个锁会偏向于Thread#1若接下来没有其他线程进入临界区则 Thread#1 再出入临界区无需再执行任何同步操作。也就是说若只有Thread#1 会进入临界区实际上只有Thread#1初次进入临界区时需要执行CAS操作以后再出入临界区都不会有同步操作带来的开销。 轻量级锁 偏向锁的场景太过于理想化更多的时候是 Thread#2 也会尝试进入临界区 如果 Thread#2 也进入临界区但是级锁的方式来获取锁 重量级锁 Thread#1 还没有执行完同步代码块时会暂停 Thread#1并且升级到轻量级锁。Thread#2通过自旋再次尝试以轻量如果Thread#1和Thread#2正常交替执行那么轻量级锁基本能够满足锁的需求。但是如果Thread#1和Thread#2同时进入临界区那么轻量级锁就会膨胀为重量级锁意味着Thread#1 线程获得了重量级锁的情况下Thread#2就会被阻塞。
Synchronized 结合 Java Object 对象中的 wait,notify,notifyAll
前面我们在讲synchronized的时候发现被阻塞的线程什么时候被唤醒取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢我们就需要借助一个信号机制 在 Object 对象中提供了wait/notify/notifyall可以用于控制线程的状态 wait/notify/notifyall 基本概念
wait表示持有对象锁的线程A准备释放对象锁权限释放cpu资源并进入等待状态。
notify表示持有对象锁的线程A准备释放对象锁权限通知 jvm 唤醒某个竞争该对象锁的线程 X。线程 A
synchronized 代码执行结束并且释放了锁之后线程X直接获得对象锁权限其他竞争线程继续等待(即使线程X同步完毕释放对象锁其他竞争线程仍然等待直至有新的notify ,notifyAll 被调用)。
notifyAllnotifyall 和 notify 的区别在于notifyAll 会唤醒所有竞争同一个对象锁的所有线程当已经获得锁的线程A 释放锁之后所有被唤醒的线程都有可能获得对象锁权限。
需要注意的是三个方法都必须在synchronized 同步关键字 所 限定的作用域中调用否则会报错java.lang.IllegalMonitorStateException 意思是因为没有同步所以线程对对象锁的状态是不确定的不能调用这些方法。 另外通过同步机制来确保线程从wait方法返回时能够感知到感知到notify线程对变量做出的修改。wait/notify 的基本原理