不可变的变量即使是共享的,也是线程安全的
可变类都不是线程安全的
例如:日期转换问题
package demo1;
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class DateTest {
public static void main(String[] args) throws Exception {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i <10 ; i++) {
new Thread(()->{
try {
sdf.parse("2021-10-11");
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
可变类在线程并发的时候会发生线程安全问题
我们可以在上面的例子上加锁来保证线程安全
package demo1;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTest {
public static void main(String[] args) throws Exception {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i <10 ; i++) {
new Thread(()->{
synchronized (sdf) {
try {
Date date= sdf.parse("2021-10-11");
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
不可变类的使用
但是用锁来解决回对性能有所影响,我们还可以jdk提供的不可变类
代码
package demo1;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
public class DateTest {
public static void main(String[] args) throws Exception {
DateTimeFormatter stf=DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i <10 ; i++) {
new Thread(() -> {
TemporalAccessor date = stf.parse("2021-10-11");
System.out.println(date);
}).start();
}
}
}
不可变类设计
另一个大家更为熟悉的String类也是不可变的I以它为例,说明- - 下不可变设计的要素
final的使用
发现该类.类中所有属性都是final的
- 属性用final修饰保证了该属性是只读的,不能修改
- 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝:
复制一个新的
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如substring等,那么下面就看-看这些方法是如何实现的,就以substring为例:
发现其内部是调用String的构造方法创建了-一个新字符串,再进入这个构造看看,是否对final char[] value做出了修改:
结果发现也没有,构造新字符串对象时,会生成新的char[] value,对内容进行复制。这种通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy)
享元模式 简介因为String不可变类每次调用方法的时候都要创建新的对象,这样会使得创建的对象过多,对于不可变类我们通常都会涉及一个设计模式-----享元模式
定义.
英文名称: Flyweight pattern.当需要重用数量有限的同一类对象时。享元模式是想最小化内存,对相同值进行一个共享
比如字符串abc,等下次要用得到的时候就不会创建了,直接使用上次创建的
在JDK中Boolean, Byte, Short, Integer, Long, Character等 包装类提供了valueOf方法,例如Long的valueOf会缓存128~-127之间的Long对象,在这个范围之间会重用对象,大于这个范围,才会新建Long对象:
注意:
Byte, Short, Long缓存的范围都是 -128—127
Character缓存的范围是0~127(大于0)
Integer的默认范围是-128-1271最小值不能变,但最大值可以通过调整虚拟机参数-
Djava. lang . Integer. IntegerCache.high来改变
Boolean缓存了TRUE和FALSE
除了包装类之外还有String串池以及BigDecimal、BigInteger都是不可变类,都是线程安全的(指的是单个方法是线程安全的,但是多个方法的组合并一定是线程安全的)。
DIY自定义一个数据库连接池
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。这时预先创建好一批连接,放入连接池。- -次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库.
package demo1;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class LianJieChi {
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();
}
}
}
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
总结
为什么获得连接的时候,用了cas还要将其进入wait等待呢?
cas适合短时间运行的代码片段,用cpu进行操作,但是我们获得数据库连接后是要进行增删改查的操作,这个时间比较长,为了避免cas不断尝试浪费cpu资源,所以将其进入的等待。
final原理
设置final变量的原理
理解了volatile原理,在对比final的是西安就比较简单了
写屏障有两个作用:
1、保证写屏障之前的指令不会被重排序到写屏障后面
2、写屏障之前的赋值操作会被同步到主存,对其他线程可见
获取final变量的原理
final修饰的变量数字比较小在栈内存中,数字比较大在常量池中,不被final修饰的变量是在堆中,在堆中读取的效率没有前两者快



