Android并发编程里的线程原理

news2024/11/25 18:26:11

1.进程和线程的概念

抛开那些官方的概念,我们可以大致理解为:进程就是手机里运行的一个个应用,他们都是一个个的进程(当然,有些App是多进程的,这个先不谈)。线程则是进程中对应的一个任务的执行控制流。如果将一个进程比喻成一个车间的话,那么这个车间里的每个生产线就可以看作是一个线程。

高速缓冲区的概念:

在最早的时候,计算机只有进程。同一个时间内,是一个进程对内存进行操作。但这样效率比较低。比如我们要进程一个文件的读写操作,需要等这个操作完成,才能干别的事。并发概念出来后,可以多个进程进行轮换切换,以达到相对意义上的“同时”对内存进行操作。

但是这样有一个问题,就是进程与进程之间的切换需要耗费很多资源。因此为了更加的高效,便设计出了线程,将内存中的数据存在高速缓冲区里,每个线程都有其对应的高速缓冲区,线程往高速缓冲区里存取数据,这样原先的进程间切换就变成了线程间的切换,更加轻量化了,从而减少资源耗损。

每个线程都有独立的高速缓冲区,就是寄存器。可以理解为CPU里的内存。一核代表了一个线程,所以假设一个4核8G的手机,一个2核就有2G。

我们知道,平时我们写的java文件,通过javac把.java文件变成.class文件,然后再通过类加载器,classLoader把.class文件加载到内存中,而jvm在运行这个.class字节码文件的时候,会进行一些内存划分。

这里面有主要两块的划分,一个是线程共享区,一个是线程独占区。 共享区就包括了方法区和堆区,而独占 区就包括了,java虚拟机栈,本地方法栈,程序计算器。所以大家记住了,堆里面的内容是共享的,而栈里的内容是独占的。所以我们定义在虚拟机栈的数据,彼此之间是不能随便访问的。如果我们用final修饰数据,就将数据从线程A的虚拟机栈中复制到方法区里,这样线程2就能从方法区里访问这个数据。

线程的生命周期:

Thread -> Runnable ->Runing ->Terminated.

在Running之后,也可以经过wait()方法进入到waiting状态。在waiting状态的线程,可以通过notify()或者notifyAll()方法唤醒,使其重新进入Runnable()状态。

 在没有锁的情况下,当一个Thread创建之后,start()调用后,就变成了Runable()状态,也就是可执行情况。然后当该线程抢占到时间片后,就变成了Running状态。也就是已经在执行状态了。当执行完成后就到了Termianted,也就是结束了。如果在还没结束的时候,调用了wait状态,就会进入等待waiting状态。然后一直等到其他线程唤醒他,也就是调用notify或者notifyAll.然后进入runnable状态,这个时候,然后如果抢占到时间片,就会再进入running状态。

当然,这是没有锁的情况下。如果涉及到锁,其实也很简单,就是 多了一个阻塞状态。blocked.

如果涉及到锁的状态,当该线程抢到锁之后,其他的线程就会进入Blocked状态,等到该线程释放锁之后,那些阻塞的线程在拿到锁喉进入Runnable状态。

这是一个线程的状态图。需要注意的是, 比如我们调用了Thread.sleep,他就会进入Timed_waiting状态,也就是等待有限时间状态。

