日期: 2021 年 5 月 31 日

android studio之读取系统联系人

1.我们想实现这个功能,我们要先了解一些事情:

a.很明显,我们要从另一个app中读取一些资料,很明显我们要用到一些工具,ContentResolver就是我们要用到的工具.

b.而这个东西的CRUD和一个叫内容uri的东西关系十分密切,所谓uri就是给我们的数据设置唯一标识,这样我们就可以根据这个uri找到某一项数据而不会出差错,而这个uri通常是如何命名的呢,具体分为2部分,authority和path,前者是包名,后者是为了区分同一包而不同表的标识符,通常获取到一个内容uri后,我们还要对他进行解释,解析后变成一个uri才能直接使用。

c.ContentResolver是如何获取一个具体的对象呢?一般是这样的

Cursor cursor=getContentResolver().query(a,b,c,d,e);
我们来解释一下这行代码的参数,a:指定该程序下的具体某一张表,b:制定查询那一列的列名,如果为空就可以任意查询,c:指定where的约束定点,d:为where中的占位符提供具体的值,e:指定查询的排序方式.  b,c,d,e都可以为空,a不行,因为那一张表你得知道把。

d.读取联系人其实已经设计到系统权限的问题,所以如何获取到权限我们也必须知道。

首先获取任何权限之前,我们都必须声明这个权限,在哪声明我就不多说了,例:

<uses-permission android:name=”android.permission.READ_CONTACTS”></uses-permission>
这样我们就声明了获取系统联系人的权限,对于危险权限,我们仅仅是声明是不够的,我们还必须亲自获取机主的同意:

if(ContextCompat.checkSelfPermission(this, Manifest.permission.权限名)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.权限名},1);
}
else
{
获取权限之后的方法名();
}
其实格式都是固定的,我们所要更改的是权限名和我们如果征得权限以后所要用到的方法。

ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);一旦这个方法被执行之后,无论是否获取权限,都会执行
public void onRequestPermissionsResult(int requestCode,String[]permisssions,int[]grantResults){
switch(requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED)
{
获取权限后所执行的方法名();
}else{
Toast.makeText(this,”NO OK”,Toast.LENGTH_SHORT).show();//获取失败后所给的提示
}
break;
default:
}
}

2.实现功能

package com.example.gdzc.myapplication;

import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
ArrayAdapter<String>adapter;
List<String> content=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView list=(ListView)findViewById(R.id.list);//引用一个list来接收系统联系人的信息
adapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,content);
list.setAdapter(adapter);
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
}
else
{
readContacts();
}
}
private void readContacts()
{
Cursor cursor=null;
try{
cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if(cursor!=null)
{
while(cursor.moveToNext())
{
String name=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String phone=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
content.add(name+”\n”+phone);
}
adapter.notifyDataSetChanged();
}
}catch (Exception e)
{
e.printStackTrace();
}
finally {
if(cursor!=null)
{
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,String[]permisssions,int[]grantResults){
switch(requestCode){
case 1:
if(grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED)
{
readContacts();
}else{
Toast.makeText(this,”NO OK”,Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}

 

Android studio之实现记住密码(SharePreference)

1.首先,我们都希望我们的信息可以得到储存,而不希望我们下一次上线我们原来的信息就没了,我们就要选择一种方法储存我们的信息,而AS里面也有很多的方法让我们储存我们的信息,如信息流之类的,但AS提供了一个可以令我们更加便捷存储我们信息的工具,就是SharePreference存储,他的使用十分方便,也很容易理解,有点类似于C++的STL里面的mulstiset.

2.如果我们要学习一个器件,那么他有什么重要的方法就尤为重要了.

a.首先它的添加方式只有MODE_PRIVATE,即是它只能在原文件末尾追加,而不能覆盖原文件

b.SharePreference.editor editor=getSharedPreferences(“文件名”,模式).editor这样我们就创建好了一个SharePrefence编辑器了,需要注意的是我们所有的这种类型的文件都是储存在一个默认的路径的,所以我们不需要增加文件路径,如果需要查看,我们只需要打开Tool里面的Device Monitor里面的某一项文件就行,至于具体到那个我就不说明了,如何往里面增加内容呢,也很简单,只需要调用.put类型(“键值”,”内容”),编辑完后,再调用.apply()就设置完成了.

c.而如何在里面拿值了,其实模式基本一样,SharePreference editor=getSharedPreferences(“文件名”,模式),然后知道键值和类型就可以通过.get类型(“键值”,”如果不存在此键值用这个字符串的内容代替”);

3.我们知道这些之后那么就很好实现了

package activitytest.example.com.file;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class MainActivity extends AppCompatActivity {

private Button button;
private EditText Name;
private EditText Passage;
private CheckBox checkBox;
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button=(Button)findViewById(R.id.button);
Name=(EditText)findViewById(R.id.nameedit);
Passage=(EditText)findViewById(R.id.password);
checkBox=(CheckBox)findViewById(R.id.pass);
sharedPreferences= PreferenceManager.getDefaultSharedPreferences(this);
boolean pang=sharedPreferences.getBoolean(“remember_password”,false);
if(pang)
{
Name.setText(sharedPreferences.getString(“Name”,””));
Passage.setText(sharedPreferences.getString(“Password”,””));
checkBox.setChecked(true);
}
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
editor=sharedPreferences.edit();
String tempname=Name.getText().toString();
String temppass=Passage.getText().toString();
if(tempname.equals(“GDZC”)&&temppass.equals(“123456”))
{
if(checkBox.isChecked())
{
editor.putString(“Name”,tempname);
editor.putString(“Password”,temppass);
editor.putBoolean(“remember_password”,true);
Toast.makeText(MainActivity.this,”success”,Toast.LENGTH_SHORT).show();
}
else {
editor.clear();
Toast.makeText(MainActivity.this,”No”,Toast.LENGTH_SHORT).show();
}
editor.apply();
}
}
});
}
布局我就不列出来了

Android Studio:使用Camera拍照(三)为相机增加取景蒙板/浮层

写在前面的话:每一个实例的代码都会附上相应的代码片或者图片,保证代码完整展示。*重要的是保证例程的完整性!!!方便自己也方便他人~欢迎大家交流讨论~

在相机预览时增加取景蒙板/浮层的思路是自定义View,用framelayout把自定义view放在surfaceview上面,在oncreat方法中计算坐标位置,调用自定义view中的set…方法设置坐标,根据坐标绘图。
接下来把上篇的自定义相机和增加蒙板那篇的代码结合起来,为相机增加取景蒙板/浮层,上代码!

新建一个Android项目
取名为Cameratwo,具体文件的命名如下

%title插图%num
values文件夹
strings.xml
<resources>
<string name=”app_name”>Cameratwo</string>
<string name=”button_name”>开始拍照</string>
</resources>

layout文件夹
activity_first.xml
<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”.FirstActivity”>

<Button
android:id=”@+id/button”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:onClick=”customCamera”
android:text=”@string/button_name” />

<ImageView
android:id=”@+id/iv”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />

</LinearLayout>

custom.xml
<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout
xmlns:android=”http://schemas.android.com/apk/res/android” android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”>

<FrameLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_alignParentTop=”true”>

<SurfaceView
android:id=”@+id/preview”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:visibility=”visible” />

<com.example.administrator.cameratwo.TranslucencyView
android:id=”@+id/transView”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />
</FrameLayout>

<ImageButton
android:id=”@+id/button3″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentBottom=”true”
android:layout_centerHorizontal=”true”
android:background=”@drawable/button3″
android:onClick=”capture” />
</RelativeLayout>

result.xml
<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android” android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<ImageView
android:id=”@+id/pic”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />

</LinearLayout>

manifests
AndroidManifest.xml
在该文件中增加以下两项

<uses-permission android:name=”android.permission.CAMERA”/>
<uses-feature android:name=”android.hardware.Camera”/>
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”/>

<activity android:name=”.Customcamera”/>
<activity android:name=”.ResultActivity”/>

Java文件夹
FirstActivity
package com.example.administrator.cameratwo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.Intent;
import android.view.View;

public class FirstActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
}
public void customCamera(View view){
startActivity(new Intent(this,Customcamera.class));
}
}

 

Customcamera
这里完全没有用到Butter Knife 框架,因为我用的不熟,就删掉了orz…

package com.example.administrator.cameratwo;

import android.content.Intent;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Customcamera extends AppCompatActivity implements SurfaceHolder.Callback{
private Camera mCamera;
private SurfaceView mPreview;
private SurfaceHolder mHolder;
private int cameraId=1;
private int mHeight;

private Camera.PictureCallback mpictureCallback=new Camera.PictureCallback(){
@Override
public void onPictureTaken(byte[] data,Camera camera){
File tempfile=new File(“/sdcard/emp.png”);
try{ FileOutputStream fos =new FileOutputStream(tempfile);
fos.write(data);
fos.close();
Intent intent=new Intent(Customcamera.this,ResultActivity.class);
intent.putExtra(“picpath”,tempfile.getAbsolutePath());
startActivity(intent);
Customcamera.this.finish();
}
catch (IOException e){e.printStackTrace();}
}
};
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.custom);
mPreview=findViewById(R.id.preview);
mPreview.setZOrderOnTop(false);
mHolder=mPreview.getHolder();
mHolder.setFormat(PixelFormat.TRANSPARENT);
mHolder.addCallback(this);
mPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCamera.autoFocus(null);
}
});
//获取imgButton的坐标,以便蒙板在imgButton处掏空,并获取页面参数
ImageButton imgButton=findViewById(R.id.button3);
TranslucencyView translucencyView=findViewById(R.id.transView);
imgButton.postDelayed(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
ImageButton imgButton=findViewById(R.id.button3);
mHeight = getSupportActionBar().getHeight();
int left = imgButton.getLeft();
int right = imgButton.getRight();
int top = imgButton.getTop();
int bottom = imgButton.getBottom();
int mCoodinate[] = {left, top, right, bottom};
TranslucencyView translucencyView=findViewById(R.id.transView);
translucencyView.setCircleLocation(mCoodinate);
}
});
}
},1200);
FrameLayout.LayoutParams layoutParams=(FrameLayout.LayoutParams) translucencyView.getLayoutParams();
translucencyView.setLayoutParams(layoutParams);
translucencyView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mCamera.autoFocus(null);
return false;
}
});
mHolder.lockCanvas();
}

public void capture(View view){
Camera.Parameters parameters=mCamera.getParameters();
parameters.setPictureFormat(ImageFormat.JPEG);
parameters.setPreviewSize(800,400);
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
mCamera.autoFocus(new Camera.AutoFocusCallback(){
@Override
public void onAutoFocus(boolean success, Camera camera) {
if(success){mCamera.takePicture(null,null, mpictureCallback);}
}
});

}

