IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 并发编程——不可变类 -> 正文阅读

[Java知识库]并发编程——不可变类

目录

一、日期转换问题

解决

二、不可变类String?

final的特点

保护性拷贝

三、享元模式

体现?

享元模式之自定义连接池

四、无状态?


一、日期转换问题

一般使用的最多的是SimpleDateFormat,但是因为是不可变类,在多个线程操作共享变量的时候内部的数据会改变,造成线程安全问题,接下来看一下测试案例。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
     for (int i = 0; i < 10; i++) {//运行10个线程
           new Thread(() -> {
            try {
                log.debug("{}", sdf.parse("1951-04-21"));
                 } catch (Exception e) {
                        log.error("{}", e);
                 }
                 }).start();
        }

? 有很大几率出现?java.lang.NumberFormatException?或者出现不正确的日期解析结果?

17:27:19.884 a.ThreadTest15 [Thread-5] - {}
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at ThreadTest15.lambda$main$0(ThreadTest15.java:16)
	at java.lang.Thread.run(Thread.java:748)
17:27:19.885 a.ThreadTest15 [Thread-0] - {}
java.lang.NumberFormatException: empty String
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at ThreadTest15.lambda$main$0(ThreadTest15.java:16)
	at java.lang.Thread.run(Thread.java:748)
17:27:19.884 a.ThreadTest15 [Thread-3] - {}
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at ThreadTest15.lambda$main$0(ThreadTest15.java:16)
	at java.lang.Thread.run(Thread.java:748)
17:27:19.885 a.ThreadTest15 [Thread-4] - {}
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at ThreadTest15.lambda$main$0(ThreadTest15.java:16)
	at java.lang.Thread.run(Thread.java:748)
17:27:19.883 a.ThreadTest15 [Thread-9] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-7] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-2] - Thu Feb 21 00:00:00 CST 1952
17:27:19.883 a.ThreadTest15 [Thread-6] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-8] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-1] - Thu Feb 21 00:00:00 CST 1952
  • 解决

1、可以使用管程的思想,给同步代码块加锁,防止指令交错执行(效率降低)

2、可以将共享变量变成局部变量,成为每个线程私有的不被共享(重复创建对象内存消耗大)

3、使用不可变的内,保证内部数据不会存在并发修改

这里为方法3提供一种不可变类的类演示,DateTimeFormatter 类,查看类的解释可以知道

//官方说明这个类是不可变的类同时也是线程安全的 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?@implSpec
This class is immutable and thread-safe.

?演示

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
                log.debug("{}", date);
             }).start();
        }

具体的其他方法可查阅API,同时也有其他的线程安全的操作时间的类比如LocalDateTime

二、不可变类String?

string类是如何做的不可变的呢?查看一下String类的底层,有几个比较重要的参数

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

//...省略

//String内是被final修饰的, final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
//String类中内容都存储在value的char数组中,被final修饰的保证该元素只能读不能修改
//hash是String的hash值,它是在第一次调用hashcode的时候会被赋值保存在String内部
//后面再使用的话都是直接调用hash就行,同时也是private修饰的,没提供对外的修改方法,安全的

正好通过这来总结下final的特点

  • final的特点

1、属性用 final 修饰保证了该属性是只读的,不能修改
2、类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

那么final修饰的是value这个引用是如何保证char数组内部的值不会被改变呢?

  • 保护性拷贝

String的构造方法

public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

可以看出,在传入char数组的时候,String并未使用其外部的char数组作为内部value的值,而是通过重新拷贝创建了一个新的char数组得到一个新的引用地址,这样对外部char数组的改变影响不了String内部char数组的值,因为已经不是同一个数组了

那么subString方法不是相当于对外部提供了改变内部char数组的方法吗?

substring方法

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

前面两个判断是做数据判断,不重要,关键是最后一行调用了new String()方法,进入看一下

 public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

看到最后一行,同样也是对修改的char数组进行了裁剪拷贝,创建了一个新的char数组,这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】,可以理解成为多个线程操作同一个string对象的时候,内部分别会对每个操作线程创建不同的副本对象,可以理解成为这些副本对象成为了每个线程的私有属性了

