Blog


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

android中使用https

发表于 2019-03-18 | 分类于 HTTP

使用okhttp实现https请求,首先要搞清楚https的请求需要什么,即一份ca证书。 购买的证书,格式为.pfx,带有公钥和私钥,附带一个密码。还有一种格式为.cer的证书,这种证书是没有私钥的。

服务器会将证书配置到tomcat中,客户端则存放在本地,app启动的时候加载进去。

本案例将ca证书放在本地,这里使用.pfx格式的证书

单向验证

有两种写法,先展示一种接近okhttp官方写法的方法:

private void setCertificates(Context context) {
    try {
        //将ca证书导入输入流
        InputStream inputStream = context.getResources().openRawResource(R.raw.aaa);

        //keystore添加证书内容和密码
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(inputStream, CLIENT_KET_PASSWORD.toCharArray());

        //证书工厂类,生成证书
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        //生成证书,添加别名
        keyStore.setCertificateEntry("test1", certificateFactory.generateCertificate(inputStream));

        //信任管理器工厂
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        //构建一个ssl上下文,加入ca证书格式,与后台保持一致
        SSLContext sslContext = SSLContext.getInstance("TLS");
        //参数,添加受信任证书和生成随机数
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

        //获得scoket工厂
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        mOkHttpClient.sslSocketFactory(sslSocketFactory);

        //设置ip授权认证:如果已经安装该证书,可以不设置,否则需要设置
        mOkHttpClient.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        inputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

第二种写法,同样有效:

private void setCertificates(Context context) {
    try {
        //将ca证书导入输入流
        InputStream inputStream = context.getResources().openRawResource(R.raw.aaa);

        //keystore添加证书内容和密码
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(inputStream, CLIENT_KET_PASSWORD.toCharArray())

        //key管理器工厂
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, CLIENT_KET_PASSWORD.toCharArray());

        //构建一个ssl上下文,加入ca证书格式,与后台保持一致
        SSLContext sslContext = SSLContext.getInstance("TLS");
        //参数,添加受信任证书和生成随机数
        sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());

        //获得scoket工厂
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        mOkHttpClient.sslSocketFactory(sslSocketFactory);

        //设置ip授权认证:如果已经安装该证书,可以不设置,否则需要设置
        mOkHttpClient.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        inputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

值得注意的是,keystore的格式,keystore拓展名对应格式:

JKS:.jks/.ks
JCEKS:.jce
PKCS12:.p12/.pfx
BKS:.bks
UBER:.ubr

所以,如果ca证书用的是.pfx,那么可以这样写:

KeyStore keyStore = KeyStore.getInstance("PKCS12");

如果是.cer的话那么,就用:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

双向验证

双向验证的前提是,你的app同样生成一个jks的密钥文件,服务器那边会同时有个“cer文件”与之对应。
注意: Java平台默认识别jks格式的证书文件,但是android平台只识别bks格式的证书文件,所以这里还需要将jks的文件转成bks

通过jks文件生成对应的cer文件:

keytool -export -alias test1.jks -file test2.cer -keystore test1.jks -storepass 123456

如果服务端报错keystore文件格式不正确,则我们再将cer文件转换成jks文件:

keytool -import -alias test2.cer -file test2.cer -keystore test3.jks

客户端代码如下:

private void setCertificates(Context context) {
    try {
        //将ca证书导入输入流
        InputStream inputStream = context.getResources().openRawResource(R.raw.aaa);

        //keystore添加证书内容和密码
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(inputStream, CLIENT_KET_PASSWORD.toCharArray());

        //证书工厂类,生成证书
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        //生成证书,添加别名
        keyStore.setCertificateEntry("test1", certificateFactory.generateCertificate(inputStream));

        //信任管理器工厂
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        //双向验证,配置服务器验证客户端的证书
        InputStream inputStream1 = context.getResources().openRawResource(R.raw.bbb);
        KeyStore keyStore1 = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore1.load(inputStream1, CLIENT_KET_PASSWORD_1.toCharArray());
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore1, CLIENT_KET_PASSWORD_1.toCharArray());

        //构建一个ssl上下文,加入ca证书格式,与后台保持一致
        SSLContext sslContext = SSLContext.getInstance("TLS");
        //参数,添加受信任证书和生成随机数
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

        //获得scoket工厂
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        mOkHttpClient.sslSocketFactory(sslSocketFactory);

        //设置ip授权认证:如果已经安装该证书,可以不设置,否则需要设置
        mOkHttpClient.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        inputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

中间人劫持攻击

https也不是绝对安全的,如下图所示为中间人劫持攻击,中间人可以获取到客户端与服务器之间所有的通信内容:

中间人截取客户端发送给服务器的请求,然后伪装成客户端与服务器进行通信;将服务器返回给客户端的内容发送给客户端,伪装成服务器与客户端进行通信。
通过这样的手段,便可以获取客户端和服务器之间通信的所有内容。
使用中间人攻击手段,必须要让客户端信任中间人的证书,如果客户端不信任,则这种攻击手段也无法发挥作用。

造成中间人劫持的原因是:没有对服务端证书及域名做校验或者校验不完整。下面是错误的写法:

正确的写法是真正实现TrustManger的checkServerTrusted(),对服务器证书域名进行强校验或者真正实现HostnameVerifier的verify()方法。
真正实现TrustManger的checkServerTrusted()代码如下:

其中serverCert是APP中预埋的服务器端公钥证书

对服务器证书域名进行强校验:

真正实现HostnameVerifier的verify()方法:

另外一种写法证书锁定,直接用预埋的证书来生成TrustManger,过程如上面介绍okhttp使用https方式

参考资料

okhttp实现https请求
okhttp官方https的api方法
手机如何抓取HTTPS的请求数据

MpAndroidChart实现多点的特殊标记

发表于 2019-03-12 | 分类于 Android知识点

最近在开发时遇到这样一种需求,为一些特殊点显示标签,类似默认显示多个markview。如下图(demo):


在网上并没有相关资料,在此做下记录分享

下面上代码:

首先创建一个类继承LineChart,重写init()方法:

@Override
protected void init() {
    super.init();
    //获取屏幕宽度,上图最边上标签,会根据屏幕宽度适配
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics metrics = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(metrics);
    mRenderer = new HbFundLineChartRenderer(this, mAnimator, mViewPortHandler, metrics.widthPixels);
}

接下来是主要内容,也就是自己实现的LineChartRenderer即渲染器,用来画点、线等.
首先是一些变量,分别是标记控件的宽高边距等,这里写的是一些根据我们需求来的默认值:

private int mWidth;//屏幕宽度,在构造方法中传进来赋值
private float hViewLength = Utils.convertDpToPixel(30f);//vie宽30dp
private float vViewLength = Utils.convertDpToPixel(20f);//view高20dp
private float viewRect= Utils.convertDpToPixel(4f);//矩形高低差

然后,在LineChartRenderer中有一个drawValues,它是主要负责根据值来画点的,我们要做的就是在super()之后加上我们自己的东西:

@Override
public void drawValues(Canvas c) {
    super.drawValues(c);
    if (isShowLabel) {
        LineDataSet dataSetByIndex = (LineDataSet) mChart.getLineData().getDataSetByIndex(0);
        Transformer trans = mChart.getTransformer(dataSetByIndex.getAxisDependency());
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿画笔
        paint.setTextSize(Utils.convertDpToPixel(textSixe));//设置字体大小

        //画首中尾三个label
        float[] firstFloat = getFloat(dataSetByIndex.getValues(), 0);//根据数据集获取点
        drawPointLabel(trans, paint, c, firstFloat);
        float[] middleFloat = getFloat(dataSetByIndex.getValues(), (dataSetByIndex.getValues().size() - 1) / 2);
        drawPointLabel(trans, paint, c, middleFloat);
        float[] endFloat = getFloat(dataSetByIndex.getValues(), dataSetByIndex.getValues().size() - 1);
        drawPointLabel(trans, paint, c, endFloat);
    }
}

首先获取点的数据集,然后得到Transformer,它可以根据点数据集里的某一点来得到这个点在屏幕中的位置
然后分别传入transformer、画笔、画布对象、点,进行绘制:

private void drawPointLabel(Transformer trans, Paint paint, Canvas c, float[] floatPosition) {
    MPPointD maxPoint = trans.getPixelForValues(floatPosition[0], floatPosition[1]);
    float highX = (float) maxPoint.x;
    float highY = (float) maxPoint.y;
    TextView view = (TextView) LayoutInflater.from(mContext).inflate(R.layout.mark_view, null, false);
    if (highX > mWidth - mWidth / 4) {//标识朝左
        view.setBackgroundResource(R.mipmap.sm_lable_bg_buy_r);
        Bitmap bitmap = createBitmap(view, (int) hViewLength, (int) vViewLength);
        c.drawBitmap(bitmap, (int) (highX - hViewLength), (int) (highY - vViewLength - viewRect), paint);
    } else if (highX < mWidth / 4) {//标识朝右
        view.setBackgroundResource(R.mipmap.sm_lable_bg_buy_l);
        Bitmap bitmap = createBitmap(view, (int) hViewLength, (int) vViewLength);
        c.drawBitmap(bitmap, (int) (highX), (int) (highY - vViewLength - viewRect), paint);
    } else {//标识居中
        view.setBackgroundResource(R.mipmap.sm_lable_bg_buy_c);
        Bitmap bitmap = createBitmap(view, (int) hViewLength, (int) vViewLength);
        c.drawBitmap(bitmap, (int) (highX - hViewLength / 2), (int) (highY - vViewLength - viewRect), paint);
    }
}

此处,我们随意定义几个点,可以根据实际需求进行设置:

private float[] getFloat(List<Entry> lists, int index) {
    float[] maxEntry = new float[2];
    maxEntry[0] = lists.get(index).getX();
    maxEntry[1] = lists.get(index).getY();
    return maxEntry;
}

view转bitmap方法如下:

private Bitmap createBitmap(View v, int width, int height) {
    //测量使得view指定大小
    int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
    int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
    v.measure(measuredWidth, measuredHeight);
    //调用layout方法布局后,可以得到view的尺寸大小
    v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
    Bitmap bmp = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bmp);
    v.draw(c);
    return bmp;
}

最后附上代码(内部测试demo,多余功能请忽略)

参考资料:
https://www.jianshu.com/p/1877b8c2fc6c

Java Lock

发表于 2019-03-07 | 分类于 同步

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁会有三种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有
2)线程执行发生异常,此时JVM会让线程自动释放锁
3)这个主要是在等待唤醒机制里面的wait()方法,在等待的时候立即释放锁,方便其他的线程使用锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此我们需要不论程序的代码块执行的如何最终都将锁对象进行释放,方便其他线程的执行。

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,同时为了更好地释放锁。为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。
2)synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,必须要手动释放锁
3)在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态
4)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
5)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
6)Lock可以提高多个线程进行读操作的效率。

