2023年3月

平台化与中台设计:新问题与解决方案

在从信息化到数字化的转型中,平台化一直是重要的主题之一。随着平台不断向业务领域延伸,平台抽象和建设的难度也呈指数级增加,出现了一系列新问题。解决这些问题的思考和探索不仅赋予了业务平台化趋势新的内涵和意义,也成为我们设计和发布新的企业架构框架的起点。这些问题的重点在于“如何”解决,而不再是简单的“做什么”,因此我们逐一总结和简述这些问题。

当平台化不断向业务领域延伸时,中台的设计也愈发重要。中台作为业务和技术的中介,需要支持不同业务的快速接入和灵活扩展,同时保证各业务之间的协同和整体性能。在这个过程中,中台也遇到了一系列新问题,例如如何平衡业务的多样性和中台的标准化,如何在复杂的业务场景下保证中台的
稳定性

可扩展性
等等。解决这些问题需要深入思考并寻找合适的解决方案,以确保中台设计和实现的有效性和可持续性。

设计通用流程和可变点的方法

当设计一个通用流程时,需要考虑将业务过程中的共性和差异性抽象出来,形成一个可复用的通用流程。通用流程中的各个环节需要考虑到可扩展性,通过定义接口或者SPI,方便新业务的接入和扩展。同时,也需要考虑到不同业务在使用通用流程时可能有自己的特殊需求,因此需要设计可变点来允许业务自定义流程的某些环节,从而满足业务的差异性需求。

在设计通用流程时,可能需要考虑几个点:

  • 定义业务流程和环节:通过对不同业务的业务流程进行归纳和总结,定义通用的业务流程和环节。

  • 抽象共性和差异性:根据业务流程和环节,抽象出共性和差异性的部分。共性的部分可以抽象成通用的接口或者SPI,差异性的部分可以抽象成可变点。

  • 设计接口和SPI:根据共性的部分,设计通用的接口或者SPI,方便新业务的接入和扩展。

  • 设计可变点:根据差异性的部分,设计可变点,允许业务自定义流程的某些环节。

  • 实现通用流程:将抽象出来的通用流程实现成具体的业务流程。

在设计可变点时,需要注意以下几点:

  • 可变点的位置:可变点的位置应该在业务流程的重要环节上,这样才能满足业务的差异性需求。

  • 可变点的扩展性:可变点需要考虑到扩展性,方便后续的需求变更和业务扩展。

  • 可变点的兼容性:可变点需要考虑到与已有业务的兼容性,避免影响已有业务的正常运行。

  • 可变点的易用性:可变点需要易于使用和配置,方便业务使用和管理。

通过设计通用流程和可变点,可以提高业务的复用性和扩展性,减少业务开发和维护的工作量,提高业务开发和上线的效率。同时,也可以帮助企业构建一个统一的业务架构,方便业务管理和维护。

如何实现多业务线共享解决方案和能力的集中管控与演进?

在现今商业环境下,企业的业务发展和IT建设已经变得密不可分。然而,当企业的业务范围扩展到足够广泛的程度时,IT建设也会随之分化,难以实现统一管控。这种分化可能会导致重复投资和多重投资的浪费,也会导致客户体验、数据共享和IT系统更新周期等方面的问题。例如,对于交易中台而言,如果上面有微商城、门店、美业、教育等行业,还要支持更多新兴的行业,那么这些业务线之间的差异性可能会导致IT建设的分化,给公司带来管理上的挑战。因此,为了避免这种情况,需要寻找一种方法来抽象和提炼可复用的业务模式和能力,以便在新的业务场景中快速复用和组装。

在这种情况下,如何实现多业务线共享解决方案和能力的集中管控与演进?这是一项重要的任务,需要解决以下问题:

  • 针对不同的业务深度,如何设计“模式”与“能力”模型,以对业务进行合理的抽象,进而识别相似度,抽象与提炼可复用的业务模式;而针对不同业务的差异性,如何在“模式”和“能力”基础上进行扩展?

  • 抽象并沉淀了业务能力之后,如何在新的业务场景中,识别、复用已有能力,应用、数据、技术及组织应该如何予以支撑?

为了解决这些问题,需要深入思考和探索,寻找合适的解决方案。同时,也需要参考实践和参考模型,以确保实现的有效性和可持续性。

企业能力共享复用机制


(图片来自ThoughtWork现代化企业架构白皮书)

基础能力
:是对领域对象的原子操作,完成一个领域对象上单一且完整的职责。比如:创建售后单、修改商品库存量等,是能力组合和复用的最小单元
能力组件
:能力组件是对基础能力的进一步封装,目的是方便业务的使用。按封装粒度不同分为两类:第一类能力组件是根据业务服务的需要编排封装的一组关联的基础能力,从而提供完整的服务。比如:
订单创建能力组件。第二类能力组件是平台针对一系列紧密关联的业务活动,设计的能力模板,可基于该模板快速定制某个具体业务的特定流程和能力,从而达到复用全部关联能力的目的。比如:“组合支付”、“快速建站”等能力组件。能力组件加快了业务接入平台的速度,让业务侧专注业务本身,不再需要耗费精力在理解平台大量的基础能力上。
解决方案
:是平台针对一类共性业务的端到端过程设计的能力模板;可基于该模板快速定制某个具体业务的特定能力和流程,从而达到业务模式级别复用的目的。比如:虚拟物品交易解决方案。

