java多线程之synchronized详解

锁的内存语义

  • 锁可以让临界区互斥执行,还可以让释放锁的线程向同一个锁的线程发送消息
  • 锁的释放要遵循Happens-before原则(锁规则:解锁必然发生在最后的加锁之前)
  • 锁在java中的具体表现时SynchronizedLock

复现步骤

通过gradle/javac编译SynchronizedDemo.java出对应的class。

注意如果使用javac编译,记得加入参数-encoding UTF-8

使用jdk命令javap查看字节码

运行 javap -v SynchronizedDemo.class

输出内容文件见:SynchronizedDemo.class.javap.txt.

jvm实现

通过查看字节码具体指令可以看到:

synchronized修饰方法时:

  public static synchronized void staticMethod() throws java.lang.InterruptedException;descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

会为方法放置一个ACC_SYNCHRONIZED标志(flags)。

当使用synchronized代码块时:

public void blockStaticMethod1() throws java.lang.InterruptedException;descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: getstatic     #17                 // Field STATIC_MONITOR:Ljava/lang/Object;3: dup4: astore_15: monitorenter6: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;9: ldc           #18                 // String 静态对象同步方法1开始11: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V14: ldc2_w        #14                 // long 3000l17: invokestatic  #9                  // Method java/lang/Thread.sleep:(J)V20: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;23: ldc           #19                 // String 静态对象同步方法1结束25: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V28: aload_129: monitorexit

在方法上并没有标志,二十在代码中加入monitorentermonitorexit指令实现。

而这在The Java® Virtual Machine Specification 3.14. Synchronization中有很详细的描述。

同步代码块原理

monitor监视器

操作系统在面对 进程/线程 间同步时,semaphore 信号量 和 mutex 互斥量是最重要的同步原语。 在使用基本的 mutex 进行并发控制时,需要程序员非常小心地控制 mutex 的 down 和 up 操作,否则很容易引起死锁等问题。 为了更容易地编写出正确的并发程序,所以在 mutex 和 semaphore 的基础上,提出了更高层次的同步原语 monitor。

操作系统本身并不支持 monitor 机制,monitor 是属于编程语言的范畴。

monitor本质上时一种通用同步工具的抽象,同一时刻只能有一个进程/线程执行该方法或过程,从而简化了并发应用的开发难度。

如果Monitor内没有线程正在执行,则线程可以进入Monitor执行方法,否则该线程被放入入口队列(entry queue)并使其挂起。当有线程从Monitor中退出时,会唤醒entry queue中的一个线程。

monitorenter和monitorexit指令

同步代码块使用monitorenter和monitorexit两个指令实现。 The Java® Virtual Machine Specification 中有关于这两个指令的介绍:

monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership

每个对象都有一个监视器。当该监视器被占用时即是锁定状态(或者说获取监视器即是获得同步锁)。 线程执行monitorenter指令时会尝试获取监视器的所有权,过程如下:

  • 若该监视器的进入次数为0,则该线程进入监视器并将进入次数设置为1,此时该线程即为该监视器的所有者
  • 若线程已经占有该监视器并重入,则进入次数+1
  • 若其他线程已经占有该监视器,则线程会被阻塞直到监视器的进入次数为0,之后线程间会竞争获取该监视器的所有权
  • 只有首先获得锁的线程才能允许继续获取多个锁

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

执行monitorexit指令将遵循以下步骤:

  • 执行monitorexit指令的线程必须是对象实例所对应的监视器的所有者
  • 指令执行时,线程会先将进入次数-1,若-1之后进入次数变成0,则线程退出监视器(即释放锁)
  • 其他阻塞在该监视器的线程可以重新竞争该监视器的所有权

由于 wait/notify 等方法底层实现是基于监视器,因此只有在同步方法(块)中才能调用wait/notify等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因

锁优化

通过上面查看的monitor机制,会发现多线程中会有频繁线程的阻塞和唤醒,这需要CPU在用户态和内核态之间频繁的切换, 这对CPU的负担很重,进而对并发性能带来很大的影响。

