文章目录
1. volatile关键字
1. JMM简介
- JMM的细节在JVM的总结中已经将结果了,在这里不在赘述,只总结必要的内容;
- Java内存模型在Java层面定义了主存,工作内存的抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等,JMM主要体现在以下的几个方面:
- 原子性:保证指令不会受线程上下文的影响;
- 可见性:保证指令不会受CPU缓存的影响;
- 有序性:保证指令不会受CPU指令并行优化的影响;
- JMM使得每一个线程拥有自己的本地内存,也叫作工作内存,他们之中存储的数据是来自主存共享数据的副本;本地内存与主存的映射是由JMM完成的;但是以上的工作机制在有些时候会出现奇怪的问题;
2. 可见性
-
对于以下的例子来说,主线程对run的修改对于其它的线程来说是不可见的,最终会导致程序出现意想不到的效果:
@Slf4j(topic = "c.Test") public class Test { static Boolean run = true; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (run) { //如果run为真,则一直执行 } }).start(); Thread.sleep(1000); System.out.println("改变run的值为false"); run = false; } }
输出:
可以看到即使主线程将run的值改编为了false,但是循环的线程还是一直在运行,并没有如期的停下来; -
使用synchronized解决以上的问题
@Slf4j(topic = "c.Test") public class Test { static Boolean run = true; final static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (run) { synchronized (lock) { } } }).start(); Thread.sleep(1000); synchronized (lock) { System.out.println("改变run的值为false"); run = false; } } }
输出:
可以看到循环的线程正常的终止了;
-
造成可见性的原因:初始状态时线程在主内存之中读取了run的值,并将其放置于自己的工作内存中;由于线程会频繁的从主内存之中读取run的值,所以JIT即时编译器就索性将run的值缓存到了自己工作内存所对应的高速缓存中,以减少对run进行的内存访问,提高运行的效率;1s之后主线程修改了run的值,并同步至主存,但是线程t是从自己的工作内存对应的高速缓存中读取的数据,并不会从主存中读取数据,结果就是读取到的值一直都是旧的值;这就是JMM带来的可见性的问题:
当JIT对数据进行缓存之后:
-
对以上问题的解决方法:一种是使用volatile关键字,表示一个变量是易变的,用来修饰变量的时候就可以避免线程从自己的高速缓存中读取数据,而是直接操作主存:
//使用易变关键字 volatile static Boolean run = true; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (run) { //如果run为真,则一直执行 } }).start(); Thread.sleep(1000); System.out.println("改变run的值为false"); run = false; }
volatile关键字虽说可以保证在多个线程之间一个线程所做的修改对其它的线程是可见的,但是并不能保证原子性,在多线程的情况之下还是会出现指令交错,volatile适合于一个写线程多个读线程的情况:
// 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值,得到的是 i = 0 getstatic i // 线程1-获取静态变量i的值,得到的是 i = 0 iconst_1 // 线程1-准备常量1 iadd // 线程1-自增,此时 i = 1 putstatic i // 更新修改 iconst_1 // 线程2-准备常量1 isub // 线程2自减 使得i = -1 putstatic i // 线程2将修改后的值更新回去,使得最终的i的值为-1,而不是我们期望的0
从以上的例子中可以看到volatile关键字是不能够保证原子性的;
3. 相关的模式
-
之前提到过可以使用interrupt方法终止线程来实现两阶段终止模式;可以使用volatile关键字优化之前的实现:
@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) throws InterruptedException { final Monitor monitor = new Monitor(); monitor.staart(); Thread.sleep(1); monitor.stop(); } } @Slf4j(topic = "c.Monitor") class Monitor { private volatile boolean stop = false; public void staart() { Thread monitor = new Thread(() -> { while (true) { if (stop) { log.debug("处理后事"); break; } log.debug("执行监控"); } }, "monitor"); monitor.start(); } public void stop() { stop = true; } }
输出:
08:35:58.222 c.Monitor [monitor] - 执行监控 08:35:58.225 c.Monitor [monitor] - 处理后事
-
也可以使用volatile关键字实现犹豫模式,犹豫模式用于当一个线程发现另一个线程已经做了某一件事的时候,该线程就不会再次做这件事,而是直接返回,复用之前的线程完成的任务;类似于单例模式:
@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) throws InterruptedException { Monitor monitor = new Monitor(); monitor.start(); monitor.start(); monitor.start(); Thread.sleep(1000); monitor.stop(); } } @Slf4j(topic = "c.Monitor") class Monitor { Thread monitor; private volatile boolean stop = false; private boolean started = false; public void start() { synchronized (this) { if (started) { // 已经启动了,直接返回 return; } // 其他线程没有执行启动的操作,本线程启动之后将标记置为true started = true; } monitor = new Thread(() -> { while (true) { if (stop) { log.debug("处理后事"); break; } log.debug("执行监控"); try { Thread.sleep(1000); } catch (InterruptedException e) { log.debug("睡眠被打断"); } } }, "monitor"); monitor.start(); } public void stop() { stop = true; monitor.interrupt(); } }
输出:
09:07:15.916 c.Monitor [monitor] - 执行监控 09:07:16.916 c.Monitor [monitor] - 睡眠被打断 09:07:16.916 c.Monitor [monitor] - 处理后事
可以看到在一个创建一次之后,其余的创建操作都不会被执行;
4. 有序性
-
JIT在执行优化的时候可能进行指令重排,使得指令的执行顺序在保证结果的情况下发生变化,但是这样会出现一些奇怪的效果;至于指令重排,是因为现代的处理器几乎都是多核的,并且支持多级指令流水线,可以提升指令执行的吞吐率;
-
对于一系列的指令,比如
取指令-指令译码-执行指令-内存访问-数据写回
,如果处理器在执行它们的时候不作任何的优化,那么效率将会是很差的,现在的处理器的做法几乎都是使用流水线的方式同时执行它们,而不仅仅是串行的执行;同时执行以上的五条指令的处理器称为五级指令流水线,这时CPU可以在一个时钟周期内同时运行五条指令的不同阶段,虽说流水线的技术不能够缩短单条指令的执行时间,但是它可以提高指令执行的吞吐率:
但是在多线程的时候指令重排会出现奇怪的效果,我们应该禁止指令重排:int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; }
线程1执行actor1之中的代码,线程2执行actor2中的代码;I_Result中有一个属性r1用来保存结果,那么以上的代码可能会产生以下的几种结果:
- 线程1先执行,这时ready = false,所以会进入else分支使得r1 = 1;
- 线程2先执行,使得num = 2,但是还没来得及使ready = true就被线程1抢占执行,结果还是 r1 = 1;
- 线程2执行到了ready = true,线程1执行使得r1 = 4;
- 但是还有0的结果,线程2先执行ready = true,切换到线程1,此时传递给现线程1的num的值是0,r1在执行赋值操作之后结果为0,之后再切换到线程2,执行num = 2;也就是说在执行actor2方法的时候改变了其中的两个赋值操作的执行顺序;这就是指令重排序的效果,虽说产生0的情况发生的几率是很小的,但是JIT还是有可能将两个赋值操作的指令进行重排序;
-
为了避免类似以上的结果的产生,可以使用volatile关键字修饰变量;比如
volatile boolean ready = false;
,这样的话对read进行的操作就不会被进行重排序,产生的结果也将会是我们所能想到的结果,而不是那些奇怪的值; -
指令重排序只会对不存在数据依赖关系的操作进行重排序,比如对于a = 1, b = a这类的操作是不能够进行重排序的,因为b的结果依赖于a,所以在编译时和处理器运行时这两个操作都不会重排序;重排序终究为了提高指令执行的效率,但是不管怎么优化,单线程下程序的执行结果是不能够被改变的,比如a=1;b=2;c=a+b这三个操作中前两个是可以被重排序的,但是第三个操作不可以被重排序;因为必须要保证c最终的结果就是a+b=3;虽说重排序在单线程的情况下是安全的,但是在多线程的时候并不能保证指令最终的结果是正确的,因为多线程是各自被重排序的,结合在一起的时候顺序仍然可能是错误的;
-
使用synchronized是不能保证有序性的,但是如果一个变量整个都在synchronized代码块的保护之中,那么变量就不会被多个线程访问,自然就不需要考虑有序性的问题;
5. volatile原理
- volatile底层是基于内存屏障实现的,对volatile变量的写指令之后会加入写屏障,写屏障会保证在屏障之前的写操作会同步到主存中;对volatile变量的读指令前会加入读屏障,保证读屏障之后的读操作都直接读取主存中的数据;所以当对一个数据进行修改的时候,再次读取的话读到的就是位于主存中的最新的数据;
- 同时写屏障和读屏障还会确保JIT不会将写屏障之前的代码重排序到写屏障之后;不会将读屏障之后的代码排在读屏障之前;所以就确保了有序性,但是不能够防止多线程时指令的交错执行;多线程下依然是不安全的,不保证原子性;
6. 双重检查锁
-
synchronized关键字可以保证原子性,可见性和有序性,但是有序性是有前提的,在synchronized代码块中的共享变量不会在代码块之外使用,否则的话有序性是无法满足的;以下的例子就展示了不能保证有序性的情况:
// 最开始的单例模式是这样的 public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } return INSTANCE; } } /* 上面代码的效率是有问题的, 当我们创建了一个单例对象后, 后来的还是会获得锁, 严重影响性能,再次判断INSTANCE==null时INSTANCE肯定不为null, 然后线程返回刚才创建的INSTANCE; 这样就导致了很多不必要的判断; 所以要双重检查, 在第一次线程调用getInstance()时直接在synchronized外判断instance对象是否存在了, 如果不存在, 才会去获取锁,然后创建单例对象; 第二个线程调用getInstance(), 会进行 if(instance==null)的判断, 如果已经有单例对象, 此时就不会再去同步块中获取锁了,提高效率 */ public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { if(INSTANCE == null) { // t2 // 首次访问会同步,而之后的使用没有 synchronized synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } // 但是上面的if(INSTANCE == null)判断代码没有在同步代码块synchronized中, // 不能享有synchronized保证的原子性、可见性、以及有序性。所以可能会导致指令重排
以上实现的特点是懒汉式单例,首次使用getInstance的时候才会使用synchronized加锁;后续使用的时候无需进行加锁的操作;在多线程的情况下getInstance的字节码如下所示:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 // 判断是否为空 // ldc是获得类对象 6: ldc #3 // class cn/itcast/n5/Singleton // 复制操作数栈栈顶的值放入栈顶, 将类对象的引用地址复制了一份 8: dup // 操作数栈栈顶的值弹出,即将对象的引用地址存到局部变量表中 // 将类对象的引用地址存储了一份,是为了将来解锁用 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 // 新建一个实例 17: new #3 // class cn/itcast/n5/Singleton // 复制了一个实例的引用 20: dup // 通过这个复制的引用调用它的构造方法 21: invokespecial #4 // Method "<init>":()V // 最开始的这个引用用来进行赋值操作 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn
其中第17行表示创建一个对象,讲对象引入栈,第20行表示复制一份对象的引用,21行表示利用一个对象引用,调用构造方法,24行表示利用一个对象引用,赋值给static INSTANCE,但是JIT可能会将其优化为先执行24,再执行21,如果在构造方法之中执行复杂的初始化操作,最终得到的对象就是不完整的;
-
解决的方式就是使用volatile关键字修饰INSTANCE变量,使得对INSTANCE变量的赋值操作(也就是写操作)不会被重排序,使得21行的操作不会被重排到24行的后面;利用volatile来确保有序性;
7. happens-before
-
该模式规定了对共享变量的写操作对其它线程是可见的,如果没有以下的这些规则的话JMM就不能保证最基本的多线程操作的合理性;
-
线程解锁之前对变量的写操作对于接下来获取到锁的线程是读可见的:
static int x; static Object m = new Object(); new Thread(()->{ synchronized(m) { x = 10; } },"t1").start(); new Thread(()->{ synchronized(m) { System.out.println(x); } },"t2").start(); // 10
-
线程对volatile变量的写操作对接下来的其它线程执行的读操作是可见的:
volatile static int x; new Thread(()->{ x = 10; },"t1").start(); new Thread(()->{ System.out.println(x); },"t2").start();
-
线程调用start前对变量的写操作对该线程开始之后对该变量进行的读操作是可见的;
static int x; x = 10; new Thread(()->{ System.out.println(x); },"t2").start();
-
线程结束前对变量的写操作在线程结束后对变量的读操作是可见的;
static int x; Thread t1 = new Thread(()->{ x = 10; },"t1"); t1.start(); t1.join(); System.out.println(x);
-
t线程在被打断之前进行的写操作对于其它线程得知t被打断之后进行的读操作是可见的:
static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); // 10, 打断了, 读取的也是打断前修改的值 break; } } },"t2"); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },"t1").start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); // 10 }
8. 有关volatile的一些习题
-
如果希望doInit方法只被执行一次,那么以下的方式可以实现吗:答案是不可以实现,因为volatile不能保证原子性,在多个线程同时调用init方法的时候,如果在一个线程得到了initialized = false的结论之后被其他的线程抢占了,那么其他的线程得到的结论也会是initialized = false,所以会多次调用提到的方法;使用synchronized关键字来保证原子性;
public class TestVolatile { volatile boolean initialized = false; void init() { if (initialized) { return; } doInit(); initialized = true; } private void doInit() { } }
-
实现线程安全的单例模式:单例模式有很多的实现方法,但是有一些实现方式是不安全的,同时各个实现方式中被分为了两大类,分别是懒汉式和饿汉式,懒汉式是指只有在对象被使用的时候才会创建对象,并且确保只会创建一份;饿汉式是指类加载的时候就创建出一个单例的对象;
-
第一种实现方式:饿汉式
public final class Singleton implements Serializable { private Singleton() {} private static final Singleton INSTANCE = new Singleton(); public static Singleton getInstance() { return INSTANCE; } public Object readResolve() { return INSTANCE; } }
-
为什么要在类声明的时候加final:为了防止子类继承,更改我们的实现是其变为非单例模式的;
-
如果实现了序列化接口,如何防止进行反序列化之后生成的新对象和单例模式创建的对象不是同一个对象:在类中添加readResolve方法,在该方法中返回创建出的对象,在反序列化的过程中如果有此方法的话就会返回该方法中的对象,而不再另行生成新的对象;
-
为什么将构造器设置为私有的:防止在使用该类的时候自行创建多个对象;
-
代码中初始化对象的方式是如何确保单例并且是线程安全的:static是类变量,JVM在进行类加载阶段的时候就会创建出类对象,并且JVM保证类加载的过程是线程安全的;
-
为什么提供了一个静态的方法而不是直接将INSTANCE设置为public的:通过向外界提供共有的方法可以体现更好的封装性,可以在方法内实现懒加载的实例,可以提供泛型等;
-
-
第二种实现:使用枚举的方式,是一种饿汉式的解决方案,enum的声明是public static final的,并且在类加载的时候就被创建了,同样是线程安全的:
// 问题1:枚举单例是如何限制实例个数的:创建枚举类的时候就已经定义好了,每个枚举常量其实就是枚举类的一个静态成员变量 // 问题2:枚举单例在创建时是否有并发问题:没有,这是静态成员变量 // 问题3:枚举单例能否被反射破坏单例:不能 // 问题4:枚举单例能否被反序列化破坏单例:枚举类默认实现了序列化接口,枚举类已经考虑到此问题,无需担心破坏单例 // 问题5:枚举单例属于懒汉式还是饿汉式:饿汉式 // 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做:添加构造方法就行了 enum Singleton { INSTANCE; }
-
使用懒汉式实现单例模式:
public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; // 分析这里的线程安全, 并说明有什么缺点:synchronized加载静态方法上,可以保证线程安全。缺点就是锁的范围过大. public static synchronized Singleton getInstance() { if( INSTANCE != null ){ return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } }
static关键字是加在方法上的,所以锁对象就是Singleton.class, 并且以上的代码是存在性能问题的,当单例对象以经创建好的时候,后来的线程在访问getInstance()方法是仍然会获取锁,但是这其实是不必要的,降低了程序的性能;应该使用双重检查的方式来进行判断;
-
双重检查的懒汉式:
public final class Singleton { private Singleton() { } // 问题1:解释为什么要加 volatile? 为了防止重排序问题 private static volatile Singleton INSTANCE = null; // 问题2:对比实现3, 说出这样做的意义:提高了效率 public static Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗?这是为了解决第一次判断时的并发问题。 if (INSTANCE != null) { // t2 return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } }
问题1:因为在synchronized外部使用到了共享变量INSTANCE, 所以synchronized无法保证instance的有序性,又因为instance = new Singleton()不是一个原子操作,可分为多个指令, 所以可能会发生指令重排,造成INSTANCE还未初始化就赋值的现象,所以要给共享变量INSTANCE加上volatile,禁止指令重排;
问题2:增加了双重判断,如果存在了单例对象,别的线程再进来就无需加锁判断,大大提高了性能;
问题3:当t1,t2线程都调用getInstance()方法时,它们都判断单例对象为空,还没有创建;此时t1先获取到锁对象,进入到synchronized中,然后创建对象,返回单例对象,释放锁。之后t2获得了锁对象,如果在代码块中没有if判断的话线程2就会认为没有单例对象,创建出多余的对象; -
内部类实现懒汉式单例:
public final class Singleton { private Singleton() { } private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
问题1:使用静态内部类创建对象的时候是懒汉式的,因为类的创建本身就是懒惰的,只有在第一次调用getInstance方法的时候才会执行内部类LazyHolder的加载;
问题2:以上的实现是否是线程安全的?是线程安全的,因为JVM本身保证了类加载的过程是线程安全的;
2. 无锁编程
1. 无锁编程提出
- Java中的synchronized和ReentrantLock等独占锁都是悲观锁思想的实现;不过悲观锁在一些情况下是不需要的,比如java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁实现的并发控制,一般都是乐观锁思想中的CAS操作;
- 我们常说的管程(监视器)monitor就是阻塞式的悲观锁来实现的并发控制,接下来要介绍的各种原子类都是基于乐观锁的;
- 假设有以下的需求,需要保证取款方法的线程安全,分别使用synchronized和原子类来解决以上的问题:
输出:@Slf4j(topic = "c.Test1") public class Test { public static void main(String[] args) { Account.demo(new AccountUnsafe(10000)); Account.demo(new AccountCAS(10000)); } } class AccountUnsafe implements Account { private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { synchronized (this) { return balance; } } @Override public void withdraw(Integer amount) { // 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题 synchronized (this) { balance -= amount; } } } class AccountCAS implements Account { private AtomicInteger balance; public AccountCAS(int balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } @Override public void withdraw(Integer amount) { while (true) { int prev = balance.get(); int next = prev - amount; if (balance.compareAndSet(prev, next)) break; } } } interface Account { // 获取余额 Integer getBalance(); // 取款 void withdraw(Integer amount); /** * Java8之后可以在接口中添加默认方法 * 方法内会启动 1000 个线程,每个线程做 -10 元的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(Account account) { List<Thread> ts = new ArrayList<>(); long start = System.nanoTime(); for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(10); })); } ts.forEach(thread -> thread.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(account.getBalance() + " cost: " + (end - start) / 1000_000 + " ms"); } }
从输出可以看到synchronized的实现最终所需的时间要比原子类的实现多出很多,所以很多情况下可以使用原子类而非是synchronized来解决并发问题;0 cost: 174 ms 0 cost: 98 ms
2. CAS + 重试的原理
- 原子类内部比较关键的就是
compareAndSet
,也即是CAS,有时也会将CAS称作是CompareAndSwap
,操作系统在底层硬件的层面之上实现了CAS操作的原子性,它的底层是lock cmpxchg指令,在单核和多核的情况下比较并交换的操作都是原子性的,所以我们不必去担心CAS操作的线程安全问题; - 以上代码的CAS操作流程:CAS的参数中一个是当前的值,一个是我们想要对其进行修改后的值,当CAS执行的时候,首先会比较prev与Account中的余额是否是相等的,如果是的话就说明prev值在进行CAS操作前没有被别的线程修改,CAS直接将prev的值修改为next;如果不是的话就说明在进行CAS操作之前已经有线程对prev的值进行了修改,此时就不会将prev修改为next,避免覆盖掉别的线程的修改操作;而是重新获取prev的值,然后再对prev的值和当前最新的值进行比较,如果相等的话就进行修改,否则的话就反复的执行以上的步骤。这样就确保了一个线程的修改不会覆盖掉其它线程进行的修改操作;
- 在多核状态下,如果一个核执行了CAS的底层操作的话,CPU就会让总线锁住直到这个核把指令执行完毕,这个过程中核的执行不会被线程的调度机制打断,保证了操作的原子性;
- 我们在创建AtomicInteger对象的时候向AtomicInteger的构造方法中传递了一个参数,该参数在AtomicInteger的源码之中对应着一个私有的变量:
private volatile int value;
该变量被volatile修饰,确保了变量的可见性,所以在使用CAS的时候可以获取到最新的数据; - 至于为什么CAS的方式比synchronized加锁的方式的效率高,是因为在使用CAS + 重试的时候即使是重试失败了,线程始终在不停地运行,并不会发生上下文的切换,但是synchronized在失败的时候会发生上下文的切换,该操作需要陷入到内核态,是一个比较耗时的操作,所以用的时间会更长;但是在无锁的情况下线程要持续的运行,需要额外的CPU时间,但是如果没有时间片分配给循环等待的线程的话,还是会导致上下文切换;
- 结合CAS和volatile实现的无锁并发只适合于线程数少,多核CPU的情况下,因为线程数太多的话会带来激烈的竞争,导致重试频繁的发生,效率反而会下降不少;只有单个CPU的话CAS就相当于是单个线程在不断地浪费CPU的资源;
3. 原子类使用
-
java.util.concurrent.atomic
并发包提供了很多的基于CAS的并发工具类,它们的使用方式是原子的,有AtomicInteger原子整数类,AtomicLong原子长整型类以及AtomicBoolean原子布尔类;由于他们提供的方法很多都是差不多的,所以这里仅仅展示AtomicInteger原子整数类的使用:@Slf4j(topic = "c.Test1") public class Test { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(0); // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++ System.out.println(i.getAndIncrement()); // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i System.out.println(i.incrementAndGet()); // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i System.out.println(i.decrementAndGet()); // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i-- System.out.println(i.getAndDecrement()); // 获取并加值(i = 0, 结果 i = 5, 返回 0) System.out.println(i.getAndAdd(5)); // 加值并获取(i = 5, 结果 i = 0, 返回 0) System.out.println(i.addAndGet(-5)); // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0) // 函数式编程接口,其中函数中的操作能保证原子,但函数需要无副作用 System.out.println(i.getAndUpdate(p -> p - 2)); // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0) // 函数式编程接口,其中函数中的操作能保证原子,但函数需要无副作用 System.out.println(i.updateAndGet(p -> p + 2)); // 获取并计算(此时i = 0, p 为 i 的当前值, 结果 i = 10, 返回 0) // 函数式编程接口,其中函数中的操作能保证原子,但函数需要无副作用 // getAndUpdate:如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的 // getAndAccumulate:可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final System.out.println(i.getAndAccumulate(10, (p, x) -> p + x)); // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1的值, 结果 i = 0, 返回 0) // 函数式编程接口,其中函数中的操作能保证原子,但函数需要无副作用 System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x)); } }
-
原子引用的使用:原子引用类型的共享变量是线程安全的,原子引用常用的是
AtomicReference
,AtomicMarkableReference
,AtomicStampedReference
,最后一个可解决ABA的问题;基本类型的原子类只能更新一个变量,如果想要更新多个变量的话就需要使用原子引用类;以下展示了使用原子引用类的使用,保证存取款的线程安全:@Slf4j(topic = "c.Test1") public class Test { public static void main(String[] args) { DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000"))); } } class DecimalAccountCas implements DecimalAccount { //原子引用,泛型类型为小数类型 private final AtomicReference<BigDecimal> balance; public DecimalAccountCas(BigDecimal balance) { this.balance = new AtomicReference<>(balance); } @Override public BigDecimal getBalance() { return balance.get(); } @Override public void withdraw(BigDecimal amount) { while (true) { BigDecimal prev = balance.get(); BigDecimal next = prev.subtract(amount); if (balance.compareAndSet(prev, next)) { break; } } } } interface DecimalAccount { // 获取余额 BigDecimal getBalance(); // 取款 void withdraw(BigDecimal amount); /** * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(DecimalAccount account) { List<Thread> ts = new ArrayList<>(); for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(BigDecimal.TEN); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(account.getBalance()); } }
最后的输出就是0;
-
CAS的ABA问题:以下的程序展示了经典的ABA问题,在另一个线程之中对一个数据进行修改之后,如果又将数据修改回原来的值,那么CAS操作是无法察觉到原先的值曾经被修改过,CAS只能判断出参数中的值和当前的最新值是不是相等的,并不能判断值是不是被修改过:
@Slf4j(topic = "c.Test1") public class Test { static AtomicReference<String> ref = new AtomicReference<>("A"); public static void main(String[] args) { new Thread(() -> { String pre = ref.get(); try { other(); } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("change A->C " + ref.compareAndSet(ref.get(), "C")); }, "main").start(); } static void other() throws InterruptedException { new Thread(() -> { log.debug("change A->B " + ref.compareAndSet(ref.get(), "B")); }, "the1").start(); Thread.sleep(500); new Thread(() -> { log.debug("change B->A " + ref.compareAndSet(ref.get(), "A")); }, "the2").start(); } }
输出:
12:14:44.065 c.Test [the1] - change A->B true 12:14:44.566 c.Test [the2] - change B->A true 12:14:45.569 c.Test [main] - change A->C true
以上的现象在很多的业务场景下是不影响结果的,但是也有不少的业务逻辑需要在数据被修改的时候操作失败,这时就可以使用
AtomicMarkableReference
来解决ABA问题;AtomicMarkableReference
可以给原子引用加上版本号,利用版本号来追踪对数据的修改,这样就可以察觉到别的线程对数据的修改:@Slf4j(topic = "c.Test") public class Test { static AtomicStampedReference< String> ref = new AtomicStampedReference<>("A", 0); public static void main(String[] args) { new Thread(() -> { String pre = ref.getReference(); int stamp = ref.getStamp(); try { other(); } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("change A->C " + ref.compareAndSet(ref.getReference(), "C", stamp, stamp + 1) + " stamp = " + stamp); }, "main").start(); } static void other() throws InterruptedException { new Thread(() -> { int stamp = ref.getStamp(); log.debug("change A->B " + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1) + " stamp = " + stamp); }, "the1").start(); Thread.sleep(500); new Thread(() -> { int stamp = ref.getStamp(); log.debug("change B->A " + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1) + " stamp = " + stamp); }, "the2").start(); } }
此时CAS比较的不仅仅是最新的当前值和参数中的值是不是相等的,还会比价并更新stamp版本号的值,每当对数据进行更新的时候,仅当数据的最新值和参数中的值是一致的并且版本号也和最新的版本号一致的时候才会对数据进行更新;并同时更新版本号;只要数据在其它线程中被修改过版本号也会跟着被修改,这样就可以解决ABA问题了;除此之外还可以根据版本号得知数据被修改了多少次;
-
如果不想知道数据被修改了多少次,而仅仅是想要知道数据是不是被修改过的话,可以使用
AtomicMarkableReference
来标记共享变量是不是被修改过;AtomicMarkableReference
构造器的参数是boolean,而不是int,所以可以用过标记:@Slf4j(topic = "c.Test") public class Test { static AtomicMarkableReference< String> ref = new AtomicMarkableReference<>("A", true); public static void main(String[] args) { new Thread(() -> { String pre = ref.getReference(); try { other(); } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("change A->C " + ref.compareAndSet(pre, "C", true, false)); }, "main").start(); } static void other() throws InterruptedException { new Thread(() -> { log.debug("change A->B " + ref.compareAndSet(ref.getReference(), "B", true, false)); }, "the1").start(); } }
输出:
16:19:17.075 c.Test [the1] - change A->B true 16:19:18.082 c.Test [main] - change A->C false
至于AtomicStampedReference和AtomicMarkableReference两者的区别,在使用等时候仅仅是AtomicStampedReference 需要我们传入 整型变量 作为版本号,来判定是否被更改过或者是被修改了几次;AtomicMarkableReference需要我们传入布尔变量作为标记,来判断是否被更改过;
-
如果使用原子引用修饰数组的话,那么我们对数组内部的元素的修改将不会是线程安全的,仅仅是对数组引用的修改是安全的,所以在多线程的情况下使用数组的时候最好是使用
AtomicXXXArray
来修饰数组;在这里以AtomicIntegerArray
为例展示对原子数组的使用,当然一个普通的数组是不具有线程安全的:@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) { demo( () -> new int[10], array -> array.length, (array, index) -> array[index]++, array -> System.out.println(Arrays.toString(array)) ); demo( () -> new AtomicIntegerArray(10), AtomicIntegerArray::length, AtomicIntegerArray::getAndIncrement, System.out::println ); } /** * 参数1,提供数组、可以是线程不安全数组或线程安全数组 * 参数2,获取数组长度的方法 * 参数3,自增方法,回传 array, index * 参数4,打印数组的方法 */ // supplier 提供者 无中生有,专门提供一个结果 ()->结果 // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果 // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void private static <T> void demo(Supplier<T> arraySupplier, Function<T, Integer> lengthFun, BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) { List<Thread> ts = new ArrayList<>(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i < length; i++) { // 创建length个线程, 每个线程对数组作 10000 次操作 ts.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { putConsumer.accept(array, j % length); } })); } ts.forEach(Thread::start); // 启动所有线程 ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 等所有线程结束 printConsumer.accept(array); } }
输出:
[9246, 9183, 9136, 9150, 9100, 9129, 9109, 9129, 9283, 9296] [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
-
字段更新器:保证多线程访问同一个对象的成员变量时, 成员变量的线程安全性;分别是:
AtomicReferenceFieldUpdater
原子的引用类型的属性,AtomicIntegerFieldUpdater
原子的整形的属性,AtomicLongFieldUpdater
原子的长整形的属性,注意利用原子字段更新器的话可以面向对象的某个域进行原子的操作,但是只能配合volatile关键字修饰的字段进行使用,否则的话会抛出异常:Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
:@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) { Student stu = new Student(); // 参数1:持有属性的类 // 参数2:被更新的属性的类 // 参数3:属性的名称 AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name"); // 期望的为null, 如果name属性没有被别的线程更改过, 默认就为null, 此时匹配, 就可以设置name为张三 System.out.println(updater.compareAndSet(stu, null, "张三")); System.out.println(updater.compareAndSet(stu, stu.name, "王五")); System.out.println(stu); } } class Student { volatile String name; @Override public String toString() { return "Student{" + "name='" + name + '\'' + '}'; } }
输出:
true true Student{name='王五'}
-
原子累加器:专门对一个数字进行累加操作的类,性能要更高,有
LongAddr,LongAccumulator,DoubleAddr,DoubleAccumulator
,在这里对LongAddr进行介绍:@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) { System.out.println("----AtomicLong----"); for (int i = 0; i < 5; i++) { demo(AtomicLong::new, AtomicLong::getAndIncrement); } System.out.println("----LongAdder----"); for (int i = 0; i < 5; i++) { demo(LongAdder::new, LongAdder::increment); } } private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) { T adder = adderSupplier.get(); long start = System.nanoTime(); List<Thread> ts = new ArrayList<>(); for (int i = 0; i < 4; i++) { ts.add(new Thread(() -> { for (int j = 0; j < 500000; j++) { action.accept(adder); } })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + " cost:" + (end - start) / 1000_000); } }
输出:
----AtomicLong---- 2000000 cost:51 2000000 cost:41 2000000 cost:39 2000000 cost:42 2000000 cost:42 ----LongAdder---- 2000000 cost:19 2000000 cost:8 2000000 cost:9 2000000 cost:10 2000000 cost:9
可以看到原子累加器的性能要高很多,性能提升的原因就是原子累加器设置了多个累加单元,且数量不会超过CPU的个数,因为超过的话就没有意义了;不同的线程在不同的单元中进行累加操作;具体的原理在源码分析的时候讲解;
4. LongAddr原理
- Cell类的实现:
// 该注解防止缓存行伪共享 @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } // 用CAS的方式进行累加操作,prev表示旧值,next表示新值 final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } // ... }
- 缓存行和伪共享:由于内存和CPU之间的访问速度的差异是非常大的,所以为了减少访问速度差异所带来的利用率问题,在主存和CPU之间添加了3级缓存,缓存的访问的访问速度比主存的访问速度要快的多,可以把内存中的数据提前读取至缓存中;但是缓存行造成了数据副本的产生,不同CPU的缓存可能存储了同一份内存数据的副本;一个CPU对数据进行修改的时候其它缓存了相同数据的CPU缓存行也要对数据进行修改;不然的话就会导致不同的CPU读取到的数据不一致,就会出现伪共享;所以为了解决以上的问题,如果某个CPU更改了数据,其它的CPU会将所对应的整个缓存行都置为失效的,从而读取到最新的数据;但是这么做会使整个缓存行中的数据在下次被使用的时候从内存中获取,损失了性能;
@sun.misc.Contended
注解就是为了解决以上的问题而添加的,它会将不同的Cell缓存在不同的缓存行中,所以当一个缓存行失效的时候另一个缓存行中的Cell仍然可以在缓存中获取,降低了性能的损失程度;将不同的Cell缓存在不同的缓存行的做法就是在使用此注解的对象或字段的前后各增加128字节大小的padding填充,从而让CPU将对象预读至缓存时占用不同的缓存行,这样就可以降低性能的损失(一个Cell为24个字节,分为16字节的对象头和8字节的value);
5. LongAdder相关方法源码分析
-
LongAdder.add(long x)方法
public void add(long x) { Striped64.Cell[] as; long b, v; int m; Striped64.Cell a; // 判断累加单元的数组是不是空的,是懒惰创建的,只有竞争出现的时候才会创建cell数组 // 没有竞争的时候cell数组就是空的,使用CAS的方式对基础累加值base进行累加 // 如果基础累加值累加成功的话就不会进入到if块,直接返回;否则的话执行longAccumulate相关的步骤 // cell数组不为空的话就意味着已经发生了竞争,所以会检查当前线程对应的cell是不是已经创建了 // 没有创建的话进入到longAccumulate相关的逻辑进行本线程cell数组的创建 // 本线程对应的cell数组已经创建了的话就在cell数组中进行累加的操作 // 累加成功就是直接返回,累加不成功进入到longAccumulate对应的逻辑中 if ((as = cells) != null || !casBase(b = base, b + x)) { boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); } }
-
LongAdder.longAccumulate
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; if ((h = getProbe()) == 0) { ThreadLocalRandom.current(); h = getProbe(); wasUncontended = true; } boolean collide = false; for (;;) { Striped64.Cell[] as; Striped64.Cell a; int n; long v; // 虽说数组已经创建好了但是线程对应的累加单元还没有创建 // 因为如果是其他的线程创建了Cell数组,只会将那一个线程对应的累加单元创建出来 // 所以会创建本数组的累加单元 if ((as = cells) != null && (n = as.length) > 0) { // 这个线程还没有创建累加单元 if ((a = as[(n - 1) & h]) == null) { if (cellsBusy == 0) { Striped64.Cell r = new Striped64.Cell(x); if (cellsBusy == 0 && casCellsBusy()) { boolean created = false; try { Striped64.Cell[] rs; int m, j; // 再次检查数组的饿情况以及累加单元的情况 if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { rs[j] = r; created = true; } } finally { cellsBusy = 0; } if (created) break; continue; } } collide = false; } else if (!wasUncontended) wasUncontended = true; // 数组和累加单元都创建了 // 进行累加操作 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // 累加失败就检查是否超过了CPU的个数 else if (n >= NCPU || cells != as) // 将collide设置为false collide = false; // 就一定会走到这个else if else if (!collide) collide = true; // 也就不会进入这个else if,防止走扩容的逻辑 // 没有超过CPU上限的时候偶会进行扩容的操作 else if (cellsBusy == 0 && casCellsBusy()) { try { if (cells == as) { // 将n扩容为原来的2倍 Striped64.Cell[] rs = new Striped64.Cell[n << 1]; for (int i = 0; i < n; ++i) rs[i] = as[i]; cells = rs; } } finally { cellsBusy = 0; } collide = false; continue; } // 改变线程对应的cell,再次重新循环 h = advanceProbe(h); } // cell数组还没有创建,cellsBusy是0表示未加锁,是1表示已经加锁 // 在创建cell数组的时候需要判断其它的线程是不是已经加锁 // cells == as 是为了查看别的线程是不是已经对cells引用进行了修改 // 也就是查看别的数组有没有将cells数组创建出来 // 其它线程没有加锁并且没有创建出cells的话本线程就加锁(对应casCellsBusy()方法)并创建cell数组 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { boolean init = false; try { // 再次判断其它的线程有没有创建cells数组 if (cells == as) { Striped64.Cell[] rs = new Striped64.Cell[2]; // 将x赋值给某一个累加单元 // 虽说数组大小是2,但是累加单元只有一个,还有一个是空的,只有在使用到的时候才会创建 rs[h & 1] = new Striped64.Cell(x); cells = rs; init = true; } } finally { // 解锁 cellsBusy = 0; } if (init) break; } // 上一个else if加锁失败的话就进入到这个else if,首先尝试对base进行累加 // 累加成功的话就直接退出,失败就循环重试 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; } }
-
LongAdder.sum()方法
// 统计最终的累加结果 public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
6. Unsafe的使用
-
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得;AtomicInteger以及其他的原子类底层使用的都是Unsafe类:
因为Unsafe是很底层的,所以说误用的话会产生安全问题,并不是其它的什么原因而命名为Unsafe; -
Unsafe的使用方式:
@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) throws Exception { Class<Unsafe> unsafeClass = Unsafe.class; Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor(); // 设置为允许访问私有的内容 constructor.setAccessible((true)); Unsafe unsafe = constructor.newInstance(); Person person = new Person(); final long name = unsafe.objectFieldOffset(Person.class.getDeclaredField("name")); final long age = unsafe.objectFieldOffset(Person.class.getDeclaredField("age")); // 参数1:要更新的数据所在的对对象的引用 // 参数2:属性的内存偏移量 // 参数3:更新时期望的值 // 参数4:旧值会被重置为的值 unsafe.compareAndSwapObject(person, name, null, "王五"); unsafe.compareAndSwapInt(person, age, 0, 24); System.out.println(person); } } class Person { // 使用CAS操作必须用volatile修饰变量以保证可见性 volatile String name; volatile int age; @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
输出:
Person{name='王五', age=24}
3. 不可变类
1. 不可变类的介绍
-
如果一个对象不能够修改其内部状态,那么就不存在并发修改,就是线程安全的;
-
日期转换问题:下面的代码在运行的时候会出现线程安全的问题,因为SimpleDateFormat本身是可变类:
@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) throws Exception { final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); for (int i = 0; i < 10; i++) { new Thread(() -> { try { log.debug("{}", simpleDateFormat.parse("2025-06-07")); } catch (ParseException e) { log.error("{}", e); } }).start(); } } }
Exception in thread "Thread-0" Exception in thread "Thread-4" Exception in thread "Thread-2" Exception in thread "Thread-3" Exception in thread "Thread-6" 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 com.zhyn.Test.lambda$main$0(Test.java:15) at java.lang.Thread.run(Thread.java:748) 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 com.zhyn.Test.lambda$main$0(Test.java:15) at java.lang.Thread.run(Thread.java:748) 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 com.zhyn.Test.lambda$main$0(Test.java:15) at java.lang.Thread.run(Thread.java:748) 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 com.zhyn.Test.lambda$main$0(Test.java:15) at java.lang.Thread.run(Thread.java:748) 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 com.zhyn.Test.lambda$main$0(Test.java:15) at java.lang.Thread.run(Thread.java:748) 14:04:25.626 c.Test [Thread-9] - Sat Jun 07 00:00:00 CST 2025 14:04:25.626 c.Test [Thread-8] - Sat Jun 07 00:00:00 CST 2025 14:04:25.626 c.Test [Thread-7] - Mon Jun 07 00:00:00 CST 202 14:04:25.626 c.Test [Thread-1] - Tue May 31 00:00:00 CST 2022 14:04:25.626 c.Test [Thread-5] - Tue May 31 00:00:00 CST 2022
可以看到很容易出现线程安全的问题;可以使用同步锁进行解决但是性能会下降;
-
在JDK8时加入了不可变日期格式化类
DateTimeFormatter
,可以使用这个类来保证线程的安全(不可变类单个方法是线程安全的,但是多个方法的组合仍然会出现线程不安全的情况):@Slf4j(topic = "c.Test") public class Test { public static void main(String[] args) throws Exception { final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); for (int i = 0; i < 10; i++) { new Thread(() -> { final TemporalAccessor parse = dateTimeFormatter.parse("2025-06-07"); log.debug("{}", parse); }).start(); } } }
输出:
14:02:07.609 c.Test [Thread-5] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-1] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-8] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-2] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-3] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-4] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-7] - {},ISO resolved to 2025-06-07 14:02:07.609 c.Test [Thread-9] - {},ISO resolved to 2025-06-07 14:02:07.609 c.Test [Thread-0] - {},ISO resolved to 2025-06-07 14:02:07.608 c.Test [Thread-6] - {},ISO resolved to 2025-06-07
2. final的使用
-
Integer、Double、String、DateTimeFormatter以及基本类型包装类都是使用final来修饰的;String 类也是不可变的,参照String类说明不可变类的设计要素:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; // 在JDK9中使用byte[]数组进行了优化,节省空间 private int hash; // ... }
其中类的声明和类的属性都是使用final修饰的,属性用 final 修饰保证了该属性是只读的,不能被修改,类用 final 修饰保证了该类是不可以被继承的,类中的方法不能被覆盖,防止子类无意间破坏不可变性;
-
保护性拷贝:在使用字符串的时候有一些方法看似是和修改相关的,但是实际上他们并没有对字符串进行修改,比如说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(value, beginIndex, subLen);
来创建了一个新的字符串,在构造新的字符串的时候会给value赋予新的值,其实就是复制了原本的字符串的一部分,这种通过创建副本对象来避免共享的手段称为保护性拷贝: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); }
-
final的原理:final 变量的赋值操作是通过 putfield 指令来完成,在这条指令之后会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况,只会出现读取到 a = 20 的情况,不会出现二义性:
在读取final修饰的变量时候如果数据表较小的话就会将final修饰的变量的值直接复制到final变量读取者的栈中,数据比较大的话就去常量池中读取final数据;不加final的话相当于是在堆中访问数据,是比较慢的。
3. 享元模式
- 在JDK中Boolean,Byte,Short,Integer,Long,Character等包装类提供了valueOf方法,例如 Long 的 valueOf 会缓存-128~127之间的 Long 对象,在这个范围之间会重用对象,只有大于这个范围的时候才会新建 Long 对象:
public static BigInteger valueOf(long val) { if (val == 0) return ZERO; if (val > 0 && val <= MAX_CONSTANT) return posConst[(int) val]; else if (val < 0 && val >= -MAX_CONSTANT) return negConst[(int) -val]; return new BigInteger(val); }
Byte, Short, Long 缓存的范围都是-128-127;
Character 缓存的范围是 0-127;
Boolean 缓存了 TRUE 和 FALSE;
Integer的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high"来改变;