我们经常new一个线程,这个线程其实底层是Linux系统的线程,我们可以跟踪来看他的start()方法:

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        // Android-changed: Replace unused threadStatus field with started field.
        // The threadStatus field is unused on Android.
        if (started)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        // Android-changed: Use field instead of local variable.
        // It is necessary to remember the state of this across calls to this method so that it
        // can throw an IllegalThreadStateException if this method is called on an already
        // started thread.
        started = false;
        try {
            // Android-changed: Use Android specific nativeCreate() method to create/start thread.
            // start0();
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

我们看到try代码块里有关键的一行:

  // Android-changed: Use Android specific nativeCreate() method to create/start thread.
            // start0();
            nativeCreate(this, stackSize, daemon);

在这里可以看到,这是android修改了这个方法,本地创建。

// Android-changed: Use Android specific nativeCreate() method to create/start thread.
    // The upstream native method start0() only takes a reference to this object and so must obtain
    // the stack size and daemon status directly from the field whereas Android supplies the values
    // explicitly on the method call.
    // private native void start0();
    private native static void nativeCreate(Thread t, long stackSize, boolean daemon);

根据前面的注释,我们翻译一下:

//Android更改了此方法,使用android特定的nativeCreate()方法创建/启动线程。

//上游的native方法start0()仅仅引用了这个对象,因此必须获得

//堆栈大小和守护进程状态来自android提供的值。

//在此方法上显示执行

//私有native方法start0()

在android8.0之前是使用start0方法,之后使用natvieCreate方法。

 我们跟踪start0()这个native方法:

static JNINativeMethod methods[] = {
    {"start0",           "(JZ)V",        (void *)&JVM_StartThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(Ljava/lang/Object;J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

可以看到start0方法,映射为JVM_StartThread()方法,这个方法通过方法名可以看到属于JVM里的函数。找到这个方法:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  // We cannot hold the Threads_lock when we throw an exception,
  // due to rank ordering issues. Example:  we might need to grab the
  // Heap_lock while we construct the exception.
  bool throw_illegal_thread_state = false;

  // We must release the Threads_lock before we can post a jvmti event
  // in Thread::start.
  {
    // Ensure that the C++ Thread and OSThread structures aren't freed before
    // we operate.
    MutexLocker mu(Threads_lock);

    // Since JDK 5 the java.lang.Thread threadStatus is used to prevent
    // re-starting an already started thread, so we should usually find
    // that the JavaThread is null. However for a JNI attached thread
    // there is a small window between the Thread object being created
    // (with its JavaThread set) and the update to its threadStatus, so we
    // have to check for this
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // Allocate the C++ Thread structure and create the native thread.  The
      // stack size retrieved from java is signed, but the constructor takes
      // size_t (an unsigned type), so avoid passing negative values which would
      // result in really large stacks.
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

      // At this point it may be possible that no osthread was created for the
      // JavaThread due to lack of memory. Check for this situation and throw
      // an exception if necessary. Eventually we may want to change this so
      // that we only grab the lock if the thread was created successfully -
      // then we can also do this check and throw the exception in the
      // JavaThread constructor.
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

我们重点看下这里:

 jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // Allocate the C++ Thread structure and create the native thread.  The
      // stack size retrieved from java is signed, but the constructor takes
      // size_t (an unsigned type), so avoid passing negative values which would
      // result in really large stacks.
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

翻译下上面的注释:

//分配C++线程结构并创建本机线程。这个从java检索的栈,是带符号的。但构造函数需要无符号的。因此要避免传递负值,导致栈的大小非常的大。

(带符号位的,第一位是符号位,如果把符号位传递给无符号的表示,则会把第一位符号位也当数,就会导致数据非常大)

这里创建了Java线程,并且,把虚拟机栈的大小也传递了进去。所以我们的虚拟机栈(在高速缓冲区)的大小也就是线程的默认大小。我们继续来追踪这个JavaThread()方法:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
  // The _osthread may be NULL here because we ran out of memory (too many threads active).
  // We need to throw and OutOfMemoryError - however we cannot do this here because the caller
  // may hold a lock and all locks must be unlocked before throwing the exception (throwing
  // the exception consists of creating the exception object & initializing it, initialization
  // will leave the VM via a JavaCall and then all locks must be unlocked).
  //
  // The thread is still suspended when we reach here. Thread must be explicit started
  // by creator! Furthermore, the thread must also explicitly be added to the Threads list
  // by calling Threads:add. The reason why this is not done here, is because the thread
  // object must be fully initialized (take a look at JVM_Start)
}

可以看到这里创建线程的方法是os::create_thread()这里传递的线程的大小就是栈大小staek_sz,继续看这个方法:

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
  assert(thread->osthread() == NULL, "caller responsible");

  ...

  // stack size
  if (os::Linux::supports_variable_stack_size()) {
    // calculate stack size if it's not specified by caller
    if (stack_size == 0) {
      stack_size = os::Linux::default_stack_size(thr_type);

      switch (thr_type) {
      case os::java_thread:
        // Java threads use ThreadStackSize which default value can be
        // changed with the flag -Xss
        assert (JavaThread::stack_size_at_create() > 0, "this should be set");
        stack_size = JavaThread::stack_size_at_create();
        break;
      case os::compiler_thread:
        if (CompilerThreadStackSize > 0) {
          stack_size = (size_t)(CompilerThreadStackSize * K);
          break;
        } // else fall through:
          // use VMThreadStackSize if CompilerThreadStackSize is not defined
      case os::vm_thread:
      case os::pgc_thread:
      case os::cgc_thread:
      case os::watcher_thread:
        if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);
        break;
      }
    }

    stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);
    pthread_attr_setstacksize(&attr, stack_size);
  } else {
    // let pthread_create() pick the default value.
  }

  ...

    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

    pthread_attr_destroy(&attr);

    ...
}

可以看到,在这个方法中,注释说明了,java的线程大小,默认是ThreadStatckSzie.可以通过 flag -Xs来设置更改。

这个默认的大小,是有一个常量来定义的:

const int os::Linux::_vm_default_page_size = (8 * K);

也就是默认8K

所以我们可以默认为如果虚拟机栈的默认大小是8K,所以高速缓冲区的默认大小也是8K。

注意看,这个方法里后面调用了pthread_create方法。这个方法就是linux和unit用来创建线程的方法,所以安卓里创建线程,最终的本质还是属于linux系统的线程。

接下来我们看看nativeCreate()方法,我们追踪下。在java_lang_Thread.cc文件里:

static JNINativeMethod gMethods[] = {
  FAST_NATIVE_METHOD(Thread, currentThread, "()Ljava/lang/Thread;"),
  FAST_NATIVE_METHOD(Thread, interrupted, "()Z"),
  FAST_NATIVE_METHOD(Thread, isInterrupted, "()Z"),
  NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),
  NATIVE_METHOD(Thread, nativeGetStatus, "(Z)I"),
  NATIVE_METHOD(Thread, nativeHoldsLock, "(Ljava/lang/Object;)Z"),
  FAST_NATIVE_METHOD(Thread, nativeInterrupt, "()V"),
  NATIVE_METHOD(Thread, nativeSetName, "(Ljava/lang/String;)V"),
  NATIVE_METHOD(Thread, nativeSetPriority, "(I)V"),
  FAST_NATIVE_METHOD(Thread, sleep, "(Ljava/lang/Object;JI)V"),
  NATIVE_METHOD(Thread, yield, "()V"),
};

