快乐成长


每天进步一点点



把学习变为一种快乐,快乐长能长久,才能坚持,才能降低学习成本,带给您学习乐趣。 每天进步点点, 一直是我们的愿望,欢迎您光临!!

为了给您增加学习乐趣,并且学到有意思,简单高效的,有用的学习,为零基础的朋友们录制了趣味编程视频,欢迎您浏览,谢谢!

标签归档:java

一次Spring Bean初始化顺序问题排查记录

最近在使用Springboot的时候需要通过静态的方法获取到Spring容器托管的bean对象,参照一些博文里写的,新建了个类,并实现ApplicationContextAware接口。代码大致如下:

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null) {
            SpringUtils.applicationContext = applicationContext;
        }
    }
   public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }  
}

然后另外一个bean需要依赖这个静态获取bean的方法,代码大致如下:

@Component
public class TestBean{

   private Object dependencyBean = SpringUtils.getBean(OtherBean.class);  
    
}

(注: 忽略代码逻辑是否合理~~ 这些代码是为演示所用简化的逻辑,肯定有同学会说:既然都是bean了为什么不注入,而是要用静态的获取呢?这个暂时不考虑,暂认为就必须要这样搞)

这两个类的层次结构和包名大致如下: 

utils

> notice

  > TestBean

> SpringUtils


就是TestBean在SpringUtils的下一级,TestBean所在包名为notice(这这个名字很重要! 直接影响到这两个bean的加载顺序,具体原理往下看)

代码就这么多,从以上代码来静态分析看,确实有些漏洞,因为没有考虑到Spring bean的加载顺序,可能导致的SpringUtils报空指针异常(在TestBean先于SpringUtils初始化的场景下),不管怎么样先执行一下看下效果,效果如下:

macOS操作系统下代码正常

windows平台下代码空指针异常

为什么这还跟平台有关了呢?难道Spring bean的初始化顺序还跟平台有关?事实证明这个猜想是正确的。下面从Spring源代码里来找原因。

这里需要重点关注的类是 ConfigurationClassPostProcessor,这个类是干什么的?它从哪里来?如何实现bean的加载的?

在Spring里可以指定甚至自定义多个BeanFactoryPostProcessor来实现在实例化bean之前做一些bean容器的更新操作,比如修改某些bean的定义、增加一些bean、删除一些bean等,而ConfigurationClassPostProcessor就是Spring为了支持基于注解bean的功能而实现的BeanFactoryPostProcessor。

web环境的Springboot默认使用的应用上下文(ApplicationContext,BeanFactoryPostProcessor就是注册到这里才会起作用的)是AnnotationConfigEmbeddedWebApplicationContext,在AnnotationConfigEmbeddedWebApplicationContext的构造方法里初始化this.reader的时候,在reader的构造方法里把ConfigurationClassPostProcessor添加到ApplicationContext里了:

 

public class AnnotationConfigEmbeddedWebApplicationContext
        extends EmbeddedWebApplicationContext {

    private final AnnotatedBeanDefinitionReader reader;

    private final ClassPathBeanDefinitionScanner scanner;

    private Class<?>[] annotatedClasses;

    private String[] basePackages;

    /**
     * Create a new {@link AnnotationConfigEmbeddedWebApplicationContext} that needs to be
     * populated through {@link #register} calls and then manually {@linkplain #refresh
     * refreshed}.
     */
    public AnnotationConfigEmbeddedWebApplicationContext() {
        this.reader = new AnnotatedBeanDefinitionReader(this);  // 重点关注这句
        this.scanner = new ClassPathBeanDefinitionScanner(this);
    }
public class AnnotatedBeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();

    private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();

    private ConditionEvaluator conditionEvaluator;

    /**
     * Create a new {@code AnnotatedBeanDefinitionReader} for the given registry and using
     * the given {@link Environment}.
     * @param registry the {@code BeanFactory} to load bean definitions into,
     * in the form of a {@code BeanDefinitionRegistry}
     * @param environment the {@code Environment} to use when evaluating bean definition
     * profiles.
     * @since 3.1
     */
    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        Assert.notNull(environment, "Environment must not be null");
        this.registry = registry;
        this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); // 这里把ConfigurationClassPostProcessor注册到上下文里了
    }

上面介绍了Spring如何注册这个ConfigurationClassPostProcessor,下面看下这个类如何实现bean定义加载的。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        // ....
        // Parse each @Configuration class
        ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);

        Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
        Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
        do {
            parser.parse(candidates);
            parser.validate();

            Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
复制代码

主要的代码是 parser.parse(candidates); 这句,当执行到这里的时候candidates已经包含Springboot的配置类,这个parse会根据配置类里定义的basePackge递归扫描这个目录下面的class文件(如果没有定义basePackage字段则把配置类所在的包作为basePackage),debug跟踪代码到最底层可以看到使用的是PathMatchingResourcePatternResolver的doRetrieveMatchingFiles方法:

protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
   if (logger.isDebugEnabled()) {
      logger.debug("Searching directory [" + dir.getAbsolutePath() +
            "] for files matching pattern [" + fullPattern + "]");
   }
   File[] dirContents = dir.listFiles();
   if (dirContents == null) {
      if (logger.isWarnEnabled()) {
         logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
      }
      return;
   }
   Arrays.sort(dirContents);
   for (File content : dirContents) {
      String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
      if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
         if (!content.canRead()) {
            if (logger.isDebugEnabled()) {
               logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
                     "] because the application is not allowed to read the directory");
            }
         }
         else {
            doRetrieveMatchingFiles(fullPattern, content, result);
         }
      }
      if (getPathMatcher().match(fullPattern, currPath)) {
         result.add(content);
      }
   }
}

在这里可以看到有句 Arrays.sort(dirContents); 这个代码,就是在遍历一个文件夹下的资源时(包括文件夹和class文件),会先把资源排序一下,这个排序决定了bean的加载顺序! 那再看下File(上面代码中的dirContents是File列表)是如何排序的:

public class File
    implements Serializable, Comparable<File>
{

    /**
     * The FileSystem object representing the platform's local file system.
     */
    private static final FileSystem fs = DefaultFileSystem.getFileSystem();


    /* -- Basic infrastructure -- */

    /**
     * Compares two abstract pathnames lexicographically.  The ordering
     * defined by this method depends upon the underlying system.  On UNIX
     * systems, alphabetic case is significant in comparing pathnames; on Microsoft Windows
     * systems it is not.
     *
     * @param   pathname  The abstract pathname to be compared to this abstract
     *                    pathname
     *
     * @return  Zero if the argument is equal to this abstract pathname, a
     *          value less than zero if this abstract pathname is
     *          lexicographically less than the argument, or a value greater
     *          than zero if this abstract pathname is lexicographically
     *          greater than the argument
     *
     * @since   1.2
     */
    public int compareTo(File pathname) {
        return fs.compare(this, pathname);
    }

使用的是FileSystem的排序方法,再看看DefaultFileSystem.getFileSystem();拿到是是什么:

mac下:

/**
 *
 * @since 1.8
 */
class DefaultFileSystem {

    /**
     * Return the FileSystem object for Unix-based platform.
     */
    public static FileSystem getFileSystem() {
        return new UnixFileSystem();
    }
}
// UnixFileSystem

class UnixFileSystem extends FileSystem {

/* -- Basic infrastructure -- */

public int compare(File f1, File f2) {
return f1.getPath().compareTo(f2.getPath());
}

}

windows下:

/**
 *
 * @since 1.8
 */
class DefaultFileSystem {

    /**
     * Return the FileSystem object for Windows platform.
     */
    public static FileSystem getFileSystem() {
        return new WinNTFileSystem();
    }
}
//WinNTFileSystem

@Override
public int compare(File f1, File f2) {
return f1.getPath().compareToIgnoreCase(f2.getPath());
}

由于windows下和mac下使用的FileSystem不同,jdk windows版FileSystem实现的compare方法在比较文件是忽略了文件名的大小写,而mac版没有忽略大小写,所以导致前面提出的同样的代码在windows下报错,在mac下就是正常的问题。

但是为什么会有这样的差别呢?不是太明白为什么

那还剩最后一个问题,这个问题如何解决呢?我这有两个方法,一是修改类名让被依赖的类排在前面,这种方法不是太优雅,而且如果以后jdk更新了排序方法可能还会出bug,第二种是在使用类上加DependOn注解,主动说明在初始化使用类时首先加载被依赖的类,这样就没有问题了,但是我感觉在开发的时候尽量避免这种依赖问题,这让容器和业务代码参杂,以后维护是个噩梦

Eclipse 运行命令行参数大全

包括英文版本和中文版本两种的说明, 特别需要值得一提的是那个 -nl 参数, 可以指定程序启动时所使用的语言. 例如:

eclipse -nl en_US
将启动英文语言, 这个特性在安装了国际化语言包以后特别有用, 可以方便的切换各个语言的版本. 注意 IBM WSAD v5.1 也支持这个功能.

运行 Eclipse

将 Eclipse 驱动程序安装(解压缩)到某个目录(例如,c:\eclipse)中之后,通过运行顶级安装目录中的 Eclipse 可执行文件来启动"工作台"。在 Windows 系统上,该可执行文件称为 eclipse.exe,而在 Linux 系统上称为 eclipse。注意:下列讨论描述 Windows 系统上的设置。Linux 上的设置是相似的。

如果您没有另行指定,则平台将缺省工作区目录创建为可执行文件的兄弟目录(例如 c:\eclipse\workspace)。此工作区目录用作项目的缺省内容区,还用于保存任何必需的元数据。要进行共享安装或多工作区安装,应明确指出工作区的位置而不是使用缺省值。有两种控制工作区位置的方法:使用当前工作目录或使用 -data 命令行自变量。

将工作区位置设置为在当前工作目录内

在此方案中,工作区位置将是当前工作目录中称为 workspace 的目录。

实现此目的最容易的方法可能是使用下列步骤来创建快捷方式:

  1. 导航到 Windows 资源管理器中的 eclipse.exe 并使用右键拖动来创建 eclipse.exe 的快捷方式。
  2. 编辑快捷方式的属性,以使启动位置:字段标识工作区位置的父目录(例如,c:\users\robert)。
  3. 关闭属性对话框并双击快捷方式(如果提供的目录为 c:\users\robert,则工作区位置将为 c:\users\robert\workspace)。

当然,您也可以使用命令提示符(通过将目录切换为工作区父目录然后运行 eclipse.exe)来获得同样的效果。

使用 -data 设置工作区的特定位置

要使用 -data 命令行自变量,只要将 -data your_workspace_location(例如,-data c:\users\robert\myworkspace)添加至快捷方式属性中的目标字段或显式地将它包括在命令行上。

使用 -vm 设置 java VM

建议显式指定在运行 Eclipse 时要使用哪个 Java VM。使用 -vm 命令行自变量(例如,-vm c:\jre\bin\javaw.exe)可以实现此目的。如果不使用 -vm,则 Eclipse 将使用在 O/S 路径上找到的一个 Java VM。当安装其它产品时,它们可更改您的路径,导致在下一次启动 Eclipse 时使用另一 Java VM。

运行 Eclipse 中的高级主题

Eclipse 可执行文件及平台本身提供了人们感兴趣的开发或调试 Eclipse 各部件的许多执行选项。运行 Eclipse 可执行文件的一般格式是:

eclipse [platform options] [-vmargs [Java VM arguments]]

<>Eclipse 启动参数 <>命令 描述

原因
-arch architecture
定义 Eclipse 平台在其上运行的处理器体系结构。Eclipse 平台通常使用 Java os.arch 属性的常用值来计算最佳设置。如果在此处指定该项,则这是 Eclipse 平台使用的值。此处指定的值可作为 BootLoader.getOSArch() 用于插件。示例值有:"x86"、"sparc"、"PA-RISC"和"ppc"。2.0
-application applicationId
要运行的应用程序。应用程序由向 org.eclipse.core.runtime.applications 扩展点提供扩展的插件来声明。通常不需要此自变量。如果指定了此项,则该值会覆盖配置提供的值。如果不指定此项,则会运行"Eclipse 工作台"。1.0
-boot bootJarURL
(建议不使用;用 -configuration 代替;支持 1.0 兼容)。Eclipse 平台的引导插件代码(boot.jar)的位置,表示为 URL。如果指定此项,则会用它来为装入 Eclipse 平台引导程序类装入器的类装入器设置类路径。仅当更改 startup.jar 和 boot.jar 的相对位置时才需要它。注意,不允许使用相对 URL。*1.0
-classloaderproperties [file]
如果指定的话,则使用给定位置处的类装入器属性文件来激活平台类类装入器增强。文件自变量可以是文件路径或 URL。注意,不允许使用相对 URL。单击此处以获得更多详细信息。2.0.2
-configuration configurationFileURL
Eclipse 平台配置文件的位置,表示为 URL。配置文件确定 Eclipse 平台、可用插件集和主要功能部件的位置。注意,不允许使用相对 URL。当安装或更新 Eclipse 平台时配置文件被写至此位置。2.0
-consolelog
将 Eclipse 平台的错误日志镜像到用来运行 Eclipse 的控制台。与 -debug 组合时很方便使用。1.0
-data workspacePath
要运行 Eclipse 平台的工作区的路径。工作区位置也是项目的缺省位置。相对于从中启动 eclipse 的目录来解释相对路径。1.0
-debug [optionsFile]
将平台置于调试方式,并从给定位置处的文件装入调试选项(如果指定的话)。此文件指示哪些调试点可用于插件以及是否已启用它们。如果未给出文件位置,则平台在启动 eclipse 的目录中查找称为".options"的文件。URL 和文件系统路径都可作为文件位置。1.0
-dev [classpathEntries]
将平台置于开发方式。将可选类路径条目(用逗号分隔的列表)添加至每个插件的运行时类路径。例如,当工作区包含要开发的插件时,指定 -dev bin 会为每个插件项目的名为 bin 的目录添加类路径条目,允许在其中存储最新生成的类文件。除去了冗余或不存在的类路径条目。1.0
-endsplash params
用于在 Eclipse 平台启动并运行时关闭闪屏的内部选项。此选项在闪屏处理链中不同的位置有不同的语法和语义。2.0
-feature featureId
主要功能部件的标识。主要功能部件为 Eclipse 的已启动实例提供了产品个性,并确定使用的产品定制信息。2.0
-keyring keyringFilePath
磁盘上授权数据库(或"密钥环"文件)的位置。此自变量必须与 -password 选项配合使用。相对于从中启动 eclipse 的目录来解释相对路径。1.0
-nl locale
定义 Eclipse 平台在其上运行的语言环境的名称。Eclipse 平台通常自动计算最佳设置。如果在此处指定该项,则这是 Eclipse 平台使用的值。此处指定的值可作为 BootLoader.getNL() 用于插件。示例值有:"en_US"和"fr_FR_EURO"。2.0
-nolazyregistrycacheloading
取消激活装入优化的平台插件注册表高速缓存。缺省情况下,仅当需要时才从注册表高速缓存(可用时)中装入扩展的配置元素,以减少内存占用。此选项将在启动时强制完全装入注册表高速缓存。2.1
-noregistrycache
绕过读写内部插件注册表高速缓存文件。2.0
-nosplash
运行平台而不显示闪屏。1.0
-os operatingSystem
定义 Eclipse 平台在其上运行的操作系统。Eclipse 平台通常使用 Java os.name 属性的常用值来计算最佳设置。如果在此处指定该项,则这是 Eclipse 平台使用的值。此处指定的值可作为 BootLoader.getOS() 用于插件,并用于解析插件清单文件中提及的路径中 $os$ 变量的出现。示例值有:"win32"、"linux"、"hpux"、"solaris"和"aix"。1.0
-password password
授权数据库的密码。与 -keyring 选项配合使用。1.0
-perspective perspectiveId
启动时要在活动工作台窗口中打开的透视图。如果没有指定该参数,则将打开关闭时活动的透视图。1.0
-plugincustomization
	  propertiesFile
包含插件首选项缺省设置的属性文件的位置。这些缺省设置覆盖在主要功能部件中指定的缺省设置。相对于从中启动 eclipse 的目录来解释相对路径。2.0
-plugins pluginsFileURL
(建议不使用;用 -configuration 代替;支持 1.0 兼容)。 指定 Eclipse 平台查找插件的文件的位置,表示为 URL。该文件为属性文件格式,其中键是任意用户定义名称,值是指向 plugin.xml 文件的显式路径或指向包含插件的目录的路径的用逗号分隔的列表。注意,不允许使用相对 URL。如果指定此项,则此选项会导致创建适当的临时配置。*1.0
-refresh
启动时执行工作区的全局刷新的选项。这将使从上次平台运行以来在文件系统中所做的任何更改一致。1.0
-showlocation
用于在窗口标题栏中显示工作区的位置的选项。在发行版 2.0 中,此选项仅与 -data 命令行自变量一起使用。2.0
-showsplash params
用于显示闪屏(由可执行的 Eclipse 平台启动器执行)的内部选项。此选项在闪屏处理链中不同的位置有不同的语法和语义。2.0
-vm vmPath
要用来运行 Eclipse 平台的"Java 运行时环境"(JRE)的位置。如果不指定此项,则 JRE 位于 jre(它是 Eclipse 可执行文件的兄弟目录)。相对于从中启动 eclipse 的目录来解释相对路径。1.0
-ws windowSystem
定义 Eclipse 平台在其上运行的 Windows 系统。Eclipse 平台通常使用 Java os.name 属性的常用值来计算最佳设置。如果在此处指定该项,则这是 Eclipse 平台使用的值。此处指定的值可作为 BootLoader.getWS() 用于插件、用于配置 SWT 以及用于解析插件清单文件中提及的路径中 $ws$ 变量的出现。示例值有:"win32"、"motif"和"gtk"。1.0

将 -vmargs 条目后面的所有自变量(但不包括 -vmargs)作为虚拟机自变量(即,在要运行的类的前面)直接传递到所指示的 Java VM。注意:如果 Eclipse 启动在 Java vm 自变量(-vmargs)之后提供的自变量(例如,-data),则 Eclipse 将不会启动并且您将接收到"JVM 已终止。出口代码为 1"的错误。

在不同的 VM 上运行

在 J9 上运行 Eclipse

当在 J9 版本 1.5 上运行 Eclipse 时,建议使用以下 VM 选项:

eclipse.exe [eclipse arguments] -vm path_to_j9w.exe 
            -vmargs -ms:32 -mm:2048 -mo:32768 -moi:32768 -mca:32 -mco:128 -mx:2000000

当在 J9 版本 2.0 上运行 Eclipse 时,J9W 选择的缺省自变量应为合适的选项。但是,要覆盖 Eclipse 可执行文件以内部方式自动设置的参数,必须指定 -vmargs 不带任何参数,如下所示:

eclipse.exe [eclipse arguments] -vm path_to_j9w.exe -vmargs

有关进一步信息,参考 J9 VM 文档和帮助。

在 IBM Developer Kit, Java(TM) Technology Edition VM 上运行 Eclipse

IBM Developer Kit, Java(TM) Technology Edition 1.3 Linux 的缺省 VM 设置适合进行初期研究工作,但在进行大型开发时是不够的。对于大型开发,应修改 VM 自变量以使有更多的堆可用。例如,下列设置将允许 Java 堆增大为 256MB:

-vmargs -Xmx256M

Copyright IBM Corporation and others 2000, 2003

Running Eclipse

After you install (unzip) the Eclipse driver in a directory (such as c:\eclipse), start the Workbench by running the Eclipse executable file found in the top level install directory. The executable file is called eclipse.exe on Windows systems and eclipse on Linux systems. Note: the following discussion describes setting up on Windows systems. Setup on Linux is analogous.

If you do not specify otherwise, the platform creates a default workspace directory as a sibling of the executable (for example, c:\eclipse\workspace). This workspace directory is used as the default content area for your projects as well as for holding any required metadata. For shared or multi-workspace installs you should explicitly state the location of your workspace rather than using the default. There are two ways to control the location of your workspace: using the current working directory or using the -data command line argument.

Setting the workspace location to be inside the current working directory

In this scenario, the workspace location will be a directory called workspace inside the current working directory.

Perhaps the easiest way of doing this is to create a shortcut using the following steps:

  1. Navigate to eclipse.exe in the Windows Explorer and using a right button drag, create a shortcut to eclipse.exe.
  2. Edit the properties of the shortcut such that the Start in: field identifies the parent directory of your workspace location (for example, c:\users\robert).
  3. Close the properties dialog and double-click on the shortcut (if the provided directory was c:\users\robert, the workspace location would be c:\users\robert\workspace).

Of course you can get the same effect using a command prompt by changing directory to your workspace parent's directory and then running eclipse.exe.

Setting a specific location for the workspace with -data

To use the -data command line argument, simply add -data your_workspace_location (for example, -data c:\users\robert\myworkspace) to the Target field in the shortcut properties, or include it explicitly on your command line.

Setting the java VM using -vm

It is recommended that you explicitly specify which Java VM to use when running Eclipse. This is achieved with the -vm command line argument (for example, -vm c:\jre\bin\javaw.exe). If you don't use -vm, Eclipse will use the first Java VM found on the O/S path. When you install other products, they may change your path, resulting in a different Java VM being used when you next launch Eclipse.

Advanced Topics in Running Eclipse

The Eclipse executable and the platform itself offer a number of execution options of interest to people developing or debugging parts of Eclipse. The general form of running the Eclipse executable is:

eclipse [platform options] [-vmargs [Java VM arguments]]

<>Eclipse Startup Parameters <>Command Description

Since
-arch architecture
Defines the processor architecture on which the Eclipse platform is running. The Eclipse platform ordinarily computes the optimal setting using the prevailing value of Java os.arch property. If specified here, this is the value that the Eclipse platform uses. The value specified here is available to plug-ins as BootLoader.getOSArch(). Example values: "x86", "sparc", "PA-RISC", "ppc".2.0
-application applicationId
The application to run. Applications are declared by plug-ins supplying extensions to the org.eclipse.core.runtime.applications extension point. This argument is typically not needed. If specified, the value overrides the value supplied by the configuration. If not specified, the Eclipse Workbench is run.1.0
-boot bootJarURL
(Deprecated; replaced by -configuration; supported for 1.0 compatibility). The location of the Eclipse platform's boot plug-in code (boot.jar), expressed as a URL. If specified, it is used to set the classpath for the class loader that loads the Eclipse platform bootstrap class loader. Only required when changing the relative location of startup.jar and boot.jar. Note that relative URLs are not allowed.*1.0
-classloaderproperties [file]
Activates platform class loader enhancements using the class loader properties file at the given location, if specified. The file argument can be either a file path or a URL. Note that relative URLs are not allowed. Click here for more details.2.0.2
-configuration configurationFileURL
The location for the Eclipse Platform configuration file, expressed as a URL. The configuration file determines the location of the Eclipse platform, the set of available plug-ins, and the primary feature. Note that relative URLs are not allowed. The configuration file is written to this location when the Eclipse platform is installed or updated.2.0
-consolelog
Mirrors the Eclipse platform's error log to the console used to run Eclipse. Handy when combined with -debug.1.0
-data workspacePath
The path of the workspace on which to run the Eclipse platform. The workspace location is also the default location for projects. Relative paths are interpreted relative to the directory that Eclipse was started from.1.0
-debug [optionsFile]
Puts the platform in debug mode and loads the debug options from the file at the given location, if specified. This file indicates which debug points are available for a plug-in and whether or not they are enabled. If a file location is not given, the platform looks in the directory that eclipse was started from for a file called ".options". Both URLs and file system paths are allowed as file locations.1.0
-dev [classpathEntries]
Puts the platform in development mode. The optional classpath entries (a comma separated list) are added to the runtime classpath of each plug-in. For example, when the workspace contains plug-ins being developed, specifying -dev bin adds a classpath entry for each plug-in project's directory named bin, allowing freshly generated class files to be found there. Redundant or non-existent classpath entries are eliminated.1.0
-endsplash params
Internal option for taking down the splash screen when the Eclipse platform is up and running. This option has different syntax and semantics at various points along the splash screen processing chain.2.0
-feature featureId
The ID of the primary feature. The primary feature gives the launched instance of Eclipse its product personality, and determines the product customization information used.2.0
-keyring keyringFilePath
The location of the authorization database (or "key ring" file) on disk. This argument must be used in conjunction with the -password option. Relative paths are interpreted relative to the directory that Eclipse was started from.1.0
-nl locale
Defines the name of the locale on which the Eclipse platform is running. The Eclipse platform ordinarily computes the optimal setting automatically. If specified here, this is the value that the Eclipse platform uses. The value specified here is available to plug-ins as BootLoader.getNL(). Example values: "en_US" and "fr_FR_EURO".2.0
-nolazyregistrycacheloading
Deactivates platform plug-in registry cache loading optimization. By default, extensions' configuration elements will be loaded from the registry cache (when available) only on demand, reducing memory footprint. This option will force the registry cache to be fully loaded at startup.2.1
-noregistrycache
Bypasses the reading and writing of an internal plug-in registry cache file.2.0
-nosplash
Runs the platform without putting up the splash screen.1.0
-os operatingSystem
Defines the operating system on which the Eclipse platform is running. The Eclipse platform ordinarily computes the optimal setting using the prevailing value of Java os.name property. If specified here, this is the value that the Eclipse platform uses. The value specified here is available to plug-ins as BootLoader.getOS(), and used to resolve occurrences of the $os$ variable in paths mentioned in the plug-in manifest file. Example values: "win32", "linux", "hpux", "solaris", "aix".1.0
-password password
The password for the authorization database. Used in conjunction with the -keyring option.1.0
-perspective perspectiveId
The perspective to open in the active workbench window on startup. If this parameter is not specified, the perspective that was active on shutdown will be opened.1.0
-plugincustomization
	  propertiesFile
The location of a properties file containing default settings for plug-in preferences. These default settings override default settings specified in the primary feature. Relative paths are interpreted relative to the directory that eclipse was started from.2.0
-plugins pluginsFileURL
(Deprecated; replaced by -configuration; supported for 1.0 compatibility). The location of the file that specifies where the Eclipse platform finds plug-ins, expressed as a URL. The file is in property file format where the keys are arbitrary user defined names and the values are comma separated lists of either explicit paths to plugin.xml files, or paths to directories containing plug-ins. Note that relative URLs are not allowed. If specified, this option causes the creation of a suitable temporary configuration.*1.0
-refresh
Option for performing a global refresh of the workspace on startup. This will reconcile any changes that were made in the file system since the platform was last run.1.0
-showlocation
Option for displaying the location of the workspace in the window title bar. In release 2.0 this option only worked in conjunction with the -data command line argument.2.0
-showsplash params
Internal option for showing the splash screen (done by the executable Eclipse platform launcher). This option has different syntax and semantics at various points along the splash screen processing chain.2.0
-vm vmPath
The location of Java Runtime Environment (JRE) to use to run the Eclipse platform. If not specified, the JRE is at jre, sibling of the Eclipse executable. Relative paths are interpreted relative to the directory that eclipse was started from.1.0
-ws windowSystem
Defines the window system on which the Eclipse platform is running. The Eclipse platform ordinarily computes the optimal setting using the prevailing value of Java os.name property. If specified here, this is the value that the Eclipse platform uses. The value specified here is available to plug-ins as BootLoader.getWS(), used to configure SWT, and used to resolve occurrences of the $ws$ variable in paths mentioned in the plug-in manifest file. Example values: "win32", "motif", "gtk".1.0

All arguments following (but not including) the -vmargs entry are passed directly through to the indicated Java VM as virtual machine arguments (that is, before the class to run). Note: If an Eclipse startup argument, such as -data, is provided after the Java vm arguments (-vmargs), Eclipse will not start and you will receive a "JVM terminated. Exit code=1" error.

Running on Different VMs

Running Eclipse on J9

When running Eclipse on J9 version 1.5, it is recommended that you use the following VM options:

eclipse.exe [eclipse arguments] -vm path_to_j9w.exe 
            -vmargs -ms:32 -mm:2048 -mo:32768 -moi:32768 -mca:32 -mco:128 -mx:2000000

When running Eclipse on J9 version 2.0, the default arguments chosen by J9W should be suitable. However, to override the parameters which are automatically set internally by the Eclipse executable, you must specify -vmargs with no following arguments as follows:

eclipse.exe [eclipse arguments] -vm path_to_j9w.exe -vmargs

Please refer to the J9 VM documentation and help for further information.

Running Eclipse on the IBM Developer Kit, Java(TM) Technology Edition VM

The default VM settings for IBM Developer Kit, Java(TM) Technology Edition 1.3 Linux work well for initial exploration, but are not sufficient for large scale development. For large scale development you should modify your VM arguments to make more heap available. For example, the following setting will allow the Java heap to grow to 256MB:

-vmargs -Xmx256M

来源: https://www.cnblogs.com/sunsonbaby/archive/2005/02/02/101112.html

 

 

不可不说的Java“锁”事

前言

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:

11

1. 乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

12

根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:

// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
	// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
	lock.lock();
	// 操作同步资源
	lock.unlock();
}

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:

