Some problems encountered and their answers in work (Android Chapter)
文件分块的几种方法
方法一:
传输
1. 定义每块文件的大小,由此确定每块文件的字节数
2. 将文件转换成字节数组
3. 将字节数组按照每块的字节数进行分割,得到每块字节数组
4. 将得到的每块字节数组转发到接收方
5. 接收方接收每块字节数组,将每块字节数组组合起来,即可将原文件重建。
方法二:
样例
以下是使用Java 实现文件分块的代码样例:
```java
import java. io. File ;
import java. io. FileInputStream ;
import java. io. IOException ;
import java. io. InputStream ;
public class FileSplitSample {
public static void fileSplit ( String filePath, int blockSize) {
File file = new File ( filePath) ;
byte [ ] fileBytes = new byte [ ( int ) file. length ( ) ] ;
try ( InputStream inputStream = new FileInputStream ( file) ) {
inputStream. read ( fileBytes) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
if ( fileBytes. length > blockSize) {
int blockCount = fileBytes. length / blockSize;
int lastBlockSize = fileBytes. length % blockSize;
for ( int i = 0 ; i < blockCount - 1 ; i++ ) {
byte [ ] blockBytes = new byte [ blockSize] ;
System . arraycopy ( fileBytes, i * blockSize, blockBytes, 0 , blockSize) ;
File newFile = new File ( filePath + "_" + i) ;
}
if ( lastBlockSize > 0 ) {
byte [ ] blockBytes = new byte [ lastBlockSize] ;
System . arraycopy ( fileBytes, blockCount * blockSize, blockBytes, 0 , lastBlockSize) ;
File newFile = new File ( filePath + "_" + blockCount) ;
}
}
}
public static void main ( String [ ] args) {
String filePath = "data.txt" ;
int blockSize = 10 ;
fileSplit ( filePath, blockSize) ;
}
}
方法三:
import java. io. * ;
public class demo{
public static void main ( String [ ] args) {
String path = "e:/test.txt" ;
int size = 1000 ;
byte [ ] b = getFileBytes ( path, size) ;
for ( int i = 0 ; i < b. length; i++ ) {
System . out. print ( b[ i] + " " ) ;
}
}
public static byte [ ] getFileBytes ( String path, int size) {
byte [ ] buffer = null ;
try {
File file = new File ( path) ;
long len = file. length ( ) ;
int num = ( int ) Math . ceil ( len / ( double ) size) ;
buffer = new byte [ num * size] ;
for ( int i = 0 ; i < num; i++ ) {
int start = i * size;
int end = ( i + 1 ) * size > len ? ( int ) len : ( i + 1 ) * size;
InputStream fis = new FileInputStream ( file) ;
fis. read ( buffer, start, end - start) ;
fis. close ( ) ;
}
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
return buffer;
}
}
方法四:
```java
import java. io. File ;
import java. io. FileInputStream ;
import java. io. FileOutputStream ;
import java. io. IOException ;
import java. nio. ByteBuffer ;
public class BlockFile {
public boolean splitFile ( String srcPath, long blockSize) {
File srcFile = new File ( srcPath) ;
if ( srcFile. isFile ( ) && srcFile. exists ( ) ) {
long fileLength = srcFile. length ( ) ;
try {
FileInputStream fis = new FileInputStream ( srcFile) ;
byte [ ] byteArray = new byte [ ( int ) blockSize] ;
int amount = 0 ;
int i = 0 ;
while ( ( amount = fis. read ( byteArray) ) != - 1 ) {
i++ ;
File newFile = new File ( srcPath + "_" + i) ;
FileOutputStream fos = new FileOutputStream ( newFile) ;
ByteBuffer bf = ByteBuffer . wrap ( byteArray, 0 , amount) ;
fos. write ( bf. array ( ) ) ;
fos. flush ( ) ;
fos. close ( ) ;
}
fis. close ( ) ;
return true ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
return false ;
}
} else {
return false ;
}
}
public static void main ( String [ ] args) {
String srcPath = "E://daily.txt" ;
BlockFile bf = new BlockFile ( ) ;
bf. splitFile ( srcPath, 2 * 1024 ) ;
System . out. println ( "文件分割完成!" ) ;
}
}
方法五:
程序
代码如下:
import java. io. * ;
public class SplitFile {
public static void split ( String filePath, int count) {
File file = new File ( filePath) ;
String fileName = file. getName ( ) ;
long fileSize = file. length ( ) ;
long size = fileSize / count;
long leftSize = fileSize;
FileInputStream fileInputStream = null ;
try {
fileInputStream = new FileInputStream ( file) ;
} catch ( FileNotFoundException e) {
e. printStackTrace ( ) ;
return ;
}
for ( int i = 0 ; i < count; i++ ) {
if ( i == count - 1 ) {
size = leftSize;
}
byte [ ] bytes = new byte [ ( int ) size] ;
int a = - 1 ;
try {
a = fileInputStream. read ( bytes) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
FileOutputStream fileOutputStream = null ;
try {
fileOutputStream = new FileOutputStream ( file. getParent ( ) + "/" + fileName. substring ( 0 , fileName. lastIndexOf ( "." ) ) + "_" + i + ".part" ) ;
fileOutputStream. write ( bytes) ;
fileOutputStream. flush ( ) ;
fileOutputStream. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
leftSize -= size;
}
}
public static void main ( String [ ] args) {
String filePath = "D:/TestFile/a.mp4" ;
split ( filePath, 3 ) ;
}
}
方法六:
用RandomAccessFile 实现文件分块,并返回一个List 集合。
*
* @param filePath
* 文件路径
* @param blockSize
* 分块大小
* @return
* /
public static List < byte [ ] > blockSplit ( String filePath, int blockSize) {
List < byte [ ] > list = new ArrayList < byte [ ] > ( ) ;
try {
RandomAccessFile rFile = new RandomAccessFile ( filePath, "rw" ) ;
long size = rFile. length ( ) ;
long blockCount = size % blockSize == 0 ? size / blockSize : size
/ blockSize + 1 ;
for ( int i = 0 ; i < blockCount; i++ ) {
byte [ ] buffer = new byte [ blockSize] ;
int readSize = ( i != blockCount - 1 ) ? blockSize
: ( int ) ( size - i * blockSize) ;
rFile. read ( buffer, 0 , readSize) ;
list. add ( buffer) ;
}
rFile. close ( ) ;
return list;
} catch ( Exception e) {
logger. error ( e. getMessage ( ) , e) ;
}
return list;
}
}
方法七:
*
* @param file 文件
* @param size 每块大小
* @return
* @throws Exception
* /
public static List < File > getFilePartInfos ( File file, long size) throws Exception {
long length = file. length ( ) ;
long blockSize = size;
int blockNum = ( int ) ( length / size) ;
if ( length % size != 0 ) {
blockNum++ ;
}
String partPath = file. getAbsolutePath ( ) + ".sp" ;
File partPathFile = new File ( partPath) ;
if ( ! partPathFile. exists ( ) ) {
partPathFile. mkdirs ( ) ;
}
RandomAccessFile raf = new RandomAccessFile ( file, "r" ) ;
List < File > fileList = new ArrayList < File > ( ) ;
long startByte = 0 ;
long endByte = 0 ;
for ( int i = 0 ; i < blockNum; i++ ) {
startByte = i * blockSize;
if ( i == blockNum - 1 ) {
endByte = length;
} else {
endByte = ( i + 1 ) * blockSize;
}
System . out. println ( "block:" + i + " startByte-> " + startByte + " endByte->" + endByte) ;
fileList. add ( cutFileBySize ( file, startByte, endByte, partPathFile. getAbsolutePath ( ) , i, raf) ) ;
}
raf. close ( ) ;
return fileList;
}
private static File cutFileBySize ( File file, long startByte, long endByte, String partPath, int part, RandomAccessFile raf) throws IOException {
File partFile = new File ( partPath + "/" + file. getName ( ) + "_" + part) ;
RandomAccessFile raf2 = new RandomAccessFile ( partFile, "rw" ) ;
try {
raf. seek ( startByte) ;
byte [ ] flush = new byte [ 1024 ] ;
int len = - 1 ;
while ( startByte < endByte) {
raf. seek ( startByte) ;
if ( endByte - startByte >= 1024 ) {
len = raf. read ( flush) ;
} else {
len = raf. read ( flush, 0 , ( int ) ( endByte - startByte) ) ;
}
if ( len > 0 ) {
raf2. write ( flush, 0 , len) ;
startByte = raf. getFilePointer ( ) ;
} else {
break ;
}
}
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
raf2. close ( ) ;
}
return partFile;
}
public void combinePartFile ( List < File > files, String destFileName) {
File preFile = null ;
try {
File destFile = new File ( destFileName) ;
BufferedOutputStream destBufOutStream = new BufferedOutputStream ( new FileOutputStream ( destFile) ) ;
byte [ ] flush = new byte [ 1024 ] ;
int len = - 1 ;
for ( File file : files) {
if ( file == null ) {
throw new IllegalArgumentException ( "file should not be a null" ) ;
}
if ( ! file. exists ( ) ) {
throw new RuntimeException ( file. getAbsolutePath ( ) + "文件不存在" ) ;
}
BufferedInputStream partBufInStream = new BufferedInputStream ( new FileInputStream ( file) ) ;
if ( preFile == null ) {
while ( ( len = partBufInStream. read ( flush) ) != - 1 ) {
destBufOutStream. write ( flush, 0 , len) ;
}
} else {
long skipLen = destFile. length ( ) - preFile. length ( ) ;
while ( skipLen > 0 ) {
long skip = partBufInStream. skip ( skipLen) ;
skipLen -= skip;
}
while ( ( len = partBufInStream. read ( flush) ) != - 1 ) {
destBufOutStream. write ( flush, 0 , len) ;
}
}
preFile = file;
partBufInStream. close ( ) ;
}
destBufOutStream. close ( ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
}
}
方法八:
import java. io. * ;
import java. util. ArrayList ;
import java. util. List ;
public class FileSplit {
private static final int DEFAULT_BLOCK_SIZE= 1024 * 1024 ;
public static List < File > splitBySize ( String file, int size, String blockSuffix) {
List < File > blocks= new ArrayList < File > ( ) ;
File f= new File ( file) ;
if ( ! f. exists ( ) ) {
System . out. println ( "指定的文件不存在。" ) ;
return blocks;
} else {
long length= f. length ( ) ;
System . out. println ( "OriginalFilelength=" + length) ;
if ( size<= 0 ) {
size= DEFAULT_BLOCK_SIZE;
}
int partNumber= ( int ) Math . ceil ( 1.0 * length/ size) ;
System . out. println ( "Number of blocks is " + partNumber) ;
int count= 1 ;
try {
FileInputStream fis = new FileInputStream ( f) ;
byte [ ] b = new byte [ 2048 ] ;
for ( int i= 0 ; i< partNumber; i++ ) {
File block= new File ( f. getParent ( ) , f. getName ( ) + "_" + count+ blockSuffix) ; ;
boolean createNewFile= block. createNewFile ( ) ;
if ( createNewFile) {
blocks. add ( block) ;
System . out. println ( "块" + count+ "创建成功,path=" + block. getAbsolutePath ( ) ) ;
}
RandomAccessFile raf= new RandomAccessFile ( block, "rw" ) ;
int readLength= 0 ;
while ( readLength< size && ( readLength= fis. read ( b) ) > 0 ) {
raf. write ( b, 0 , readLength) ;
}
raf. close ( ) ;
count++ ;
}
} catch ( IOException e) {
System . out. println ( "分块失败。" ) ;
e. printStackTrace ( ) ;
}
}
return blocks;
}
public static void main ( String [ ] args) {
String FilePath = "D:\\Project\\MyEclipseIDEworkspace\\page18.jpg" ;
List < File > blocks = splitBySize ( FilePath , 0 , ".block" ) ;
if ( blocks!= null ) {
blocks. forEach ( block -> {
System . out. println ( block. getName ( ) + "\tSize:" + block. length ( ) ) ;
} ) ;
}
}
}
方法九:
```java
import java. io. * ;
import java. util. ArrayList ;
import java. util. List ;
public class SplitFileUtil {
public static List < File > splitFile ( File src, int blocksNumber) throws IOException {
List < File > result = new ArrayList < > ( ) ;
long srcSize = src. length ( ) ;
int blockSize = ( int ) Math . ceil ( srcSize* 1.0f / blocksNumber) ;
FileInputStream fis = null ;
FileOutputStream fos = null ;
byte [ ] buf = new byte [ blockSize] ;
try {
fis = new FileInputStream ( src) ;
int len = 0 ;
while ( ( len= fis. read ( buf) ) != - 1 ) {
File block = new File ( src. getParent ( ) , src. getName ( ) + String . valueOf ( result. size ( ) ) ) ;
fos = new FileOutputStream ( block) ;
fos. write ( buf, 0 , len) ;
result. add ( block) ;
}
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
fis. close ( ) ;
fos. close ( ) ;
}
return result;
}
public static void main ( String [ ] args) throws IOException {
File src = new File ( "beforeSplit.txt" ) ;
List < File > splitFiles = splitFile ( src, 3 ) ;
for ( int i= 0 ; i< splitFiles. size ( ) ; i++ ) {
System . out. println ( splitFiles. get ( i) . getAbsoluteFile ( ) ) ;
}
}
}
自定义EditText实现限制输入指定字符
要实现只允许输入指定字符的EditText ,可以重写EditText 的onTextChanged方法,并在其中过滤出指定字符,然后使用setSelection方法将光标定位到文本末尾。
以下是一个示例代码:
```
public class LimitedEditText extends EditText {
private String mAllowedCharacters = "" ;
private String mPreviousText = "" ;
private int mPreviousLength = 0 ;
public LimitedEditText ( Context context) {
super ( context) ;
}
public LimitedEditText ( Context context, AttributeSet attrs) {
super ( context, attrs) ;
}
public LimitedEditText ( Context context, AttributeSet attrs, int defStyleAttr) {
super ( context, attrs, defStyleAttr) ;
}
public void setAllowedCharacters ( String allowedCharacters) {
mAllowedCharacters = allowedCharacters;
}
@Override
protected void onTextChanged ( CharSequence text, int start, int lengthBefore, int lengthAfter) {
super . onTextChanged ( text, start, lengthBefore, lengthAfter) ;
if ( text. toString ( ) . equals ( mPreviousText) ) {
return ;
}
String currentText = text. toString ( ) ;
int currentLength = currentText. length ( ) ;
StringBuilder filteredText = new StringBuilder ( ) ;
for ( int i = 0 ; i < currentLength; i++ ) {
String c = String . valueOf ( currentText. charAt ( i) ) ;
if ( mAllowedCharacters. contains ( c) ) {
filteredText. append ( c) ;
}
}
if ( ! currentText. equals ( filteredText. toString ( ) ) ) {
setText ( filteredText. toString ( ) ) ;
setSelection ( getText ( ) . length ( ) ) ;
}
mPreviousText = getText ( ) . toString ( ) ;
mPreviousLength = mPreviousText. length ( ) ;
}
}
```
使用时,可以通过setAllowedCharacters方法设置允许输入的字符,例如:
```
LimitedEditText editText = findViewById ( R . id. editText1) ;
editText. setAllowedCharacters ( "ABC123" ) ;
```
这样,只有字符A 、B 、C 、1 、2 、3 才能被输入。
public class MyEditText extends EditText {
private String mAllowStr;
public MyEditText ( Context context) {
super ( context) ;
}
public MyEditText ( Context context, AttributeSet attrs) {
super ( context, attrs) ;
}
public MyEditText ( Context context, AttributeSet attrs, int defStyleAttr) {
super ( context, attrs, defStyleAttr) ;
}
public void setAllowStr ( String allowStr) {
mAllowStr = allowStr;
}
@Override
protected void onTextChanged ( CharSequence text, int start, int lengthBefore, int lengthAfter) {
super . onTextChanged ( text, start, lengthBefore, lengthAfter) ;
if ( TextUtils . isEmpty ( mAllowStr) ) {
return ;
}
if ( text. toString ( ) . contains ( mAllowStr) ) {
setText ( mAllowStr) ;
setSelection ( mAllowStr. length ( ) ) ;
}
}
}
获取Gps卫星数量
以下是一个获取GPS卫星数量的示例代码:
```
public class MainActivity extends AppCompatActivity {
private TextView satelliteCountTextView;
private LocationManager locationManager;
private GpsStatus. Listener gpsStatusListener;
@Override
protected void onCreate ( Bundle savedInstanceState) {
super . onCreate ( savedInstanceState) ;
setContentView ( R . layout. activity_main) ;
satelliteCountTextView = findViewById ( R . id. satellite_count_textview) ;
locationManager = ( LocationManager ) getSystemService ( Context . LOCATION_SERVICE) ;
gpsStatusListener = new GpsStatus. Listener ( ) {
@Override
public void onGpsStatusChanged ( int event) {
if ( event == GpsStatus . GPS_EVENT_SATELLITE_STATUS) {
int count = 0 ;
Iterable < GpsSatellite > satellites = locationManager. getGpsStatus ( null ) . getSatellites ( ) ;
for ( GpsSatellite satellite : satellites) {
if ( satellite. usedInFix ( ) ) {
count++ ;
}
}
satelliteCountTextView. setText ( "GPS卫星数量:" + count) ;
}
}
} ;
}
@Override
protected void onResume ( ) {
super . onResume ( ) ;
locationManager. addGpsStatusListener ( gpsStatusListener) ;
}
@Override
protected void onPause ( ) {
super . onPause ( ) ;
locationManager. removeGpsStatusListener ( gpsStatusListener) ;
}
}
```
该示例代码包括以下步骤:
1. 创建一个 LocationManager 对象来管理位置信息。
2. 创建一个 GpsStatus. Listener 对象,当 GPS 状态发生变化时会调用该对象的 onGpsStatusChanged 方法。
3. 在 onResume 方法中注册 GpsStatus. Listener 对象,以便能够接收到 GPS 状态变化的通知。
4. 在 onPause 方法中取消注册 GpsStatus. Listener 对象,以避免在后台不必要的通知。
5. 在 onGpsStatusChanged 方法中获取 GPS 卫星数量。首先调用 LocationManager 对象的 getGpsStatus 方法来获取 GPS 状态信息,然后遍历所有的 GpsSatellite 对象,检查是否被用于位置定位并计算已用于定位的卫星数量。
6. 最后将数量显示到界面上。
请注意,您需要在 AndroidManifest . xml 文件中添加以下权限:
```
< uses - permission android: name= "android.permission.ACCESS_FINE_LOCATION" / >
刷新RecycleView数据:
setNewData:
```
public void setNewData ( List < T > newData) {
if ( mData != null ) {
mData. clear ( ) ;
mData. addAll ( newData) ;
} else {
mData = new ArrayList < > ( newData) ;
}
notifyDataSetChanged ( ) ;
}
```
clearSelections:
```
public void clearSelections ( ) {
mSelectedItems. clear ( ) ;
notifyDataSetChanged ( ) ;
}
```
setNewData方法实现:
public void setNewData ( @Nullable List < T > data) {
mData. clear ( ) ;
if ( data != null ) {
mData. addAll ( data) ;
}
notifyDataSetChanged ( ) ;
}
clearSelections方法实现:
public void clearSelections ( ) {
for ( int i = 0 ; i < mSelectedItems. size ( ) ; i++ ) {
int position = mSelectedItems. keyAt ( i) ;
notifyItemChanged ( position) ;
}
mSelectedItems. clear ( ) ;
}
setNewData方法实现:
public void setNewData ( List < T > newData) {
mData = newData;
notifyDataSetChanged ( ) ;
}
clearSelections方法实现:
public void clearSelections ( ) {
for ( int i = 0 ; i < mSelectedPositions. size ( ) ; i++ ) {
int position = mSelectedPositions. keyAt ( i) ;
mSelectedPositions. put ( position, false ) ;
}
mSelectedPositions. clear ( ) ;
notifyDataSetChanged ( ) ;
}
其中mData为存放数据的列表,mSelectedPositions为记录选中状态的集合,key为位置,value为状态(true 表示选中,false 表示未选中)。同时,notifyDataSetChanged ( ) 方法是用来通知RecyclerView 数据已改变,需要刷新数据显示的。
setNewData的实现:
首先判断传入的list是否为空,如果是则直接赋空值给mData。
如果不为空,则将传入的list赋值给mData,并且清空之前的选择状态及选择数据列表。
最后调用notifyDataSetChanged ( ) 方法通知适配器数据发生了改变。
```
public void setNewData ( @Nullable List < T > list) {
if ( list == null ) {
mData = null ;
} else {
mData = new ArrayList < > ( list) ;
mSelectedPositions. clear ( ) ;
mSelectedData. clear ( ) ;
}
notifyDataSetChanged ( ) ;
}
```
clearSelections的实现:
循环遍历选择状态的列表mSelectedPositions,将其中的每个位置对应的选择状态设置为false 。
然后清空选择数据列表mSelectedData。
最后调用notifyDataSetChanged ( ) 方法通知适配器数据发生了改变。
```
public void clearSelections ( ) {
for ( int i = 0 ; i < mSelectedPositions. size ( ) ; i++ ) {
mSelectedPositions. set ( i, false ) ;
}
mSelectedData. clear ( ) ;
notifyDataSetChanged ( ) ;
}
```
Android中的静态广播setPackage()方法里面传的参数是什么
用于指定静态广播接收者的包名。如果指定了包名,那么只有该包名的应用程序才能接收到该广播。下面是一个示例代码:
Intent intent = new Intent ( "com.example.MY_CUSTOM_ACTION" ) ;
intent. putExtra ( "message" , "Hello World!" ) ;
intent. setPackage ( "com.example.myapp" ) ;
sendBroadcast ( intent) ;
在上面的示例代码中,我们定义了一个自定义的广播动作 `com. example. MY_CUSTOM_ACTION` ,并向其添加了一个
`message` 的字符串额外信息。调用 `setPackage ( ) ` 方法,将广播发送到包名为 `com. example. myapp`
的应用程序中。这样,只有在包名为 `com. example. myapp` 的应用程序中注册了接收器的情况下,才会接收到该广播。
需要注意的是,如果不指定包名,则所有应用程序都可以接收到该广播。另外,包名应该是目标应用程序的准确包名,
并且应该具有正确的权限来接收广播。
Java多次调用单例模式类,类中的构造函数会重复执行吗:
在 Java 中,单例模式是一种创建只有一个对象的类设计模式。在多次调用单例模式类时,类中的构造函数只会执行一次。
这是因为单例模式的本质是将构造函数私有化,使得外界无法通过构造函数来创建对象。相反,单例模式通过提供一个静态方法来获取单例对象。在该方法中,会先检查单例对象是否已经存在,如果不存在则创建一个新的对象,并保存到静态变量中,以便于下一次获取单例对象时直接返回。
因此,多次调用单例模式类时,只会执行一次构造函数。下面是一个简单的单例模式示例代码:
public class Singleton {
private static Singleton instance;
private Singleton ( ) {
System . out. println ( "Singleton Constructor" ) ;
}
public static Singleton getInstance ( ) {
if ( instance == null ) {
instance = new Singleton ( ) ;
}
return instance;
}
}
在上面的示例代码中,只有在单例对象不存在时才会创建一个新的对象,并输出一条日志。因此,无论调用多少次 `getInstance ( ) ` 方法,都只会输出一次日志。
需要注意的是,单例模式在多线程环境下需要特别注意线程安全问题,可以使用双重检查锁定等方式来实现线程安全的单例模式。
单例双重检查锁定的实现方式:
在多线程环境下,为了确保只有一个对象被创建,需要使用双重检查锁定(Double - checked locking)的方式来实现线程安全的单例模式。
双重检查锁定的实现方式包含以下几个步骤:
1. 将构造函数私有化,以禁止外部直接创建对象。
2. 定义一个私有的静态变量,用于保存单例对象。
3. 提供一个公共的静态方法,用于获取单例对象。
4. 在公共方法中使用双重检查锁定的方式来确保线程安全。
以下是一个使用双重检查锁定实现线程安全的单例模式示例代码:
public class Singleton {
private static volatile Singleton instance;
private Singleton ( ) { }
public static Singleton getInstance ( ) {
if ( instance == null ) {
synchronized ( Singleton . class ) {
if ( instance == null ) {
instance = new Singleton ( ) ;
}
}
}
return instance;
}
}
在上面的示例代码中,使用了 `volatile ` 关键字来确保多个线程都可以正确地访问 `instance` 变量。在 `getInstance ( ) ` 方法中,第一次检查 `instance` 是否已经被创建,如果未被创建,则进入同步代码块进行第二次检查。如果依然未被创建,则创建并返回单例对象。
由于使用了双重检查锁定的方式,所以即使在多线程环境下也能正确地保证只有一个对象被创建。