locks包结构

java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,结构如下图所示:

  1. Lock和ReadWriteLock是两大锁根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
    Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
    ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。

  2. Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

Lock的使用

下面我们就来探讨一下java.util.concurrent.locks包中常用的类和接口。通过查看Lock的源码可知,Lock是一个接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }catch (InterruptedException e){  

    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有在进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

ReentrantLock

ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

ReentrantLock的类图如下:

ReentrantLock的内部类Sync继承了AQS(AQS根本上是通过一个双向队列来实现的;线程构造成一个节点,一个线程先尝试获得锁,如果获取锁失败,就将该线程加到队列尾部),分为公平锁FairSync和非公平锁NonfairSync。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。

在获取锁的tryAcquire()方法中,非公平锁与公平锁唯一不同是多了以下判断hasQueuedPredecessors()。该方法主要是对同步队列中当前节点是否有前驱节点进行判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。方法如下:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    // 同步队列尾节点
    Node t = tail; // Read fields in reverse initialization order
    // 同步队列头节点
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

事实上,公平锁往往没有非公平锁的效率高,但是,并不是任何场景都是以TPS作为唯一指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越能够得到优先满足。

平锁与非公平锁相比,耗时更多,线程上下文切换次数更多。公平锁保证了锁的获取按照FIFO原则,而代价则是进行大量的线程切换。非公平锁虽然可能导致线程饥饿,但却有极少的线程切换,保证了其更大的吞吐量。

Condition

同jdk中的等待/通知机制类似,只不过Condition是用在重入锁这里的。有了Condition,线程就可以在合适的时间等待,在合适的时间继续执行。

Condition接口包含以下方法:

// 让当前线程等待,并释放锁
void await() throws InterruptedException;
// 和await类似,但在等待过程中不会相应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒等待中的线程
void signal();
// 唤醒等待中的所有线程
void signalAll();

ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

ReentrantReadWriteLock

ReentrantReadWriteLock实现了ReadWriteLock接口。ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

thread1和thread2可以同时进行读操作,这样就大大提升了读操作的效率。

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

锁的相关概念

  1. 可重入锁
        如果锁具备可重入性,则称作为可重入锁。像synchronized和Lock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
        我们在之前文章已经讲解过,详见: Java Synchronized探究

  2. 可中断锁
        顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
        前面的lockInterruptibly()已经体现了Lock的可中断性。

  3. 公平锁
        公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
        非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
        在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
        而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

  4. 读写锁
        读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
        ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

参考资料

https://www.cnblogs.com/dolphin0520/p/3923167.html
https://blog.csdn.net/chengyuqiang/article/details/79181229
https://www.cnblogs.com/fuck1/p/5432806.html
https://blog.csdn.net/qq_38293564/article/details/80515718#t3
https://blog.csdn.net/i_am_kop/article/details/80958856

Java中的volatile

发表于 2019-03-04 | 分类于 同步

volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级。

volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识

Java内存模型

首先来看看如下代码

public class TestVolatile {
    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        status = true;
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){
            System.out.println("running....");
        }
    }
}