注意这行:

  NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"), navtiveCreate被映射成java_lang_thread.cc文件里的Thread_nativeCreate函数:

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
                                jboolean daemon) {
  // There are sections in the zygote that forbid thread creation.
  Runtime* runtime = Runtime::Current();
  if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
    jclass internal_error = env->FindClass("java/lang/InternalError");
    CHECK(internal_error != nullptr);
    env->ThrowNew(internal_error, "Cannot create threads in zygote");
    return;
  }

  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

最后是调用Thread::CreateNativeThread函数,这个函数在Thread.cc文件里:

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->self;

  ...

  Thread* child_thread = new Thread(is_daemon);
  // Use global JNI ref to hold peer live while child thread starts.
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);

  ...

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
    CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
                       "PTHREAD_CREATE_DETACHED");
    CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
    pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);
    CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

    if (pthread_create_result == 0) {
      // pthread_create started the new thread. The child is now responsible for managing the
      // JNIEnvExt we created.
      // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
      //       between the threads.
      child_jni_env_ext.release();
      return;
    }
  }

里面有这句:

stack_size = FixStackSize(stack_size);

这个获取栈大小,我们来看下这个FixStackSize方法:

static size_t FixStackSize(size_t stack_size) {
  // A stack size of zero means "use the default".
  if (stack_size == 0) {
    stack_size = Runtime::Current()->GetDefaultStackSize();
  }

  // Dalvik used the bionic pthread default stack size for native threads,
  // so include that here to support apps that expect large native stacks.
  stack_size += 1 * MB;

  ...

  return stack_size;
}