@Override
protected void onResume() {
super.onResume();
if (mCamera==null){
mCamera=getCamera();
if(mHolder!=null){
setStartPreview(mCamera,mHolder);}
}
}

@Override
protected void onPause() {
super.onPause();
releaseCamera(); }

private Camera getCamera(){
Camera camera;
try{
camera=Camera.open(cameraId);
}
catch (Exception e){
camera=null;
e.printStackTrace(); }
return camera;
}

private void setStartPreview(Camera camera,SurfaceHolder holder){
try{
camera.setPreviewDisplay(holder);
camera.setDisplayOrientation(90);
camera.startPreview();
}
catch (Exception e){
e.printStackTrace(); }
}
private void releaseCamera(){
if(mCamera!=null){
mCamera.stopPreview();
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera=null;
}
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
setStartPreview(mCamera,mHolder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mCamera.stopPreview();
setStartPreview(mCamera,mHolder);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus); }

}

TranslucencyView
package com.example.administrator.cameratwo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.ImageView;

public class TranslucencyView extends ImageView{
private final Context mContext;
private int[] mCircleLocation;//声明存放坐标的数组

public TranslucencyView(Context context){this(context,null);}
public TranslucencyView(Context context, AttributeSet attributeSet){this(context,attributeSet,0);}
public TranslucencyView(Context context,AttributeSet attributeSet,int defStyleAttr){
super(context,attributeSet,defStyleAttr);
this.mContext=context;
initView();
}

private void initView(){setBackgroundColor(Color.parseColor(“#7f000000”));}//设置半透明底色
public void setCircleLocation(int[] location){
this.mCircleLocation=location;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mCircleLocation!=null){
//掏空一个圆形
Paint paintarc=new Paint(Paint.ANTI_ALIAS_FLAG);//创建一个画笔实例
PorterDuffXfermode porterDuffXfermode=new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
paintarc.setXfermode(porterDuffXfermode);
paintarc.setAntiAlias(true);
RectF rectF=new RectF(mCircleLocation[0],mCircleLocation[1],mCircleLocation[2],mCircleLocation[3]);
canvas.drawArc(rectF,0,360,true,paintarc);
//画虚线
Paint paintdashed=new Paint(Paint.ANTI_ALIAS_FLAG);
paintdashed.setStyle(Paint.Style.STROKE);
paintdashed.setColor(Color.WHITE);
paintdashed.setStrokeWidth(5);
PathEffect pathEffect=new DashPathEffect(new float[]{10,10},0);
paintdashed.setPathEffect(pathEffect);
canvas.drawArc(rectF,0,360,true,paintdashed);
//画矩形框
/** Paint paintrect=new Paint(Paint.ANTI_ALIAS_FLAG);
PorterDuffXfermode porterDuffXfermode1=new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
paintrect.setXfermode(porterDuffXfermode1);
paintrect.setAntiAlias(true);
//paintrect.setStrokeWidth(5);
canvas.drawRect(200, 400, 900, 1300, paintrect);*/
//画椭圆
Paint paintoval=new Paint(Paint.ANTI_ALIAS_FLAG);
PorterDuffXfermode porterDuffXfermode2=new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
paintoval.setXfermode(porterDuffXfermode2);
paintoval.setAntiAlias(true);
//paintoval.setStrokeWidth(10);
canvas.drawOval(100,200,1000,1200,paintoval);
}
}
}

这里画了个椭圆,注意用的都是*对坐标,我估计在不同的手机中可能会出现适配问题,不过还没想好相对的怎么画啦

ResultActivity
这个的代码和之前的3.3ResultActivity.java代码一模一样,大家直接看
Android Studio:使用Camera拍照(二)自定义相机

小结
上完了代码,我要小结一下给相机加取景蒙版和之前一篇的直接添加(文章后面有附上)有什么差别,我*开始的时候确实是很简单的两处代码合在一起,然后执行时发现两个问题:
1.整个显示出来的是深蓝色的
2.在蒙版还没稳定出现前,surfaceview尚在预览,蒙板稳定出现后,surfaceview瞬间卡在了那时的页面
解决问题1:我又回去看了自定义相机的执行(那个项目叫startcamera),发现surfaceview从开始到预览确实有个从暗变深蓝再变浅的过程,于是我猜是 showMask() 方法(这个方法中的内容后来被单独拿出来放到了onCreate中)中设置的时间太短,该时间用于测量view是够了(因为蒙板能顺利显示,如果时间不够,整个页面其实会闪退),那就是通过intent传给TranslucencyActivity.java的时间太短,surfaceview还没构造完,intent就把坐标信息给TranslucencyActivity了,于是我把时间从500ms改到1200ms
解决问题2:其实从解决问题1的*后几句话已经能看出问题2的端倪了,就是原本的showMask() *后有几句代码:

Intent intent = new Intent(SecondActivity.this, TranslucencyActivity.class);
intent.putExtra(“Location”, mCoodinate);
startActivity(intent);

这就意味着信使不仅将坐标传给TranslucencyActivity,还打开了TranslucencyActivity!!那这个界面就把surfaceview定住了,因为TranslucencyActivity没有任何写预览的方法,它只是把自定义的view和自己的页面绑在一起,所以要弄取景框蒙板没有TranslucencyActivity任何事情,我就把它删掉了,其他所做的更改上面代码已详细给出。

执行效果:

%title插图%num

2021年5月中国采购经理指数运行情况

国家统计局服务业调查中心中国物流与采购联合会   一、中国制造业采购经理指数运行情况   5月份,中国制造业采购经理指数(PMI)为51.0%,微低于上月0.1个百分点,继续位于临界点以上,制造业保持平稳扩张。    从企业规模看,大、中型企业PMI分别为51.8%和51.1%,比上月上升0.1和0.8个百分点,均高于临界点;小型企业PMI为48.8%,比上月下降2.0个百分点,低于临界点。   从分类指数看,在构成制造业PMI的5个分类指数中,生产指数和新订单指数均高于临界点,原材料库存指数、从业人员指数和供应商配送时间指数均低于临界点。   生产指数为52.7%,比上月上升0.5个百分点,高于临界点,表明制造业生产扩张力度有所增强。   新订单指数为51.3%,比上月回落0.7个百分点,高于临界点,表明制造业市场需求增长有所放缓。   原材料库存指数为47.7%,比上月下降0.6个百分点,低于临界点,表明制造业主要原材料库存量有所减少。   从业人员指数为48.9%,比上月下降0.7个百分点,低于临界点,表明制造业企业用工景气度较上月回落。   供应商配送时间指数为47.6%,比上月下降1.1个百分点,低于临界点,表明制造业原材料供应商交货时间进一步延长。 表1 中国制造业PMI及构成指数(经季节调整)                                              单位:%   PMI   生产 新订单 原材料 库存 从业人员 供应商配送时间 2020年5月 50.6 53.2 50.9 47.3 49.4 50.5 2020年6月 50.9 53.9 51.4 47.6 49.1 50.5 2020年7月 51.1 54.0 51.7 47.9 49.3 50.4 2020年8月 51.0 53.5 52.0 47.3 49.4 50.4 2020年9月 51.5 54.0 52.8 48.5 49.6 50.7 2020年10月 51.4 53.9 52.8 48.0 49.3 50.6 2020年11月 52.1 54.7 53.9 48.6 49.5 50.1 2020年12月 51.9 54.2 53.6 48.6 49.6 49.9 2021年1月 51.3 53.5 52.3 49.0 48.4 48.8 2021年2月 50.6 51.9 51.5 47.7 48.1 47.9 2021年3月 51.9 53.9 53.6 48.4 50.1 50.0 2021年4月 51.1 52.2 52.0 48.3 49.6 48.7 2021年5月 51.0 52.7 51.3 47.7 48.9 47.6  表2 中国制造业PMI其他相关指标情况(经季节调整)                                              单位:%   新出口 订单 进口 采购量 主要原材料购进价格 出厂 价格 产成品 库存 在手 订单 生产经营活动预期 2020年5月 35.3 45.3 50.8 51.6 48.7 47.3 44.1 57.9 2020年6月 42.6 47.0 51.8 56.8 52.4 46.8 44.8 57.5 2020年7月 48.4 49.1 52.4 58.1 52.2 47.6 45.6 57.8 2020年8月 49.1 49.0 51.7 58.3 53.2 47.1 46.0 58.6 2020年9月 50.8 50.4 53.6 58.5 52.5 48.4 46.1 58.7 2020年10月 51.0 50.8 53.1 58.8 53.2 44.9 47.2 59.3 2020年11月 51.5 50.9 53.7 62.6 56.5 45.7 46.7 60.1 2020年12月 51.3 50.4 53.2 68.0 58.9 46.2 47.1 59.8 2021年1月 50.2 49.8 52.0 67.1 57.2 49.0 47.3 57.9 2021年2月 48.8 49.6 51.6 66.7 58.5 48.0 46.1 59.2 2021年3月 51.2 51.1 53.1 69.4 59.8 46.7 46.6 58.5 2021年4月 50.4 50.6 51.7 66.9 57.3 46.8 46.4 58.3 2021年5月 48.3 50.9 51.9 72.8 60.6 46.5 45.9 58.2    二、中国非制造业采购经理指数运行情况   5月份,非制造业商务活动指数为55.2%,较上月上升0.3个百分点,非制造业扩张步伐有所加快。    分行业看,建筑业商务活动指数为60.1%,高于上月2.7个百分点。服务业商务活动指数为54.3%,低于上月0.1个百分点。从行业情况看,铁路运输、航空运输、住宿、电信广播电视卫星传输服务、文化体育娱乐等行业商务活动指数位于60.0%以上高位景气区间;水上运输、资本市场服务、房地产等行业商务活动指数位于临界点以下。     新订单指数为52.2%,比上月上升0.7个百分点,高于临界点,表明非制造业市场需求继续增长。分行业看,建筑业新订单指数为53.8%,比上月上升1.4个百分点;服务业新订单指数为52.0%,比上月上升0.7个百分点。   投入品价格指数为57.7%,比上月上升2.8个百分点,高于临界点,表明非制造业企业用于经营活动的投入品价格涨幅加大。分行业看,建筑业投入品价格指数为73.6%,比上月上升8.9个百分点;服务业投入品价格指数为54.9%,比上月上升1.8个百分点。   销售价格指数为52.8%,比上月上升1.6个百分点,高于临界点,表明非制造业销售价格上涨幅度加大。分行业看,建筑业销售价格指数为57.0%,比上月上升0.3个百分点;服务业销售价格指数为52.0%,比上月上升1.8个百分点。   从业人员指数为48.9%,比上月回升0.2个百分点,表明非制造业用工景气度略有提升。分行业看,建筑业从业人员指数为53.0%,比上月上升2.0个百分点;服务业从业人员指数为48.2%,比上月下降0.1个百分点。   业务活动预期指数为62.9%,比上月微落0.1个百分点,继续位于高位景气区间,表明非制造业企业对行业发展保持乐观。分行业看,建筑业业务活动预期指数为65.7%,比上月上升0.9个百分点;服务业业务活动预期指数为62.4%,比上月回落0.3个百分点。 表3 中国非制造业主要分类指数(经季节调整)                                             单位:%    商务活动 新订单 投入品 价格 销售价格 从业人员 业务活动 预期 2020年5月 53.6 52.6 52.0 48.6 48.5 63.9 2020年6月 54.4 52.7 52.9 49.5 48.7 60.3 2020年7月 54.2 51.5 53.0 50.1 48.1 62.2 2020年8月 55.2 52.3 51.9 50.1 48.3 62.1 2020年9月 55.9 54.0 50.6 48.9 49.1 63.0 2020年10月 56.2 53.0 50.9 49.4 49.4 62.9 2020年11月 56.4 52.8 52.7 51.0 48.9 61.2 2020年12月 55.7 51.9 54.3 52.3 48.7 60.6 2021年1月 52.4 48.7 54.5 51.4 47.8 55.1 2021年2月 51.4 48.9 54.7 50.1 48.4 64.0 2021年3月 56.3 55.9 56.2 52.2 49.7 63.7 2021年4月 54.9 51.5 54.9 51.2 48.7 63.0 2021年5月 55.2 52.2 57.7 52.8 48.9 62.9  表4 中国非制造业其他分类指数(经季节调整)                                              单位:%   新出口订单 在手订单 存货 供应商配送时间 2020年5月 41.3 44.3 47.8 52.9 2020年6月 43.3 44.8 48.0 52.1 2020年7月 44.5 44.9 48.1 51.9 2020年8月 45.1 44.6 48.5 52.4 2020年9月 49.1 46.3 48.5 52.2 2020年10月 47.0 44.9 48.7 52.3 2020年11月 49.0 45.2 48.8 51.8 2020年12月 47.5 44.7 47.0 51.2 2021年1月 48.0 44.0 47.4 49.8 2021年2月 45.7 44.0 45.9 49.8 2021年3月 50.3 45.9 48.2 51.8 2021年4月 48.1 45.8 47.2 50.9 2021年5月 47.6 44.7 47.2 50.8    三、中国综合PMI产出指数运行情况   5月份,综合PMI产出指数为54.2%,比上月上升0.4个百分点,表明我国企业生产经营活动总体延续稳步扩张态势。    附注   1.主要指标解释   采购经理指数(PMI),是通过对企业采购经理的月度调查结果统计汇总、编制而成的指数,它涵盖了企业采购、生产、流通等各个环节,包括制造业和非制造业领域,是国际上通用的监测宏观经济走势的先行性指数之一,具有较强的预测、预警作用。综合PMI产出指数是PMI指标体系中反映当期全行业(制造业和非制造业)产出变化情况的综合指数。PMI高于50%时,反映经济总体较上月扩张;低于50%,则反映经济总体较上月收缩。   2.调查范围   涉及《国民经济行业分类》(GB/T4754-2017)中制造业的31个行业大类,3000家调查样本;非制造业的43个行业大类,4200家调查样本。   3.调查方法   采购经理调查采用PPS(Probability Proportional to Size)抽样方法,以制造业或非制造业行业大类为层,行业样本量按其增加值占全部制造业或非制造业增加值的比重分配,层内样本使用与企业主营业务收入成比例的概率抽取。   本调查由国家统计局直属调查队具体组织实施,利用国家统计联网直报系统对企业采购经理进行月度问卷调查。   4.计算方法   (1)分类指数的计算方法。制造业采购经理调查指标体系包括生产、新订单、新出口订单、在手订单、产成品库存、采购量、进口、主要原材料购进价格、出厂价格、原材料库存、从业人员、供应商配送时间、生产经营活动预期等13个分类指数。非制造业采购经理调查指标体系包括商务活动、新订单、新出口订单、在手订单、存货、投入品价格、销售价格、从业人员、供应商配送时间、业务活动预期等10个分类指数。分类指数采用扩散指数计算方法,即正向回答的企业个数百分比加上回答不变的百分比的一半。由于非制造业没有合成指数,国际上通常用商务活动指数反映非制造业经济发展的总体变化情况。   (2)制造业PMI指数的计算方法。制造业PMI是由5个扩散指数(分类指数)加权计算而成。5个分类指数及其权数是依据其对经济的先行影响程度确定的。具体包括:新订单指数,权数为30%;生产指数,权数为25%;从业人员指数,权数为20%;供应商配送时间指数,权数为15%;原材料库存指数,权数为10%。其中,供应商配送时间指数为逆指数,在合成制造业PMI指数时进行反向运算。   (3)综合PMI产出指数的计算方法。综合PMI产出指数由制造业生产指数与非制造业商务活动指数加权求和而成,权数分别为制造业和非制造业占GDP的比重。   5.季节调整   采购经理调查是一项月度调查,受季节因素影响,数据波动较大。现发布的指数均为季节调整后的数据。 

Java 8 中的 Streams API 详解

转载自:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html

为什么需要 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,*大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

什么是聚合操作

在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:

  • 客户每月平均消费金额
  • *昂贵的在售商品
  • 本周完成的有效订单(排除了无效的)
  • 取十个数据样本作为首页推荐

这类的操作。

但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而 Java 的集合 API 中,仅仅有*少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在 Java 7 中,如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,我们需要这样写:

清单 1. Java 7 的排序、取值实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

List<Transaction> groceryTransactions = new Arraylist<>();

for(Transaction t: transactions){

 if(t.getType() == Transaction.GROCERY){

 groceryTransactions.add(t);

 }

}

Collections.sort(groceryTransactions, new Comparator(){

 public int compare(Transaction t1, Transaction t2){

 return t2.getValue().compareTo(t1.getValue());

 }

});

List<Integer> transactionIds = new ArrayList<>();

for(Transaction t: groceryTransactions){

 transactionsIds.add(t.getId());

}

而在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。

清单 2. Java 8 的排序、取值实现

1

2

3

4

5

List<Integer> transactionsIds = transactions.parallelStream().

 filter(t -> t.getType() == Transaction.GROCERY).

 sorted(comparing(Transaction::getValue).reversed()).

 map(Transaction::getId).

 collect(toList());

Stream 总览

什么是流

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。Java 的并行 API 演变历程基本如下:

  1. 1.0-1.4 中的 java.lang.Thread
  2. 5.0 中的 java.util.concurrent
  3. 6.0 中的 Phasers 等
  4. 7.0 中的 Fork/Join 框架
  5. 8.0 中的 Lambda

Stream 的另外一大特点是,数据源本身可以是无限的。

流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

图 1. 流管道 (Stream Pipeline) 的构成

图 1.  流管道 (Stream Pipeline) 的构成

有多种方式生成 Stream Source:

  • 从 Collection 和数组
    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()

    从 BufferedReader

    • java.io.BufferedReader.lines()
  • 静态工厂
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • 自己构建
    • java.util.Spliterator

    其它

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的*后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

清单 3. 一个流操作的示例

1

2

3

4

int sum = widgets.stream()

.filter(w -> w.getColor() == RED)

 .mapToInt(w -> w.getWeight())

 .sum();

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,*后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

流的使用详解

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个*终结果,或者导致一个副作用(side effect)。

流的构造与转换

下面提供*常见的几种构造 Stream 的样例。

清单 4. 构造流的几种常见方法

1

2

3

4

5

6

7

8

9

// 1. Individual values

Stream stream = Stream.of("a", "b", "c");

// 2. Arrays

String [] strArray = new String[] {"a", "b", "c"};

stream = Stream.of(strArray);

stream = Arrays.stream(strArray);

// 3. Collections

List<String> list = Arrays.asList(strArray);

stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

清单 5. 数值流的构造

1

2

3

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);

IntStream.range(1, 3).forEach(System.out::println);

IntStream.rangeClosed(1, 3).forEach(System.out::println);

清单 6. 流转换为其它数据结构

1

2

3

4

5

6

7

8

9

// 1. Array

String[] strArray1 = stream.toArray(String[]::new);

// 2. Collection

List<String> list1 = stream.collect(Collectors.toList());

List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));