作者:小牛呼噜噜 |
https://xiaoniuhululu.com
计算机内功、JAVA底层、面试、职业成长相关资料等更多精彩文章在公众号「
小牛呼噜噜

大家好,我是呼噜噜,最近一直在梳理Java并发,但内容杂且偏晦涩,今天我们一起来聊聊Java 线程的状态及转换 先来夯实一下基础,万丈高楼平地起,路还是得慢慢走。

Java线程的生命周期

我们先来看下Java线程的生命周期图:

上图也是本文的大纲,我们下面依次聊聊java各个线程状态及其他们的转换。

线程初始状态

线程初始状态(NEW): 当前线程处于
线程被创建出来但没有被调用start()

在Java线程的时间中,关于线程的一切的起点是从Thread 类的对象的创建开始,一般实现Runnable接口 或者 继承Thread类的类,实例化一个对象出来,线程就进入了初始状态

Thread thread = new Thread()

由于线程在我们操作系统中也是非常宝贵的资源,在实际开发中,我们常常用
线程池
来重复利用现有的线程来执行任务,避免多次创建和销毁线程,从而降低创建和销毁线程过程中的代价。Java 给我们提供了 Executor 接口来使用线程池,查看其
JDK1.8源码
,发现其内部封装了
Thread t = new Thread()

public class Executors {
    ...
  static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        ...

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
    ...
}

在thread类源码中,我们还能发现线程状态的枚举类
State

    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

所谓
线程的状态
,在java源码中都是通过
threadStatus
的值来表示的

   /* Java thread status for tools,
     * initialized to indicate thread 'not yet started'
     */

    private volatile int threadStatus = 0;

State

threadStatus
通过
toThreadState
方法映射转换

    public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }

//--- --- ---

    public static State toThreadState(int var0) {
        if ((var0 & 4) != 0) {
            return State.RUNNABLE;
        } else if ((var0 & 1024) != 0) {
            return State.BLOCKED;
        } else if ((var0 & 16) != 0) {
            return State.WAITING;
        } else if ((var0 & 32) != 0) {
            return State.TIMED_WAITING;
        } else if ((var0 & 2) != 0) {
            return State.TERMINATED;
        } else {
            return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
        }
    }

到这里我们就可以发现,
Thread t = new Thread()
在Java中只是设置了线程的状态,操作系统中并没有的实际线程的创建

线程运行状态

线程运行状态(RUNNABLE),线程被调用了
start()
等待运行的状态

在Linux操作系统层面,包含
Running

Ready
状态。其中Ready状态是等待 CPU 时间片。现今主流的JVM,比如hotspot虚拟机都是把Java 线程,映射到操作系统OS底层的线程上,把调度委托给了操作系统。而操作系统比如Linux,它是多任务操作系统,充分利用CPU的高性能,
将CPU的时间分片
,让单个CPU实现"同时执行"多任务的效果。

更多精彩文章在公众号「
小牛呼噜噜

Linux的任务调度又采用
抢占式轮转调度,
我们不考虑特权进程的话

OS会选择在CPU上占用的时间最少进程,优先在cpu上分配资源,其对应的线程去执行任务,尽可能地维护任务调度公平。
Running

Ready
状态的线程在CPU中切换状态非常短暂。大概只有 0.01 秒这一量级,区分开来意义不大,java将这2个状态统一用
RUNNABLE
来表示

thread.start()源码解析

我们接下来看看为什么说执行
thread.start()
后,线程的才"真正的创建"

