Java并发编程学习(7):线程安全与反序列化安全的单例模式

news/2024/5/19 5:20:40 标签: java, 反射, 设计模式, 单例模式, 线程安全

单例模式

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

特点

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

优缺点

单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

扩展

单例模式可扩展为有限的多例(Multitcm)模式,这种模式可生成有限个实例并保存在 ArrayList 中,客户需要时可随机获取。

线程安全单例模式

Java可以通过以下5中方式实现线程安全单例模式

实现1:饿汉式

类加载时会同时创建单例。

java">public final class Singleton1 {
    private Singleton1(){}
    
    private static final Singleton1 INSTANCE = new Singleton1();
    
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

问题:

  1. 为什么类声明上要加final
    答:防止该类在被继承时修改了构造方法或getInstance()方法,进而导致单例逻辑被破坏。
  2. 如果单例类实现了序列化接口,还要做什么来防止反序列化破坏单例?
    答:需要在单例类中额外实现一个方法readResolve(),并在方法中将单例对象返回。之所以可以这么做,是因为java在反序列化时,如果发现该类存在readResolve()方法,会将该方法的返回值作为反序列化的结果,而不会对序列化后的对象进行反序列化。
java">public final class Singleton1 implements Serializable {
    private Singleton1(){}

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance(){
        return INSTANCE;
    }
    public Object readResolve(){
        return INSTANCE;
    }
}
  1. 为什么将构造方法设置为私有的?
    答:将构造方法设置为私有,可以防止其它类通过new Singleton1()的方式创建对象
  2. 单例模式是否能够防止通过反射创建新的实例?
    答:不能防止通过反射创建新的实例。通过反射中的getDeclaredConstructor()方法,我们可以获得类的无参构造器,然后可以通过setAccessible()方法修改构造器的访问权限,之后便可以通过该构造器创建实例对象,具体使用方法如下。注意:无法通过getConstructor()获得无参构造器,因为getConstructor()只能够获得类型为public的构造器。
java">class ReflectDemo{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 通过getInstance方法获得类对象的实例
        Singleton1 instanceFromMethod = Singleton1.getInstance();
        // 获得Singleton1的无参构造器
        Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
        // 将构造器变成可访问的
        constructor.setAccessible(true);
        // 用该构造器创建实例
        Singleton1 instanceFromConstructor = constructor.newInstance();
        // 比较两个实例是否相同
        System.out.println("instanceFromMethod == instanceFromConstructor ? " + (instanceFromMethod == instanceFromConstructor));
    }
}
  1. 能否保证private static final Singleton1 INSTANCE = new Singleton1();在创建单例对象时是线程安全的?
    答:可以。在第一次调用Singleton1时,代码private static final Singleton1 INSTANCE = new Singleton1();会在类加载器中执行,JVM会保证类加载时的线程安全(加锁的),进而保证了创建单例对象时的线程安全。加载类的代码如下所示,通常情况下getClassLoadingLock(name)会返回this,即类加载器对象本身。
java">    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            // 加载类的具体操作...
            return c;
        }
    }
  1. 为什么要提供静态方法而不是直接将INSTANCE设置为public?
    答:首先,将INSTANCE设置为public是没有问题的。但是提供静态方法能够:(1)提供了更好的封装性(2)解耦合,方便后续代码的修改和改进(3)提供泛型的支持。

实现2:使用方法级synchronized的懒汉式

java">public final class Singleton2 implements Serializable {
    private Singleton2(){}

    private static Singleton2 INSTANCE = null;

    public synchronized static Singleton2 getInstance(){
        if (INSTANCE != null){
            return INSTANCE;
        }
        INSTANCE = new Singleton2();
        return INSTANCE;
    }

    public Object readResolve(){
        return getInstance();
    }
}

缺点:将整个getInstance()方法锁死,锁的开销较大

实现3:使用double-check的懒汉式

java">public final class Singleton3 implements Serializable {
    private Singleton3(){}

    private static volatile Singleton3 INSTANCE = null;

    public static Singleton3 getInstance(){
        if (INSTANCE != null){
            return INSTANCE;
        }
        synchronized (Singleton3.class){
            if (INSTANCE == null){
                INSTANCE = new Singleton3();
            }
            return INSTANCE;
        }
    }

    public Object readResolve(){
        return getInstance();
    }
}

问题

  1. 为什么要加 volatile?
    答:为了防止指令重排现象的产生。在执行INSTANCE = new Singleton3();时,通常的做法为先初始化对象,然后给静态变量赋值。但是JVM可能会对代码进行指令重排,其先后顺序变成先给静态变量赋值然后初始化对象,这种情况会造成比较严重的bug,具体案例见下图。