上面这个例子,在多线程环境里,假设线程1执行changeStatus()方法后,线程2运行run()方法,可以保证输出”running…..”吗?答案是NO! 因为对于共享变量status来说,线程A的修改,对于线程B来讲,是”不可见”的。也就是说,线程B此时可能无法观测到status已被修改为true。那么什么是可见性呢?
所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。

java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下

需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存。当然如果是出于理解的目的,这样对应起来也无不可。

大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,比如我们上文中的status,线程1将其修改为true这个动作发生在线程1的本地内存中,此时还未同步到主内存中去;而线程2缓存了status的初始值false,此时可能没有观测到status的值被修改了,所以就导致了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了。比较合理的方式其实就是volatile

volatile具备两种特性:

1.保证此变量对所有的线程的可见性。 当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的缓存无效
2.禁止指令重排序优化。 有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置;指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)

上面的例子只需将status声明为volatile,即可保证在线程A将其修改为true时,线程B可以立刻得知

volatile boolean status = false;
  1. 可见性:

  通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

  在Java 中 volatile、synchronized 和 final 实现可见性。

  1. 原子性:

  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。再比如y = x;实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

  1. 有序性:

  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

Volatile原理

  Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

  当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

  而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过上图的 CPU cache 这一步。

  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

留意复合类操作

需要注意的是,我们一直在拿volatile和synchronized做对比,仅仅是因为这两个关键字在某些内存语义上有共通之处,volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile并不能胜任。看下这个例子:

public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

执行结果: 238921

针对这个示例,一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是300000么?

问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:

  1.读取

  2.加一

  3.赋值

所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。

解决num++操作的原子性问题

针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的。

public class Counter {
  //使用原子操作类
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num.incrementAndGet();//原子性的num++,通过循环CAS方式
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

执行结果: 300000

禁止指令重排序

volatile还有一个特性:禁止指令重排序优化。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:

1.重排序操作不会对存在数据依赖关系的操作进行重排序。

    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

    比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

  重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a

public class TestVolatile {
    int a = 1;
    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        a = 2;//1
        status = true;//2
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){//3
            int b = a+1;//4
            System.out.println(b);
        }
    }
}

假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么? 答案依然是无法保证!
上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

  volatile禁止指令重排序也有一些规则,简单列举一下:

  1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序

  2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序

  3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序

使用volatile关键字的场景

ynchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

下面代码显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界:

@NotThreadSafe 
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

这种方式限制了范围的状态变量,因此将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原子化 —— 而将字段定义为 volatile 类型是无法实现这一目的的。

  下面列举几个Java中使用volatile的几个场景。

1.状态标记量

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}



volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);    

2.double check

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

总结

  简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。

参考资料

https://www.cnblogs.com/zhengbin/p/5654805.html
https://www.cnblogs.com/chengxiao/p/6528109.html
https://www.cnblogs.com/dolphin0520/p/3920373.html
不变式举例

Java Synchronized探究

发表于 2019-03-01 | 分类于 同步

在java中,每一个对象都有一把内置锁,当程序中的某一块代码被同步块包起来的时候(synchronized(this){…}),相当于电脑用this指向的对象的内置锁把这块代码锁起来了,只有拥有能解开着这把锁钥匙的线程才能进入到同步块,其他的线程只能在同步块外面排队,只有等拥有钥匙的人执行完同步块归还钥匙的时候,电脑在把钥匙随机分配给外面等待的一个线程。

根据锁的对象不同可以分为两种:对象锁和类锁,对象锁指的是java中的实例对象,类锁指的是Class对象(说到底,不管是对象锁还是类锁,其实锁的都是对象,只是类锁锁的对象是全局唯一的;类锁如static函数和class literals)。但归根结底还是一个对象对应一把内置锁。

需要明确的几个问题:

  1. synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果 再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
  2. 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
  3. 每个对象只有一个锁(lock)与之相关联。被synchronized修饰的方法被锁的对象不同,则实际运行中线程之间互不干扰。
  4. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
  5. synchronized在修饰方法的时候如果没有使用“()”指明被锁的对象,默认是调用这个方法的对象

synchronized 代码块