13

根据定义我们可以看出各属性的作用:

  • unsafe: 获取并操作内存的数据。
  • valueOffset: 存储value在AtomicInteger中的偏移量。
  • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
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;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

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

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

2. 自旋锁 VS 适应性自旋锁

在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

15

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

16

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

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

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

首先为什么Synchronized能实现线程同步?

在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:

锁状态存储内容存储内容
无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

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

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

整体的锁状态升级流程如下:

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

4. 公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。

17

如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:

18

接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。

根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

下面我们来看一下公平锁与非公平锁的加锁方法的源码:

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

5. 可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。

还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

19

但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。

20

之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

6. 独享锁 VS 共享锁

独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

下图为ReentrantReadWriteLock的部分源码:

22

我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。 在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。

在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

了解了概念之后我们再来看代码,先看写锁的加锁源码:

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState(); // 取到当前锁的个数
	int w = exclusiveCount(c); // 取写锁的个数w
	if (c != 0) { // 如果已经有线程持有了锁(c!=0)
    // (Note: if c != 0 and w == 0 then shared count != 0)
		if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
      throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
		return false;
	setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
	return true;
}
  • 这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount©; ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
  • 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
  • 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
  • 如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。
  • 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!

tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。

接着是读锁的代码:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                   // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:

我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。