这里可以看到,默认大小是1M,比之前大很多。

我们再回头看上面那个Thread::CreateNativeThread方法,这句代码:

pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);

这里,最终,又调用了pthread_create方法。也是LINUX的线程创建机制。

接下来我们看看synchrozie的原理(这点很重要):

我们来模拟下两个线程同时对a进行自增的情况:

package com.example.myapplication.Thread;

public class LockRunnable implements Runnable{
    private static int a = 0;
    @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            a ++;
        }
    }

    public static void main(String[] args) {
        LockRunnable lockRunnable = new LockRunnable();
        Thread thread1 = new Thread(lockRunnable);
        Thread thread2 = new Thread(lockRunnable);
        thread1.start();
        thread2.start();
        try{
            thread2.join();
            thread1.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(a);
    }
}

大家猜下结果是什么:

 并不是20000,而是118161.

这个就是 线程不安全问题。什么原因呢?我们来分析下:

其实就是两个线程没有同步。就是线程2没有等待线程1把run方法执行完,中途加进去执行。如果线程1,在对a进行一次自增后,还没有将增加后的值,写到方法区,线程2从方法区里拿到的值,还是自增前的。比如之前值是1,线程1,自增后,变成2.但是没有更新到方法区。线程2拿到的还是1.线程2给他加1,变成2.然后写到方法区。a的值变成2.然后线程1把刚刚自增后的值2,写到方法区,a的值仍然是2.这里。a自增了2次,却,只从1变成了2.

这个时候我们加锁试下:

  @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            add();
        }
    }

    private synchronized static void add(){
        a ++;
    }

来看下结果:

 注意,这里锁的是调用该方法的对象。锁同一个对象才有作用。

分析下synchronized关键字,如果大家对一个方法加锁的话,最终编译的字节码文件,可以看出了一些指令:

monitor-enter v1 和 monitor -exit v1。

再看下关于虚拟机里关于这个monitor的函数:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

可以看到,意思就是UseBiasedLocking是否是偏向锁,如果是的话,则执行快速得到锁的逻辑。

我们知道,如果我们用sychronized关键字修饰一个方法,线程去调用该方法的时候,就会获取锁对象,然后执行,其他线程则要等该线程执行完才能去竞争这个锁对象。如果这个逻辑非常耗时,那么执行效率就会变得很低。所以为了优化这个情况,会定义个Object对象,然后锁上需要同步的代码:

  private Object lockObject = new Object();
    @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            add();
        }
    }

    private  void add(){
        //。。。。其他耗时代码 开始
        //。。。。其他耗时代码 结束
        synchronized (lockObject){
            //需要同步的代码
            a ++;
        }
    }

这样的话,效率高一些。既然可以锁住Object,那么就可以锁住任何对象。我们来弄清楚,一个对象的锁状态信息是如何记录的:

因为对象是在堆内存区的。所以我们可以知道对象的内存结构:

 对象的锁状态信息就是记录在对象头里的。

对象的对象头记录的信息比较多,除了锁,还记录了垃圾回收机制的信息。比如是老年代,还是年轻代,还有hashcode等。记录锁状态是根据系统来决定用32位还是64位决定的:

 总的来说,就是这里有锁标志位,当两三个线程竞争的时候,是轻量级锁,偏向锁那里记录了锁的ID,EPOCH是一个时间戳,判断是否超时。超时的话就变成无锁状态。没超时表示扔在抢占。多的话就变成重量级。

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

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

相关文章

广播机制-案例