Set set1 = stream.collect(Collectors.toSet());

Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));

// 3. String

String str = stream.collect(Collectors.joining()).toString();

一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

流的操作

接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。

  • Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

我们下面看一下 Stream 的比较典型用法。

map/flatMap

我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。

清单 7. 转换大写

1

2

3

List<String> output = wordList.stream().

map(String::toUpperCase).

collect(Collectors.toList());

这段代码把所有的单词转换为大写。

清单 8. 平方数

1

2

3

4

List<Integer> nums = Arrays.asList(1, 2, 3, 4);

List<Integer> squareNums = nums.stream().

map(n -> n * n).

collect(Collectors.toList());

这段代码生成一个整数 list 的平方数 {1, 4, 9, 16}。

从上面例子可以看出,map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

清单 9. 一对多

1

2

3

4

5

6

7

Stream<List<Integer>> inputStream = Stream.of(

 Arrays.asList(1),

 Arrays.asList(2, 3),

 Arrays.asList(4, 5, 6)

 );

Stream<Integer> outputStream = inputStream.

flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的层级结构扁平化,就是将*底层元素抽出来放到一起,*终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

filter

filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

清单 10. 留下偶数

1

2

3

Integer[] sixNums = {1, 2, 3, 4, 5, 6};

Integer[] evens =

Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。

清单 11. 把单词挑出来

1

2

3

4

List<String> output = reader.lines().

 flatMap(line -> Stream.of(line.split(REGEXP))).

 filter(word -> word.length() > 0).

 collect(Collectors.toList());

这段代码首先把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了。

forEach

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

清单 12. 打印姓名(forEach 和 pre-java8 的对比)

1

2

3

4

5

6

7

8

9

10

// Java 8

roster.stream()

 .filter(p -> p.getGender() == Person.Sex.MALE)

 .forEach(p -> System.out.println(p.getName()));

// Pre-Java 8