结语

本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。

其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。

参考资料

  1. 《Java并发编程艺术》
  2. Java中的锁
  3. Java CAS 原理剖析
  4. Java并发——关键字synchronized解析
  5. Java synchronized原理总结
  6. 聊聊并发(二)——Java SE1.6中的Synchronized
  7. 深入理解读写锁—ReadWriteLock源码分析
  8. 【JUC】JDK1.8源码分析之ReentrantReadWriteLock
  9. Java多线程(十)之ReentrantReadWriteLock深入分析
  10. Java–读写锁的实现原理

作者简介

  • 家琪,美团点评后端工程师。2017 年加入美团点评,负责美团点评境内度假的业务开发。

来源: https://tech.meituan.com/2018/11/15/java-lock.html

 

转微服务写的最全的一篇文章

今年有人提出了2018年微服务将疯狂至死,可见微服务的争论从未停止过。在这我将自己对微服务的理解整理了一下,希望对大家有所帮助。

1.什么是微服务

1)一组小的服务(大小没有特别的标准,只要同一团队的工程师理解服务的标识一致即可)

2)独立的进程(java的tomcat,nodejs等)

3)轻量级的通信(不是soap,是http协议)

4)基于业务能力(类似用户服务,商品服务等等)

5)独立部署(迭代速度快)

