文章目录
- 1. 问题提出
- 2. 不可变对象的设计
- 3. 设计模式—享元模式
- 4. 享元模式案例—自定义连接池
- 5. final原理
1. 问题提出
我们知道,在并发环境中,引起并发问题的根源是共享变量的存在,而之所以共享变量之所以不安全,是因为多线程可以对它进行写操作。如果一个共享资源它是只读不可写的那么它自然不会引发并发安全问题。例如下面代码:
SimpleDataFormat sdf=new SimpleDataFormat("yyyy-MM-dd");
for(int i=0;i<10;i++){
new Thread(()->{
try{
log.debug("{}",sdf.parse("1951-04-21"));
}catch(Exception e){
log.error("{}",e);
}
}).start();
}
可变类多线程环境下就会出现下面问题会发生下面错误
下面对其进行改进:
@Slf4j
public class TestJMM {
public static void main(String[] args) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
for(int i=0;i<10;i++){
new Thread(()->{
{
synchronized (sdf){
try{
System.out.println(sdf.parse("1951-04-21"));;
} catch (Exception e) {
log.error("{}", e);
}
}
}
}).start();
}
}
}
我们对其加锁就解决了线程安全问题,但锁会对程序性能会有很大的影响。所以我们考虑使用不变类(DateTimeFormatter)来实现:
@Slf4j
public class TestJMM {
public static void main(String[] args) {
DateTimeFormatter sdf=DateTimeFormatter.ofPattern("yyyy-MM-dd");
for(int i=0;i<10;i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
2. 不可变对象的设计
这里拿常用的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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
/**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
从源码我们可以看出一些关键点:
- final关键字
- 属性用final修饰保证了该属性是只读的,不能修改
- 类用final修饰保证了类中的方法不能被覆盖,防止子类无意间破坏不可变性
- 保护性拷贝
String再对字符串进行修改时都会创建一个新的字符串对象,而不会对原有的字符串对象进行修改(这就是保护性拷贝)
3. 设计模式—享元模式
在第二部分介绍Stirng类时,在其对共享变量进行保护时利用了保护性拷贝技术。有修改行为发生时会创建新的对象,但这也带来了一个问题,就是会创建太多的对象。因此我们引入了一种设计模式—享元模式。
享元模式是一种结构型设计模式,旨在有效地支持大量细粒度的对象共享。它通过共享对象来减少内存消耗和提高性能。在某些情况下,一个应用程序可能需要创建大量相似的对象,这些对象之间的差异仅在于一些内部状态。如果为每个对象都分配独立的内存,会导致内存占用量急剧增加,降低系统的性能和效率。享元模式通过共享相同的对象实例,来减少对内存的需求。享元模式的核心思想是将对象的状态分为内部状态和外部状态。内部状态是对象的固定部分,可以在多个对象之间共享,而外部状态是对象的变化部分,需要在使用时传递给对象。享元模式将内部状态存储在享元对象中,并将外部状态作为参数传递给方法。
享元模式包含以下几个关键组件:
- 享元接口:定义了享元对象的通用接口,通过该接口可以获取内部状态和操作享元对象。
- 具体享元:实现了享元接口,并包含内部状态。具体享元对象需要是可共享的,也就是说它们可以在多个上下文中共享。
- 享元工厂:负责创建和管理享元对象。它维护一个享元池,用于存储已经创建的享元对象,以便在需要时进行复用。
当客户端需要使用享元对象时,可以通过享元工厂获取对象的实例。如果享元池中已经存在相应的对象实例,则直接返回该实例;如果不存在,则创建一个新的享元对象并添加到享元池中。这样可以确保相同的对象在多个地方共享使用,减少内存消耗。享元模式的优点在于减少内存消耗和提高性能。它适用于存在大量相似对象的场景,特别是当对象的内部状态较少并且可以共享时。然而,享元模式的缺点是需要维护一个享元池,可能会引入额外的复杂性。享元模式通过共享对象实例来减少内存消耗,提高系统性能。它是一种优化内存使用的设计模式,适用于需要大量细粒度对象共享的情况。
具体体现享元模式的思想的时包装类,载JDK中Boolean,Byte,Short,Long,Integer,Charaver等包装类提供了valueOf方法,例如Long的valueOf会环次-128-127之间的Long对象,在这个范围内重用对象,大于这个范围才会使用Long对象:
public static Long valueOf(long l){
final int offset=128;
if(l> -128 && l<=127){
return LongCache.cache[(int)l+offset];
}
return new Long(1);
}
注意:Byte,Short,Long的缓存范围都是-128~127,,Character缓存范围时0~-127,Integer的缓存范围时-128~127,最小值不能变,但最大值可以通过调整虚拟机参数-Ojava.lang.Integer.IntegerCache.high来改变,Boolean缓存了TRUE和FALSE
4. 享元模式案例—自定义连接池
- 分析:
例如:一个线上商城应用,QPS达到数干,如果每次都重新创建和关闭数据库连接,性能会受到极大的影响。这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样子既节约了连接的建立和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
public class TestJMM {
public static void main(String[] args) {
Pool pool=new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(()->{
Connection connection=pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.free(connection);
}).start();
}
}
}
class Pool{
//1. 连接池大小
private final int poolSize;
//2. 连接对象数组
private Connection[] connections;
//3. 连接状态数组
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();
}
}
//5. 从连接池中获取连接
public Connection borrow(){
while(true){
for (int i = 0; i < poolSize; i++) {
//获取空闲连接
if (states.get(i)==0) {
states.compareAndSet(i,0,1);
System.out.println("获取连接:"+i);
return connections[i];
}
}
//如果没有连接,当前线程进入连接
synchronized (this){
try{
System.out.println("没有连接进入等待");
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//6. 归还连接
public void free(Connection connection){
for (int i = 0; i < poolSize; i++) {
if (connections[i]==connection) {
states.set(i,0);
synchronized (this){
System.out.println("归还连接:"+i);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection{....}
5. final原理
- 设置final变量的原理
理解了Volatile原理,再对比final的实现就简单多了。首先给出一段程序:
public class TestFinal{
final int a=20;
}
我们使用javap查看其字节码
发现final变量的赋值操作也是通过putfield指令来完成的,同样这条指令之后也会加入写屏障,保证在其它线程读到它的时候不会出现0的情况。
- 获取final变量的原理
在字节码层面中,在读取final变量时不会直接读取final变量,而是将值比较小的常量的值拷贝到自己的线程的栈中,对值比较大的常量拷贝到自己的常量池中,访问速度会大大提高。