若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。Java 为我们提供了更好的解决办法,那就是 synchronized 块。
除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象。
这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable {
       private byte[] lock = new byte[0]; // 特殊的instance变量    
       Public void methodA() {      
         synchronized(lock) { //… }
       }
       //…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

synchronized 静态方法

将synchronized作用于static 函数,示例代码如下:

Class Foo {
  // 同步的static 函数
  public synchronized static void methodAAA()  {
  //….
  }
  public void methodBBB() {
       synchronized(Foo.class)   // class literal(类名称字面常量)
  }    
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

synchronized底层原理

Java 虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。同步方法是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

在JVM中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。如下:

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Monitor:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

Java虚拟机对synchronize的优化:

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。

  1. 偏向锁
    偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
  2. 轻量级锁
    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
  3. 自旋锁
    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
  4. 锁消除
    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
  5. 锁膨胀
    如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。 如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(膨胀)到整个操作序列的外部(由多次加锁编程只加锁一次)。

    /**
    * 消除StringBuffer同步锁
    * /
    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
            //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    
    }
    

synchronize的可重入性:

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

线程中断:

正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

等待唤醒机制与synchronize:所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

多线程下数据同步

这类锁/关键字主要是为了维护数据在高并发情况下的一致性/稳定性。

数据库中的锁

共享锁(Share Lock)

又称为读锁

多个线程可并发的获得某个数据的共享锁锁,并行读取数据。在数据存在共享锁期间,不能修改数据,不能加排他锁。
如MySQL中,在查询语句最后加上LOCK IN SHARE MODE。

排他锁(eXclusive Lock)

又称为写锁

同能只能有一个线程可以获得某个数据的排他锁。在线程获取排他锁后,该线程可对数据读写,但是其他线程不能对该数据添加任何锁。

volatile

如果一个共享变量被声明成volatile,java线程内存模型将会确保所有线程看到这个变量的值是一致的。

基本策略: 写操作时,会有Lock前缀指定,处理器会立马将修改直接写回系统内存,并且其他处理器会将该值在其上的高速缓存标为无效。
可能带来的性能消耗: 写操作实时写回内存,锁总线/锁内存。
优势: 一些场景上相比synchronized,执行成本更低(不会引起线程上下文切换以及调度),使用更方便。

关于volatile的详细理解,可以参考我的这篇文章: Java中的volatile

lock

synchronized存在问题:如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此我们需要不论程序的代码块执行的如何最终都将锁对象进行释放,方便其他线程的执行。

Lock提供了比synchronized更多的功能,但并非内置特性。详见:Java Lock

参考资料

https://www.jianshu.com/p/ea9a482ece5f
https://www.cnblogs.com/mingyao123/p/7424911.html
https://blog.dreamtobe.cn/2015/11/13/java_synchronized/

OKHttp深入理解

发表于 2019-02-27 | 分类于 HTTP

OKHttp请求流程

OKHttp的请求流程图如下所示:

如下为使用OKHttp进行Get请求的步骤:

//1.新建OKHttpClient客户端
OkHttpClient client = new OkHttpClient();
//新建一个Request对象
Request request = new Request.Builder()
        .url(url)
        .build();
//2.Response为OKHttp中的响应
Response response = client.newCall(request).execute();

首先,我们会在请求的时候初始化一个Call的实例,然后根据同步和异步的不同,分别调用它的 execute() 和 enqueue() 方法,但是它们进行网络访问的逻辑都是一样的,内部最后都会执行到getResponseWithInterceptorChain()方法,这个方法里面通过拦截器组成的责任链,依次经过用户自定义普通拦截器、重试拦截器、桥接拦截器、缓存拦截器、连接拦截器和用户自定义网络拦截器以及访问服务器拦截器等拦截处理过程,来获取到一个响应并交给用户。

分发器Dispatcher

使用 OkHttp 的时候,我们会创建一个 RealCall 并将其加入到双端队列中。但是请注意这里的双端队列的名称是 runningSyncCalls,也就是说这种请求是同步请求,会在当前的线程中立即被执行。所以,下面的 getResponseWithInterceptorChain() 就是这个同步的执行过程。而当我们执行完毕的时候,又会调用 Dispatcher 的 finished(RealCall) 方法把该请求从队列中移除。所以,这种同步的请求无法体现分发器的“分发”功能。

除了同步的请求,还有异步类型的请求:当我们拿到了 RealCall 的时候,调用它的 enqueue(Callback responseCallback) 方法并设置一个回调即可。该方法会执行下面这行代码:

client.dispatcher().enqueue(new AsyncCall(responseCallback));

当我们调用了 Dispatcher 的 enqueue(AsyncCall) 方法的时候也会将 AsyncCall 加入到一个队列中,并会在请求执行完毕的时候从该队列中移除,只是这里的队列是 runningAsyncCalls 或者 readyAsyncCalls。它们都是一个双端队列,并用来存储异步类型的请求。它们的
区别是,runningAsyncCalls 是正在执行的队列,当正在执行的队列达到了限制的时候,就会将其放置到就绪队列 readyAsyncCalls 中:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
}

当把该请求加入到了正在执行的队列之后,我们会立即使用一个线程池来执行该 AsyncCall。这样这个请求的责任链就会在一个线程池当中被异步地执行了。这里的线程池由 executorService() 方法返回:

public synchronized ExecutorService executorService() {
    if (executorService == null) {
        executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
}

显然,当线程池不存在的时候会去创建一个线程池。除了上面的这种方式,我们还可以在构建 OkHttpClient 的时候,自定义一个 Dispacher,并在其构造方法中为其指定一个线程池。

拦截器

  1. 在配置 OkHttpClient时设置的interceptors;[eg. 最常用的:日志拦截器]

  2. 负责失败重试以及重定向的 RetryAndFollowUpInterceptor;会根据服务器返回的信息判断这个请求是否可以重定向,或者是否有必要进行重试

  3. 桥拦截器 BridgeInterceptor 用于从用户的请求中构建网络请求,然后使用该请求访问网络,最后从网络响应当中构建用户响应。[简单的说: 只是用来对请求进行包装,并将服务器响应转换成用户友好的响应]

  4. 负责读取缓存直接返回、更新缓存的 CacheInterceptor

  5. 负责和服务器建立连接的ConnectInterceptor;这里并没有真正地从网络中获取数据,而仅仅是打开一个连接。在获取连接对象的时候,使用了连接池 ConnectionPool 来复用连接。

    public final class ConnectInterceptor implements Interceptor {
    
        @Override public Response intercept(Chain chain) throws IOException {
            RealInterceptorChain realChain = (RealInterceptorChain) chain;
            Request request = realChain.request();
            StreamAllocation streamAllocation = realChain.streamAllocation();
    
            boolean doExtensiveHealthChecks = !request.method().equals("GET");
            HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
            RealConnection connection = streamAllocation.connection();
    
            return realChain.proceed(request, streamAllocation, httpCodec, connection);
        }
    }
    

    这里的HttpCodec 用来编码请求并解码响应,RealConnection 用来向服务器发起连接。它们会在下一个拦截器中被用来从服务器中获取响应信息。

    StreamAllocation相当于一个管理类,维护了服务器连接、并发流和请求之间的关系,该类还会初始化一个 Socket 连接对象,获取输入/输出流对象。当我们调用 streamAllocation 的 newStream() 方法的时候,最终会经过一系列的判断到达 StreamAllocation 中的 findConnection() 方法。该方法会被放置在一个循环当中被不停地调用以得到一个可用的连接。它优先使用当前已经存在的连接,不然就使用连接池中存在的连接,再不行的话,就创建一个新的连接。我们使用连接复用的一个好处就是省去了进行 TCP 和 TLS 握手的一个过程。因为建立连接本身也是需要消耗一些时间的,连接被复用之后可以提升我们网络访问的效率。

  6. 配置 OkHttpClient 时设置的 networkInterceptors;[for web socket,自行了解]

  7. 服务器请求拦截器 CallServerInterceptor 用来向服务器发起请求并获取数据。
    位置决定了功能,最后一个 Interceptor 一定是负责和服务器实际通讯的,重定向、缓存等一定是在实际通讯之前的

源码如下:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
        interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
            originalRequest, this, eventListener, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

这里,我们创建了一个列表对象之后把 client 中的拦截器、重连拦截器、桥拦截器、缓存拦截器、网络连接拦截器和服务器请求拦截器等依次加入到列表中。然后,我们用这个列表创建了一个拦截器链。这里使用了责任链设计模式,每当一个拦截器执行完毕之后会调用下一个拦截器或者不调用并返回结果。显然,我们最终拿到的响应就是这个链条执行之后返回的结果。当我们自定义一个拦截器的时候,也会被加入到这个拦截器链条里。

连接管理:ConnectionPool

与请求的缓存类似,OkHttp 的连接池也使用一个双端队列来缓存已经创建的连接:

private final Deque<RealConnection> connections = new ArrayDeque<>();

OkHttp 的缓存管理分成两个步骤,一边当我们创建了一个新的连接的时候,我们要把它放进缓存里面;另一边,我们还要来对缓存进行清理。在 ConnectionPool 中,当我们向连接池中缓存一个连接的时候,只要调用双端队列的 add() 方法,将其加入到双端队列即可,而清理连接缓存的操作则交给线程池来定时执行。

在 ConnectionPool 中存在一个静态的线程池:

private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
    Integer.MAX_VALUE /* maximumPoolSize */, 
    60L /* keepAliveTime */,
    TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(), 
    Util.threadFactory("OkHttp ConnectionPool", true));

每当我们向连接池中插入一个连接的时候就会调用下面的方法,将连接插入到双端队列的同时,会调用上面的线程池来执行清理缓存的任务:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
        cleanupRunning = true;
        // 使用线程池执行清理任务
        executor.execute(cleanupRunnable);
    }
    // 将新建的连接插入到双端队列中
    connections.add(connection);
}