for (Person p : roster) {

 if (p.getGender() == Person.Sex.MALE) {

 System.out.println(p.getName());

 }

}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了*紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

1

2

stream.forEach(element -> doOneThing(element));

stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。

清单 13. peek 对每个元素执行操作并返回一个新的 Stream

1

2

3

4

5

6

Stream.of("one", "two", "three", "four")

 .filter(e -> e.length() > 3)

 .peek(e -> System.out.println("Filtered value: " + e))

 .map(String::toUpperCase)

 .peek(e -> System.out.println("Mapped value: " + e))

 .collect(Collectors.toList());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

findFirst

这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的*个元素,或者空。

这里比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException。

清单 14. Optional 的两个用例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

String strA = " abcd ", strB = null;

print(strA);

print("");

print(strB);

getLength(strA);

getLength("");

getLength(strB);

public static void print(String text) {

 // Java 8

 Optional.ofNullable(text).ifPresent(System.out::println);

 // Pre-Java 8

 if (text != null) {

 System.out.println(text);

 }

 }

public static int getLength(String text) {

 // Java 8

return Optional.ofNullable(text).map(String::length).orElse(-1);

 // Pre-Java 8

// return if (text != null) ? text.length() : -1;

 };

在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能*大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

reduce

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的*个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 或

Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

清单 15. reduce 的用例

1

2

3

4

5

6

7

8

9

10

11

12

// 字符串连接,concat = "ABCD"

String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);

// 求*小值,minValue = -3.0

double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);

// 求和,sumValue = 10, 有起始值

int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

// 求和,sumValue = 10, 无起始值

sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();

// 过滤,字符串连接,concat = "ace"

concat = Stream.of("a", "B", "c", "D", "e", "F").

 filter(x -> x.compareTo("Z") > 0).

 reduce("", String::concat);

上面代码例如*个示例的 reduce(),*个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。

limit/skip

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。

清单 16. limit 和 skip 对运行次数的影响

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public void testLimitAndSkip() {

 List<Person> persons = new ArrayList();

 for (int i = 1; i <= 10000; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<String> personList2 = persons.stream().

map(Person::getName).limit(10).skip(3).collect(Collectors.toList());

 System.out.println(personList2);

}

private class Person {

 public int no;

 private String name;

 public Person (int no, String name) {

 this.no = no;

 this.name = name;

 }

 public String getName() {

 System.out.println(name);

 return name;

 }

}

输出结果为:

1

2

3

4

5

6

7

8

9

10

11

name1

name2

name3

name4

name5

name6

name7

name8

name9

name10

[name4, name5, name6, name7, name8, name9, name10]

这是一个有 10,000 个元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的 getName() 方法的执行次数为 limit 所限定的 10 次,而*终返回结果在跳过前 3 个元素后只有后面 7 个返回。

有一种情况是 limit/skip 无法达到 short-circuiting 目的的,就是把它们放在 Stream 的排序操作后,原因跟 sorted 这个 intermediate 操作有关:此时系统并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全没有被 limit 或者 skip 一样。

清单 17. limit 和 skip 对 sorted 后的运行次数无影响

1

2

3

4

5

6

7

8

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().sorted((p1, p2) ->

p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());

System.out.println(personList2);

上面的示例对清单 13 做了微调,首先对 5 个元素的 Stream 排序,然后进行 limit 操作。输出结果为:

1

2

3

4

5

6

7

8

9

name2

name1

name3

name2

name4

name3

name5

name4

[stream.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]

即虽然*后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。

*后有一点需要注意的是,对一个 parallel 的 Steam 管道来说,如果其元素是有序的,那么 limit 操作的成本会比较大,因为它的返回对象必须是前 n 个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用 parallel Stream。

sorted

对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。我们对清单 14 进行优化:

清单 18. 优化:排序前进行 limit 和 skip

1

2

3

4

5

6

7

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());

System.out.println(personList2);

结果会简单很多:

1

2

3

name2

name1

[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]

当然,这种优化是有 business logic 上的局限性的:即不要求排序后再取值。

min/max/distinct

min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求*大*小值是很常见的操作。

清单 19. 找出*长一行的长度

1

2

3

4

5

6

7

BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));

int longest = br.lines().

 mapToInt(String::length).

 max().

 getAsInt();

br.close();

System.out.println(longest);

下面的例子则使用 distinct 来找出不重复的单词。

清单 20. 找出全文的单词,转小写,并排序

1

2

3

4

5

6

7

8

9

List<String> words = br.lines().

 flatMap(line -> Stream.of(line.split(" "))).

 filter(word -> word.length() > 0).

 map(String::toLowerCase).

 distinct().

 sorted().

 collect(Collectors.toList());

br.close();

System.out.println(words);

Match

Stream 有三个 match 方法,从语义上说:

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true
  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true

它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。对清单 13 中的 Person 类稍做修改,加入一个 age 属性和 getAge 方法。

清单 21. 使用 Match

1

2

3

4

5

6

7

8

9

10

11

12

List<Person> persons = new ArrayList();

persons.add(new Person(1, "name" + 1, 10));

persons.add(new Person(2, "name" + 2, 21));

persons.add(new Person(3, "name" + 3, 34));

persons.add(new Person(4, "name" + 4, 6));

persons.add(new Person(5, "name" + 5, 55));

boolean isAllAdult = persons.stream().

 allMatch(p -> p.getAge() > 18);

System.out.println("All are adult? " + isAllAdult);

boolean isThereAnyChild = persons.stream().

 anyMatch(p -> p.getAge() < 12);

System.out.println("Any child? " + isThereAnyChild);

输出结果:

1

2

All are adult? false

Any child? true

进阶:自己生成流

Stream.generate

通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

清单 22. 生成 10 个随机整数

1

2

3

4

5

6

Random seed = new Random();

Supplier<Integer> random = seed::nextInt;

Stream.generate(random).limit(10).forEach(System.out::println);

//Another way

IntStream.generate(() -> (int) (System.nanoTime() % 100)).

limit(10).forEach(System.out::println);

Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。

清单 23. 自实现 Supplier

1

2

3

4

5

6

7

8

9

10

11

Stream.generate(new PersonSupplier()).

limit(10).

forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));

private class PersonSupplier implements Supplier<Person> {

 private int index = 0;

 private Random random = new Random();

 @Override

 public Person get() {

 return new Person(index++, "StormTestUser" + index, random.nextInt(100));

 }

}

输出结果:

1

2

3

4

5

6

7

8

9

10

StormTestUser1, 9

StormTestUser2, 12

StormTestUser3, 88

StormTestUser4, 51

StormTestUser5, 22

StormTestUser6, 28

StormTestUser7, 81

StormTestUser8, 51

StormTestUser9, 4

StormTestUser10, 76

Stream.iterate

iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的*个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。

清单 24. 生成一个等差数列

1 Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));.

输出结果:

1 0 3 6 9 12 15 18 21 24 27

与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。

进阶:用 Collectors 来进行 reduction 操作

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。

groupingBy/partitioningBy

清单 25. 按照年龄归组

1

2

3

4

5

6

7

8

Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.groupingBy(Person::getAge));

Iterator it = personGroups.entrySet().iterator();

while (it.hasNext()) {

 Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();

 System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());

}

上面的 code,首先生成 100 人的信息,然后按照年龄归组,相同年龄的人放到同一个 list 中,可以看到如下的输出:

1

2

3

4

5

6

7

Age 0 = 2

Age 1 = 2

Age 5 = 2

Age 8 = 1

Age 9 = 1

Age 11 = 2

……

清单 26. 按照未成年人和成年人归组

1

2

3

4

5

Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.partitioningBy(p -> p.getAge() < 18));

System.out.println("Children number: " + children.get(true).size());

System.out.println("Adult number: " + children.get(false).size());

输出结果:

1

2

Children number: 23

Adult number: 77

在使用条件“年龄小于 18”进行分组后可以看到,不到 18 岁的未成年人是一组,成年人是另外一组。partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。

结束语

总之,Stream 的特性可以归纳为:

  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  • 它也*不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 你可以请求*个元素,但无法请求第二个,第三个,或*后一个。不过请参阅下一项。
  • 很容易生成数组或者 List
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了*后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  • 可以是无限的
    • 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

浅析DNS域名解析过程

一、DNS域名解析步骤

下图是DNS域名解析的一个示例图,它涵盖了基本解析步骤和原理。
这里写图片描述
下面DNS解析步骤进行讲解,后面将采用命令行的形式来跟踪DNS解析过程。当用户在地址栏键入www.baidu.com并敲下回车键之后,域名解析就开始了。

*步:检查浏览器缓存中是否缓存过该域名对应的IP地址

用户通过浏览器浏览过某网站之后,浏览器就会自动缓存该网站域名对应的IP地址,当用户再次访问的时候,浏览器就会从缓存中查找该域名对应的IP地址,因为缓存不仅是有大小限制,而且还有时间限制(域名被缓存的时间通过TTL属性来设置),所以存在域名对应的IP找不到的情况。当浏览器从缓存中找到了该网站域名对应的IP地址,那么整个DNS解析过程结束,如果没有找到,将进行下一步骤。对于IP的缓存时间问题,不宜设置太长的缓存时间,时间太长,如果域名对应的IP发生变化,那么用户将在一段时间内无法正常访问到网站,如果太短,那么又造成频繁解析域名。

第二步:如果在浏览器缓存中没有找到IP,那么将继续查找本机系统是否缓存过IP

如果*个步骤没有完成对域名的解析过程,那么浏览器会去系统缓存中查找系统是否缓存过这个域名对应的IP地址,也可以理解为系统自己也具备域名解析的基本能力。在Windows系统中,可以通过设置hosts文件来将域名手动绑定到某IP上,hosts文件位置在C:\Windows\System32\drivers\etc\hosts。对于普通用户,并不推荐自己手动绑定域名和IP,对于开发者来说,通过绑定域名和IP,可以轻松切换环境,可以从测试环境切换到开发环境,方便开发和测试。在XP系统中,黑客常常修改他的电脑的hosts文件,将用户常常访问的域名绑定到他指定的IP上,从而实现了本地DNS解析,导致这些域名被劫持。在Linux或者Mac系统中,hosts文件在/etc/hosts,修改该文件也可以实现同样的目的。

前两步都是在本机上完成的,所以没有在上面示例图上展示出来,从第三步开始,才正在地向远程DNS服务器发起解析域名的请求。

第三步:向本地域名解析服务系统发起域名解析的请求