6)无集中式管理(无须统一技术栈,可以根据不同的服务或者团队进行灵活选择)

ps:微服务的先行者Netflix公司,开源了一些好的微服务框架,后续会有介绍。

2. 怎么权衡微服务的利于弊

利:

强模块边界 。(模块化的演化过程:类-->组件/类库(sdk)-->服务(service),方式越来越灵活)

可独立部署。

技术多样性。

弊:

分布式复杂性。

最终一致性。(各个服务的团队,数据也是分散式治理,会出现不一致的问题)

运维复杂性。

测试复杂性。

3. 企业在什么时候考虑引入微服务

从生产力和系统的复杂性这两个方面来看。公司一开始的时候,业务复杂性不高,这时候是验证商业模式的时候,业务简单,用单体服务反而生产力很高。随着公司的发展,业务复杂性慢慢提高,这时候就可以采用微服务来提升生产力了。至于这个转化的点,需要团队的架构师来进行各方面衡量,就个人经验而言,团队发展到百人以上,采用微服务就很有必要了。

有些架构师是具有微服务架构能力,所以设计系统时就直接设计成了微服务,而不是通过单服务慢慢演化发展成微服务。在这里我并不推荐这种做法,因为一开始对业务领域并不是很了解,并且业务模式还没有得到验证,这时候上微服务风险比较高,很有可能失败。所以建议大家在单服务的应用成熟时,并且对业务领域比较熟悉的时候,如果发现单服务无法适应业务发展时,再考虑微服务的设计和架构。

4.微服务的组织架构

如上图左边,传统的企业中,团队是按职能划分的。开发一个项目时,会从不同的职能团队找人进行开发,开发完成后,再各自回到自己的职能团队,这种模式实践证明,效率还是比较低的。

如上图右边,围绕每个业务线或产品,按服务划分团队。团队成员从架构到运维,形成一个完整的闭环。一直围绕在产品周围,进行不断的迭代。不会像传统的团队一样离开。这样开发效率会比较高。至于这种团队的规模,建议按照亚马逊的两个披萨原则,大概10人左右比较好。

5:怎么理解中台战略和微服务

中台战略的由来:马云2015年去欧洲的一家公司supersell参观,发现这个公司的创新能力非常强,团队的规模很小,但是开发效率很高。他们就是采用中台战略。马云感触很深,回国后就在集团内部推出了中台战略。

简单的理解就是把传统的前后台体系中的后台进行了细分。阿里巴巴提出了大中台小前台的战略。就是强化业务和技术中台,把前端的应用变得更小更灵活。当中台越强大,能力就越强,越能更好的快速响应前台的业务需求。打个比喻,就是土壤越肥沃,越适合生长不同的生物,打造好的生态系统。

6:服务分层

每个公司的服务分层都不相同,有的公司服务没有分层,有的怎分层很多。目前业界没有统一的标准。

下面推荐一个比较容易理解的两层结构。

1:基础服务: 比如一个电商网站,商品服务和订单服务就属于基础服务(核心领域服务)。缓存服务,监控服务,消息队列等也属于基础服务(公共服务)