线程1 线程2 INSTANCE 初始化时INSTANCE为null 线程1启动 调用getInstance() 判断INSTANCE是否为null:为null 加锁 判断INSTANCE是否为null:为null 发生指令重排 给INSTANCE赋值,但还没有初始化 线程2启动 判断INSTANCE是否为null:不为null 获得INSTANCE的引用 调用INSTANCE的方法 程序出错,线程退出 初始化INSTANCE 获得INSTANCE的引用 解锁 后续操作 线程1 线程2 INSTANCE
  1. 为什么synchronized中需要再次进行空判断?
    答:由于synchronized的存在,多个线程中仅有一个线程能够创建对象,当该线程退出时,会有其它线程抢到锁。如果后续线程不进行空判断,那么它们每个线程都会生成一个对象,不符合单例模式的要求。因此,在synchronized中进行空判断,可以确认是否已经有其它线程已经创建了实例,如果是,这直接使用创建好的实例即可。

实现4:枚举类

枚举类实现单例时Java官方推荐的做法。

java">public enum Singleton4{
    /**
     *  单例
     */
    INSTANCE;
}

它在IDEA中反编译的代码

java">public enum Singleton4 {
    INSTANCE;

    private Singleton4() {
    }
}

它在JclassLib中反编译后的<clinit>方法如下

java"> 0 new #4 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4>
 3 dup
 4 ldc #7 <INSTANCE>
 6 iconst_0
 7 invokespecial #8 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.<init>>
10 putstatic #9 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.INSTANCE>
13 iconst_1
14 anewarray #4 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4>
17 dup
18 iconst_0
19 getstatic #9 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.INSTANCE>
22 aastore
23 putstatic #1 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.$VALUES>
26 return

问题

  1. 枚举单例是如何限制实例个数的?
    答:从IDEA的反编译结果中可以看到,枚举类的构造器是私有的,因此不能通过构造器创建对象。另一方面,通过JclassLib反编译后的字节码可以看出,INSTANCE其实是枚举类中实例化的一个静态成员变量。
  2. 枚举单例在创建时是否有并发问题?
    答:没有。INSTANCE作为静态成员变量,会在类加载时被创建,由类加载器保证其线程安全
  3. 枚举单例能否被反射破坏单例?
    答:不能。我们可以用类似在实现1中所用到的方反射法进行尝试,但是会发现无法通过getDeclaredConstructor()方法获得枚举类的无参构造器。然而,我们可以使用getDeclaredConstructors()获得该类的所有构造器,然后对它们进行遍历。但是,在执行newInstance()方法时,代码会抛出异常IllegalArgumentException: Cannot reflectively create enum objects,我们也无法通过发射实例化枚举类的对象,进而无法破坏单例模式
java">class ReflectEnumDemo{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 通过getInstance方法获得类对象的实例
        Singleton4 instanceFromEnum = Singleton4.INSTANCE;
        // 获得Singleton4的所有构造器
        Constructor<?>[] constructors = Singleton4.class.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            constructor.setAccessible(true);
            Object instanceFromConstructor = constructor.newInstance();
            System.out.println("(instanceFromConstructor == instanceFromEnum) ? " + (instanceFromConstructor == instanceFromEnum));
        }
    }
}
  1. 枚举单例是否可以被序列化和反序列化?如果可以,枚举类能否被反序列化破坏单例?
    答: 枚举单例可以被序列化和反序列化,因为枚举类Enum实现了Serializable接口。但是反序列化无法破坏枚举类单例。这其实涉及到反序列化原理,枚举类与普通类在反序列化的实现上并不相同,具体的源码分析见本文最后一部分。普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。枚举类型在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.EnumvalueOf()方法来根据名字查找枚举对象。
  2. 枚举单例属于懒汉式还是饿汉式?
    答:单例对象作为枚举类的静态成员变量,会随着类的加载而产生,属于饿汉式。
  3. 枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?
    答:枚举类可以重写其构造方法,以便于在创建单例时加入初始化逻辑,例如下面这段代码。
public enum Singleton4{
    /**
     *  单例
     */
    INSTANCE("单例");
    
    String name;
    String desc;

    Singleton4(String name) {
        this.name = name;
        this.desc = "这是一个"+name;
    }
}

实现5:静态内部类

java">public final class Singleton5 implements Serializable {
    private Singleton5(){}

    private static class LazyHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance(){
        return LazyHolder.INSTANCE;
    }

    public Object readResolve(){
        return getInstance();
    }
}

问题

  1. 此方法属于懒汉式还是饿汉式?
    答:此方法属于懒汉式。因为类的加载本身时懒惰的,它只有在被用到时才会加载。在本场景中,如果只使用了Singleton5而没有调用其getInstance()方法,则不会调用到其中静态内部类LazyHolder,也不会触发静态内部类的加载,因此此时LazyHolder中的INSTANCE还没有被实例化,直到有方法调用了Singleton5.getInstance()

  2. 在调用getInstance()时是否会有并发问题?
    答:不会。类加载会保证创建时的线程安全

