流水线技术:减少切换状态,增加整体速度,减少动态电路的切换(依靠半导体,半导体,随着电压的变化在导体和绝缘体之间切换, 产生逻辑电路)频率,减少CPU调度,可能产生顺序改变的问题,导致指令重排序,导致并发问题,少数场景才发生
①加锁 synchronized
并不是加锁就一定能保证线程的安全性,一定到写操作完成之后再释放锁
如果一个方法加上synchronized,任何一个线程想要拷贝这个方法,要事先对这个方法进行加锁,其它线程则不允许拷贝这个方法入栈执行,即同时只能有一个线程拷贝入栈执行。
当某个线程调用某个静态加锁的方法时,其它线程不允许调用其它静态加锁类方法
一定要在写完之后再对其进行加锁
public class Test2 {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
for(int i=0; i<10000; i++) {
// m1();
int w = getA();
setA(w+1);
}
}
};
Thread t2 = new Thread() {
public void run() {
for(int i=0; i<10000; i++) {
// m1();
int w = getA();
setA(w+1);
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
public synchronized static void m1() {
a++; //有效加锁,保证了写之后进行加锁
}
public synchronized static int getA() {
return a; //无效加锁
}
public synchronized static void setA(int x) {
a=x; //无效加锁
}
}
② 比较并且交换(CAS)算法
CAS(Compare and Swap)是一种用于实现并发控制的技术,通常用于多线程编程中。在Java中,CAS算法是由java.util.concurrent.atomic包中的原子类(如AtomicInteger、AtomicLong等)实现的。
CAS算法的基本思想是,通过原子方式更新一个值,它包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B;否则,处理器不做任何操作。这个比较并交换的操作是一个原子操作,由硬件级别保证其原子性。
在Java中,CAS操作由java.util.concurrent.atomic.AtomicInteger等类中的compareAndSet()方法实现。这个方法接受一个当前值和一个新值作为参数,并尝试将当前值更新为新值。如果当前值确实等于预期值(即当前值),那么这个操作会成功,否则操作会失败。
CAS算法对于实现无锁(lock-free)数据结构和算法非常重要,它能够提供比基于锁的方法更高的并发性能。然而,CAS算法也有其限制,例如ABA问题,即在使用CAS操作时可能出现ABA问题。这是因为CAS操作只比较内存位置的当前值和预期值,而不会检查这个值在此期间是否被其他线程修改过。如果其他线程修改了这个值,然后又改回原来的值,那么对于使用CAS操作的线程来说,这个值就像被修改成了新的值(实际上没有),这就是ABA问题。
操作系统通过给总线加锁,保证线程安全
无法手动写代码实现CAS算法,只能借助计算机封装的底层方法实现
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。可以修饰变量,只能保证读是准确的,但是往回更新时可能会覆盖原来数据,保证不了写操作。synchronized可以。例如,Object x = new Object(),有可能读x时为空,没有链接到Object的内存地址,还没有new,正常操作可能导致这种读不正确,加上volatile后能保证读x是正确的。
在Java中,volatile是一个关键字,用于声明变量。volatile的主要作用是确保变量的可见性和禁止指令重排序优化。
可见性:当一个共享变量被volatile修饰时,它会保证修改后的值对所有线程立即可见。简单来说,当一个线程修改了一个volatile变量的值,新值对其他线程立即变得可见。
禁止指令重排序优化:在多线程环境中,为了提高执行效率,编译器和处理器可能会对指令进行重排序。但是,这种重排序可能导致一些问题,特别是当一个线程在修改一个变量时,另一个线程可能看到这个变量修改之前的值。volatile关键字会禁止这种重排序优化,确保变量的修改按照代码的顺序执行。volatile修饰的语句上一行和下一行不能改变执行顺序。
然而,volatile并不提供原子性保证。也就是说,如果一个操作包含多个步骤,且这些步骤不是原子的(例如,先读取一个值,然后基于这个值进行计算并更新),那么在多线程环境中,其他线程可能会看到中间的状态。有volatile修饰的变量,当该变量往回更新数据的时候,其它线程同地址的变量数据会变得无效,但是在队列等着往回更新的同地址变量不会,所以依然会产生覆盖。
为了解决原子性问题,Java提供了其他机制,如synchronized关键字和java.util.concurrent.atomic包中的原子类。
需要注意的是,过度使用volatile也可能导致性能问题。因为volatile禁止指令重排序优化,所以它可能会导致代码执行速度变慢。因此,在使用volatile时应该权衡其利弊。
内存屏障:事先约定的方式,不允许访问某块内存
缓冲行:CPU内部高速缓存的存储单位,64字节,
原子操作:不能被中断的操作,要不一个操作的所有步骤都成功,要不都失败,如果一个操作的其中一个步骤出错,其它步骤需要全部重来
缓存命中:CPU需要读取的数据在高速缓存中有
对象锁标志的存储位置
1、Java对象头
已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例数据和对齐填充。
对象头的作用是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)
实例数据存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
对齐填充,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的
在32位的虚拟机中:
在64位的虚拟机中:
线程的生命周期存在5个状态,start、running、waiting、blocking和dead
对于一个synchronized修饰的方法(代码块)来说:
1、当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
2、当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
3、当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程放入_EntryList队列,重新等待获取锁monitor对象。
4、如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
(1)Synchronized修饰代码块:
Synchronized代码块同步在需要同步的代码块开始的位置插入monitorenter指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。
(2)Synchronized修饰方法:
Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。
Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
①对于普通同步方法,锁是当前实例对象。对象锁,当一个线程进入这个方法并获得该对象的锁时,其他线程不能同时进入该对象的任何其他synchronized方法,因为它们都使用同一个锁。
java`public class MyClass {
public synchronized void method1() {
// ...
}
public synchronized void method2() {
// ...
}
}
//如果一个线程正在执行method1(),那么其他线程不能执行method2(),因为它们都使用同一个对象锁。
②对于静态同步方法,锁是当前类的Class对象。类锁,当一个线程进入这个静态方法并获得该类的类锁时,其他线程不能同时进入该类的任何其他静态synchronized方法,因为它们都使用同一个类锁。
java`public class MyClass {
public static synchronized void method1() {
// ...
}
public static synchronized void method2() {
// ...
}
}`
//如果一个线程正在执行method1(),那么其他线程不能执行method2(),因为它们都使用同一个类锁。
③对于同步方法块,锁是Synchonized括号里配置的对象。当一个线程进入这个同步代码块并获得该对象的锁时,其他线程不能同时进入与这个锁关联的其他同步代码块。
java`public class MyClass {
private Object lock = new Object();
public void method() {
synchronized(lock) {
// ... some code ...
}
}
}`
//如果一个线程正在执行synchronized代码块并持有lock对象的锁,那么其他线程不能进入与这个锁关联的其他同步代码块。
同一个对象中,加锁方法不能影响未加锁方法执行
使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。
使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。
线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。
一个线程对其加了锁,当时间片到了,回到就绪队列,锁依然存在;多线程竞争加锁失败的线程,会进入阻塞队列(即不会被CPU选中执行),这样能节省时间,因为加了锁的线程未执行完锁依然存在,时间片到期之后其它线程即使分配了时间片也不能够执行,只能浪费CPU时间,因此未被选中的加锁线程直接进入阻塞队列,不会被CPU选中执行。
同步:只能一个一个执行; 异步:能同时执行
偏向锁(Biased Locking),用于减少无竞争条件下的线程同步开销。它的核心思想是尽量减少对对象的加锁次数,将锁偏向于被频繁访问的对象。首先检查对象是否加锁,如果加锁,则直接执行,不需要经过加锁释放锁再通知其它线程的过程;如果没加锁,则检查是否正在加锁,有可能加锁加到一半时间片到期,如果正在加锁,则接着加锁执行;否则采用CAS竞争锁。适用于当前只有一个加锁线程的情况。
当一个线程访问同步块并获取锁时,会在对象头和方法中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
在偏向锁中,对象头信息不再包含同步元数据,而是存储一个指向该对象的锁定对象(Lock Record)的指针。当线程访问该对象时,首先检查该对象的锁记录是否已经存在,如果存在,则直接获取该锁记录的引用并继续执行。如果锁记录不存在,则尝试将该对象的锁记录指向当前线程的锁记录,并获取该锁记录的引用。
偏向锁的优点是在无竞争条件下减少了线程同步开销,提高了程序的性能。然而,当多个线程同时访问同一个对象时,偏向锁需要撤销并重新竞争锁,这会导致一定的性能开销。
轻量级锁Lightweight Locking):对于竞争失败的加锁进程不进入阻塞队列,而是一直采用CAS操作进行自旋来获取锁。适用于当前加锁线程较少且时间短的情况。
竞争失败的加锁线程不进入阻塞队列,是一种在无竞争情况下提供高效率的锁机制。轻量级锁的主要目的是在无竞争的线程之间提高性能。
在Java中,轻量级锁的实现在java.util.concurrent.locks包中的ReentrantLock类中。当一个线程尝试获取锁时,如果锁没有被其他线程持有,该线程会获得锁并继续执行。如果锁已经被其他线程持有,那么该线程会尝试使用CAS(Compare-and-Swap)操作来自旋(即不断探索当前线程是否加锁)尝试获取锁。
轻量级锁的优势在于它减少了线程在获取锁时的阻塞时间,从而提高了并发性能。然而,如果多个线程频繁地竞争同一个锁,轻量级锁的开销可能会增加,因为线程需要不断地尝试获取锁。快速感知到锁是否被释放。
需要注意的是,轻量级锁并不适用于所有情况。在某些情况下,例如当多个线程频繁地长时间持有锁时,使用重量级锁(如synchronized关键字)可能会更合适。因此,在使用轻量级锁时,需要根据具体的应用场景和需求进行评估和选择。
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁(竞争失败的锁进入阻塞队列)。
重量级锁:对于竞争失败的锁,进入阻塞队列,等到其它所释放的通知再重现采用CAS操作竞争锁。适用于高并发的场景,当前加锁线程多且时间长。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址
总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下
使用缓存锁定代替总线锁定来进行优化。
“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效
有两种情况下处理器不会使用缓存锁定。
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
Vector:这是一个旧的线程安全容器类,提供了可调整大小的数组支持。
Hashtable:这是一个线程安全的哈希表实现,与HashMap类似,但所有的public方法都加上了synchronized关键字。
ConcurrentHashMap:这是一个线程安全的哈希表实现,与Hashtable不同,它使用了分段锁技术,将整个Map分成多个Segment,每个Segment维护自己的数据结构,从而实现了更细粒度的同步。
CopyOnWriteArrayList:这是一个线程安全的ArrayList实现,它在修改操作时会创建一份新的数组副本,以保证修改操作不会影响其他线程。
Atomic类:Java提供了一组原子类,如AtomicInteger、AtomicLong等,这些类在多线程环境下能够保证操作的一致性和安全性。
(1)使用循环CAS实现原子操作
(2)CAS实现原子操作的三大问题
1)ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化 则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从 Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个 类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
(3)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
可以控制什么时候加锁和释放锁,synchronized不能
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态。
AbstractQueuedSynchronizer:锁的底层实现,包括state、acquire、release,sychronized是锁的原理
AbstractQueuedSynchronizer(AQS)是Java并发包java.util.concurrent.locks中的一个核心组件,它为构建同步器(如锁和队列)提供了一种框架。AQS通过使用一个整型的state字段来跟踪同步状态,并通过使用一个FIFO队列来保存等待线程。
以下是AQS中一些关键方法:
state:这是一个整型字段,用于存储同步状态。这个状态可以由子类根据其同步逻辑来设置和读取。
acquire(int arg):这个方法用于获取同步状态。如果当前线程未持有同步状态,则它将进入等待状态,直到获得同步状态。这个方法有一个整型参数,子类可以根据自己的需要来使用这个参数。
release(int arg):这个方法用于释放同步状态。如果当前线程持有同步状态,则它将释放这个状态,并唤醒一个等待的线程。这个方法也有一个整型参数,子类可以根据自己的需要来使用这个参数。
AQS通过使用一个内部FIFO队列来保存等待线程,当一个线程试图获取同步状态时,如果它不能立即获得状态(因为其他线程持有状态),那么它将进入等待状态,并被添加到队列中。当一个线程释放同步状态时,它将唤醒队列中的一个等待线程。
总的来说,AQS是一个灵活的框架,它提供了一种构建同步器的基础结构,通过使用state字段来跟踪同步状态,使用FIFO队列来保存等待线程,以及提供acquire和release方法来获取和释放同步状态。
公平锁和非公平锁是锁的两种实现方式,主要区别体现在获取锁的方式上。
公平锁遵循先来先得的原则,多个线程按照请求锁的顺序获取锁。在公平锁中,如果有多个线程等待获取锁,锁会依次分配给等待时间最长的线程,这样可以避免线程饥饿的情况。公平锁的实现比较复杂,需要维护一个线程等待队列,因此性能会比较低。
非公平锁则不遵循先来先得的原则,多个线程按照竞争获取锁的顺序获取锁。在非公平锁中,如果有多个线程等待获取锁,锁可能会直接分配给等待时间较短的线程,这样可能会导致一些线程一直无法获取锁,出现线程饥饿的情况。非公平锁的实现比较简单,不需要维护一个线程等待队列,因此性能会比较高。
总的来说,公平锁和非公平锁的选择取决于应用场景和需求。如果需要保证多个线程获取锁的顺序,可以选择公平锁;如果需要提高锁的性能,可以选择非公平锁。
实现代码
//加锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的开始,首先读volatile变量state
if (c == 0) {
if (isFirst(current) &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 释放锁的最后,写volatile变量state
return free;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
重入锁是一种在多线程编程中用于控制对共享资源访问的锁机制。它的特点是允许同一个线程多次获得同一把锁,也就是说,线程可以进入由它已经持有的锁所保护的代码块,而不会被自己持有的锁所阻塞。这种特性被称为锁的“可重入性”或“递归性”。重入锁的主要目标是解决多线程环境下的互斥访问问题,保证只有一个线程可以进入临界区(被锁保护的代码段),从而避免了竞态条件(Race Condition)和数据不一致的问题。
对于final域,编译器和处理器要遵守两个重排序规则。
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
双重检验锁,懒加锁模式,用到的时候才new对象
饿汉模式,一开始就new对象
单例模式:一个类只有一个对象,(枚举类,类刚加载进内存,对象就存在),对内存压力有点大
public class Singleton { //volatile保证及时知道singleton是否被创建
private volatile static Singleton singleton; //让静态的方法getSingleton能调用,因此是静态的
private Singleton (){} //构造方法私有,防止new对象
public static Singleton getSingleton() { //访问器,通过此方法获取该类的对象,可以通过类获取,因为是静态
if (singleton == null) { //拦截其它竞争失败的线程
synchronized (Singleton.class) { //加锁,保证同时只有一个线程创建对象
if (singleton == null) { //拦截第一次竞争加锁失败的线程
singleton = new Singleton();
}
}
}
return singleton;
}
}