2:聚合服务 :例如网关服务就算一种聚合服务(适配服务)。

这是一种逻辑划分,不是物理划分,实际设计的东西很多很复杂。

7:微服务的技术架构体系

下图是一个成型的互联网微服务的架构体系:

1:接入层 负载均衡作用,运维团队负责

2:网关层 反向路由,安全验证,限流等

3:业务服务层 基础服务和领域服务

4:支撑服务层

5:平台服务

6:基础设施层 运维团队负责。(或者阿里云)

8:微服务的服务发现的三种方式

第一种:如下图所示,传统的服务发现(大部分公司的做法)。服务上线后,通知运维,申请域名,配置路由。调用方通过dns域名解析,经过负载均衡路由,进行服务访问。缺点: LB的单点风险,服务穿透LB,性能也不是太好

第二种:也叫客户端发现方式。如下图所示。通过服务注册的方式,服务提供者先注册服务。消费者通过注册中心获取相应服务。

并且把LB的功能移动到了消费者的进程内,消费者根据自身路由去获取相应服务。优点是,没有了LB单点问题,也没有了LB的中间一跳,性能也比较好。但是这种方式有一个非常明显的缺点就是具有非常强的耦合性。针对不同的语言,每个服务的客户端都得实现一套服务发现的功能。

第三种:也叫服务端发现方式,如下图所示。和第二种很相似。但是LB功能独立进程单独部署,所以解决了客户端多语言开发的问题。唯一的缺点就是运维成比较高,每个节点都得部署一个LB的代理,例如nginx。

9.微服务网关

网关就好比一个公司的门卫。屏蔽内部细节,统一对外服务接口。

下图是一个网关所处位置的示例图。

10:Netflix Zuul网关介绍

核心就是一个servlet,通过filter机制实现的。主要分为三类过滤器:前置过滤器,过滤器和后置过滤器。

主要特色是,这些过滤器可以动态插拔,就是如果需要增加减少过滤器,可以不用重启,直接生效。原理就是:通过一个db维护过滤器(上图蓝色部分),如果增加过滤器,就将新过滤器编译完成后push到db中,有线程会定期扫描db,发现新的过滤器后,会上传到网关的相应文件目录下,并通知过滤器loader进行加载相应的过滤器。

整个网关调用的流程

上图从左变http Request开始经过三类过滤器,最终到最右边的Http Response,这就是Zull网关的整个调用流程。

11:微服务的路由发现体系

整个微服务的路由发现体系,一般由服务注册中心和网关两部分组成。以NetFlix为例子,Eureka和Zull这两个组件支撑了netFlix整个的路由发现体系。如下图所示,首先外部请求发送到网关,网关去服务注册中心获取相应的服务,进行调用。其次内部服务间的调用,也通过服务注册中心进行的

12.微服务配置中心

目前大部分公司都是把配置写到配置文件中,遇到修改配置的情况,成本很高。并且没有修改配置的记录,出问题很难追溯。配置中心就接解决了以上的问题。

可配置内容:数据库连接,业务参数等等

配置中心就是一个web服务,配置人员通过后台页面修改配置,各个服务就会得到新的配置参数。实现方式主要有两种,一种是push,另一种是pull。两张方式各有优缺点。push实时性较好,但是遇到网络抖动,会丢失消息。pull不会丢失消息但是实时性差一些。大家可以同时两种方式使用,实现一个比较好的效果。如下图所示,这是一个国内知名互联网公司的配置中心架构图。

开源地址:http://github.com/ctripcorp/appollo

13:RPC遇到了REST

内部一些核心服务,性能要求比较高的可以采用RPC,对外服务的一般可以采用rest。

14:服务框架和治理

微服务很多的时候,就需要有治理了。一个好的微服务框架一般分为以下14个部分。如下图所示。这就是开篇所说的,微服务涉及的东西很多,有些初创公司和业务不成熟的产品是不太适合的,成本比较高。

目前国内比较好的微服务框架就是阿里巴巴的DUBBO了,国外的就是spring cloud,大家可以去研究一下.

15:监控体系

监控是微服务治理的重要环节。一般分为以下四层。如下图所示。

监控的内容分为五个部分:日志监控,Metrics监控(服务调用情况),调用链监控,告警系统和健康检查。

日志监控,国内常用的就是ELK+KAFKA来实现。健康检查和Metrics,像spring boot会自带。Nagios也是一个很好的开源监控框架。

16:Trace调用链监控

调用链监控是用来追踪微服务之前依赖的路径和问题定位。例如阿里的鹰眼系统。主要原理就是子节点会记录父节点的id信息。

下图是目前比较流行的调用链监控框架。

17:微服务的限流熔断

假设服务A依赖服务B和服务C,而B服务和C服务有可能继续依赖其他的服务,继续下去会使得调用链路过长。如果在A的链路上某个或几个被调用的子服务不可用或延迟较高,则会导致调用A服务的请求被堵住,堵住的请求会消耗占用掉系统的线程、io等资源,当该类请求越来越多,占用的计算机资源越来越多的时候,会导致系统瓶颈出现,造成其他的请求同样不可用,最终导致业务系统崩溃。

一般情况对于服务依赖的保护主要有两种方式:熔断和限流。目前最流行的就是Hystrix的熔断框架。

下图是Hystrix的断路器原理图:

限流方式可以采用zuul的API限流方法。

18.Docker 容器部署技术&持续交付流水线

随着微服务的流行,容器技术也相应的被大家重视起来。容器技术主要解决了以下两个问题:

1:环境一致性问题。例如java的jar/war包部署会依赖于环境的问题(操着系统的版本,jdk版本问题)。

2:镜像部署问题。例如java,rubby,nodejs等等的发布系统是不一样的,每个环境都得很麻烦的部署一遍,采用docker镜像,就屏蔽了这类问题。

下图是Docker容器部署的一个完整过程。

更重要的是,拥有如此多服务的集群环境迁移、复制也非常轻松,只需选择好各服务对应的Docker服务镜像、配置好相互之间访问地址就能很快搭建出一份完全一样的新集群。

19.容器调度和发布体系

目前基于容器的调度平台有Kubernetes,mesos,omega。下图是mesos的一个简单架构示意图。

下图是一个完整的容器发布体系

在此我向大家推荐一个架构学习交流群。交流学习群号:478030634  里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《Java烂猪皮》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多..............

来源: https://www.jianshu.com/p/0a3e959ecb81

 

微服务架构介绍

最近在整理单位的微服务的事情, 因此在网络上搜索到一些资料, 感觉一些内容很全, 需要记录一下就记录下来。

下面这个文章整理的很好, 内容丰富, 但是后面一些内容感觉太复杂了, 学习成本, 实施成本,运维成本都很高, 作为学习的资料还是很好的。

微服务核心架构梳理

业界主流微服务框架的核心原理,包括服务发现,网关,配置中心,监控等组件,功能和架构原理的简单介绍。感谢阅读!

什么是微服务

微服务Microservices之父,马丁.福勒,对微服务大概的概述如下:

就目前而言,对于微服务业界并没有一个统一的、标准的定义(While there is no precise definition of this architectural style ) 。 但通在其常而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API ) 。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。 另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务。可以使用不同的语言来编写服务,也可以使用不同的数据存储。

根据马丁.福勒的描述,我总结了一下几点:


(字差,勿嫌)

小服务

小服务,没有特定的标准或者规范,但他在总体规范上一定是小的。

进程独立

每一组服务都是独立运行的,可能我这个服务运行在tomcat容器,而另一个服务运行在jetty上。可以通过进程方式,不断的横向扩展整个服务。

通信

过去的协议都是很重的,就像ESB,就像SOAP,轻通信,着意味着相比过去更智能更轻量的服务相互调用,就所谓smart endpoints and dumb pipes,这些endpoint都是解耦的,完成一个业务通信调用串起这些micro service就像是linux系统中通过管道串起一系列命令业务。

过去的业务,我们通常会考虑各种各样的依赖关系,考虑系统耦合带来的问题。微服务,可以让开发者更专注于业务的逻辑开发。

部署

不止业务要独立,部署也要独立。不过这也意味着,传统的开发流程会出现一定程度的改变,开发的适合也要有一定的运维指责

管理

传统的企业级SOA服务往往很大,不易于管理,耦合性高,团队开发成本比较大。微服务,可以让团队各思其政的选择技术实现,不同的service可以根据各自的需要选择不同的技术栈来实现其业务逻辑。

微服务的利与弊

为什么用微服务呢?因为好玩?

不是的。下面是我从网络上找到说的比较全的优点:

优点每个服务足够内聚,足够小,代码容易理解这样能聚焦一个指定的业务功能或业务需求

开发简单、开发效率提高,一个服务可能就是专一的只干一件事。

微服务能够被小团队单独开发,这个小团队是 2 到 5 人的开发人员组成。

微服务是松藕合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。

微服务能使用不同的语言开发。

易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如Jenkins,Hudson,bamboo。

微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需- - 通过合作才能体现价值。微服务允许你利用融合最新技术。

微服务只是业务逻辑的代码,不会和 HTML,CSS或其他界面组件混合。

每个微服务都有自己的存储能力,可以有自己的数据库。也可以有统一数据库。

总的来说,微服务的优势,就是在于,面对大的系统,可以有效的减少复杂程度,使服务架构的逻辑更清晰明了。

但是这样也会带来很多问题,就譬如分布式环境下的数据一致性,测试的复杂性,运维的复杂性。

什么组织适合使用微服务?

微服务带了种种优点,种种弊端,那么什么组织适合使用微服务?

墨菲定律(设计系统)和康威定律(系统划分)

康威定律,是一个五十多年前就被提出来的微服务概念。在康威的这篇文章中,最有名的一句话就是:

Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. - Melvin Conway(1967)

中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。看看下面的图片(来源于互联网,侵删),再想想Apple的产品、微软的产品设计,就能形象生动的理解这句话。


感兴趣的各位可以研究一下

架构演化

架构是不断演化出来的,微服务也是这样,当从各大科技公司,规模大到一定程度,完全需要演化成更进一步管理的技术架构体系。

传统的团队,都是面向过程化的,产品想完了去找策划,策划完了找开发,接着顺着一步一步找。我们做技术都是为了产品的,一旦过程出来了什么问题,回溯寻找问题会非常耗时。

使用了微服务架构体系,团队组织方式需要转变成跨职能团队,即每个团队都有产品专家,策划专家,开发专家,运维专家,他们使用API方式发布他们的功能,而平台使用他们的功能发布产品

微服务技术架构体系

下面我分享一下大部分公司都使用的微服务技术架构体系。

服务发现

主流的服务发现,分为三种

第一种,开发人员开发了程序以后,会找运维配一个域名,服务的话通过dns就能找到我们对应的服务

缺点是,由于服务没有负载均衡功能,对负载均衡服务,可能会有相当大的性能问题。


第二种,是目前普遍的做法。可以参考我上篇博客分析的zuul网关,每一个服务都通过服务端内置的功能注册到注册中心,服务消费者不断轮询注册中心发现对应的服务,使用内置负载均衡调用服务。

缺点是,对多语言环境不是很好,你需要单独给消费者的客户端开发服务发现和负载均衡功能。当然了,这个方法通常都是用在spring cloud上的。


第三种,是将客户端和负载均衡放在同一个主机,而不是同一个进程内。

这种方法相对第一种第二种方法来说,改善了他们的缺点,但是会极大增加运维成本。

网关

微服务的网关是什么?

我们可以联系生活实际想一下。每一个大的公司,都会有一偏属于自己的建筑区,而这建筑区内,都有不少的门卫。如果有外来人员进入公司,会先和门卫打好招呼,才能进去。

将生活实际联系到微服务上,就不难理解网关的意思了。

网关有什么用


反向路由:很多时候,公司不想让外部人员看到我们公司的内部,就需要网关来进行反向路由。即将外部请求转换成内部具体服务条用
安全认证:网络中会有很多恶意访问,譬如爬虫,譬如黑客攻击,网关维护安全功能。
限流熔断:参考我学好分布式zookepper的博客,当请求很多服务不堪重负,会让我们的服务自动关闭,导致不能用服务。限流熔断可以有效的避免这类问题
日志监控:所有的外面的请求都会经过网关,这样我们就可以使用网关来记录日志信息
灰度发布,蓝绿部署。是指能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。

开源网关Zuul架构


zuul网关核心其实是一个servlet,所有请求都会经过zuul servlet传到zuulFilter Runner,然后分发到三种过滤器。

先说说架构图左半部分,分别是使用Groovy实现的前置路由过滤器,路由过滤器,后置路由过滤器。

一般请求都会先经过前置路由过滤器处理,一般的自定义java封装逻辑也会在这里实现。

路由过滤器,实现的是找到对应的微服务进行调用。

调用完了,响应回来,会经过后置路由过滤器,通过后置路由过滤器我们可以封装日志审计的处理。

可以说zuul网关最大的特色就是它三层过滤器。

架构图右半部分,是zuul网关设计的自定义过滤器加载机制。网关内部会有生产者消费者模型,自动的将过滤器脚本发布到zuul网关读取加载运行。

配置中心

以前,开发人员把配置文件放在开发文件里面,这样会有很多隐患。譬如,配置规范不同,无法追溯配置人员。一旦需要大规模改动配置,改动时间会很长,无法追溯配置人员,从而影响整个产品,后果是我们承担不起的。

因此就有配置中心这个喽~

现在的开源中心有百度配置中心 Disconf,spring cloud config,Apollo,今天重点说说现在应用质量不错的配置中心阿波罗。

携程开源的Apollo   https://github.com/ctripcorp/apollo

apollo的配置中心规模比较大,本地应用会有响应的配置中心客户端,可以定时同步配置中心里的配置。如果配置中心怠机,会使用缓存来进行配置。

通讯方式

关于通讯方式,一般市面也就是两种远程调用方式,我整理了一个表格:

 

监控预警

监控预警对于微服务很重要,一个可靠的监控预警体系对微服务运行至关重要。一般监控分为如下层次:


从基础设施到用户端,层层有监控,全方位,多角度,每一个层面都很重要。总体来说,微服务可分5个监控点:日志监控,Metrics监控,健康检查,调用链检查,告警系统

监控架构

下面的图是大部分公司的一种监控架构图。每一个服务都有一个agent,agent收集到关键信息,会传到一些MQ中,为了解耦。同时将日志传入ELK,将Metrics传入InfluxDB时间序列库。而像nagios,可以定期向agent发起信息检查微服务。


调用链监控APM

很多公司都有调用链监控,就譬如阿里有鹰眼监控,点评的Cat,大部分调用链监控(没错,我指的Zipkin)架构是这样的

1、Pinpoint github地址:GitHub - naver/pinpoint: Pinpoint is an open source APM (Application Performance Management) tool for large-scale distributed systems written in Java. 对java领域的性能分析有兴趣的朋友都应该看看这个开源项目,这个是一个韩国团队开源出来的,通过JavaAgent的机制来做字节码代码植入,实现加入traceid和抓取性能数据的目的。 NewRelic、Oneapm之类的工具在java平台上的性能分析也是类似的机制。

2、SkyWalking github地址:wu-sheng/sky-walking 这是国内一位叫吴晟的兄弟开源的,也是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统,在github上也有400多颗星了。 功能相对pinpoint还是稍弱一些,插件还没那么丰富,不过也很难得了。

3、Zipkin 官网:OpenZipkin · A distributed tracing system github地址:GitHub - openzipkin/zipkin: Zipkin is a distributed tracing system 这个是twitter开源出来的,也是参考Dapper的体系来做的。

Zipkin的java应用端是通过一个叫Brave的组件来实现对应用内部的性能分析数据采集。 Brave的github地址:github.com/openzipkin/… 这个组件通过实现一系列的java拦截器,来做到对http/servlet请求、数据库访问的调用过程跟踪。 然后通过在spring之类的配置文件里加入这些拦截器,完成对java应用的性能数据采集。

4、CAT github地址:GitHub - dianping/cat: Central Application Tracking 这个是大众点评开源出来的,实现的功能也还是蛮丰富的,国内也有一些公司在用了。 不过他实现跟踪的手段,是要在代码里硬编码写一些“埋点”,也就是侵入式的。 这样做有利有弊,好处是可以在自己需要的地方加埋点,比较有针对性;坏处是必须改动现有系统,很多开发团队不愿意。

5、Xhprof/Xhgui 这两个工具的组合,是针对PHP应用提供APM能力的工具,也是非侵入式的。 Xhprof github地址:GitHub - preinheimer/xhprof: XHGUI is a GUI for the XHProf PHP extension, using a database backend, and pretty graphs to make it easy to use and interpret. Xhgui github地址:GitHub - perftools/xhgui: A graphical interface for XHProf data built on MongoDB 我对PHP不熟,不过网上介绍这两个工具的资料还是蛮多的。

wi988

熔断、隔离、限流、降级

面对巨大的突发流量下,大型公司一般会采用一系列的熔断(系统自动将服务关闭防止让出现的问题最大化)、隔离(将服务和服务隔离,防止一个服务挂了其他服务不能访问)、限流(单位时间内之允许一定数量用户访问)、降级(当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用)措施。

下面介绍一下hystrix的运行流程(没找到架构图不好意思):

每一个微服务调用时,都会使用hystrix的command方式(上图的左上角那个),然后使用command同步的,或者是响应式的,或者是异步的,判断电路是否熔断(顺着图从左往右看),

如果断路则走降级fallback;

如果这个线闭合着,但是线程资源没了,队列满了,则走限流措施(看图的第5步);

如果走完了,执行成功了,则走run()方法,获取response,但是这个过程如果出错了,则继续走降级fallback.

同时,看图最上面有一个后缀是health的,这是一个计算整个链路是否健康的组件,每一步操作都被它记录着。

容器与服务编排引擎

从物理机到虚拟机,从虚拟机到容器;从物理集群到open stack,open stack到kubernetes;科技不断的变化,我们的认知也没刷新。

我们从容器开始说起,它首先是一个相对独立的运行环境,在这一点有点类似于虚拟机,但是不像虚拟机那样彻底。   虚拟机会将虚拟硬件、内核(即操作系统)以及用户空间打包在新虚拟机当中,虚拟机能够利用“虚拟机管理程序”运行在物理设备之上。虚拟机依赖于hypervisor,其通常被安装在“裸金属”系统硬件之上,这导致hypervisor在某些方面被认为是一种操作系统。一旦 hypervisor安装完成, 就可以从系统可用计算资源当中分配虚拟机实例了,每台虚拟机都能够获得唯一的操作系统和负载(应用程序)。简言之,虚拟机先需要虚拟一个物理环境,然后构建一个完整的操作系统,再搭建一层Runtime,然后供应用程序运行。       对于容器环境来说,不需要安装主机操作系统,直接将容器层(比如LXC或libcontainer)安装在主机操作系统(通常是Linux变种)之上。在安装完容器层之后,就可以从系统可用计算资源当中分配容器实例了,并且企业应用可以被部署在容器当中。但是,每个容器化应用都会共享相同的操作系统(单个主机操作系统)。容器可以看成一个装好了一组特定应用的虚拟机,它直接利用了宿主机的内核,抽象层比虚拟机更少,更加轻量化,启动速度极快。

相比于虚拟机,容器拥有更高的资源使用效率,因为它并不需要为每个应用分配单独的操作系统——实例规模更小、创建和迁移速度也更快。这意味相比于虚拟机,单个操作系统能够承载更多的容器。云提供商十分热衷于容器技术,因为在相同的硬件设备当中,可以部署数量更多的容器实例。此外,容器易于迁移,但是只能被迁移到具有兼容操作系统内核的其他服务器当中,这样就会给迁移选择带来限制。因为容器不像虚拟机那样同样对内核或者虚拟硬件进行打包,所以每套容器都拥有自己的隔离化用户空间,从而使得多套容器能够运行在同一主机系统之上。我们可以看到全部操作系统层级的架构都可实现跨容器共享,惟一需要独立构建的就是二进制文件与库。正因为如此,容器才拥有极为出色的轻量化特性。

我们最常用的容器是daocker,网址如下

Apache mesos

mesos是基于master,slave架构,框架决定如何利用资源,master负责管理机器,slave会定期的将机器情况报告给master,master再将信息给框架。master是高可用的,因为zk,也有leader的存在。下面是架构图

kubernetes
kubernetes是最近十分火热的开源容器编排引擎,具体可以参考kubernetes中文文档

Kubernetes设计理念和功能其实就是一个类似Linux的分层架构,先说说每一个Kubernetes节点内部,kubelet管理全局全局pod,而每一个pod承载着一个或多个容器,kube-proxy负责网络代理和负载均衡 。
Kubernetes节点外部,则是对应的控制管理服务器,负责统一管理各个节点调度分配与运行。

来源: https://juejin.im/post/5c0ba2bef265da614d08fefe