广播机制-案例 1.静态注册案例-接收开机广播 1.案例&#xff1a;接收开机的广播 创建自定义的BroadcastReceiver用于处理监听到的系统广播。//接收系统开机的广播事件 public class BootCompleteReceiver extends BroadcastReceiver {Overridepublic void onReceive(Context co…

2023跨境出海指南:马来西亚网红营销白皮书

当前的东南亚市场可谓是是企业出海的大热门&#xff0c;马来西亚作为东南亚地区的第三大经济体&#xff0c;其发展形势也是一片大好。疫情出现后&#xff0c;马来西亚的娱乐和消费转移到线上&#xff0c;对社媒的依赖也催发了网红经济的发展。本文Nox聚星就和大家探讨一下&…

海康威视人脸识别设备对接(一)环境搭建

需要对接海康威视人脸识别设备&#xff0c;这里选择明眸门禁&#xff0c;还有其他的没研究过 打开海康威视开放平台 https://open.hikvision.com/ 选择开放体系&#xff0c;一直选择到设备集成SDK 这里我选择设备网络SDK 点击查看详情 选择对应的版本&#xff0c;我用笔记…

AD入门学习—原理图的绘制3

目录 2.4 CAN&24C02及DS18B20温度传感单元的绘制 2.5 USB单元的绘制 2.6 SD卡及TFT单元的绘制 2.7 NRF24L01单元的绘制 2.8 COM口及PS/2接口的绘制 2.9 DCDC电源输入单元的绘制 2.10 原理图的统一编号及编译检查 学习目录 2.4 CAN&24C02及DS18B20温度传感单元的绘制…

Vue基本指令

1、前端技术的发展&#xff08;html、CSS、JavaScript&#xff09; ​ &#xff08;1&#xff09;jQuery&#xff1a;是对JavaScript进行了封装&#xff0c;使得操作DOM、事件处理、动画处理、ajax交互变得非常简洁、方便。是JavaScript的 库。 ​ &#xff08;2&#xff09…

Moonbeam与Wormhole的Relayer Engine之间的跨链互连合约

如果您不了解Moonbeam&#xff0c;用一句话简单概括来说Moonbeam是跨链通信的中心枢纽。像Axelar、LayerZero和Hyperlane等的协议允许不同EVM上的智能合约互相通信&#xff0c;为Web3 dApp解锁功能方面前所未见的规模。但就目前来说&#xff0c;上述的几个协议的智能合约通信仅…

计算机网络——数据报与虚电路

简介 本篇接上一篇数据交换的内容继续 分组交换其实包含数据报交换和虚电路交换 数据报方式&#xff1a;为网咯层提供外连接服务 虚电报&#xff1a;为网络层提供连接服务 无连接服务&#xff1a;不事先为分组传输确定传输的路径&#xff0c;每个分组独立确定传输路径&#x…

如何实现带动画的动态面包屑,来看看?

大家好&#xff0c;我是派大星&#xff0c;最近在自己手动搭建一个后台管理平台&#xff0c;将其命名为 “雷达行动 Radar-Solution” &#xff0c;在开发的过程中对比了一下其他已经成型的后台解决方案&#xff0c;发现都存在一个共性&#xff0c;就是在Layout的头部都有一个面…

分布式理论之分布式事务

写在前面 我们知道&#xff0c;像MySQL的InnoDB存储引擎提供了事务的能力&#xff0c;严格遵守AICD的事务要求&#xff0c;但是在分布式环境中&#xff0c;一个请求会在多个服务实例存在多个事务&#xff0c;如购物&#xff0c;会有订单系统&#xff0c;支付系统&#xff0c;物…

springboot够用就好系列-1.自定义LengthJudge注解校验字段长度

类似NonNull注解标注在参数之上&#xff0c;表示对应的值不可以为空&#xff0c;利用java的元注解及注解处理器实现检查属性长度的功能。 目录 程序效果 实现过程 样例代码 参考资料 程序效果 截图1.用户名超长提示 检查登录时“用户名”、“密码”字段的长度&#xff0c;此…