如果在本机上无法完成域名的解析,那么系统只能请求本地域名解析服务系统进行解析,本地域名系统LDNS一般都是本地区的域名服务器,比如你连接的校园网,那么域名解析系统就在你的校园机房里,如果你连接的是电信、移动或者联通的网络,那么本地域名解析服务器就在本地区,由各自的运营商来提供服务。对于本地DNS服务器地址,Windows系统使用命令ipconfig就可以查看,在LinuxMac系统下,直接使用命令cat /etc/resolv.conf来查看LDNS服务地址。LDNS一般都缓存了大部分的域名解析的结果,当然缓存时间也受域名失效时间控制,大部分的解析工作到这里就差不多已经结束了,LDNS负责了大部分的解析工作。

第四步:向根域名解析服务器发起域名解析请求

本地DNS域名解析器还没有完成解析的话,那么本地域名解析服务器将向根域名服务器发起解析请求。

第五步:根域名服务器返回gTLD域名解析服务器地址

本地DNS域名解析向根域名服务器发起解析请求,根域名服务器返回的是所查域的通用顶级域(Generic top-level domain,gTLD)地址,常见的通用顶级域有.com.cn.org.edu等。

第六步:向gTLD服务器发起解析请求

本地域名解析服务器向gTLD服务器发起请求。

第七步:gTLD服务器接收请求并返回Name Server服务器

gTLD服务器接收本地域名服务器发起的请求,并根据需要解析的域名,找到该域名对应的Name Server域名服务器,通常情况下,这个Name Server服务器就是你注册的域名服务器,那么你注册的域名的服务商的服务器将承担起域名解析的任务。

第八步:Name Server服务器返回IP地址给本地服务器

Name Server服务器查找域名对应的IP地址,将IP地址连同TTL值返回给本地域名服务器。

第九步:本地域名服务器缓存解析结果

本地域名服务器缓存解析后的结果,缓存时间由TTL时间来控制。

第十步:返回解析结果给用户

解析结果将直接返回给用户,用户系统将缓存该IP地址,缓存时间由TTL来控制,至此,解析过程结束。

这里对DNS解析的步骤进行了一个简单的介绍分析,后面将通过命令行的形式来解析一个域名的具体解析过程。

二、DNS域名解析过程分析

在正式开始分析解析过程之前,先来介绍几个基本的域名解析方式的概念。域名解析记录主要分为A记录MX记录CNAME记录NS记录以及TXT记录

  • A记录A代表的是Address,用来指定域名对应的IP地址,比如将map.baidu.com指定到180.97.34.157,将zhidao.baidu.com指定到180.149.131.245A记录允许将多个域名解析到一个IP地址,但不允许将一个域名解析到多个IP地址上。
  • MX记录MX代表的是Mail Exchage,就是可以将某个域名下的邮件服务器指向自己的Mail Server,如baidu.com域名的A记录IP地址是180.97.34.157,如果将MX记录设置为180.97.34.154,即xxx@baidu.com的邮件路由,那么DNS会将邮件发送到180.97.34.154所在的服务器,而正常web请求仍然会解析到A记录的IP地址180.97.34.157
  • CNAME记录CNAME指的就是Canonical Name,也就是别名解析,可以将指定的域名解析到其他域名上,而其他域名就是指定域名的别名,整个解析过程称为别名解析。比如将baidu.com解析到itlemon.cn,将csdn.net解析到itlemon.cn,那么itlemon.cn就是baidu.comCSDN.net的别名。
  • NS记录:就是为某个域名指定了特定的DNS服务器去解析。
  • TXT记录:为某个主机名或者域名设置特定的说明,比如为itlemon.cn设置的的TXT记录为“Lemon的技术笔记”,这个TXT记录为itlemon.cn的说明。

上面概念中的IP地址都是假定的,帮助理解。下面将通过解析域名baidu.com为例,进一步说明域名解析流程。

直接查看域名结果,可以通过命令nslookup加上域名来查看:
这里写图片描述
上图中Non-authoritative answer表示解析结果来自非权威服务器,也就是说这个结果来自缓存,并没有完全经历全部的解析过程,从某个缓存中读取的结果,这个结果存在一定的隐患,比如域名对应的IP地址已经更变。
这只是一个快捷的解析结果,如果需要浏览全部的解析过程,那么可以使用dig命令来查看解析过程。
这里写图片描述
分析上图DNS解析过程,我们可以看出:
*步:从本地DNS域名解析服务器获取到13个根DNS域名服务器(.)对应的主机名。
这里写图片描述
第二步:从13个根域名服务器中的其中一个(这里是h.root-servers.net)获取到顶级com.的服务器IP(未显示)和名称。
这里写图片描述
第三步:向com.域的一台服务器192.43.172.30(i.gtld-servers.net)请求解析,它返回了baidu.com域的服务器IP(未显示)和名称,百度有四台顶级域的服务器。
这里写图片描述
第四步:向百度的顶级域服务器220.181.37.10(ns3.baidu.com)请求www.baidu.com,它发现这个www有个别名,而不是一台主机,别名是www.a.shifen.com
这里写图片描述
一般情况下,DNS解析到别名就停止了,返回了具体的IP地址,如果想看到具体的IP地址,可以进一步对别名进行解析,解析结果如下:
这里写图片描述
这时候看到*后的解析结果是180.97.33.107180.97.33.108。在解析别名的过程中,可以发现shifen.combaidu.com都是指定了相同的域名解析服务器。以上是一个域名的解析过程,*后的解析结果和一开始的使用nslookup的结果一致。

HashMap ConcurrentHashMap详解

转载自:https://mp.weixin.qq.com/s/QhRWDFgpjQ83Yz66V_6scQ

前言

 

Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。

 

本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。

 

HashMap

 

众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

 

Base 1.7

 

1.7 中的数据结构图:

 

 

 

先来看看 1.7 中的实现。

 

 

 

这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?

 

  1. 初始化桶大小,因为底层是数组,所以这是数组默认的大小。
  2. 桶*大值。
  3. 默认的负载因子(0.75)
  4. table 真正存放数据的数组。
  5. Map 存放数量的大小。
  6. 桶大小,可在初始化时显式指定。
  7. 负载因子,可在初始化时显式指定。

 

重点解释下负载因子:

 

由于给定的 HashMap 的容量大小是固定的,比如默认初始化:

 

 

 

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

 

因此通常建议能提前预估 HashMap 的大小*好,尽量的减少扩容带来的性能损耗。

根据代码可以看到其实真正存放数据的是

 

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 

这个数组,那么它又是如何定义的呢?

 

 

 

Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:

 

  • key 就是写入时的键。
  • value 自然就是值。
  • 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
  • hash 存放的是当前 key 的 hashcode。

 

知晓了基本结构,那来看看其中重要的写入、获取函数:

 

put 方法

 

 

 

  • 判断当前数组是否需要初始化。
  • 如果 key 为空,则 put 一个空值进去。
  • 根据 key 计算出 hashcode。
  • 根据计算出的 hashcode 定位出所在桶。
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
  • 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。

 

 

 

当调用 addEntry 写入 Entry 时需要判断是否需要扩容。

 

如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。

 

而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。

 

get 方法

 

再来看看 get 函数:

 

 

 

  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 判断该位置是否为链表。
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
  • 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
  • 啥都没取到就直接返回 null 。

Base 1.8

 

不知道 1.7 的实现大家看出需要优化的点没有?

 

其实一个很明显的地方就是:

 

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

 

因此 1.8 中重点优化了这个查询效率。

 

1.8 HashMap 结构图:

 

 

 

先来看看几个核心的成员变量:

 

%title插图%num

 

和 1.7 大体上都差不多,还是有几个重要的区别:

 

  • TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
  • HashEntry 修改为 Node。

 

Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。

 

再来看看核心方法。

 

put 方法

 

 

 

看似要比 1.7 的复杂,我们一步步拆解:

 

  1. 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
  2. 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
  3. 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
  4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  5. 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
  6. 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  7. 如果在遍历过程中找到 key 相同时直接退出遍历。
  8. 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
  9. *后判断是否需要进行扩容。

 

get 方法

 

%title插图%num

 

get 方法看起来就要简单许多了。

 

  • 首先将 key hash 之后取得所定位的桶。
  • 如果桶为空则直接返回 null 。
  • 否则判断桶的*个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 如果*个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就按照树的查找方式返回值。
  • 不然就按照链表的方式遍历匹配返回值。

 

从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。

 

但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。

 

%title插图%num

 

但是为什么呢?简单分析下。

 

看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。

 

如下图:

 

%title插图%num

%title插图%num

遍历方式

 

还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种:

 

%title插图%num

 

强烈建议使用*种 EntrySet 进行遍历。

 

*种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。

 

简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。

 

因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。

 

坚持看到这里的朋友算是已经把 ConcurrentHashMap 的基础已经打牢了,下面正式开始分析。

ConcurrentHashMap

 

ConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。

 

Base 1.7

 

先来看看 1.7 的实现,下面是他的结构图:

 

%title插图%num

 

如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

 

它的核心成员变量:

 

%title插图%num

 

Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

 

%title插图%num

 

看看其中 HashEntry 的组成:

 

%title插图%num

 

和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

 

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

 

下面也来看看核心的 put get 方法。

 

put 方法

 

%title插图%num

 

首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。

 

%title插图%num

%title插图%num

 

虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。

 

首先*步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

 

%title插图%num

 

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

 

%title插图%num

 

再结合图看看 put 的流程。

 

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. *后会解除在 1 中所获取当前 Segment 的锁。

 

get 方法

 

%title插图%num

 

get 逻辑比较简单:

 

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

 

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是*新值。

 

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

 

Base 1.8

 

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。

 

那就是查询遍历链表效率太低。

 

因此 1.8 做了一些数据结构上的调整。

 

首先来看下底层的组成结构:

 

 

 

看起来是不是和 1.8 HashMap 结构类似?

 

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

 

 

 

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。

 

其中的 val next 都用了 volatile 修饰,保证了可见性。

 

put 方法

 

重点来看看 put 函数:

 

 

 

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

 

get 方法

 

 

 

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。

 

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

总结

 

看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。

 

其实这块也是面试的重点内容,通常的套路是:

 

  1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。
  2. 1.8 做了什么优化?
  3. 是线程安全的嘛?
  4. 不安全会导致哪些问题?
  5. 如何解决?有没有线程安全的并发容器?
  6. ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

 

这一串问题相信大家仔细看完都能怼回面试官。

 

除了面试会问到之外平时的应用其实也蛮多,像之前谈到的 Guava 中 Cache 的实现就是利用 ConcurrentHashMap 的思想。

 

同时也能学习 JDK 作者大牛们的优化思路以及并发解决方案。

 

