关注「Java视界」公众号,获取更多技术干货

【六】多线程 —— 共享模型之不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

比如类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性。

一、日期转换的问题

SimpleDateFormat 不是线程安全的,可能出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果,例如:

public class SimpleDateFormatTest {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    ConsoleUtil.print(sdf.parse("1951-04-21").toString());
                } catch (Exception e) {
                    ConsoleUtil.print(e.toString());
                }
            }).start();
        }
    }
}
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951E41951195119511951195119511951195119511E4"
2021-08-03 14:34:55 Mon Jul 21 00:00:00 CST 178958921
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: empty String
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951E4"
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: multiple points
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951"
2021-08-03 14:34:55 java.lang.NumberFormatException: multiple points

使用同步锁虽能解决问题,但带来的是性能上的损失,并不算很好, 加锁耗性能。

如果一个对象在不能够修改其内部状态(属性),因为不存在并发修改,那么它就是线程安全的,这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:

public class SimpleDateFormatTest01 {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    ConsoleUtil.print(dtf.parse("1951-04-21").toString());
                } catch (Exception e) {
                    ConsoleUtil.print(e.toString());
                }
            }).start();
        }
    }
}
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21

可以看 DateTimeFormatter 的文档:
在这里插入图片描述

二、final 的使用

IntegerDoubleStringDateTimeFormatterString以及基本类型包装类, 都是使用final来修饰的。

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

final原理

public class TestFinal {
	final int a = 20; 
}

字节码:

0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
 <-- 写屏障
10: retu

final 变量的赋值通过 putfield 指令来完成,在这条指令之后会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。

三、实现一个简单的连接池

一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。

这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。

/**
 * Description: 简易连接池
 *
 * @author guizy
 * @date 2020/12/29 21:21
 */
public class Test2 {
    public static void main(String[] args) {
        /*使用连接池*/
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.borrow();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(conn);
            }).start();
        }
    }
}

@Slf4j(topic = "guizy.Pool")
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]);//使用AtomicIntegerArray保证states的线程安全
        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)) {//使用compareAndSet保证线程安全
                        log.debug("borrow {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待, 如果不写这个synchronized,其他线程不会进行等待, 
            // 一直在上面while(true), 空转, 消耗cpu资源
            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) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class MockConnection implements Connection {

    private String name;

    public MockConnection(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MockConnection{" +
                "name='" + name + '\'' +
                '}';
    }
    
    // Connection 实现方法略
}
2021-08-03 15:01:01 wait...
2021-08-03 15:01:01 borrowMockConnection{name='连接2'}
2021-08-03 15:01:01 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接1'}
2021-08-03 15:01:02 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接1'}
2021-08-03 15:01:02 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接2'}
2021-08-03 15:01:02 borrowMockConnection{name='连接2'}
2021-08-03 15:01:03 freeMockConnection{name='连接2'}
2021-08-03 15:01:03 freeMockConnection{name='连接1'}

在这里插入图片描述

posted @ 2022-06-25 14:01  沙滩de流沙  阅读(13)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货