本篇文章讲的东西都是Android开源网络框架NoHttp的核心点,固然线程、多线程、数据安全这是Java中就有的,为了运行快咱们用一个Java项目来说解。java
当多个子线程访问同一块数据的时候,因为非同步访问,因此数据可能被同时修改,因此这时候数据不许确不安全。git
假如一个银行账号能够存在多张银行卡,三我的去不一样营业点同时往账号存钱,假设账号原来有100块钱,如今三我的每人存钱100块,咱们最后的结果应该是100 + 3 * 100 = 400块钱。可是因为多我的同时访问数据,可能存在三我的同时存的时候都拿到原帐号有100,而后加上存的100块再去修改数据,可能最后是200、300或者400。这种清状况下就须要锁,当一我的操做的时候把原帐号锁起来,不能让另外一我的操做。github
一、程序入口,启动三个线程在后台循环执行任务,添加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); } }
二、那咱们再来看看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
在子线程中执行任务。多线程
三、咱们在来看看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,若是应用到咱们的银行系统中,那这不是坑爹了麽,因此咱们在多线程开发的过后就用到了锁。ide
多线程开发中不可避免的要用到锁,一段被加锁的代码被一个线程执行以前,线程要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁),若是这个时候同步对象的锁被其余线程拿走了,这个线程就只能等了(线程阻塞在锁池等待队列中)。拿到权限(锁)后,他就开始执行同步代码,线程执行完同步代码后立刻就把锁还给同步对象,其余在锁池中等待的某个线程就能够拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。Java中经常使用的锁有synchronized和Lock两种。
锁的特色:每一个对象只有一把锁,不论是synchronized仍是Lock它们锁定的只能是某个具体对象,也就是说该对象必须是惟一的,才能被锁起,不被多个线程同时使用。性能
同步锁,当它锁定的方法或者代码块发生异常的时候,它会在自动释放锁;可是若是被它锁定的资源被线程竞争激烈的时候,它的表现就没那么好了。
一、咱们来看下下面这段代码:
// 添加100个任务让三个线程执行 for (int i = 0; i < 100; i++) { Tasker tasker = new Tasker(); blockingQueue.add(tasker); }
这段代码是文章最开头的一段,只是把Tasker.getInstance()
改成了new Tasker();
,咱们如今给Tadker
的void 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()
的锁也是惟一的了。
二、难道咱们要给每个任务都要写一个单例模式麽,咱们每次改变对象的属性岂不是把以前以前的对象属性给改变了?因此咱们使用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; } }
三、 synchronized的代码块遇到异常后自动释放锁。咱们上面提到synchronized遇到异常后自动释放锁,因此若是咱们不能保证代码块是否会发生异常的状况下(当时是资源不紧张时)是可使用synchronized,咱们模拟一下:
public void change() { synchronized (object) { value++; System.out.println(value); } if (value == 50) throw new RuntimeException(""); }
上面代码应该很清楚了,但value增长到50的时候,这个线程会发生异常,根据咱们的推断,执行50的这个线程发生崩溃,可是其余两个线程应该仍是正常执行的,咱们来测试一下:
咱们看到以前是三个数字一块儿打印,后来变成两个线程一块儿打印了,很显然一个线程崩溃了以后还有两个线程在执行,说明object
这个锁被释放了。
因为咱们提到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欢迎你们Star:https://github.com/yanzhenjie/NoHttp
对新手颇有指导意义。。。。。。