本文Demo下载传送门

写在前面

本篇文章讲的东西都是Android开源网络框架NoHttp的核心点,当然线程、多线程、数据安全这是Java中就有的,为了运行快我们用一个Java项目来讲解。

为什么要保证线程安全/数据同步

当多个子线程访问同一块数据的时候,由于非同步访问,所以数据可能被同时修改,所以这时候数据不准确不安全。

现实生活中的案例

假如一个银行帐号可以存在多张银行卡,三个人去不同营业点同时往帐号存钱,假设帐号原来有100块钱,现在三个人每人存钱100块,我们最后的结果应该是100 + 3 * 100 = 400块钱。但是由于多个人同时访问数据,可能存在三个人同时存的时候都拿到原账号有100,然后加上存的100块再去修改数据,可能最后是200、300或者400。这种清情况下就需要锁,当一个人操作的时候把原账号锁起来,不能让另一个人操作。

案例(非线程安全)代码实现:

1、程序入口,启动三个线程在后台循环执行任务,添加100个任务到队列:

/**
* 程序入口
*/
public void start() {
// 启动三个线程
for (int i = 0; i < 3; i++) {
new MyTask(blockingQueue).start();
} // 添加100个任务让三个线程执行
for (int i = 0; i < 100; i++) {
Tasker tasker = Tasker.getInstance();
blockingQueue.add(tasker);
}
}

 2、那我们再来看看MyTask这个线程是怎么回事,它是怎么执行Tasker这个任务的。

public class MyTask extends Thread {

    ...

    @Override
public void run() {
while (true) {
try {
Tasker person = blockingQueue.take();
person.change();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} }

 

分析一下上面的代码,就是一直等待循环便利队列,每拿到一个Tasker时去调用void change()方法让Tasker在子线程中执行任务。

3、我们在来看看Tasker对象怎么执行,单例模式的对象,被重复添加到队列中执行void change()方法:

public class Tasker implements Serializable, Comparable<Tasker> {

    private static Integer value = 0;

    public void change() {
value++;
System.out.println(value);
}
...
}

  我们来分析一下上面的代码,void change()每被调用一次,属性value的值曾加1,理论上应该是0 1 2 3 4 5 6 7 8 9 10…这样的数据被打印出来,最差的情况下也是1 3 4 6 5 2 8 7 9 10 12 11…这样顺序乱一下而已,但是我们运行起来看看:

我们发现了为什么会有3 4 3 3 这种重复数据出现呢?嗯对了,这就是文章开头说的多个线程拿到的value字段都是2,然后各自+1后打印出来的结果都是3,如果应用到我们的银行系统中,那这不是坑爹了麽,所以我们在多线程开发的事后就用到了锁。

多线程保证数据的线程安全与数据同步

多线程开发中不可避免的要用到锁,一段被加锁的代码被一个线程执行之前,线程要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁),如果这个时候同步对象的锁被其他线程拿走了,这个线程就只能等了(线程阻塞在锁池等待队列中)。拿到权限(锁)后,他就开始执行同步代码,线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。Java中常用的锁有synchronized和Lock两种。
锁的特点:每个对象只有一把锁,不管是synchronized还是Lock它们锁定的只能是某个具体对象,也就是说该对象必须是唯一的,才能被锁起,不被多个线程同时使用。

synchronized的特点

同步锁,当它锁定的方法或者代码块发生异常的时候,它会在自动释放锁;但是如果被它锁定的资源被线程竞争激烈的时候,它的表现就没那么好了。

1、我们来看下下面这段代码:

// 添加100个任务让三个线程执行
for (int i = 0; i < 100; i++) {
Tasker tasker = new Tasker();
blockingQueue.add(tasker);
}

  这段代码是文章最开头的一段,只是把Tasker.getInstance()改为了new Tasker();,我们现在给Tadkervoid change()方法加上synchronized锁:

/**
* 执行任务;synchronized锁定方法。
*/
public synchronized void change() {
value++;
System.out.println(value);
}