这里的清理任务是 cleanupRunnable,是一个 Runnable 类型的实例。它会在方法内部调用 cleanup() 方法来清理无效的连接。

在从缓存的连接中取出连接来判断是否应该将其释放的时候使用到了两个变量 maxIdleConnections 和 keepAliveDurationNs,分别表示最大允许的闲置的连接的数量和连接允许存活的最长的时间。默认空闲连接最大数目为5个,keepalive 时间最长为5分钟。该方法会对缓存中的连接进行遍历,以寻找一个闲置时间最长的连接,然后根据该连接的闲置时长和最大允许的连接数量等参数来决定是否应该清理该连接。

Response

bytes()大小有限制,建议用byteStream()。源码如下:

public final byte[] bytes() throws IOException {
    long contentLength = contentLength();
    if (contentLength > Integer.MAX_VALUE) {
      throw new IOException("Cannot buffer entire body for content length: " + contentLength);
    }
    ...
}
public final InputStream byteStream() {
    return source().inputStream();
}

缓存

使用okhttp的cache,首先需指定缓存路径和大小

private OkHttpClient initClient() {
        File cacheFile = new File(config.getCacheFilePath());
        if (!cacheFile.exists()) {
            cacheFile.mkdir();
        }
        //缓存大小为30M
        int cacheSize = 30 * 1024 * 1024;
        //创建缓存对象
        Cache cache = new Cache(getContext(), cacheFile, cacheSize);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.addInterceptor(new SercurityKeyInteraptor())
                .addInterceptor(new HttpLoggingInterceptor())
                .connectTimeout(config.getConnectTimeout(), TimeUnit.SECONDS)
                .writeTimeout(config.getWriteTimeout(), TimeUnit.SECONDS)
                .readTimeout(config.getReadTimeout(), TimeUnit.SECONDS)
                .cache(cache)
                .cookieJar(new FundCookie());
        return mOkHttpClient = builder.build();
    }

其次在构造Request时配置缓存策略

CacheControl cc = new CacheControl.Builder()  
            //不使用缓存,但是会保存缓存数据  
            //.noCache()  
            //不使用缓存,同时也不保存缓存数据  
           // .noStore()  
            //只使用缓存,(如果我们要加载的数据本身就是本地数据时,可以使用这个,不过目前尚未发现使用场景)  
            //.onlyIfCached()  
            //手机可以接收响应时间小于当前时间加上10s的响应  
            //  .minFresh(10,TimeUnit.SECONDS)  
            //手机可以接收有效期不大于10s的响应  
            //  .maxAge(10,TimeUnit.SECONDS)  
            //手机可以接收超出5s的响应  
            .maxStale(5,TimeUnit.SECONDS)  
            .build();  
    Request request = new Request.Builder()  
            .cacheControl(cc)  
            .url("http://192.168.152.2:8080/cache").build();  

如果直接使用CacheControl中的常量,则不用调用上面那么多的方法,使用方式如下:

Request request = new Request.Builder()  
            //强制使用网络  
            // .cacheControl(CacheControl.FORCE_NETWORK)  
            //强制使用缓存  
            .cacheControl(CacheControl.FORCE_CACHE)  
            .url("http://192.168.152.2:8080/cache").build();  

OkHttp的Cache是根据URL以及请求参数来生成的,并且不支持POST请求。

CacheInterceptor拦截器实现读写操作,读写操作都是通过okio实现,快速,高效流

读: 根据缓存策略实现读取缓存,返回Response,Okhttp中实现的是轻量级 LruCache缓存模式[最近最少使用原则]。然后关于DiskLruCache是如何管理缓存文件的,这个其实也很好理解,首先的原则就是按照LRU这种最近最少使用删除的原则,当总的大小超过限定大小后,删除最近最少使用的缓存文件,它的LRU算法是使用LinkedHashMap进行维护的,这样来保证,保留的缓存文件都是更常使用的。

写: 根据缓存策略,将服务端返回的数据写入磁盘

Okhttp缓存相关的类有如下:

CacheControl(HTTP中的Cache-Control和Pragma缓存控制)
CacheControl是用于描述HTTP的Cache-Control和Pragma字段的类,用于指定缓存的规则。

CacheStrategy(缓存策略类)
CacheStrategy是用于判定使用缓存数据还是网络请求的决策类。

Cache(缓存类)
对外开放的缓存类,提供了缓存的增删改查接口。

InternalCache(内部缓存类)
对内使用的缓存类接口,没有具体实现,只是封装了Cache的使用。