public class ThreadTest {
    /**
     * 继承Thread类
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is child thread");
        }
    }
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

其中
thread.start()
方法的源码中,会去调用
start0()
方法,而
start0()

private native void start0();
JVM调用Native方法的话,会进入到不受JVM控制的世界里


Thread类
实例化的同时,会首先调用
registerNatives
方法,注册本地Native方法,动态绑定JVM方法

private static native void registerNatives();
    static {
        registerNatives();
    }


Thread
类中通过
registerNatives
将指定的本地方法绑定到指定函数,比如
start0
本地方法绑定到
JVM_StartThread
函数:

...
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    ...

源码见:
http://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/native/java/lang/Thread.c

JVM_StartThread
是JVM层函数,抛去各种情况的处理,主要是通过
new JavaThread(&thread_entry, sz)
来创建
JVM线程对象

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

	//表示是否有异常,当抛出异常时需要获取Heap_lock。
  bool throw_illegal_thread_state = false;

  // 在发布jvmti事件之前,必须释放Threads_lock
  // in Thread::start.
  {
    // 获取 Threads_lock锁
    MutexLocker mu(Threads_lock);


    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));
      
        // 创建JVM线程(用JavaThread对象表示)
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);
      ...
    }
  }

  ...

  Thread::start(native_thread);//启动内核线程

JVM_END

源码见:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/prims/jvm.cpp

我们再来看看
JavaThread
的实现,发现内部通过
os::create_thread(this, thr_type, stack_sz);
来调用不同操作系统的创建线程方法创建线程。

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);//调用不同操作系统的创建线程方法创建线程

}

源码见:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/thread.cpp

我们都知道Java是跨平台的,但是native各种方法底层c/c++代码对各平台都需要有对应的兼容,我们这边以linux为例,其他平台就大家自行去查阅了

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

  // Allocate the OSThread object
  OSThread* osthread = new OSThread(NULL, NULL);
  if (osthread == NULL) {
    return false;
  }

  // set the correct thread state
  osthread->set_thread_type(thr_type);

  // Initial state is ALLOCATED but not INITIALIZED
  osthread->set_state(ALLOCATED);

  thread->set_osthread(osthread);

  // init thread attributes
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

  // 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.
  }

  // glibc guard page
  pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));

  ThreadState state;

  {
    // Serialize thread creation if we are running with fixed stack LinuxThreads
    bool lock = os::Linux::is_LinuxThreads() && !os::Linux::is_floating_stack();
    if (lock) {
      os::Linux::createThread_lock()->lock_without_safepoint_check();
    }

    pthread_t tid;
      //通过pthread_create方法创建内核级线程 !
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

    pthread_attr_destroy(&attr);

    if (ret != 0) {
      if (PrintMiscellaneous && (Verbose || WizardMode)) {
        perror("pthread_create()");
      }
      // Need to clean up stuff we've allocated so far
      thread->set_osthread(NULL);
      delete osthread;
      if (lock) os::Linux::createThread_lock()->unlock();
      return false;
    }

    // Store pthread info into the OSThread
    osthread->set_pthread_id(tid);

    // Wait until child thread is either initialized or aborted
    {
      Monitor* sync_with_child = osthread->startThread_lock();
      MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
      while ((state = osthread->get_state()) == ALLOCATED) {
        sync_with_child->wait(Mutex::_no_safepoint_check_flag);
      }
    }

    if (lock) {
      os::Linux::createThread_lock()->unlock();
    }
  }

  // Aborted due to thread limit being reached
  if (state == ZOMBIE) {
      thread->set_osthread(NULL);
      delete osthread;
      return false;
  }

  // The thread is returned suspended (in state INITIALIZED),
  // and is started higher up in the call chain
  assert(state == INITIALIZED, "race condition");
  return true;
}

源码见:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/os/linux/vm/os_linux.cpp

主要通过
pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread)
,它是unix 创建线程的方法,linux也继承了。调用后在linux系统中会创建一个内核级的线程。
也就是说这个时候操作系统中线程才真正地诞生

更多精彩文章在公众号「
小牛呼噜噜

但此时线程才诞生,那是怎么启动的?我们回到
JVM_StartThread
源码中,
Thread::start(native_thread)
很明显这行代码就表示启动
native_thread = new JavaThread(&thread_entry, sz)
创建的线程,我们来继续看看其源码

void Thread::start(Thread* thread) {
  trace("start", thread);
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
      // 设置线程状态
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

源码:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/thread.cpp

os::start_thread
它封装了
pd_start_thread(thread)
,执行该方法,操作系统会去启动指定的线程

void os::start_thread(Thread* thread) {
  // guard suspend/resume
  MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE);
  pd_start_thread(thread);
}

当操作系统的线程启动完之后,我们再回到
pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread)
,会去
java_start
这个线程入口函数进行OS内核级线程的初始化,并开始启动
JavaThread

// Thread start routine for all newly created threads
static void *java_start(Thread *thread) {
  // Try to randomize the cache line index of hot stack frames.
  // This helps when threads of the same stack traces evict each other's
  // cache lines. The threads can be either from the same JVM instance, or
  // from different JVM instances. The benefit is especially true for
  // processors with hyperthreading technology.
  static int counter = 0;
  int pid = os::current_process_id();
  alloca(((pid ^ counter++) & 7) * 128);

  ThreadLocalStorage::set_thread(thread);

  OSThread* osthread = thread->osthread();
  Monitor* sync = osthread->startThread_lock();

  // non floating stack LinuxThreads needs extra check, see above
  if (!_thread_safety_check(thread)) {
    // notify parent thread
    MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);
    osthread->set_state(ZOMBIE);
    sync->notify_all();
    return NULL;
  }

  // thread_id is kernel thread id (similar to Solaris LWP id)
  osthread->set_thread_id(os::Linux::gettid());

  if (UseNUMA) {
    int lgrp_id = os::numa_get_group_id();
    if (lgrp_id != -1) {
      thread->set_lgrp_id(lgrp_id);
    }
  }
  // initialize signal mask for this thread
  os::Linux::hotspot_sigmask(thread);

  // initialize floating point control register
  os::Linux::init_thread_fpu_state();

  // handshaking with parent thread
  {
    MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);

    // notify parent thread
    osthread->set_state(INITIALIZED);
    sync->notify_all();

    // 等待,直到操作系统级线程全部启动
    while (osthread->get_state() == INITIALIZED) {
      sync->wait(Mutex::_no_safepoint_check_flag);
    }
  }

  // 开始运行JavaThread::run
  thread->run();

  return 0;
}

源码:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/os/linux/vm/os_linux.cpp

thread->run()
其实就是
JavaThread::run()
也表明方法开始回调,从OS层方法回到JVM层方法
,我们再来看下其实现:

// The first routine called by a new Java thread
void JavaThread::run() {
  // initialize thread-local alloc buffer related fields
  this->initialize_tlab();

  // used to test validitity of stack trace backs
  this->record_base_of_stack_pointer();

  // Record real stack base and size.
  this->record_stack_base_and_size();

  // Initialize thread local storage; set before calling MutexLocker
  this->initialize_thread_local_storage();

  this->create_stack_guard_pages();

  this->cache_global_variables();

  // Thread is now sufficient initialized to be handled by the safepoint code as being
  // in the VM. Change thread state from _thread_new to _thread_in_vm
  ThreadStateTransition::transition_and_fence(this, _thread_new, _thread_in_vm);

  assert(JavaThread::current() == this, "sanity check");
  assert(!Thread::current()->owns_locks(), "sanity check");

  DTRACE_THREAD_PROBE(start, this);

  // This operation might block. We call that after all safepoint checks for a new thread has
  // been completed.
  this->set_active_handles(JNIHandleBlock::allocate_block());

  if (JvmtiExport::should_post_thread_life()) {
    JvmtiExport::post_thread_start(this);
  }

  JFR_ONLY(Jfr::on_thread_start(this);)

  // We call another function to do the rest so we are sure that the stack addresses used
  // from there will be lower than the stack base just computed
  thread_main_inner();//!!!注意此处方法

  // Note, thread is no longer valid at this point!
}

void JavaThread::thread_main_inner() {
  assert(JavaThread::current() == this, "sanity check");
  assert(this->threadObj() != NULL, "just checking");

  // Execute thread entry point unless this thread has a pending exception
  // or has been stopped before starting.
  // Note: Due to JVM_StopThread we can have pending exceptions already!
  if (!this->has_pending_exception() &&
      !java_lang_Thread::is_stillborn(this->threadObj())) {
    {
      ResourceMark rm(this);
      this->set_native_thread_name(this->get_thread_name());
    }
    HandleMark hm(this);
    this->entry_point()(this, this);//JavaThread对象中传入的entry_point为Thread对象的Thread::run方法
  }

  DTRACE_THREAD_PROBE(stop, this);

  this->exit(false);
  delete this;
}


源码:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/thread.cpp

由于
JavaThread
定义可知
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz)
中参数
entry_point
是外部传入,那我们想想
JavaThread
是什么时候实例化的?

没错,就是我们一开始的
JVM_StartThread

native_thread = new JavaThread(&thread_entry, sz);
也就是说
this->entry_point()(this, this)
实际上是回调的
thread_entry
方法

thread_entry
源码:

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          vmSymbols::run_method_name(),
                          vmSymbols::void_method_signature(),
                          THREAD);
}

源码:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/prims/jvm.cpp
通过
JavaCalls::call_virtual
方法,又从JVM层 回到了Java语言层 ,即
MyThread thread = new MyThread(); thread.start();

一切又回到了起点,这就是Java
thread.start()
内部完整的一个流程,
HotSpot虚拟机
实现的Java线程其实是对Linux内核级线程的直接映射,将Java涉及到的
所有线程调度、内存分配都交由操作系统进行管理

线程终止状态

线程终止状态(TERMINATED),表示该线程已经运行完毕。

当一个线程执行完毕,或者主线程的main()方法完成时,我们就认为它终止了。终止的线程无法在被使用,如果调用
start()
方法,会抛出
java.lang.IllegalThreadStateException
异常,这一点我们可以从start源码中很容易地得到

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    ...
}

线程阻塞状态

线程阻塞状态(BLOCKED),需要
等待锁释放
或者说
获取锁失败
时,线程阻塞

public class BlockedThread implements Runnable {
    @Override
    public void run() {
        synchronized (BlockedThread.class){
            while (true){
                
            }
        }
    }
}

从Thread源码的注释中,我们可以知道
等待锁释放
或者说
获取锁失败
,主要有下面3中情况:

  1. 进入 synchronized 方法时
  2. 进入 synchronized 块时
  3. 调用 wait 后, 重新进入 synchronized 方法/块时

其中第三种情况,大家可以先思考一下,我们留在下文
线程等待状态
再详细展开

线程等待状态

线程等待状态(WAITING),表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

wait/notify/notifyAll

我们紧接着上一小节,调用
wait 后, 重新进入synchronized 方法/块时
,我们来看看期间发生了什么?


线程1
调用对象A的
wait
方法后,会释放当前的锁,然后让出CPU时间片,线程会进入该对象的
等待队列中
,线程状态变为
等待状态WAITING

当另一个
线程2
调用了对象A的
notify()/notifyAll()
方法

notify()方法只会唤醒沉睡的线程,不会立即释放之前占有的对象A的锁,必须执行完notify()方法所在的synchronized代码块后才释放。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区

线程1
收到通知后退出等待队列,并进入
线程运行状态RUNNABLE
,等待 CPU 时间片分配, 进而执行后续操作,接着
线程1
重新进入 synchronized 方法/块时,竞争不到锁,线程状态变为
线程阻塞状态BLOCKED
。如果竞争到锁,就直接接着运行。线程等待状态 切换到线程阻塞状态,无法直接切换,需要经过线程运行状态。

我们再来看一个例子,巩固巩固:

public class WaitNotifyTest {
    public static void main(String[] args) {
        Object A = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1等待获取 对象A的锁...");
                synchronized (A) {
                    try {
                        System.out.println("线程1获取了 对象A的锁");
                        Thread.sleep(3000);
                        System.out.println("线程1开始运行wait()方法进行等待,进入到等待队列......");
                        A.wait();
                        System.out.println("线程1等待结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2等待获取 对象A的锁...");
                synchronized (A) {
                    System.out.println("线程2获取了 对象A的锁");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2将要运行notify()方法进行唤醒线程1");
                    A.notify();
                }
            }
        }).start();
    }
}

结果:

线程1等待获取 对象A的锁...
线程1获取了 对象A的锁
线程2等待获取 对象A的锁...
线程1开始运行wait()方法进行等待,进入到等待队列......
线程2获取了 对象A的锁
线程2将要运行notify()方法进行唤醒线程1
线程1等待结束

需要注意的是,
wait/notify/notifyAll 只能在synchronized修饰的方法、块中使用

notify 是只随机唤醒一个线程,而 notifyAll 是唤醒所有等待队列中的线程

join

Thread类中的join方法的主要作用
能让线程之间的并行执行变为串行执行
,当前线程等该加入该线程后面,等待该线程终止

public static void main(String[] args) {
  Thread thread = new Thread();
  thread.start();
  thread.join();
  ...
}

上面一个例子表示,程序在main主线程中调用thread线程的join方法,意味着main线程放弃CPU时间片(主线程会变成 WAITING 状态),并返回thread线程,继续执行直到线程thread执行完毕,换句话说
在主线程执行过程中,插入thread线程,还得等thread线程执行完后,才轮到主线程继续执行

如果查看JDK
thread.join()
底层实现,会发现其实内部封装了
wait(),notifyAll()

park/unpark

LockSupport.park() 挂起当前线程;LockSupport.unpark(暂停线程对象) 恢复某个线程

package com.zj.ideaprojects.demo.test3;

import java.util.concurrent.Executors;
import java.util.concurrent.locks.LockSupport;

public class ThreadLockSupportTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("start.....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("park....");
            LockSupport.park();
            System.out.println("resume.....");

        });
        thread.start();
        Thread.sleep(3000);
        System.out.println("unpark....");
        LockSupport.unpark(thread);

    }
}


结果:

start.....
park....
unpark....
resume.....

当程序调用
LockSupport.park()
,会让当前线程A的线程状态会从 RUNNABLE 变成 WAITING,然后main主线程调用
LockSupport.unpark(thread)
,让指定的线程即线程A,从 WAITING 回到 RUNNABLE 。我们可以发现
park/unpark

wait/notify/notifyAll
很像,但是他们有以下的区别:

  1. wait,notify 和 notifyAll 必须事先获取对象锁,而 unpark 不必
  2. park、unpark 可以先 unpark ,而 wait、notify 不能先 notify,必须先wait
  3. unpark 可以精准唤醒某一个确定的线程。而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所以等待线程,就不那么精确

超时等待状态

超时等待状态(TIMED_WAITING),也叫
限期等待
,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

这部分比较简单,它和线程等待状态(WAITING)状态 非常相似,区别就是方法的参数舒服传入限制时间,在
Timed Waiting
状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如
notify

相关方法主要有:

1. Object.wait(long)
2. Thread.join(long) 
3. LockSupport.parkNanos(long)
4. LockSupport.parkUntil(long)
5. Thread.sleep(long)

需要注意的是
Thread.sleep(long)
,当线程执行
sleep
方法时,不会释放当前的锁(如果当前线程进入了同步锁),也不会让出CPU。
sleep(long)
可以用指定时间使它自动唤醒过来,如果时间不到只能调用
interrupt
方法强行打断。

参考资料:

https://hg.openjdk.java.net/jdk8u

《并发编程的艺术》

https://www.jianshu.com/p/216a41352fd8


本篇文章到这里就结束啦,如果我的文章对你有所帮助,还请帮忙一键三连:
点赞、关注、收藏
,你的支持会激励我输出更高质量的文章,感谢!

原文镜像:
原来还能这样看Java线程的状态及转换

计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「
小牛呼噜噜
」,我们下期再见!

1. 抽象方法与虚方法的区别

先说两者最大的区别:抽象方法是需要子类去实现的。虚方法是已经实现了的,可以被子类覆盖,也可以不覆盖,取决于需求。
因为抽象类无法实例化,所以抽象方法没有办法被调用,也就是说抽象方法永远不可能被实现。

如果需要了解虚方法及抽象方法具体的定义和语句,请移步:
C#多态性学习,虚方法、抽象方法、接口等用法详解

我们具体看个例子来帮助理解,首先是
虚方法

public class Shape
{
    public virtual double CalculateArea()
    {
         return 0;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Square : Shape
{
    public double SideLength { get; set; }

    public override double CalculateArea()
    {
        return SideLength * SideLength;
    }
}

在这个例子中,Shape类定义了一个虚方法CalculateArea(),它返回0。Circle和Square类分别继承Shape类并重写了该方法,实现了自己的计算面积的方法。

抽象方法

public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Square : Shape
{
    public double SideLength { get; set; }

    public override double CalculateArea()
    {
        return SideLength * SideLength;
    }
}

在这个例子中,Shape类定义了一个抽象方法CalculateArea(),因为它是一个抽象方法,所以没有提供实现。Circle和Square类继承Shape类并
强制实现了该方法

抽象类是无法被实例化的,但是它可以作为父类被继承。抽象类中可以定义抽象方法,子类必须实现这些抽象方法。这种方式也称为“强制静态绑定”。

总结

虚方法和抽象方法都是实现多态性的方式,但它们有以下几点不同:

1.实现方式不同:虚方法是在父类中声明方法并使用virtual关键字标识,子类可以使用override关键字对该方法进行重写并实现自己的行为;而抽象方法是在父类中声明方法并使用abstract关键字标识,子类必须实现这些抽象方法。
抽象方法只能在抽象类中声明,虚方法不是。如果类包含抽象方法,那么该类也是抽象的,也必须声明类是抽象的。

2.是否需要实现:虚方法可以有默认的实现,而抽象方法必须由子类进行实现。如果一个子类没有实现其父类中定义的所有抽象方法,则子类必须被声明为抽象类。

3.类型限制:使用虚方法实现多态性时,代码会被解释为运行时代码,程序需要查找对象类型以确定调用的方法。使用抽象方法时,代码会被解释为静态代码,编译器会强制实现所有抽象方法。

4.设计用途:虚方法适用于有默认实现的情况,且子类可能需要更改其行为的情况,例如重构代码时需要改变方法的实现。抽象方法适用于接口定义和强制子类实现该类的一些操作的情况。

总的来说,虚方法和抽象方法是实现多态性的两种不同方式,具体使用哪一种方式取决于代码的设计需要。虚方法允许子类通过重写方法实现自己的行为,而抽象方法通常用于定义接口和限制子类的操作。

声明(
叠甲
):鄙人水平有限,本文为作者的学习总结,仅供参考。


1. 搜索介绍

搜索算法包括深度优先搜索(DFS)和广度优先搜索(BFS)这两种,从起点开始,逐渐扩大寻找范围,直到找到需要的答案为止。从时间复杂度来说这与一般的暴力枚举来说没来太大的区别,这样的话我们为什么要使用搜索算法,而不直接使用暴力法呢?首先,搜索算法是暴力法一种优雅的写法,即优雅的暴力,可以为我们的代码减少冗长的嵌套 for 循环。其次搜索通过剪枝操作可以跳过一些无效状态,降低问题规模,从而使效率比直接的枚举所有答案要高。


2. DFS 与 BFS 的区别

类别 DFS BFS
搜索类型 试探搜索 地毯搜索
所用的数据结构 栈(vector也是可以的) 队列
适用的题目 求方案总数 求最短路径
实现方法 一般结合回溯算法一同实现 将可行行方案放入队列,然后一一遍历


3. 举些栗子

3.1 BFS--
马的遍历

题目描述

有一个 $ n * m $ 的棋盘,在某个点 $ (x, y) (x,y) $上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。

这是一道经典 BFS 题,可说使模板题了,在解题前先介绍一下 BFS 的实现思路如下:

【1】 构建对应结构体与队列
【2】 初始化数据和初始点
【3】 根据初始点与遍历关系遍历其它符合要求的点
【4】 查询答案

根据 BFS 的实现思路可以容易的得到该题的代码如下

#include <bits/stdc++.h>
#define N_MAX 400
using namespace std;
int mp[N_MAX][N_MAX]; // mp[i][j] 表示马到(i,j)点所需的最少次数
int n,m,x,y;
// 定义 dx dy 便于运算
int dx[] = {-1,1,2,2,1,-1,-2,-2};
int dy[] = {-2,-2,-1,1,2,2,1,-1};
// [1] 定义数据结构体与duilie
struct point{
    int x,y; // 点的坐标
    int t;   // 马到该点的最少次数
};
queue<point> que;

int main()
{
    // [2] 初始化数据
    memset(mp,-1,sizeof(mp));
    cin >> n >> m >> x >> y;
    mp[x][y] = 0; // 初始点为 0

    // [3] 搜索
    que.push((point){x,y,mp[x][y]}); // 先向队列中压入初始点
    while(!que.empty())
    {
        // 从队列中一个一个的遍历
        point p = que.front();
        que.pop(); // 记得弹出
        // 寻找满足条件的点,并压入队列中
        for(int i = 0;i < 8;i++)
        {
            int nx = p.x + dx[i];
            int ny = p.y + dy[i];
            // 判断是否合法
			if(nx >= 1 && ny >= 1 && nx <= n && ny <= m && mp[nx][ny] == -1)
			{
				mp[nx][ny] = p.t + 1;
            	que.push((point){nx,ny,mp[nx][ny]});
			} 	
        }
    }
    // 输出结果
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {    
            cout << mp[i][j] << " ";
        }
        cout << endl;
    }
        
    return 0;
}

3.2 BFS--
奇怪的电梯

题目描述

呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第
\(i\)
层楼(
\(1 \le i \le N\)
)上有一个数字
\(K_i\)

\(0 \le K_i \le N\)
)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如:
\(3, 3, 1, 2, 5\)
代表了
\(K_i\)

\(K_1=3\)

\(K_2=3\)
,……),从
\(1\)
楼开始。在
\(1\)
楼,按“上”可以到
\(4\)
楼,按“下”是不起作用的,因为没有
\(-2\)
楼。那么,从
\(A\)
楼到
\(B\)
楼至少要按几次按钮呢?

这题也是一道 BFS 的模板题了,算是用于巩固了,具体 AC 代码如下

#include <bits/stdc++.h>
using namespace std;
#define N_MAX 201
struct point{
	int f;  // 所在层数
	int ki; // 拥有的数字
	int t;  // 需要按的次数 
};
queue<point> que;
int ans[N_MAX];
int n,a,b;
int k[N_MAX];

int main()
{
	memset(ans,-1,sizeof(ans));
	cin >> n >> a >> b;
	for(int i = 1;i <= n;i++)
	{
		cin >> k[i];
	}
	ans[a] = 0;
	// bfs
	que.push((point){a,k[a],ans[a]});
	while(!que.empty())
	{
		point p = que.front();
		que.pop();
		int nf = p.f + p.ki; // 上 
		if(nf <= n && ans[nf] == -1)
		{
			ans[nf] = p.t+1;
			que.push((point){nf,k[nf],ans[nf]});	
		}
		nf = p.f - p.ki;    // 下  
		if(nf >= 1 && ans[nf] == -1)
		{
			ans[nf] = p.t+1;
			que.push((point){nf,k[nf],ans[nf]});	
		}		
	}  
	cout << ans[b] << endl;
	return 0;
}

3.4 DFS--
数的组合

题目描述

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

这是一到典型的 DFS 题,DFS 组要就是利用回溯算法进行解决,回溯的具体思路如下,其难点在于确定递归参数的确定

【1】 写递归出口(收果子)
【2】 循环遍历搜索,并进行剪枝优化
【3】 处理节点
【4】 递归
【5】 回溯,即取消处理节点时的朝左
该题代码如下:

class Solution {
public:
    vector<vector<int>> ret; // 用于存储最后的结果
    vector<int> path;       // 用于存储中间的结果
    
    void bnf(int st,int n,int k)
    {
        // 收果子 (中止条件)
        if(path.size() == k)
        {
            ret.push_back(path);
            return;
        }
        // 循环,并进行剪枝优化
        for(int i = st;i <= n - k + path.size() + 1;++i)
        {
            // 处理节点
            path.push_back(i);
            // 递归
            bnf(i+1,n,k);
            // 回溯
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        bnf(1,n,k);
        return ret;
    }
};

4.参考

代码随想录
洛谷搜索算法推荐题库
马的遍历的洛谷题解
本文到此结束,希望对您有所帮助。

//业务需求:我们需要一个微信小程序码,但是是需要提供给别人扫码的但是只有一个纯粹的小程序码是不好看的,所以需要推广的海报图片。再结合文字


最终效果

准备工作  1、需要海报的底图  2、小程序码的图片

代码部分结合YII2但不影响使用

完整过程

第一步:生成小程序码图片

第二步:缩放小程序码的图片大小  (如果尺寸符合海报大小可省略) 280-1280px

第三步:将缩放后的小程序图片合成到背景图片

第四步:合成文字信息

第一步:生成小程序码图片 (我使用的场景是无限制小程序码code地址 三种自行选择)

//微信小程序 小程序码
    public static function getWeChatSmallProgramCode($scene)
{
$AccessToken = self::getAccessToken();$url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" . $AccessToken;$postData =['scene' => $scene, 'page' => 'pages/index/index', 'width'=>930];$postData = json_encode($postData);$contentData = self::sendPost($url, $postData);return $contentData; //如果图片大小符合这开启base64位图片地址也可以完成图片的合并合文字的合并//return self::base64UrlCode($contentData, 'image/png'); }protected static function sendPost($url, $post_data)
{
$options = array('http' => array('method' => 'POST', 'header' => 'Content-type:application/json', //header 需要设置为 JSON 'content' => $post_data, 'timeout' => 60 //超时时间 )
);
$context = stream_context_create($options);return file_get_contents($url, false, $context);
}
//二进制转图片image/png public static function base64UrlCode($contents, $mime)
{
$base64 = base64_encode($contents);return ('data:' . $mime . ';base64,' . $base64);
}

第二步:缩放小程序码的图片大小

 /**
* 缩放图片尺寸
* @param $img_path string 图片地址
* @param $new_width
* @param $new_height
* @param $new_img_path string 新的图片地址
*/ public static function picZoom($img_path,$new_width,$new_height,$new_img_path)
{
//获取尺寸 list($width, $height, $img_type, $attr) = getimagesize($img_path);$imageinfo =['width' => $width, 'height' => $height, 'type' => image_type_to_extension($img_type, false), 'attr' => $attr];$fun = "imagecreatefrom" . $imageinfo['type'];$image = $fun($img_path);//创建新的幕布 $image_thump = imagecreatetruecolor($new_width, $new_height);//复制源文件 imagecopyresampled($image_thump, $image, 0, 0, 0, 0, $new_width, $new_height, $imageinfo['width'], $imageinfo['height']);
imagedestroy(
$image);$image = $image_thump;$func = 'image' . $imageinfo['type'];$func($image, $new_img_path);
}

第三步:将缩放后的小程序图片合成到背景图片