其实写这篇的前提是源于 GitHub 上的一个 Issues,也希望大家能参与进来,共同维护好这个项目。

Java NIO:Buffer、Channel 和 Selector

转载自:http://www.importnew.com/28007.html

本文将介绍 Java NIO 中三大组件 Buffer、Channel、Selector 的使用。

本来要一起介绍非阻塞 IO 和 JDK7 的异步 IO 的,不过因为之前的文章真的太长了,有点影响读者阅读,所以这里将它们放到另一篇文章中进行介绍。

Buffer

一个 Buffer 本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。

java.nio 定义了以下几个 Buffer 的实现,这个图读者应该也在不少地方见过了吧。

6

其实核心是*后的 ByteBuffer,前面的一大串类只是包装了一下它而已,我们使用*多的通常也是 ByteBuffer。

我们应该将 Buffer 理解为一个数组,IntBuffer、CharBuffer、DoubleBuffer 等分别对应 int[]、char[]、double[] 等。

MappedByteBuffer 用于实现内存映射文件,也不是本文关注的重点。

我觉得操作 Buffer 和操作数组、类集差不多,只不过大部分时候我们都把它放到了 NIO 的场景里面来使用而已。下面介绍 Buffer 中的几个重要属性和几个重要方法。

position、limit、capacity

就像数组有数组容量,每次访问元素要指定下标,Buffer 中也有几个重要属性:position、limit、capacity。

5

*好理解的当然是 capacity,它代表这个缓冲区的容量,一旦设定就不可以更改。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值。

position 和 limit 是变化的,我们分别看下读和写操作下,它们是如何变化的。

position 的初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置。读操作的时候也是类似的,每读一个值,position 就自动加 1。

从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。

Limit:写操作模式下,limit 代表的是*大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。

7

初始化 Buffer

每个 Buffer 实现类都提供了一个静态方法 allocate(int capacity) 帮助我们快速实例化一个 Buffer。如:

1

2

3

4

ByteBuffer byteBuf = ByteBuffer.allocate(1024);

IntBuffer intBuf = IntBuffer.allocate(1024);

LongBuffer longBuf = LongBuffer.allocate(1024);

// ...

另外,我们经常使用 wrap 方法来初始化一个 Buffer。

1

2

3

public static ByteBuffer wrap(byte[] array) {

    ...

}

填充 Buffer

各个 Buffer 类都提供了一些 put 方法用于将数据填充到 Buffer 中,如 ByteBuffer 中的几个 put 方法:

1

2

3

4

5

6

7

// 填充一个 byte 值

public abstract ByteBuffer put(byte b);

// 在指定位置填充一个 int 值

public abstract ByteBuffer put(int index, byte b);

// 将一个数组中的值填充进去

public final ByteBuffer put(byte[] src) {...}

public ByteBuffer put(byte[] src, int offset, int length) {...}

上述这些方法需要自己控制 Buffer 大小,不能超过 capacity,超过会抛 java.nio.BufferOverflowException 异常。

对于 Buffer 来说,另一个常见的操作中就是,我们要将来自 Channel 的数据填充到 Buffer 中,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中。

1 int num = channel.read(buf);

上述方法会返回从 Channel 中读入到 Buffer 的数据大小。

提取 Buffer 中的值

前面介绍了写操作,每写入一个值,position 的值都需要加 1,所以 position *后会指向*后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)。

如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式。注意,通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作,初学者需要理清楚这个。

调用 Buffer 的 flip() 方法,可以进行模式切换。其实这个方法也就是设置了一下 position 和 limit 值罢了。

1

2

3

4

5

6

public final Buffer flip() {

    limit = position; // 将 limit 设置为实际写入的数据数量

    position = 0// 重置 position 为 0

    mark = -1// mark 之后再说

    return this;

}

对应写入操作的一系列 put 方法,读操作提供了一系列的 get 方法:

1

2

3

4

5

6

// 根据 position 来获取数据

public abstract byte get();

// 获取指定位置的数据

public abstract byte get(int index);

// 将 Buffer 中的数据写入到数组中

public ByteBuffer get(byte[] dst)

附一个经常使用的方法:

1 new String(buffer.array()).trim();

当然了,除了将数据从 Buffer 取出来使用,更常见的操作是将我们写入的数据传输到 Channel 中,如通过 FileChannel 将数据写入到文件中,通过 SocketChannel 将数据写入网络发送到远程机器等。对应的,这种操作,我们称之为写操作。

1 int num = channel.write(buf);

mark() & reset()

除了 position、limit、capacity 这三个基本的属性外,还有一个常用的属性就是 mark。

mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position,便于后续需要的时候使用。

1

2

3

4

public final Buffer mark() {

    mark = position;

    return this;

}

那到底什么时候用呢?考虑以下场景,我们在 position 为 5 的时候,先 mark() 一下,然后继续往下读,读到第 10 的时候,我想重新回到 position 为 5 的地方重新来一遍,那只要调一下 reset() 方法,position 就回到 5 了。

1

2

3

4

5

6

7

public final Buffer reset() {

    int m = mark;

    if (m < 0)

        throw new InvalidMarkException();

    position = m;

    return this;

}

rewind() & clear() & compact()

rewind():会重置 position 为 0,通常用于重新从头读写 Buffer。

1

2

3

4

5

public final Buffer rewind() {

    position = 0;

    mark = -1;

    return this;

}

clear():有点重置 Buffer 的意思,相当于重新实例化了一样。

通常,我们会先填充 Buffer,然后从 Buffer 读取数据,之后我们再重新往里填充新的数据,我们一般在重新填充之前先调用 clear()。

1

2

3

4

5

6

public final Buffer clear() {

    position = 0;

    limit = capacity;

    mark = -1;

    return this;

}

compact():和 clear() 一样的是,它们都是在准备往 Buffer 填充新的数据之前调用。

前面说的 clear() 方法会重置几个属性,但是我们要看到,clear() 方法并不会将 Buffer 中的数据清空,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了。

而 compact() 方法有点不一样,调用这个方法以后,会先处理还没有读取的数据,也就是 position 到 limit 之间的数据(还没有读过的数据),先将这些数据移到左边,然后在这个基础上再开始写入。很明显,此时 limit 还是等于 capacity,position 指向原来数据的右边。

Channel

所有的 NIO 操作始于通道,通道是数据来源或数据写入的目的地,主要地,我们将关心 java.nio 包中实现的以下几个 Channel:

8

  • FileChannel:文件通道,用于文件的读和写
  • DatagramChannel:用于 UDP 连接的接收和发送
  • SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
  • ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

这里不是很理解这些也没关系,后面介绍了代码之后就清晰了。还有,我们*应该关注,也是后面将会重点介绍的是 SocketChannel 和 ServerSocketChannel。

Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

9

10

至少读者应该记住一点,这两个方法都是 channel 实例的方法。

FileChannel

我想文件操作对于大家来说应该是*熟悉的,不过我们在说 NIO 的时候,其实 FileChannel 并不是关注的重点。而且后面我们说非阻塞的时候会看到,FileChannel 是不支持非阻塞的。

这里算是简单介绍下常用的操作吧,感兴趣的读者瞄一眼就是了。

初始化:

1

2

FileInputStream inputStream = new FileInputStream(new File("/data.txt"));

FileChannel fileChannel = inputStream.getChannel();

当然了,我们也可以从 RandomAccessFile#getChannel 来得到 FileChannel。

读取文件内容:

1

2

3

ByteBuffer buffer = ByteBuffer.allocate(1024);

 

int num = fileChannel.read(buffer);

前面我们也说了,所有的 Channel 都是和 Buffer 打交道的。

写入文件内容:

1

2

3

4

5

6

7

8

ByteBuffer buffer = ByteBuffer.allocate(1024);

buffer.put("随机写入一些内容到 Buffer 中".getBytes());

// Buffer 切换为读模式

buffer.flip();

while(buffer.hasRemaining()) {

    // 将 Buffer 中的内容写入文件

    fileChannel.write(buffer);

}

SocketChannel

我们前面说了,我们可以将 SocketChannel 理解成一个 TCP 客户端。虽然这么理解有点狭隘,因为我们在介绍 ServerSocketChannel 的时候会看到另一种使用方式。

打开一个 TCP 连接:

1 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("https://www.javadoop.com"80));

当然了,上面的这行代码等价于下面的两行:

1

2

3

4

// 打开一个通道

SocketChannel socketChannel = SocketChannel.open();

// 发起连接

socketChannel.connect(new InetSocketAddress("https://www.javadoop.com"80));

SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。

1

2

3

4

5

6

7

// 读取数据

socketChannel.read(buffer);

 

// 写入数据到网络连接中

while(buffer.hasRemaining()) {

    socketChannel.write(buffer);  

}

不要在这里停留太久,先继续往下走。

ServerSocketChannel

之前说 SocketChannel 是 TCP 客户端,这里说的 ServerSocketChannel 就是对应的服务端。

ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。

1

2

3

4

5

6

7

8

9

// 实例化

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 监听 8080 端口

serverSocketChannel.socket().bind(new InetSocketAddress(8080));

 

while (true) {

    // 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理

    SocketChannel socketChannel = serverSocketChannel.accept();

}

这里我们可以看到 SocketChannel 的第二个实例化方式

到这里,我们应该能理解 SocketChannel 了,它不仅仅是 TCP 客户端,它代表的是一个网络通道,可读可写。

ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接。

DatagramChannel

UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。

科普一下,UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的

监听端口:

1

2

3

4

5

6

DatagramChannel channel = DatagramChannel.open();

channel.socket().bind(new InetSocketAddress(9090));

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

 

channel.receive(buf);

发送数据:

1

2

3

4

5

6

7

8

9

String newData = "New String to write to file..."

                    + System.currentTimeMillis();

 

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

 

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com"80));

Selector

NIO 三大组件就剩 Selector 了,Selector 建立在非阻塞的基础之上,大家经常听到的 多路复用 在 Java 世界中指的就是它,用于实现一个线程管理多个 Channel。

读者在这一节不能消化 Selector 也没关系,因为后续在介绍非阻塞 IO 的时候还得说到这个,这里先介绍一些基本的接口操作。

  • 首先,我们开启一个 Selector。你们爱翻译成选择器也好,多路复用器也好。
1 Selector selector = Selector.open();
  • 将 Channel 注册到 Selector 上。前面我们说了,Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论*常见的 SocketChannel 和 ServerSocketChannel。
1

2

3

4

// 将通道设置为非阻塞模式,因为默认都是阻塞模式的

channel.configureBlocking(false);

// 注册

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

 