枚举类反序列化的源码解读

反序列化的使用例

java">public class EnumUnserialize {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        TestEnum instance = TestEnum.INSTANCE;
        String pathname = "instance.obj";
        // 1. 序列化
        // 1.1 搭建输出目标流,这里将其输出到文件中
        FileOutputStream fos = new FileOutputStream(pathname);
        // 1.2 搭建对象输出流
        ObjectOutput objectOutput = new ObjectOutputStream(fos);
        // 1.3 写入对象
        objectOutput.writeObject(instance);
        // 1.4 关闭流
        objectOutput.close();

        // 2. 反序列化
        // 2.1 获取文件对象
        File file = new File(pathname);
        // 2.2 获取文件输入流
        FileInputStream fis = new FileInputStream(file);
        // 2.3 搭建对象输入流
        ObjectInput objectInput = new ObjectInputStream(fis);
        // 2.4 读出对象,并进行强转
        TestEnum object = (TestEnum)objectInput.readObject();
        // 2.5 关闭流
        objectInput.close();

    }
}

enum TestEnum{
    INSTANCE;
}

运行该代码,没有发现异常,说明代码正常执行,观察文件目录,发现确实生成了instance.obj这一文件。

在该代码中,我们主要关注objectInput.readObject()的具体实现过程,下面对该代码进行一个解读。

源码解读

readObject()方法

源码中readObject()方法并不长,我们也很容易将其进行拆解。

java"> public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
                freeze();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

可以观察到,最后返回返回值来源有两处,一处位于前3行的if语句中,另一处则来源于readObject0(false)
我们首先对if语句进行判断,发现enableOverride只会在初始化ObjectInputStream对象时会被赋值。
其中,我们调用public ObjectInputStream(InputStream in)时会赋值为false,调用protected ObjectInputStream()时会被赋值为true。显然,我们无法调用后者,因此enableOverride可以默认为false,此时判断条件不成立,返回值只会来源于readObject0(false)

readObject0()方法

源码中readObject0()方法比较长,但是不难发现,其中大部分的返回来源于switch语句,其返回策略依赖于变量tc

java">private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

通过代码变量的含义,不难猜到普通对象的返回值来源于return checkResolve(readOrdinaryObject(unshared));,而枚举类对象的返回值来源于return checkResolve(readEnum(unshared));。但是为了严谨起见,我们还是先考察变量tc的产生方式。

考察bin.peekByte()的含义

变量tctc = bin.peekByte()处被赋值,bin是一个流对象,bin.peekByte()的大意是获取流对象中的一个字节,而且该字节恰好是文件的第一个字节
这里设计到了序列化时的一点源码,在ObjectOutputStream的序列化方法writeObject0()中,我们能过够看到这样的语句,其中obj就是我们传入的对象。可以看到,根据obj类型的不同,ObjectOutputStream会采取不同的序列化方法。

java">            // remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

我们主要考察writeEnum()writeOrdinaryObject()这两个方法。
writeEnum()方法中,我们发现代码的第一行就是bout.writeByte(TC_ENUM);;而在writeOrdinaryObject()中也存在首先运行bout.writeByte(TC_OBJECT);的情况。而其中的TC_ENUMTC_OBJECT恰好与之前readOnject0()中的switch语句对应上了!
因此,我们之前的猜想是正确的:序列化后的普通对象会通过checkResolve(readOrdinaryObject(unshared))进行反序列化;而序列化后的枚举类对象会通过checkResolve(readEnum(unshared))进行反序列化。

readOrdinaryObject()方法

源码中readOrdinaryObject()比较复杂,我节选了我们关心的部分进行解读。

java">Object obj;
try {
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}
// 其它处理。。。
if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}
return obj;

其中,变量desc是一个ObjectStreamClass类型的变量,它可以看作obj的类对象的包装。
可以看到,obj的首要生成方式为调用newInstance()进行实例化,此时JVM会为其创建一个新的实例对象,这种做法会破坏单例模式
但是在后续代码中,如果发现类对象存在readResolve()方法,则会在Object rep = desc.invokeReadResolve(obj);调用其方法并在handles.setObject(passHandle, obj = rep);将原来的obj替换掉。因此,我们可以通过重写readResolve()方法的维持单例模式

readEnum()方法