DiskLruCache(文件化的LRU缓存类)
这是真正实现缓存功能的类,将数据存储在文件中,并使用LRU规则(由LinkedHashMap实现),控制对缓存文件的增删改查。

Cookies

3.0之后OKHttp是加了CookieJar和Cookie两个类的,通过实现CookieJar即可管理cookie。
加载Cookie时,IP地址与域名是有区别的。如果访问的是IP地址,Cookie是不会从publicsuffixes.gz文件中读取Cookie数据。
publicsuffixes.gz 就是一个类似apk一样的压缩文件,可以解压通过Txt查看里面的内容。
官文提供的原始文件内容: https://publicsuffix.org

private class FundCookie implements CookieJar {

    private final ConcurrentHashMap<String, List<Cookie>> cookieStore = new ConcurrentHashMap<>();

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        /*
         * Cookie name 不能重复:需要人为管控
         */
        cookieStore.put(url.host(), cookies);
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        /*
         * 不能用url.host来获取Cookie值,因为在请求过程中可能存在 301 重定向问题,导致重定向的url无法获取Cookie值,
         * 但它与其它接口属于同一个 domain
         * 解决办法:将本地所有的Cookie都上传给接口,后台解析会去匹配 KEY-VALUE[SESSION name- Cookie value]
         * 所以必要保证 不同的domain对应的SESSION name 不能重复
         */
        List<Cookie> curCookies = new ArrayList<>();
        for (List<Cookie> entry : cookieStore.values()) {
            curCookies.addAll(entry);
        }
        return curCookies;
    }
}

HTTPS

Okhttp默认是支持https网络请求的,但是支持的Https网站必须是CA机构认证了的,对于自签名的网址,还是不能访问的,访问直接抛出如下异常信息:

onFailure: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

针对https的处理,目前主要有两种方式:

客户端默认信任全部证书
对自签名网址进行证书的单独处理

具体可以参看我的这篇文章:android中使用https

Gzip

http

request header中声明Accept-Encoding: gzip,告知服务器客户端接受gzip的数据。

服务器支持的情况下,返回gzip后的response body,同时加入以下header:

Content-Encoding: gzip:表明body是gzip过的数据

Content-Length:117:表示body gzip压缩后的数据大小,便于客户端使用。
或 Transfer-Encoding: chunked:分块传输编码

Okhttp

如果header中没有Accept-Encoding,默认自动添加 ,且标记变量transparentGzip为true。

针对返回结果,如果同时满足以下三个条件:

transparentGzip为true,即之前自动添加了Accept-Encoding

header中标明了Content-Encoding为gzip

有body

移除 Content-Encoding、Content-Length,并对结果进行解压缩。

开发者没有添加Accept-Encoding时,自动添加Accept-Encoding: gzip

自动添加的request,response支持自动解压

手动添加不负责解压缩

自动解压时移除Content-Length,所以上层Java代码想要contentLength时为-1

自动解压时移除 Content-Encoding

自动解压时,如果是分块传输编码,Transfer-Encoding: chunked不受影响。

HttpUrlConnection:

4.4版本之后与okhttp相仿

其它:网络框架实现步骤

1.封装请求参数
2.封装响应数据
3.根据前两步,封装请求任务
4.创建线程池管理类(队列,线程池)
5.封装”使用工具”、添加重试机制等

参考资料

https://juejin.im/post/5bc89fbc5188255c713cb8a5
让 okhttp 支持 post缓存
https://jsonchao.github.io/2018/12/01/Android%E4%B8%BB%E6%B5%81%E4%B8%89%E6%96%B9%E5%BA%93%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%80%E3%80%81%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3OKHttp%E6%BA%90%E7%A0%81%EF%BC%89/
手撸一个简单的网络框架

android异步处理

发表于 2019-02-27 | 分类于 Android知识点

前言

异步处理实现方式主要有:

  • 实现Thread的run()方法或者实现Runable接口
  • HandlerThread
  • AsyncTask(已废弃)
  • LoaderManager
  • WorkManager

Thread

直接使用Thread实现方式,这种方式简单,但不是很优雅。适合数量很少(偶尔一两次)的异步任务,但要处理的异步任务很多的话,使用该方式会导致创建大量的线程,这会影响用户交互。

  1. 关键字join、sleep、yield

    join() method suspends the execution of the calling thread until the object called finishes its execution.
    也就是说,t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。

    join()方法是让出执行资源(如:CPU时间片),使得其它线程可以获得执行的资源。所以调用join()方法会使进入阻塞状态,该线程被唤醒后会进入runable状态,等待下一个时间片的到来才能再次执行。

    sleep()不会让出资源,只是处于睡眠状态(类似只执行空操作)。调用sleep()方法会使进入等待状态,当等待时间到后,如果还在时间片内,则直接进入运行状态,否则进入runable状态,等待下个时间片。

    Yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield()方法将不会起作用。

    suspend()可能导致死锁,因此弃用

HandlerThread

HandlerThread,这种方式适合子线程有序的执行异步操作,异步任务的执行一个接着一个。

HandlerThread的内部实现机制很简单,在创建新的线程后,使该线程成为一个Looper线程,让该线程不断的从MessageQueue取出消息并处理。

就应用程序而言,Android系统中JAVA的应用程序和其他系统上相同,都是靠消息驱动来工作的,他们大致的工作原理如下:

1、有一个消息队列,可以往这个消息队列中投递消息。

2、有一个消息循环,不断从消息队列中取出消息,然后处理。

在Android中,一个线程对应一个Looper对象,而一个Looper对象又对应一个MessageQueue(用于存放message)。

循环者Looper类,消息处理类Handler,消息类Message。

Looper对象用来为一个线程开启一个消息循环,用来操作MessgeQueue。默认情况下,Android中新创建的线程是没有开启消息循环的。(主线程除外)

消息处理类(Handler)允许发送和处理Message和Rannable对象到其所在线程的MessageQueue中。(它主要有两个作用:1、将Message或Runnable应用post()方法或sendMessage()方法发送到MessageQueue中,在发送时可以指定延时时间、发送时间或者要携带的bundle数据。当MessageQueue循环到该Message时,调用相应的Handler对象的handlerMessage()方法对其进行处理。2、在子线程中与主线程进行通信,也就是在工作线程中与UI线程进行通信。)

另外,在一个线程中只能有一个Looper和MessageQueue,但是可以有多个Handler,而且这些Handler可以共享一个Looper和MessageQueue。