  我们再次执行后发现,艾玛怎么还是有重复的数字打印呢,不是锁起来了麽?但是细心的读者注意到我们添加Tasker到队列中的时候是每次都new Tasker();,这样每次添加进去的任务都是一个新的对象,所以每个对象都有一个自己的锁,一共3个线程,每个线程持有当前task出的对象的锁,这必然不能产生同步的效果。换句话说,如果要对value同步,那么这些线程所持有的对象锁应当是共享且唯一的!这里就验证了上面讲的锁的特点了。那么正确的代码应该是:

Tasker tasker = new Tasker();
for (int i = 0; i < 100; i++) {
blockingQueue.add(tasker);
}

  或者给这个任务提供单例模式:

for (int i = 0; i < 100; i++) {
Tasker tasker = Tasker.getInstance();
blockingQueue.add(tasker);
}

 这样对象是唯一的,那么public synchronized void change()的锁也是唯一的了。

2、难道我们要给每一个任务都要写一个单例模式麽,我们每次改变对象的属性岂不是把之前之前的对象属性给改变了?所以我们使用synchronized还有一种方案:在执行任务的代码块放一个静态对象,然后用synchronized加锁。我们知道静态对象不跟着对象的改变而改变而是一直在内存中存在,所以:

private static Object object = new Object();

public void change() {
synchronized (object) {
value++;
System.out.println(value);
}
}

  这样就能保证锁对象的唯一性了,无论我们用new Tasker();Tasker.getInstance();都不受影响。
我们知道,对于同步静态方法,对象锁就是该静态放发所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例,就是唯一的Tasker.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个。所以我们上面的代码也可以改为:

public void change() {
synchronized (Tasker.class) {
value++;
System.out.println(value);
}
}

 根据上面的经验,我们的Tasker.getInstance();方法的具体应该就是:

private static Tasker tasker;

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

 3、 synchronized的代码块遇到异常后自动释放锁。我们上面提到synchronized遇到异常后自动释放锁,所以如果我们不能保证代码块是否会发生异常的情况下(当时是资源不紧张时)是可以使用synchronized,我们模拟一下:

public void change() {
synchronized (object) {
value++;
System.out.println(value);
}
if (value == 50)
throw new RuntimeException("");
}

 上面代码应该很清楚了,但value增加到50的时候,这个线程会发生异常,根据我们的推断,执行50的这个线程发生崩溃,但是其他两个线程应该还是正常执行的,我们来测试一下: 

我们看到之前是三个数字一起打印,后来变成两个线程一起打印了,很显然一个线程崩溃了之后还有两个线程在执行,说明object这个锁被释放了。

Lock

由于我们提到synchronized无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。所以JSR 166小组花时间为我们开发了java.util.concurrent.lock框架,当Lock锁定的方法或者代码块发生异常的时候,它不会自动释放锁;它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
Lock的实现类有哪些?我们在代码中选中Lock,按下Ctrl + T,显示出如下:

我们看到有一个读出锁ReadLock、一个写入锁WriteLock、一个重入锁ReenTrantLock,我们这里主要说在多线程开发中用的最多的重入锁ReenTrantLock
废话不多说了,其实代码上来讲和上面原来一样的,我们看看怎么实现:

/** Lock模块事例 **/
private static Lock lock = new ReentrantLock(); public void change() {
lock.lock(); {// 代码块
value++;
System.out.println(value);
} lock.unlock();
}

  我们看到使用也蛮简单,而且扩展性更好。但是呢我们上面提到如果我们在这里发生了异常呢:

{// 代码块
value++;
System.out.println(value);
}

  经测试,果然被锁起来,所有线程都拿不到执行权限了,所以呢这里也给出一解决方案,哈哈也许你早就想到了,就是咱的try {} finally {}

public void change() {
lock.lock();
try {
value++;
System.out.println(value);
if (value == 50)
throw new RuntimeException("");
} finally {
lock.unlock();
}
}

  

我们看到我们在上面的代码中加了一个和synchronized一样的异常,我们再次测试后发现,完全没有发生异常啊是不是哈哈哈,这就是ReentrantLock,这位看的朋友你会用了吗?


NoHttp 源码及Demo托管在Github欢迎大家Starhttps://github.com/yanzhenjie/NoHttp

对新手很有指导意义。。。。。。

线程安全、数据同步之 synchronized 与 Lock的更多相关文章

  1. Java多线程之线程同步【synchronized、Lock、volatitle】

    线程同步 线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多. ...