测试一下

         char[] a = {'h','e','l','l','o'};
        String string = new String(a);
       Thread t1 = new Thread(() -> {

            log.debug(string.substring(1));
       },"t1");
        new Thread(() -> {
            log.debug(string.substring(2));
        },"t2").start();
        new Thread(() -> {
            log.debug(string.substring(3));
        },"t3").start();
        new Thread(() -> {
            log.debug(string.substring(3));
        },"t4").start();
        
        //线程t2,t3,t4执行完以后t1启动,主线程获取string的值
        Thread.sleep(1000);
        t1.start();
        log.debug(string);
    
18:40:47.133 a.ThreadTest15 [t3] - lo
18:40:47.133 a.ThreadTest15 [t4] - lo
18:40:47.133 a.ThreadTest15 [t2] - llo
18:40:48.137 a.ThreadTest15 [t1] - ello
18:40:48.137 a.ThreadTest15 [main] - hello

不难看出,每个线程无论对其进行了什么样的操作,都影响不了其他线程再次对其进行操作,证实了上面的说法,但是这样重复的创建副本会造成内存的消耗,有什么办法呢?String中就提供了享元模式这种解决办法。

三、享元模式

定义?英文名称:Flyweight pattern.?当需要重用数量有限的同一类对象时

什么情况下使用享元模式
(1)一个系统有大量的对象?
(2)这些对象耗费大量的内存?
(3)这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中删除时,每一个组都可以仅用一个对象代替?
(4)使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源,因此应当在有足够多的享元实现可供共享时才值的使用享元模式.?

享元模式的优缺点
享元模式的优点在于它大幅度地降低内存中的对象的数量,但是也为这一点付出了代价:?
(1)为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化?
(2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长.

比如一个字符串对象“abc”,当再次需要使用“abc”的时候直接调用已经创建了的“abc”对象

  • 体现?

在JDK中?Boolean,Byte,Short,Integer,Long,Character?等包装类提供了?valueOf?方法,String?串池,BigDecimal BigInteger

来看一下Long中的valueof方法?


public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // 当在这个范围内的时候,会直接在cache中选择
    return LongCache.cache[(int)l + offset];
     }
    //如果不在才会创建对象
    return new Long(l);
    }

 private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];
        //该类加载的时候创建了cache数组
        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
   }

注意:

1、Byte, Short, Long 缓存的范围都是 -128~127

2、Character 缓存的范围是 0~127

3、Integer的默认范围是 -128~127

?integer最小值不能变但最大值可以通过调整虚拟机参数
?????????????`?-Djava.lang.Integer.IntegerCache.high` 来改变

4、Boolean 缓存了 TRUE 和 FALSE

  • 享元模式之自定义连接池

class Pool {
    // 1. 连接池大小
    private final int poolSize;
    // 2. 连接对象数组
    private Connection[] connections;
    // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
    private AtomicIntegerArray states;
    // 4. 构造方法初始化
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("连接" + (i+1));
         }
        }
        // 5. 借连接
        public Connection borrow() {
        while(true) {
            for (int i = 0; i < poolSize; i++) {
                // 获取空闲连接
                if(states.get(i) == 0) {
                    //防止多个线程对同一连接对象的修改,保证原子性
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("borrow {}", connections[i]);
                        return connections[i];
                 }
             }
         }
        // 如果没有空闲连接,当前线程进入等待
         synchronized (this) {
            try {
                log.debug("wait...");
                this.wait();
                 } catch (InterruptedException e) {
                    e.printStackTrace();
             }
         }
       }
     }
       // 6. 归还连接
      public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                //只有持有当前链接的对象才能归还,所以不存在安全问题,普通set方法即可
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                   }
                        break;
           }
         }
     }
    }
class MockConnection implements Connection {
// 实现略
}

以上实现没有考虑:
1、连接的动态增长与收缩
2、连接保活(可用性检测)
3、等待超时处理
4、分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache?commons pool,例如redis连接池可以参考jedis中关于连接池的实现

四、无状态?

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】,在?设计?Servlet?时为了保证其线程安全,都会有这样的建议,不要为?Servlet?设置成员变量,这种没有任何成员变量的类是线程安全的。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-08 11:07:41  更:2021-08-08 11:08:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 23:26:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码