消息类(Message)被存放在MessageQueue中,一个MessageQueue中可以包含多个Message对象。每个Message对象可以通过Messhe.obtain()方法或者Handler.obtainMessage()方法获得。Message是一个final类,所以不可被继承。

AsyncTask(已废弃)

AsyncTask的内部使用了两个线程池,使用AsyncTask执行异步操作时,会先在SerialExecutor进行一个顺序排队, 后再用ThreadPoolExcutor线程池为你分配一个线程并执行。而整个应用的AsyncTask任务都在排同一条队,有可能等待排队的任务很多,所以一般不会使用AsyncTask执行一些优先级比较高的异步任务。

当然我们是可以跳过不需要进行排队,直接就通过线程池分配一个线程并执行异步任务,但需要注意同时执行太多的异步任务,会影响用户体验,我想Google就是为了限制同时创建太多的线程才会采用一个排队机制的

/** @hide */
public static void setDefaultExecutor(Executor exec) {
    sDefaultExecutor = exec;
}

该方法是隐藏,但可使用反射,设置一个线程池。

AsyncTask, 通常用于耗时的异步处理,且时效性要求不是非常高的那种异步操作。如果时效性要求非常高的操作,不建议使用这个方式,因为AsyncTask的默认实现是有内部排队机制,且是整个应用的AsyncTask的任务进行排队,所以不能保证异步任务能很快的被执行。

问题如下:

  1. 并行串行问题:AsyncTasks should ideally be used for short operations (a few seconds at the most)
  2. 错误处理问题:AsyncTask没有对发生的一些异常进行处理,你只能在onBackground里进行一些判断,但之外的一些异常情况发生你都无法了解,比如线程异常退出等。
  3. 多个任务的管理问题:如果需要多个后台任务,需要新建多个AsyncTask来执行任务,在需要退出的时候你需要对每一个都进行一定的处理来避免内存泄露以及UI问题,这是一个很麻烦的事情。

LoaderManager

LoaderManager,当请求处理时机需要根据Activity的生命周期进行调整,或需要时刻监测数据的变化,那LoaderManager是很不错的解决方案。

LoaderManager可以解决的问题包括:

1.加载的数据有变化时,会自动通知我们,而不自己监控数据的变化情况,如:用CursorLoader来加载数据库数据,当数据库数据有变化时,可是个展示变化的数据

2.数据的请求处理时机会结合Activity和Fragment的生命周期进行调整,如:若Acivity销毁了,那就不会再去请求新的数据

1.LoaderManager

LoaderManager用来负责管理与Activity或者Fragment联系起来的一个或多个Loaders对象.

每个Activity或者Fragment都有唯一的一个LoaderManager实例(通过getLoaderManager()方法获得),用来启动,停止,保持,重启,关闭它的Loaders,这些功能可通过调用initLoader()/restartLoader()/destroyLoader()方法来实现.

LoaderManager并不知道数据如何装载以及何时需要装载.相反,它只需要控制它的Loaders们开始,停止,重置他们的Load行为,在配置变换或数据变化时保持loaders们的状态,并使用接口来返回load的结果.

2.Loader

Loades负责在一个单独线程中执行查询,监控数据源改变,当探测到改变时将查询到的结果集发送到注册的监听器上.Loader是一个强大的工具,具有如下特点

(1)它封装了实际的数据载入.

Activity或Fragment不再需要知道如何载入数据.它们将该任务委托给了Loader,Loader在后台执行查询要求并且将结果返回给Activity或Fragment.

(2)客户端不需要知道查询如何执行.Activity或Fragment不需要担心查询如何在独立的线程中执行,Loder会自动执行这些查询操作.

(3)它是一种安全的事件驱动方式.

Loader检测底层数据,当检测到改变时,自动执行并载入最新数据.

这使得使用Loader变得容易,客户端可以相信Loader将会自己自动更新它的数据.

Activity或Fragment所需要做的就是初始化Loader,并且对任何反馈回来的数据进行响应.除此之外,所有其他的事情都由Loader来解决.

Loader:该类用于数据的加载 ,类型参数D用于指定Loader加载的数据类型

public class Loader<D> {
}

一般我们不直接继承Loader,而是继承AsyncTaskLoader,因为Loader的加载工作并不是在异步线程中。而AsyncTaskLoader实现了异步线程,加载流程在子线程中执行。注意:对该类的调用应该在主线程中完成。

Loader负责数据加载逻辑,LoaderManager负责Loader的调度,开发者只需要自定义自己的Loader,实现数据的加载逻辑,而不再关注数据加载时由于Activity销毁引发的问题。

注意:其实AsyncTaskLoader内部实现异步的方式是使用AsyncTask完成的,上面我们说过AsyncTask的内部是有一个排队机制,但AsyncTaskLoader内部使用AsyncTask进行数据异步加载时,异步任务并不进行排队。而直接由线程池分配新线程来执行。

WorkManager

WorkManager最适用于可以延迟的任务,即使应用程序或设备重新启动(例如,使用后端服务定期同步数据并上载日志或分析数据),仍然可以运行。

特点:

  • 允许在任务运行时设置约束,例如网络状态或充电状态;
  • 支持异步一次性和周期性任务;
  • 支持带输入和输出的链式任务;
  • 即使应用程序或设备重新启动,也可确保任务执行

使用WorkManager,可以轻松添加网络可用性或计费状态等约束。任务将在满足约束时运行,并在运行时失败时自动重试。例如,如果任务需要网络可用,则当网络不再可用时将停止该任务,并在以后重试。
不仅如此,它还可以使用LiveData监视工作状态并检索工作结果,这样可以在任务完成时通知您的UI。如果任务执行失败,可以通过配置退避的处理方式来控制工作的重试方式。如果发生应用程序或设备重新启动,WorkManager还可以使用本地数据库中的工作记录重新安排工作。
利用OneTimeWorkRequest进行一次性调度或使用PeriodicWorkRequest进行重复调度。并且,我们还可以将一次性工作请求链接到按顺序或并行运行,如果链中的任何工作失败,WorkManager将确保不会运行剩余的工作链。

WorkManager API 可以很容易的指定可延迟的异步任务。允许你创建任务,并把它交给WorkManager来立即运行或在适当的时间运行。WorkManager根据设备API的级别和应用程序状态等因素来选择适当的方式运行任务。如果WorkManager在应用程序运行时执行你的任务,它会在应用程序进程的新线程中执行。如果应用程序没有运行,WorkManager会根据设备API级别和包含的依赖项选择适当的方式安排后台任务,可能会使用JobScheduler、Firebase JobDispatcher或AlarmManager。你不需要编写设备逻辑来确定设备有哪些功能和选择适当的API;相反,你只要把它交给WorkManager让它选择最佳的方式。