 /**
* 图片合并
* 将源图片覆盖到目标图片上
* @param string $dstPath 目标图片路径 背景图
* @param string $srcPath 源图片路径 内容图
* @param int $dstX 源图片覆盖到目标的X轴坐标
* @param int $dstY 源图片覆盖到目标的Y轴坐标
* @param int $srcX
* @param int $srcY
* @param int $pct 透明度
* @param string $filename 输出的文件名,为空则直接在浏览器上输出显示
* @return string $filename 合并后的文件名
*/ public static function picMerge($dstPath, $srcPath, $dstX = 0, $dstY = 0, $srcX = 0, $srcY = 0, $pct = 100, $filename = '')
{
//创建图片的实例 $dst = imagecreatefromstring(file_get_contents($dstPath));$src = imagecreatefromstring(file_get_contents($srcPath));//获取水印图片的宽高 list($src_w, $src_h) = getimagesize($srcPath);//将水印图片复制到目标图片上,最后个参数50是设置透明度,这里实现半透明效果
// imagecopymerge($dst, $src, 80, 125, 0, 0, $src_w, $src_h, 100);
imagecopymerge($dst, $src, $dstX, $dstY, $srcX, $srcY, $src_w, $src_h, $pct);//如果水印图片本身带透明色,则使用imagecopy方法
//imagecopy($dst, $src, 10, 10, 0, 0, $src_w, $src_h);
//输出图片
list($dst_w, $dst_h, $dst_type) = getimagesize($dstPath);switch ($dst_type) {case 1://GIF if (!$filename) {header('Content-Type: image/gif');
imagegif(
$dst);
}
else{
imagegif(
$dst, $filename);
}
break;case 2://JPG if (!$filename) {header('Content-Type: image/jpeg');
imagejpeg(
$dst);
}
else{
imagejpeg(
$dst, $filename);
}
break;case 3://PNG if (!$filename) {header('Content-Type: image/png');
imagepng(
$dst);
}
else{
imagepng(
$dst, $filename);
}
break;default: break;
}
imagedestroy(
$dst);
imagedestroy(
$src);
}