安全智能分析 环境迁移

环境迁移 Platfor m Ops for AI 作为整合了 DataOps、MLOps、ModelOps 的复杂技术平台&#xff0c;在项目开发时仅使用一套系统无法支撑平台的稳定搭建&#xff0c;往往需要开发系统、集成测试系统、正式 环境系统在项目生命周期 中协作配合。将表、索引、并发程序、配置内容等…

新手想做短视频可以选择什么领域,这三个可以无脑尝试

大家好&#xff0c;我是蝶衣王的小编 对于小白来说&#xff0c;如果你想通过短视频来赚钱&#xff0c;你不能在流行的领域去做。因为坑不是你能接受的&#xff0c;而且有太多的同行&#xff0c;你的竞争优势没法显现出来。下面分享一下新手适合做的短视频领域​。 一、盘点类型…

海思嵌入式开发-001-基于Ubuntu20.04搭建开发环境

海思嵌入式开发-001-基于Ubuntu20.04搭建开发环境一、虚拟机安装ubuntu20.041、安装虚拟机VMware2、基于虚拟机安装ubuntu20.04二、开发环境配置1、参考资料2、问题汇总一、虚拟机安装ubuntu20.04 1、安装虚拟机VMware 主机配置为Windows 10系统&#xff0c;CPU为i7-8550U 4核…

虹科新闻|ATTO 宣布支持 Apple 最新操作系统 macOS® 13 Ventura

一、即时发布 近期&#xff0c;虹科的合作伙伴ATTO公司宣布支持Apple最新操作系统macOS13 Ventura&#xff0c;所有HK-ATTO适配器、软件和实用程序都已经过新操作系统的测试和验证。 ATTO 34年来为数据密集型计算环境提供网络、存储连接和基础架构解决方案的全球领导者&#…

一文读懂什么是低代码开发?

世界在应用程序上运行&#xff0c;商业世界也不例外。面对变化&#xff0c;企业过去依赖的传统应用程序开发流程可能不再有效。从头开始构建软件解决方案需要花费数月甚至数年的时间来规划、设计、测试和部署。当您的组织需要快速解决方案时&#xff0c;等待负担过重的开发人员…

真实世界的人工智能应用落地——OpenAI篇 ⛵

&#x1f4a1; 作者&#xff1a;韩信子ShowMeAI &#x1f4d8; 深度学习实战系列&#xff1a;https://www.showmeai.tech/tutorials/42 &#x1f4d8; 本文地址&#xff1a;https://www.showmeai.tech/article-detail/414 &#x1f4e2; 声明&#xff1a;版权所有&#xff0c;转…

CVE-2018-1273漏洞复现

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是CVE-2018-1273漏洞复现。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授…

Web3中文|未来的工作模式:VR头显、元宇宙和供应商协作

根据Meta最近发布的一份关于未来工作模式的报告&#xff0c;大约三分之一的美英劳动力在进行远程办公&#xff0c;因此企业必须重新定义工作方式和管理机制&#xff0c;并探索元宇宙和虚拟现实等创新领域。 报告称&#xff1a;“这将推动企业寻求更具创造性和创新性的方法来凝…

canal中间件集成springboot实战落地

目录 一、数据库开启相关权限功能&#xff1a; 二、canal 服务端配置启动&#xff1a;从官网下载程序和源码到本地环境 三、canal客户端配置启动&#xff1a; canal中间件集成springboot实战落地开始分享&#xff0c;这是目前互联网很常见的中间件&#xff0c;监听数据库变化…

Harbor镜像仓库的安装以及Docker从Harbor上传与下载镜像

Harbor镜像仓库的安装与使用 简介&#xff1a;Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器&#xff0c;除了Harbor这个私有镜像仓库外&#xff0c;还有Docker官方提供的Registry。相对Registry&#xff0c;Harbor具有很多优势,本文主要介绍Harbor镜像仓库的安…