在介绍readEnum()之前,我们可以先看一下在ObjectOutputStream中的writeEnum()方法,以便于更好的理解readEnum()
下面是writeEnum()方法的源码,一共就5行。其中第一行之前已经解释过了,向文件中写入了1字节的对象类型。在第三行,又写入了一个ObjectStreamClass,这里可以理解为写入了枚举对象所在类的信息。在第五行,方法写入了枚举对象的name属性(这里的name指的是作为枚举类对象的属性,在实现5中指的是"INSTANCE",而并非我们定义在枚举类中的成员变量name)。

java">    private void writeEnum(Enum<?> en,
                           ObjectStreamClass desc,
                           boolean unshared)
        throws IOException
    {
        bout.writeByte(TC_ENUM);
        ObjectStreamClass sdesc = desc.getSuperDesc();
        writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
        handles.assign(unshared ? null : en);
        writeString(en.name(), false);
    }

综上,我们大概了解了序列化后的枚举对象的组成部分,现在回头查看readEnum()方法的源码,可以发现一一对应之处。

java">    private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

在该源码中,方法首先通过readByte()读取了1字节的数据并进行类型判断。
然后,方法通过readClassDesc(false)读取了类对象的信息,并判断该类是否真实是枚举类。
最后,方法通过String name = readString(false);读取了枚举对象的name属性。但与readOrdinaryObject()不同的是,方法并没有调用newInstance()产生实例,而是通过调用Enum<?> en = Enum.valueOf((Class)cl, name);获取枚举类中已有的对象。
综上,在整个过程中,没有新的实例对象产生,单例模式得到维护。


http://www.niftyadmin.cn/n/1279175.html

相关文章

用Perl在终端上打印彩色字符

如果在使用Perl的过程中&#xff0c;要在终端上打印出彩色字符&#xff0c;可以使用CPAN中的Term::ANSIColor模块。现在简单地介绍一下这个模块的几种使用方法&#xff1a;1&#xff09;use Term::ANSIColor;color()是把任意数量的颜色属性串成一个用空格分隔的字符串并存到变量…

如何装ipython_安装ipython以及完善ipython等功能

安装ipython下载&#xff1a;ipython-2.3.0.tar.gz及ActivePython-2.7.8.10-linux-x86_64.tar.gz和readline-6.2.4.1.tar.gz安装Python2.7&#xff1a;tar zxvf ActivePython-2.7.8.10-linux-x86_64.tar.gzcd ActivePython-2.7.8.10-linux-x86_64./install.shln -s /opt/Active…

软件测试接口测试的测试用例类型

接口测试的目的是为了测试接口&#xff08;听起来怪怪的&#xff09;&#xff0c;尤其是那些与系统相关联的外部接口&#xff0c;测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;还包括处理的次数。本文主要介绍了接口测试用例类型&#xff0c;让我们…

bootstrap在index页中嵌入其他页面_如何在PPT中保存特殊字体?

很多同学在做ppt的时候都遇到过这样的问题&#xff0c;原本我们设计得美美的PPT页面&#xff0c;精心选用了高端大气有品位字体&#xff0c;结果拿到别的电脑上一播放&#xff0c;文字突然就变丑了。甚至排版也因为文字样式的不同&#xff0c;变得不再整齐有序。你的设计心血就…

mongodb-java-driver基本用法

1、先下载mongodb-java-driver 目前最新版本是2.9.3 2、下面是基本的CRUD示例代码: 1 package com.cnblogs.yjmyzz.cache.test;2 3 import com.google.gson.Gson;4 import com.mongodb.BasicDBObject;5 import com.mongodb.DB;6 import com.mongodb.DBCollection;7 import com.…

快速学习-智能合约概述

智能合约概述 Solidity中合约 一组代码&#xff08;合约的函数 )和数据&#xff08;合约的状态 &#xff09;&#xff0c;它们位于以太坊区块链的一个特定地址上代码行 uint storedData; 声明一个类型为 uint (256位无符号整数&#xff09;的状态变量&#xff0c;叫做 stored…

Java并发编程学习(8):CAS机制、原子变量

示例引入 我们需要执行一个高并发削减账户余额的逻辑。为方便起见&#xff0c;我们将账户类设计为一个抽象类&#xff0c;并且只对中的静态demo()方法进行了实现。 在demo()方法中&#xff0c;我们会生成若干线程&#xff0c;每个线程执行同样的逻辑&#xff1a;扣除账户中的余…

redhat 复制文件夹及子文件夹_Linux系统怎么复制文件夹下的全部文件到另外文件夹?...

在Linux系统中复制或拷贝文件我们可以用cp或者copy命令&#xff0c;但要对一个文件夹中的全部文件复制到另外一个文件夹中去&#xff0c;如何进行操作呢? 下面简单来介绍一下。copy命令1、copy ,cp&#xff0c;该命令的功能是将给出的文件或目录拷贝到另外一个文件或目录中。语…