第四步:合成文字信息

/**
* 添加文字到图片上
* @param $dstPath string 目标图片
* @param $fontPath string 字体路径
* @param $fontSize string 字体大小
* @param $text string 文字内容
* @param $dstY string 文字Y坐标值
* @param string $filename 输出文件名,为空则在浏览器上直接输出显示
* @return string 返回文件名
*/ public static function addFontToPic($dstPath, $fontPath, $fontSize, $text, $dstY, $filename = '')
{
ob_end_clean();//创建图片的实例 $dst = imagecreatefromstring(file_get_contents($dstPath));//打上文字 $fontColor = imagecolorallocate($dst, 255, 255, 255);//字体颜色 $width = imagesx($dst);$height = imagesy($dst);$fontBox = imagettfbbox($fontSize, 0, $fontPath, $text);//文字水平居中实质 imagettftext($dst, $fontSize, 0, ceil(($width - $fontBox[2]) / 2), $dstY, $fontColor, $fontPath, $text);//输出图片 list($dst_w, $dst_h, $dst_type) = getimagesize($dstPath);switch ($dst_type) {case 1://GIF if (!$filename) {header('Content-Type: image/gif');
imagegif(
$dst);
}
else{
imagegif(
$dst, $filename);
}
break;case 2://JPG if (!$filename) {header('Content-Type: image/jpeg');
imagejpeg(
$dst);
}
else{
imagejpeg(
$dst, $filename);
}
break;case 3://PNG if (!$filename) {header('Content-Type: image/png');
imagepng(
$dst);
}
else{
imagepng(
$dst, $filename);
}
break;default: break;
}
imagedestroy(
$dst);return $filename;
}