让我们仔细对情况进行分析,逐步根据各种情况进行锁性能的思考:

  1. 我们知道线程切换的开销很高,当线程争抢锁时,当前线程会经历阻塞/唤醒的状态,但是cpu阻塞/唤醒线程代价很高,但是如果实际上这里只需要稍稍等一下,也就是说如果线程阻塞/唤醒时间其实远远大于等待的时间,我们是否应该考虑不让线程切换呢?

  2. 看看实际情况中,如果我们写这段代码只是为了应付某些临界情况(万一有另外一个线程争抢锁),一般来说这个锁很快就用完了,根本没有其他的线程来进行竞争,我们还需要申请系统的锁吗?

  3. 当上面的情况发生转变,出现了一个新的线程争抢锁,或者说少量的线程来争抢,和大量争抢的情况呢?

  4. 查看下面这种代码

        public void doSomethingMethod(){synchronized(lock){//do some thing}//do somethingsynchronized(lock){//do other thing}}
    

    如果请求两个同样的锁,中间只是很简单的一个计算,时间很短,这是线程会对同一个锁多次请求、同步、释放。甚至更极端的情况如下:

        for(int i=0;i<size;i++){synchronized(lock){}}
    

    多次请求同一个锁的情况,如果还需要频繁请求释放,会大大降低效率,那么此时是否应该把这些同步代码合并呢?

  5. 如果说此时写了一段代码

     public void append(String str1,String str2){StringBuffer stringBuffer = new StringBuffer();stringBuffer.append(str1).append(str2);}
    

    这种情况下,虽然StringBuffer内部有锁,但是很明显就能分析出,这是本地变量,实际上不需要进行同步,那么是否可以消除掉这种无意义的锁呢?

如此诸多情况,我们接下来一个一个分析:

自旋锁

jdk针对第一种情况引入了自旋锁。

痛点:由于线程的阻塞/唤醒需要CPU在用户态和内核态间切换,频繁的转换对CPU负担很重,进而对并发性能带来很大的影响

其原理是 通过执行一段无意义的空循环让线程等待一段时间,不会立即被挂起,看持有锁的线程是否很快释放锁, 如果锁很快被释放,那当前线程就有机会不用阻塞就能拿到锁,从而减少切换,提高性能。

循环中要进行什么操作呢?先看看需求:

  • 我们需要先查看原始的状态是否为未加锁
  • 如果已经加锁,则表示没有获取到锁
  • 如果没有加锁,则需要替换为已加锁的状态

我们正常的思路大概是这样:

if(state == 0){state = 1;
}

但是这会产生一个问题,我们如何保证第一行执行而第二行还未执行的情况下,没有另外一个线程把state修改为1了呢?如果不保证那么会出现多个线程都去判断修改并且都成功了的情况。

于是为了解决这个问题,我们需要使用一个由cpu提供的CAS原子指令,而java对此进行了相应的调用封装,这就是Java Unsafe类中提供的compareAndSwap*方法,包括:

  • compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  • compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • compareAndSwapLong(Object var1, long var2, long var4, long var6);

将上述三行代码进行原子合并,具体原理大概是:CPU可以自动更新共享数据,而且能够检测到其他线程的干扰。更深处就不再追究了。 我们只需要了解CAS即可,多线程中CAS由大量的应用,务必牢记。

而自旋锁则是编写一个do-while循环,如果修改数值失败则通过循环来执行自旋,直至修改成功。

代码展示可以查看Unsafe的getAndAddInt方法:

public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}

至此逻辑也就清晰了,自旋锁实际上就是CAS指令+循环实现。

CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

  • ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  • 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  • 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

但是这又带来了新的问题,如通过锁能够很快就被释放,那么自选效率确实很好(CPU空转时间少),但是如果锁一直被占用, 那么长时间的自旋毫无意义,并且白白占用了CPU资源,造成资源的浪费。

自旋次数必须有限度,如果超过自旋字数还没获得锁,就要被阻塞挂起,使用JDK1.6以上默认开启:-XX:+UseSpinning,自旋次数可通过-XX:PreBlockSpin调整,默认10次

当出现自旋次数超过时,就说明到了需要阻塞的情况了,但是从自旋锁原理上,我们能看到会有一个自旋次数/时间的限制,但是自旋锁只能指定固定的自旋次数,而线程的争抢肯定会跟随 不同的任务情况有所不同,于是就能够想到,是否能够根据锁每次的自旋情况来自适应呢?

于是引入了自适应自旋锁:

自适应自旋锁

痛点:由于自旋锁只能指定固定的自旋次数,但由于任务的差异,导致每次的最佳自旋次数有差异

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中, 那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程, 直接阻塞线程,避免浪费处理器资源。

1: 有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,JVM对锁的状况预测会越来越准确,JVM会变得越来越智能

偏向锁

第二个问题中,当只有一个线程访问的时候,jvm使用了偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

那么如何实现呢?

当一个线程访问同步块并获取到锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID, 以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁, 而是先简单检查对象头的MarkWord中是否存储了线程:

  • 如果已存储,说明线程已经获取到锁,继续执行任务即可
  • 如果未存储,则需要再判断当前锁否是偏向锁(即对象头中偏向锁的标识是否设置为1,锁标识位为01)
  • 如果没有设置,则使用CAS竞争锁(说明此时并不是偏向锁,一定是等级高于它的锁)
  • 如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,也就是结构中的线程ID

从这里看到偏向锁的算法无法使用自旋锁进行优化,因为只要有其他线程在竞争锁,那么偏向锁本身就没有存在的意义了,那么就需要进入下一个阶段:

轻量级锁

当锁处于偏向锁状态后,这时出现了一个搅局者,被另外的线程访问了,这时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但是不会阻塞,从而提高了性能。

  • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
  • 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
  • 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
  • 若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

当在解锁时:

  • 使用CAS操作将Displaced Mark Word替换回到对象头
  • 如果解锁成功,则表示没有竞争发生
  • 如果解锁失败,表示当前锁存在竞争,锁会膨胀成重量级锁,需要在释放锁的同时唤醒被阻塞的线程,之后线程间要根据重量级锁规则重新竞争重量级锁

通过上述步骤,可以观察到,轻量级锁有一个使用前提:没有多线程竞争环境。一旦越过这个前提,除了互斥的开销外,还会增加额外的自旋操作的开销,如果大量线程争抢,会消耗大量的CPU操作开销,这个时候轻量级锁甚至比重量级锁还要慢。

于是采用最终极的解决方法:重量级锁

重量级锁

这也就是依赖于操作系统的锁,我们知道CPU存在内核态用户态,如果我们要申请一把锁,那么需要去内核态申请,内核中的锁数量是有限的,所以还需要返还,而与内核态打交道消耗是很大的。 这也是为什么我们会尽量采用在用户态进行优化,而不是去内核中申请锁的原因。但是当多线程争抢导致的CPU竞争消耗比锁还要大的时候,我们就不得不去内核中申请锁了。

重量级锁是通过操作系统底层的MutexLock实现的,它内部会为到达的线程维护一个队列(默认无序)。MutexLock最核心的理念就是 尝试获取锁.若可得到就占有.若不能,就进入睡眠等待。

锁粗话和锁细化

通过4问题,我们可以看到,通常情况下,为了保证多线程间的有效并发, 会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放, 会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗, 这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。

于是引入锁粗化,把很多次锁的请求合并成一个请求,从而降低短时间内大量锁请求、同步、释放带来的性能损耗。

而通过5问题,我们可以看到,有时候我们写的代码完全不需要加锁,却执行了加锁操作。

    public void append(String str1,String str2){StringBuffer stringBuffer = new StringBuffer();stringBuffer.append(str1).append(str2);}

StringBuffer使用了synchronized关键字,它是线程安全的,但我们可能仅在线程内部把StringBuffer当作局部变量使用,可以通过JVM在编译时通过对运行上下文的描述,去除不可能存在共享资源竞争的锁,通过这种方式消除无用锁,即删除不必要的加锁操作,从而节省开销

逃逸分析和锁消除分别可以使用参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(锁消除必须在-server模式下)开启

在JDK内置的API中,例如StringBuffer、Vector、HashTable都会存在隐性加锁操作,可消除

锁升级

  • 从JDK1.6开始,锁一共有四种状态:无锁状态、偏向锁状态、轻量锁状态、重量锁状态
  • 锁的状态会随着竞争情况逐渐升级,锁允许升级但不允许降级
  • 不允许降级的目的是提高获得锁和释放锁的效率

锁升级过程:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

补充: 锁升级过程中的数据结构变化(Hotspot)

锁状态25位31位1位4bit1bit 偏向锁位2bit锁标志位
无锁态hashCode(如果有调用)分代年龄00 1
锁状态54位2位1位4bit1bit 偏向锁位2bit锁标志位
偏向锁当前线程指针JavaThread*Epoch101
锁状态62位2bit锁标志位
轻量级锁指向线程栈中额Lock Record的指针0 0
重量级锁指向互斥量(重量级锁)的指针1 0
GC标记信息CMS过程中用到的标记信息1 1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/21680.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SaaS 电商设计 (十一) 那些高并发电商系统的限流方案设计

目录 一.什么是限流二.怎么做限流呢2.1 有哪些常见的系统限流算法2.1.1 固定窗口2.1.1 滑动窗口2.1.2 令牌桶2.1.3 漏桶算法 2.2 常见的限流方式2.2.1 单机限流&集群限流2.2.2 前置限流&后置限流 2.3 实际落地是怎么做的2.3.1 流量链路2.3.2 各链路限流2.3.2.1 网关层2…

Spring Boot(七十七):SpringBoot实现接口内容协商功能

1 什么是内容协商 简单说就是服务提供方根据客户端所支持的格式来返回对应的报文,在 Spring 中,REST API 基本上都是以 json 格式进行返回,而如果需要一个接口即支持 json,又支持其他格式,开发和维护多套代码显然是不合理的,而 Spring 又恰好提供了该功能,那便是Conten…

重学java 56. Map集合

我们要拥有一定成功的信念 —— 24.6.3 一、双列集合的集合框架 HashMap 1.特点: a.key唯一,value可重复 b.无序 c.无索引 d.线程不安全 e.可以存null键,null值 2.数据结构:哈希表 LinkedHashMap&#xff08;继承HashMap&#xff09; 1.特点: a.key唯一,value可重复 b.有序 c.无…

Spring All系列教程学习下

SpringAll SpringAll对应的博客 java-developer-document - 别人的学习笔记 Spring All系列教程 该仓库为个人博客https://mrbird.cc中Spring系列源码&#xff0c;包含Spring Boot、Spring Boot & Shiro、Spring Cloud&#xff0c;Spring Boot & Spring Security &a…

前端 JS 经典:LRU 缓存算法

前言&#xff1a;什么是 LRU 呢&#xff0c;单词全拼 Least Recently Used&#xff0c;意思是最久未使用。这个算法是做缓存用的&#xff0c;比如&#xff0c;你要缓存一组数据&#xff0c;你要划分缓存块出来&#xff0c;因为不可能每个数据都做缓存&#xff0c;那么划出来的这…

矩阵连乘问题

#include<iostream> using namespace std; #define N 7 void MatrixChain(int p[N],int n,int m[N][N],int s[N][N]) {for(int i1;i<n;i)m[i][i]0;for(int r2;r<n;r)//有多少个相乘(规模){for(int i1;i<n-r1;i){int jir-1;m[i][j]m[i][i]m[i1][j]p[i]*p[i1]*p[j…

小熊家务帮day10- 门户管理

门户管理 1 门户介绍1.1 介绍1.2 常用技术方案 2 缓存技术方案2.1 需求分析2.1.1 C端用户界面原型2.1.2 缓存需求2.1.3 使用的工具 2.2 项目基础使用2.2.1 项目集成SpringCache2.2.2 测试Cacheable需求Service测试 2.1.3 缓存管理器&#xff08;设置过期时间&#xff09;2.1.4 …

深入理解序列化:概念、应用与技术

在计算机科学中&#xff0c;序列化&#xff08;Serialization&#xff09;是指将数据结构或对象状态转换为可存储或传输的格式的过程。这个过程允许将数据保存到文件、内存缓冲区&#xff0c;或通过网络传输至其他计算机环境&#xff0c;不受原始程序语言的限制。相对地&#x…

URL编码:讲解,抓包

URL 编码&#xff08;也称为百分号编码&#xff09;是一种在 URLs 中编码数据的方法。它将特殊字符转换为由百分号&#xff08;%&#xff09;后跟两个十六进制数字组成的格式。URL 编码通常用于将数据传递到网页或 Web 服务器时&#xff0c;以确保 URL 在传输过程中保持一致和安…

167.二叉树:另一棵树的字树(力扣)

代码解决 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* Tre…

2.3 OpenCV随手简记(四)

阈值处理是很多高级算法底层处理的预方法之一。 自己求图像平均阈值&#xff1a; # -*- codingGBK -*- import cv2 as cv import numpy as np #求出图像均值作为阈值来二值化 def custom_image(image): gray cv.cvtColor(image, cv.COLOR_BGR2GRAY) cv.imshow("原来&qu…

【JavaScript】---DOM操作1:获取元素

【JavaScript】—DOM操作1&#xff1a;获取元素 文章目录 【JavaScript】---DOM操作1&#xff1a;获取元素一、什么是DOM&#xff1f;1.1 概念1.2 图例演示 二、查找HTML元素2.1 getElementById()2.2 getElementsByTagName()2.3 getElementsByClassName()2.4 querySelector()2.…

Go语言 几种常见的IO模型用法 和 netpoll与原生GoNet对比

【go基础】16.I/O模型与网络轮询器netpoller_go中的多路io复用模型-CSDN博客 字节开源的netPoll多路复用器源码解析-CSDN博客 一、几种常见的IO模型 1. 阻塞I/O (1) 解释&#xff1a; 用户调用如accept、read等系统调用&#xff0c;向内核发起I/O请求后&#xff0c;应用程序…

【Spring Cloud Alibaba】服务注册与发现+远程调用

目录 注册微服务到Nacos&#xff08;服务提供者&#xff09;创建项目修改依赖信息添加启动注解添加配置信息启动服务&#xff0c;Nacos控制台查看服务列表 注册微服务到Nacos&#xff08;服务消费者&#xff09;创建项目添加依赖信息添加启动注解添加配置信息启动服务&#xff…

基于卷积神经网络(CNN)的深度迁移学习在声发射(AE)监测螺栓连接状况的应用

螺栓结构在工业中用于组装部件&#xff0c;它们在多种机械系统中扮演着关键角色。确保这些连接结构的健康状态对于航空航天、汽车和建筑等各个行业至关重要&#xff0c;因为螺栓连接的故障可能导致重大的安全风险、经济损失、性能下降和监管合规问题。 在早期阶段检测到螺栓松动…

vue3路由详解,从0开始手动配置路由(vite,vue-router)

创建一个不含路由的vue项目 &#xff08;查看路由配置可以直接跳过这一段&#xff09; 输入npm指令&#xff0c;然后写一个项目名称&#xff0c;之后一路回车即可 npm create vuelatest 注意这里我们不选引入vue router&#xff0c;成功后可以 查看目录 然后按提示信息输入指…

python导出手机可执行

流程&#xff1a; 梦想->安装打包工具->编写程序->生成打包配置->执行打包命令->生成手机可执行文件->OK完成梦想 步骤1&#xff1a;安装打包工具 # 安装PyInstaller pip install pyinstaller 步骤2&#xff1a;编写Python程序 接下来&#xff0c;你需要编…

新闻出版署发布新规定,腾讯游戏限制未成年人端午期间每天一小时

原标题&#xff1a;腾讯游戏端午节期间针对未成年人的游戏时间限制措施 易采游戏网6月3日消息&#xff1a;近日国家新闻出版署针对未成年人沉迷网络游戏问题发布了《关于进一步严格管理 切实防止未成年人沉迷网络游戏的通知》&#xff0c;旨在加强对未成年人保护的力度&#xf…

GIS之arcgis系列06:线划图缓冲区分析

缓冲区工具将在输入要素周围指定距离内创建缓冲区面。 缓冲区例程将遍历输入要素的每个折点并创建缓冲区偏移。 通过这些偏移创建输出缓冲区要素 原理&#xff1a; 01.打开文件 02.确定单位&#xff0c;在文件属性里。 03.工具箱-->分析工具-->邻域分析-->缓冲区。 …

PDF格式分析(八十三)——屏幕注释(screen)

屏幕注释(PDF 1.5及其以上版本支持)&#xff0c;在指定页面区域内播放媒体剪辑。它也可以被actiond的动作进行触发。 下表显示了该型注释的字典条目&#xff1a; 条目类型详细Subtypename(必填)本词典描述的注释类型;必须为Screen。Ttext string(可选)屏幕注释的标题。MKdicti…