  2. java——多线程的实现方式、三种办法解决线程赛跑、多线程数据同步(synchronized)、死锁

    多线程的实现方式:demo1.demo2 demo1:继承Thread类,重写run()方法 package thread_test; public class ThreadDemo1 extends ...

  3. 【Java线程】锁机制:synchronized、Lock、Condition

    http://www.infoq.com/cn/articles/java-memory-model-5  深入理解Java内存模型(五)——锁 http://www.ibm.com/develope ...

  4. 【Java线程】锁机制:synchronized、Lock、Condition(转)

    原文地址 1.synchronized 把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility). 1.1 原子性 ...

  5. 同步锁Synchronized与Lock的区别?

    synchronized与Lock两者区别: 1:Lock是一个接口,而Synchronized是关键字. 2:Synchronized会自动释放锁,而Lock必须手动释放锁. 3:Lock可以让等待 ...

  6. 线程池,多线程,线程异步,同步和死锁,Lock接口

    线程池 线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源. 除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源.线程 ...

  7. python 线程之 数据同步 Queue

    Queue:将数据从一个线程发往另外一个线程比较通用的方式是使用queue模块的Queue类 1, 首先创建一个Queue模块的对象,创建Queue对象可以传递maxsize也可以不传递 2. 使用对 ...

  8. java线程基础巩固---数据同步引入并结合jconsole,jstack以及汇编指令认识synchronized关键字

    对于多线程编程而言其实老生成谈的就是数据同步问题,接下来就会开始接触这块的东东,比较麻烦,但是也是非常重要,所以按部就班的一点点去专研它,下面开始. 数据同步引入: 这里用之前写过的银行叫号的功能做为 ...

  9. synchronized和lock以及synchronized和volatile的区别

    synchronized和volatile区别synochronizd和volatile关键字区别: 1. volatile关键字解决的是变量在多个线程之间的可见性:而sychronized关键字解决 ...

随机推荐

  1. 移植SlidingMenu Android library,和安装example出现的问题解决

    很多项目都用到类似左侧滑动菜单的效果,比如facebook,evernote,VLC for android等等,这很酷 源代码可以从GitHub的https://github.com/jfeinst ...

  2. android调试输出

    测试时不想直接debug总要调试输出一些字符串信息,那以下方法可选: 1.用Log.i(TAG, "onCreate");日志输出. 先要引用 import android.uti ...

  3. Android:StatFs类 获取系统/sdcard存储空间信息

    在存储文件时,为了保证有充足的存储空间大小,通常需要知道系统内部或者sdcard的剩余存储空间大小,这里就需要用到StatFs类. 1. 判断 SDCard 是否存在,并且是否具有可读写权限 /** ...

  4. laravel吐槽系列之一

    最近项目中经常使用到了laravel框架,对于这个框架之前只是弱弱地接触了一点,没有深入接触,这下有时间好好研究它了(主要是不得不研究了).说实话,laravel让我打开眼界了,之前对框架的使用一直停 ...

  5. CSS 居中方法集锦

    记录收集纯CSS层面实现的水平.垂直居中方法可用于块级.行内快.内联元素以及文字图片等. 水平或垂直居中 1.1 text-align 1.2 margin 1.3 line-height 1.4 p ...

  6. 【原创】Kakfa api包源代码分析

    既然包名是api,说明里面肯定都是一些常用的Kafka API了. 一.ApiUtils.scala 顾名思义,就是一些常见的api辅助类,定义的方法包括: 1. readShortString: 从 ...

  7. LeetCode4:Add Two Numbers

    题目: You are given two linked lists representing two non-negative numbers. The digits are stored in r ...

  8. 【Java每日一题】20161102

    package Nov2016; public class Ques1102 implements Parent{ @Override public void method() { } } inter ...

  9. [范例] Firemonkey 弹簧动画

    弹簧动画效果: 不用写任何代码,只需设定下面动画属性: 参考动画曲线: http://monkeystyler.com/guide/Interpolation-and-AnimationType-Il ...

  10. webservice入门(1)

    前段时间学习了webservice的用法,虽然只是一些简单的用法,但是如果久了还是会忘记的,所以将学到了记录下来. 一:schema和http协议. 1.schema约束: schema规范中: . ...