外部的调用

 /**
* 根据店铺id 和名称 合成A5 图片小程序图片
* @param $shop_id
* @param $shop_name
* @return array
*/ public static function generateWeChatAppletImage($shop_id, $shop_name)
{
//1 生成小程序码
//2 合成小程序码到背景图片
$sceneStr = '?shop_id=' . $shop_id;$weChatAppImgBaseData = WxTools::getWeChatSmallProgramCode($sceneStr);$weChatAppImgPath = './weChatAppImg/shop_code_' . $shop_id . '.jpg';file_put_contents($weChatAppImgPath, $weChatAppImgBaseData);//合并到背景图片中 $beiJinImgPath = './weChatAppImg/weChatBJ.jpg';$mergeImgFile = './weChatAppImg/shop_mini_program' . $shop_id . '.jpg';
GenerateCodeImg
::picMerge($beiJinImgPath, $weChatAppImgPath, 408, 714, $srcX = 0, $srcY = 0, $pct = 100, $mergeImgFile);//3 合成文字 $fontPath = './plus/fonts/SourceHanSansCN-Bold.ttf';$fontSize = 40;$dstY = 640;
GenerateCodeImg
::addFontToPic($mergeImgFile, $fontPath, $fontSize, $shop_name, $dstY, $mergeImgFile);$weChatCodeImgUrL = \Yii::$app->request->hostInfo . '/weChatAppImg/shop_code_' . $shop_id . '.jpg';$weChatAppImgUrl = \Yii::$app->request->hostInfo . '/weChatAppImg/shop_mini_program' . $shop_id . '.jpg';return['weChatCodeImgUrL' => $weChatCodeImgUrL, 'weChatAppImgUrl' => $weChatAppImgUrl,];
}

常见的问题

1文字合并的时候出现乱码?

第一检测一下字体是否是正常tff字体  如果不知道去C://windows/Fonts 随便找一个 微软雅黑都行

2、英文阿拉布数字正常 中文乱码

$text = mb_convert_encoding("呵呵呵","UTF-8","GBK");

$text = mb_convert_encoding("呵呵呵","html-entities","UTF-8");

设置看看