基础功能:

  • 使用WorkManager创建运行在你选择的环境下的单个任务或指定间隔的重复任务
  • WorkManager API使用几个不同的类,有时,你需要继承一些类。
  • Worker 指定需要执行的任务。有一个抽象类Worker,你需要继承并在此处工作。在后台线程同步工作的类。WorkManager在运行时实例化Worker类,并在预先指定的线程调用doWork方法(见Configuration.getExecutor())。此方法同步处理你的工作,意味着一旦方法返回,Worker被视为已经完成并被销毁。如果你需要异步执行或调用异步API,应使用ListenableWorker。如果因为某种原因工作没抢占,相同的Worker实例不会被重用。即每个Worker实例只会调用一次doWork()方法,如果需要重新运行工作单元,需要创建新的Worker。Worker最大10分钟完成执行并ListenableWorker.Result。如果过期,则会被发出信号停止。(Worker的doWork()方法是同步的,方法执行完则结束,不会重复执行,且默认超时时间是10分钟,超过则被停止。)
  • WorkRequest 代表一个独立的任务。一个WorkRequest对象至少指定哪个Worker类应该执行该任务。但是,你还可以给WorkRequest添加详细信息,比如任务运行时的环境。每个WorkRequest有一个自动生成的唯一ID,你可以使用ID来取消排队的任务或获取任务的状态。WorkRequest是一个抽象类,你需要使用它一个子类,OneTimeWorkRequest或PeriodicWorkRequest。
    1)WorkRequest.Builder 创建WorkRequest对象的帮助类,你需要使用子类OneTimeWorkRequest.Builder或PeriodicWorkRequest.Builder。
    2)Constraints(约束) 指定任务执行时的限制(如只有网络连接时)。使用Constraints.Builder创建Constraints对象,并在创建WorkRequest对象前传递给WorkRequest.Builder。
  • WorkManager 排队和管理WorkRequest。将WorkRequest对象传递给WorkManager来将任务添加到队列。WorkManager 使用分散加载系统资源的方式安排任务,同时遵守你指定的约束。
    1)WorkManager使用一种底层作业调度服务基于下面的标注
    2)使用JobScheduler API23+
    3)使用AlarmManager + BroadcastReceiver API14-22
  • WorkInfo 包含有关特定任务的信息。WorkManager为每个WorkRequest对象提供一个LiveData。LiveData持有WorkInfo对象,通过观察LiveData,你可以确定任务的当前状态,并在任务完成后获取任何返回的值。

注意:

  1. WorkManager组件库里面提供了一个专门做周期性任务的类PeriodicWorkRequest。但是PeriodicWorkRequest类有一个限制条件最小的周期时间是15分钟。
  2. 链式任务的任务链里面的任何一个任务返回WorkerResult.FAILURE,则整个任务链终止;WorkManager会把上一个任务的输出自动作为下一个任务的输入。链式任务的关键在WorkContinuation,通过WorkContinuation来整理好队列(是顺序执行,还是组合执行)然后入队执行。

参考资料

https://blog.csdn.net/baidu_36385172/article/details/79705915
https://www.cnblogs.com/diysoul/p/5124886.html
WorkManager浅析
Android架构组件WorkManager详解

github博客搭建

发表于 2019-02-20 | 分类于 其他

基本命令

hexo clean #/清除静态页面缓存(清除 public 文件夹)

hexo g #生成或 hexo generate

hexo s #启动本地服务器 或者hexo server,这一步之后就可以通过localhost:4000查看了

hexo d #部署到github

hexo clean & hexo g & hexo s #一键启动

hexo new page xxx #创建页面

命令       文件目录            

post    source/_post       新建一个文章

draft   source/_drafts     新建一个草稿文件

page    source             新建一个页面文件

hexo添加分类和标签:

---
title: title #文章標題
date: 2016-06-01 23:47:44 #文章生成時間
categories: "Hexo教程" #文章分類目錄 可以省略
tags: #文章標籤 可以省略
     - 标签1
     - 标签2
 description: #你對本頁的描述 可以省略
---

hexo目录结构

markdown编辑器

说明:在Hexo中插入图片时,请按照以下步骤进行设置

(1)将站点配置文件中的 post_asset_folde 选项设置成 true

(2)在站点文件夹中打开 git bash,输入命令 npm install hexo-asset-image –save 安装插件

(3)此时使用 hexo new title 创建文章时,将同时在 source/_post 文件夹中生成一个与 title 同名的文件夹,我们只需将待添加的图片放进此文件夹中,然后在文章中通过 Markdown 语法进行引用即可例如,在资源文件夹(就是那个与 title 同名的文件夹)中添加了图片 example.PNG,则可以在对应的文章中使用语句 ![示例图片](title/example.PNG “示例图片”) 添加图片

使用 Hexo Admin 插件(难用)

Hexo Admin 是一个本地在线式文章管理器,可以用直观可视化的方式新建、编辑博客文章、page页面,添加标签、分类等,并且支持剪贴板粘贴图片(自动在source_images_目录中创建文件)

在Hexo网站目录下,安装 Hexo Admin 插件

npm install –save hexo-admin

启动本地服务器并打开管理界面,即可使用

hexo server -d

open localhost:4000/admin/

markdown表格调整宽度

1
2
3
4
5
6
7
8
9
<style>
table th:first-of-type {
width: 100px;
}
</style>

<!-- 下方是表格的 Markdown 语法 --!>
名称|值|备注
---|---|---

这里需要一点 CSS 知识,选择器的问题,首先 <th> 存在于 <table> 中;其次 th:first-of-type 的意思是每个 <th> 为其父级的第一个元素,这里指的就是围绕着【名称】的 <th>。同理第二、三个使用 th:nth-of-type(2)、th:nth-of-type(3) 就可以了,以此类推。上述的 th:first-of-type 等于 th:nth-of-type(1)。

Markdown编辑表格时如何输入竖线

主要思路: 竖线用 &#124 或者 &#x7C 来代替,后加分号

参考资料:

https://www.cnblogs.com/jackyroc/p/7681938.html

https://www.cnblogs.com/fengxiongZz/p/7707219.html

https://blog.csdn.net/wsmrzx/article/details/81478945

https://www.jianshu.com/p/df46bca5889d

1…78
Shuming Zhao

Shuming Zhao

78 日志
14 分类
31 标签

© 2021 Shuming Zhao
访客数人, 访问量次 |
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4