register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:

  • SelectionKey.OP_READ

    对应 00000001,通道中有数据可以进行读取

  • SelectionKey.OP_WRITE

    对应 00000100,可以往通道中写入数据

  • SelectionKey.OP_CONNECT

    对应 00001000,成功建立 TCP 连接

  • SelectionKey.OP_ACCEPT

    对应 00010000,接受 TCP 连接

我们可以同时监听一个 Channel 中的发生的多个事件,比如我们要监听 ACCEPT 和 READ 事件,那么指定参数为二进制的 00010001 即十进制数值 17 即可。

注册方法返回值是 SelectionKey 实例,它包含了 Channel 和 Selector 信息,也包括了一个叫做 Interest Set 的信息,即我们设置的我们感兴趣的正在监听的事件集合。

  • 调用 select() 方法获取通道信息。用于判断是否有我们感兴趣的事件已经发生了。

Selector 的操作就是以上 3 步,这里来一个简单的示例,大家看一下就好了。之后在介绍非阻塞 IO 的时候,会演示一份可执行的示例代码。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

Selector selector = Selector.open();

 

channel.configureBlocking(false);

 

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

 

while(true) {

  // 判断是否有事件准备好

  int readyChannels = selector.select();

  if(readyChannels == 0continue;

 

  // 遍历

  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

 

    if(key.isAcceptable()) {

        // a connection was accepted by a ServerSocketChannel.

 

    else if (key.isConnectable()) {

        // a connection was established with a remote server.

 

    else if (key.isReadable()) {

        // a channel is ready for reading

 

    else if (key.isWritable()) {

        // a channel is ready for writing

    }

 

    keyIterator.remove();

  }

}

小结

到此为止,介绍了 Buffer、Channel 和 Selector 的常见接口。

Buffer 和数组差不多,它有 position、limit、capacity 几个重要属性。put() 一下数据、flip() 切换到读模式、然后用 get() 获取数据、clear() 一下清空数据、重新回到 put() 写入数据。

Channel 基本上只和 Buffer 打交道,*重要的接口就是 channel.read(buffer) 和 channel.write(buffer)。

Selector 用于实现非阻塞 IO,这里仅仅介绍接口使用。

深入浅出NIO之Selector实现原理

转载自:https://www.jianshu.com/p/0d497fe5484a

前言

Java NIO 由以下几个核心部分组成:
1、Buffer
2、Channel
3、Selector

Buffer和Channel在深入浅出NIO之Channel、Buffer一文中已经介绍过,本文主要讲解NIO的Selector实现原理。

之前进行socket编程时,accept方法会一直阻塞,直到有客户端请求的到来,并返回socket进行相应的处理。整个过程是流水线的,处理完一个请求,才能去获取并处理后面的请求,当然也可以把获取socket和处理socket的过程分开,一个线程负责accept,一个线程池负责处理请求。

但NIO提供了更好的解决方案,采用选择器(Selector)返回已经准备好的socket,并按顺序处理,基于通道(Channel)和缓冲区(Buffer)来进行数据的传输。

Selector

这里出来一个新概念,selector,具体是一个什么样的东西?

想想一个场景:在一个养鸡场,有这么一个人,每天的工作就是不停检查几个特殊的鸡笼,如果有鸡进来,有鸡出去,有鸡生蛋,有鸡生病等等,就把相应的情况记录下来,如果鸡场的负责人想知道情况,只需要询问那个人即可。

在这里,这个人就相当Selector,每个鸡笼相当于一个SocketChannel,每个线程通过一个Selector可以管理多个SocketChannel。

%title插图%num

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:

1、connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
3、read:读事件,对应值为SelectionKey.OP_READ(1)
4、write:写事件,对应值为SelectionKey.OP_WRITE(4)

这个很好理解,每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,*后写回数据返回。

所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。

服务端代码

为了更好的理解,先看一段服务端的示例代码

  1. ServerSocketChannel serverChannel = ServerSocketChannel.open();
  2. serverChannel.configureBlocking(false);
  3. serverChannel.socket().bind(new InetSocketAddress(port));
  4. Selector selector = Selector.open();
  5. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  6. while(true){
  7. int n = selector.select();
  8. if (n == 0) continue;
  9. Iterator ite = this.selector.selectedKeys().iterator();
  10. while(ite.hasNext()){
  11. SelectionKey key = (SelectionKey)ite.next();
  12. if (key.isAcceptable()){
  13. SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
  14. clntChan.configureBlocking(false);
  15. //将选择器注册到连接到的客户端信道,
  16. //并指定该信道key值的属性为OP_READ,
  17. //同时为该信道指定关联的附件
  18. clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
  19. }
  20. if (key.isReadable()){
  21. handleRead(key);
  22. }
  23. if (key.isWritable() && key.isValid()){
  24. handleWrite(key);
  25. }
  26. if (key.isConnectable()){
  27. System.out.println(“isConnectable = true”);
  28. }
  29. ite.remove();
  30. }
  31. }

服务端操作过程

1、创建ServerSocketChannel实例,并绑定指定端口;
2、创建Selector实例;
3、将serverSocketChannel注册到selector,并指定事件OP_ACCEPT,*底层的socket通过channel和selector建立关联;
4、如果没有准备好的socket,select方法会被阻塞一段时间并返回0;
5、如果底层有socket已经准备好,selector的select方法会返回socket的个数,而且selectedKeys方法会返回socket对应的事件(connect、accept、read or write);
6、根据事件类型,进行不同的处理逻辑;

在步骤3中,selector只注册了serverSocketChannel的OP_ACCEPT事件
1、如果有客户端A连接服务,执行select方法时,可以通过serverSocketChannel获取客户端A的socketChannel,并在selector上注册socketChannel的OP_READ事件。
2、如果客户端A发送数据,会触发read事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据。

Selector实现原理

SocketChannel、ServerSocketChannel和Selector的实例初始化都通过SelectorProvider类实现,其中Selector是整个NIO Socket的核心实现。

  1. public static SelectorProvider provider() {
  2. synchronized (lock) {
  3. if (provider != null)
  4. return provider;
  5. return AccessController.doPrivileged(
  6. new PrivilegedAction<SelectorProvider>() {
  7. public SelectorProvider run() {
  8. if (loadProviderFromProperty())
  9. return provider;
  10. if (loadProviderAsService())
  11. return provider;
  12. provider = sun.nio.ch.DefaultSelectorProvider.create();
  13. return provider;
  14. }
  15. });
  16. }
  17. }

SelectorProvider在windows和linux下有不同的实现,provider方法会返回对应的实现。

这里不禁要问,Selector是如何做到同时管理多个socket?

下面我们看看Selector的具体实现,Selector初始化时,会实例化PollWrapper、SelectionKeyImpl数组和Pipe。

  1. WindowsSelectorImpl(SelectorProvider sp) throws IOException {
  2. super(sp);
  3. pollWrapper = new PollArrayWrapper(INIT_CAP);
  4. wakeupPipe = Pipe.open();
  5. wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
  6. // Disable the Nagle algorithm so that the wakeup is more immediate
  7. SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
  8. (sink.sc).socket().setTcpNoDelay(true);
  9. wakeupSinkFd = ((SelChImpl)sink).getFDVal();
  10. pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
  11. }

pollWrapper用Unsafe类申请一块物理内存pollfd,存放socket句柄fdVal和events,其中pollfd共8位,0-3位保存socket句柄,4-7位保存events。

%title插图%num

%title插图%num

 

pollWrapper提供了fdVal和event数据的相应操作,如添加操作通过Unsafe的putInt和putShort实现。

  1. void putDescriptor(int i, int fd) {
  2. pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);
  3. }
  4. void putEventOps(int i, int event) {
  5. pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);
  6. }

先看看serverChannel.register(selector, SelectionKey.OP_ACCEPT)是如何实现的

  1. public final SelectionKey register(Selector sel, int ops, Object att)
  2. throws ClosedChannelException {
  3. synchronized (regLock) {
  4. SelectionKey k = findKey(sel);
  5. if (k != null) {
  6. k.interestOps(ops);
  7. k.attach(att);
  8. }
  9. if (k == null) {
  10. // New registration
  11. synchronized (keyLock) {
  12. if (!isOpen())
  13. throw new ClosedChannelException();
  14. k = ((AbstractSelector)sel).register(this, ops, att);
  15. addKey(k);
  16. }
  17. }
  18. return k;
  19. }
  20. }
  1. 如果该channel和selector已经注册过,则直接添加事件和附件。
  2. 否则通过selector实现注册过程。
  1. protected final SelectionKey register(AbstractSelectableChannel ch,
  2. int ops, Object attachment) {
  3. if (!(ch instanceof SelChImpl))
  4. throw new IllegalSelectorException();
  5. SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
  6. k.attach(attachment);
  7. synchronized (publicKeys) {
  8. implRegister(k);
  9. }
  10. k.interestOps(ops);
  11. return k;
  12. }
  13. protected void implRegister(SelectionKeyImpl ski) {
  14. synchronized (closeLock) {
  15. if (pollWrapper == null)
  16. throw new ClosedSelectorException();
  17. growIfNeeded();
  18. channelArray[totalChannels] = ski;
  19. ski.setIndex(totalChannels);
  20. fdMap.put(ski);
  21. keys.add(ski);
  22. pollWrapper.addEntry(totalChannels, ski);
  23. totalChannels++;
  24. }
  25. }

1、以当前channel和selector为参数,初始化SelectionKeyImpl 对象selectionKeyImpl ,并添加附件attachment。
2、如果当前channel的数量totalChannels等于SelectionKeyImpl数组大小,对SelectionKeyImpl数组和pollWrapper进行扩容操作。
3、如果totalChannels % MAX_SELECTABLE_FDS == 0,则多开一个线程处理selector。
4、pollWrapper.addEntry将把selectionKeyImpl中的socket句柄添加到对应的pollfd。
5、k.interestOps(ops)方法*终也会把event添加到对应的pollfd。

所以,不管serverSocketChannel,还是socketChannel,在selector注册的事件,*终都保存在pollArray中。

接着,再来看看selector中的select是如何实现一次获取多个有事件发生的channel的,底层由selector实现类的doSelect方法实现,如下:

  1. protected int doSelect(long timeout) throws IOException {
  2. if (channelArray == null)
  3. throw new ClosedSelectorException();
  4. this.timeout = timeout; // set selector timeout
  5. processDeregisterQueue();
  6. if (interruptTriggered) {
  7. resetWakeupSocket();
  8. return 0;
  9. }
  10. // Calculate number of helper threads needed for poll. If necessary
  11. // threads are created here and start waiting on startLock
  12. adjustThreadsCount();
  13. finishLock.reset(); // reset finishLock
  14. // Wakeup helper threads, waiting on startLock, so they start polling.
  15. // Redundant threads will exit here after wakeup.
  16. startLock.startThreads();
  17. // do polling in the main thread. Main thread is responsible for
  18. // first MAX_SELECTABLE_FDS entries in pollArray.
  19. try {
  20. begin();
  21. try {
  22. subSelector.poll();
  23. } catch (IOException e) {
  24. finishLock.setException(e); // Save this exception
  25. }
  26. // Main thread is out of poll(). Wakeup others and wait for them
  27. if (threads.size() > 0)
  28. finishLock.waitForHelperThreads();
  29. } finally {
  30. end();
  31. }
  32. // Done with poll(). Set wakeupSocket to nonsignaled for the next run.
  33. finishLock.checkForException();
  34. processDeregisterQueue();
  35. int updated = updateSelectedKeys();
  36. // Done with poll(). Set wakeupSocket to nonsignaled for the next run.
  37. resetWakeupSocket();
  38. return updated;
  39. }

其中 subSelector.poll() 是select的核心,由native函数poll0实现,readFds、writeFds 和exceptFds数组用来保存底层select的结果,数组的*个位置都是存放发生事件的socket的总数,其余位置存放发生事件的socket句柄fd。

  1. private final int[] readFds = new int [MAX_SELECTABLE_FDS + 1];
  2. private final int[] writeFds = new int [MAX_SELECTABLE_FDS + 1];
  3. private final int[] exceptFds = new int [MAX_SELECTABLE_FDS + 1];
  4. private int poll() throws IOException{ // poll for the main thread
  5. return poll0(pollWrapper.pollArrayAddress,
  6. Math.min(totalChannels, MAX_SELECTABLE_FDS),
  7. readFds, writeFds, exceptFds, timeout);
  8. }

执行 selector.select() ,poll0函数把指向socket句柄和事件的内存地址传给底层函数。
1、如果之前没有发生事件,程序就阻塞在select处,当然不会一直阻塞,因为epoll在timeout时间内如果没有事件,也会返回;
2、一旦有对应的事件发生,poll0方法就会返回;
3、processDeregisterQueue方法会清理那些已经cancelled的SelectionKey;
4、updateSelectedKeys方法统计有事件发生的SelectionKey数量,并把符合条件发生事件的SelectionKey添加到selectedKeys哈希表中,提供给后续使用。

在早期的JDK1.4和1.5 update10版本之前,Selector基于select/poll模型实现,是基于IO复用技术的非阻塞IO,不是异步IO。在JDK1.5 update10和linux core2.6以上版本,sun优化了Selctor的实现,底层使用epoll替换了select/poll。

read实现

通过遍历selector中的SelectionKeyImpl数组,获取发生事件的socketChannel对象,其中保存了对应的socket,实现如下

  1. public int read(ByteBuffer buf) throws IOException {
  2. if (buf == null)
  3. throw new NullPointerException();
  4. synchronized (readLock) {
  5. if (!ensureReadOpen())
  6. return1;
  7. int n = 0;
  8. try {
  9. begin();
  10. synchronized (stateLock) {
  11. if (!isOpen()) {
  12. return 0;
  13. }
  14. readerThread = NativeThread.current();
  15. }
  16. for (;;) {
  17. n = IOUtil.read(fd, buf, –1, nd);
  18. if ((n == IOStatus.INTERRUPTED) && isOpen()) {
  19. // The system call was interrupted but the channel
  20. // is still open, so retry
  21. continue;
  22. }
  23. return IOStatus.normalize(n);
  24. }
  25. } finally {
  26. readerCleanup(); // Clear reader thread
  27. // The end method, which
  28. end(n > 0 || (n == IOStatus.UNAVAILABLE));
  29. // Extra case for socket channels: Asynchronous shutdown
  30. //
  31. synchronized (stateLock) {
  32. if ((n <= 0) && (!isInputOpen))
  33. return IOStatus.EOF;
  34. }
  35. assert IOStatus.check(n);
  36. }
  37. }
  38. }

*终通过Buffer的方式读取socket的数据。

wakeup实现

  1. public Selector wakeup() {
  2. synchronized (interruptLock) {
  3. if (!interruptTriggered) {
  4. setWakeupSocket();
  5. interruptTriggered = true;
  6. }
  7. }
  8. return this;
  9. }
  10. // Sets Windows wakeup socket to a signaled state.
  11. private void setWakeupSocket() {
  12. setWakeupSocket0(wakeupSinkFd);
  13. }
  14. private native void setWakeupSocket0(int wakeupSinkFd);

看来wakeupSinkFd这个变量是为wakeup方法使用的。
其中interruptTriggered为中断已触发标志,当pollWrapper.interrupt()之后,该标志即为true了;因为这个标志,连续两次wakeup,只会有一次效果。

epoll原理

epoll是Linux下的一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄。

三个epoll相关的系统调用:

  • int epoll_create(int size)
    epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的*大句柄数,多于这个*大数时内核可不保证效果。
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
    epoll_ctl可以操作epoll_create创建的epoll,如将socket句柄加入到epoll中让其监控,或把epoll正在监控的某个socket句柄移出epoll。
  • int epoll_wait(int epfd, struct epoll_event events,int maxevents, int timeout)
    epoll_wait在调用时,在给定的timeout时间内,所监控的句柄中有事件发生时,就返回用户态的进程。

epoll内部实现大概如下:

  1. epoll初始化时,会向内核注册一个文件系统,用于存储被监控的句柄文件,调用epoll_create时,会在这个文件系统中创建一个file节点。同时epoll会开辟自己的内核高速缓存区,以红黑树的结构保存句柄,以支持快速的查找、插入、删除。还会再建立一个list链表,用于存储准备就绪的事件。
  2. 当执行epoll_ctl时,除了把socket句柄放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后,就把socket插入到就绪链表里。
  3. 当epoll_wait调用时,仅仅观察就绪链表里有没有数据,如果有数据就返回,否则就sleep,超时时立刻返回。

Maven实战— dependencies与dependencyManagement的区别

转载自:https://www.jianshu.com/p/ee15cda51d9d

一句话解释

  • 项目中多个模块间公共依赖的版本号、scope的控制

业务场景

  • 一个项目有很多模块,每个模块都会用到一些公共的依赖
  • 这些公共的依赖若交由各个模块独自管理,若每个模块同一个依赖的版本号不一致,会给项目的整
    • 打包和开发测试环境下对同一 jar 包不同版本号的处理可能不一致,造成运行时和测试时结果不一致
    • 项目升级时,会造成修改版本号时遍地开花的问题
  • 该标签通常适用于多模块环境下定义一个top module来专门管理公共依赖的情况下

项目中依赖包版本号判断途径

  • 若 dependencies 里的 dependency 自己没有声明 version 元素,那么maven 就会 到 depenManagement 里去找有没有该 artifactId 和 groupId 进行过版本声明,若存在,则继承它,若没有则报错,你必须为dependency声明一个version**
  • 若 dependencies 中的 dependency 声明了version,则 dependencyManagement 中的声明无效

单一模块情况下 pom.xml

  1. //只是对版本号进行管理,不会实际引入jar
  2. <dependencyManagement>
  3. <dependencies>
  4. <dependency>
  5. <groupId>org.springframework</groupId> //jar 包身份限定
  6. <artifactId>spring-core</artifactId>
  7. <version>3.2.7</version> //版本号的声明
  8. </dependency>
  9. </dependencies>
  10. </dependencyManagement>
  11. //会实际下载jar包
  12. <dependencies>
  13. <dependency>
  14. <groupId>org.springframework</groupId>
  15. <artifactId>spring-core</artifactId> //不声明version 标签,则会继承
  16. </dependency>
  17. </dependencies>

多模块情况:
parent-module 顶层模块,son1-module 和 son2-module 并列继承 parent-module

  1. parent-module 中 pom.xml
  2. <properties>
  3. // 集中在properties 标签中定义所有 依赖的版本号
  4. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  5. <org.eclipse.persistence.jpa.version>1.2.6</org.eclipse.persistence.jpa.version>
  6. <developer.organization>xxx</developer.organization>
  7. <javaee-api.version>1.8</javaee-api.version>
  8. </properties>
  9. <dependencyManagement>
  10. //定义公共依赖的版本号
  11. <dependencies>
  12. <dependency>
  13. <groupId>org.eclipse.persistence</groupId>
  14. <artifactId>org.eclipse.persistence.jpa</artifactId>
  15. <version>${org.eclipse.persistence.jpa.version}</version>
  16. <scope>provided</scope>
  17. </dependency>
  18. <dependency>
  19. <groupId>javax</groupId>
  20. <artifactId>javaee-api</artifactId>
  21. <version>${javaee-api.version}</version>
  22. </dependency>
  23. </dependencies>
  24. </dependencyManagement>
  1. son-module1 中 的 pom.xml
  2. <!–继承父类–>
  3. <parent>
  4. <artifactId>parent-module</artifactId> //声明父类的身份信息
  5. <groupId>com.ppd</groupId>
  6. <version>0.0.1-SNAPSHOT</version>
  7. <relativePath>../parent-module/pom.xml</relativePath> //声明父类的pom 文件路径
  8. </parent>
  9. <modelVersion>4.0.0</modelVersion>
  10. <artifactId>son-module</artifactId>
  11. <packaging>ejb</packaging>
  12. <!–依赖关系–>
  13. <dependencies>
  14. <dependency>
  15. <groupId>javax</groupId>
  16. <artifactId>javaee-api</artifactId> //继承父类
  17. </dependency>
  18. <dependency>
  19. <groupId>com.fasterxml.jackson.core</groupId>
  20. <artifactId>jackson-annotations</artifactId> //继承父类
  21. </dependency>
  22. <dependency>
  23. <groupId>org.eclipse.persistence</groupId>
  24. <artifactId>org.eclipse.persistence.jpa</artifactId> //继承父类
  25. <scope>provided</scope>
  26. </dependency>
  27. </dependencies>

与 dependencies 标签下 dependency 的区别

  • 所有声明在d ependencies 里的依赖都会自动引入,并默认被所有的子项目继承
  • dependencies 即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项(全部继承)
  • dependencyManagement 只是声明依赖的版本号,该依赖不会引入,因此子项目需要显示声明所需要引入的依赖,若不声明则不引入
  • 子项目声明了依赖且未声明版本号和scope,则会继承父项目的版本号和scope,否则覆盖
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速