彻底搞懂线程这一块,看这一篇就够了


前言

本系列详细讲解并发的知识,从基础到底层,让大家彻底搞懂线程和锁的原理,当然里面会涉及到一些内存结构的知识,所以如果为了更好地阅读效果,也可以先去看以下这两篇:

豁然开朗篇:安卓开发中关于内存那些事
豁然开朗篇:安卓开发中关于线程那些事(上篇)

当然如果已经对线程本身有了解的同学也可以先看该篇,因为本篇是对(上篇)的补充,锁原理的扩展知识对于以后要自定义锁会有很大帮助,以及线程池、klass等等该篇都会讲到。希望大家读完本文之后对线程有一个更深的理解,彻底能把线程、锁以及线程池等相关知识彻底搞懂。


提示:以下是本篇文章正文内容

一、锁原理

1.synchronized的缺点

经常使用synchronized应该都知道它是有一些不好的地方的:

(1)无法判断锁状态,不知当前线程是否锁住了,从java层上来讲是无法知道线程是否锁住了该对象。
(2)不可中断,如果线程一直获取不到锁,那么就会一直等待,直到占用锁的线程把锁释放。
(3)synchronized不是公平锁,新来的线程与等待的线程都有同样机会获得锁。其实大多数情况下,实际开发中都是要公平锁的。
(4)因为是关键字的方式实现获取锁和释放锁,导致中间过程不可控
(5)当升级为重量级锁后会造成用户空间切换到内核空间,资源会耗费较大。

2.并发三大方法

(1)synchronized关键字,原理是monitor机制(上篇已经讲过了)
(2)Atomic包,AtomicInteger,AtomicBoolean,AtomicLong等,原理是应用Volatile关键字和CAS理论实现并发
(3)Locks包,原理是应用CAS理论实现并发

3.Atomic

3.1单例模式用Volatile修饰

正常我们写一个单例模式应该会这样写:

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

这就是DCL懒汉式的写法,为什么要用两个if判断instance 是否为空呢?用一个if加上synchronized不行吗?比如写成这样:

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

这样写的话,线程1拿到锁对象然后执行instance = new SingleInstance()完后继续往下走,而此时要释放锁,线程2这时已经执行到拿到synchronized(SingleInstance.class)这一行了,在等待锁对象的释放,然后线程1此时执行完instance = new SingleInstance()就释放锁,最后走return instance,紧接着线程2拿到锁后又执行一次instance = new SingleInstance(),这就等于又创建多一个SingleInstance()对象,不符合单例模式了。

因此要在线程2拿到锁后再加多一个判断,判断instance是否为空,为空的才去执行instance = new SingleInstance(),因此此时线程1已经返回了instance对象,所以线程2此时拿到锁后判断instance不为空,就不再执行,释放锁,也就不用再抢占锁资源了。这就是DCL懒汉式单例模式。不过它其实也有有可能会造成运行时异常的。

接下来先把该代码的getInstance()方法编译成dex文件指令,然后输出到一个txt文件里(可以看回豁然开朗篇:安卓开发中关于线程那些事(上篇),里面有dx工具的使用命令)看看getInstance()方法的dex指令是怎样的:

已经将一些无关紧要的指令省略掉了,如图所示就是getInstance()方法在dex文件里的指令,源码中ourInstance = new UserManger()对应的指令是000b行、000d行和0010三行指令,也就是说平时我们所写的构造对象代码,其实虚拟机执行的就是这三行指令:new-instance,invoke-direct,sput-object,只有经过这三个指令执行后,一个对象才能构造成功,然后被使用。

new-instance指令的作用是构造对象头和对齐数据(忘记对象在堆区里的结构的可以去看回豁然开朗篇:安卓开发中关于内存那些事):

然后invoke-direct则把类信息(也就是源码中定义的那些属性等)填充到实例数据区:

最后sput-object执行完后,就完成了变量引用指向该对象:

而这就有个问题,就是虚拟机在加载指令的时候,它是并发(多线程)执行的,这就会发生指令重排序,也就是在加载这三条指令时,不一定是按照顺序1-2-3来执行,有可能是1-3-2。当然不是所有代码都会发生重排序,只要当重排序后不会对原先代码发生的结果导致不一致的话,那就可以重排序:

1)a=1;b=a; 这是先写了一个变量,然后读取这个变量的值
2)a=1;b=1; 这是先写了一个变量,然后又写了一个变量
3)a=b;b=1; 这是读取了一个变量,然后写这个变量

以上这三种代码如果发生重排序后,结果肯定就跟原来不一样,所以它们不会发生指令重排序。而其他情况,比如我们的构造对象涉及的这三条指令就可以发生指令重排序了。

那么如果当new-instance,invoke-direct,sput-object这三条指令执行的顺序变成1-3-2执行,就代表当线程1执行完new-instance和sput-object指令后,该对象是被构造出来,而且并有变量引用指向,但此时是没有实例数据的,所以当线程2执行代码到 if(instance == null) 这一行的时候,认为该对象不为null,从而直接返回,那么它再使用该对象时它的实例数据都没有,肯定就会空指针异常了(虽然概率很低,但也不能忽视)。

所以为了避免发生这种问题的风险,就可以使用关键字volatile:

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

用关键字volatile去修饰instance变量,这样SingleInstance()对象对于线程1和线程2来说是可见的,他们互相监听该对象,线程2监听到该对象还没被完全构造完,就会等线程1执行完第2步指令invoke-direct后才会去使用该对象。

所以volatile的作用能让对象公开化,这样线程A修改该对象后,会同步更新到另一个线程上。这是怎么做到的,之前在豁然开朗篇:安卓开发中关于线程那些事(上篇)说过,每个线程里有独立的高速缓冲区,一核代表一个线程,每个线程先在自己的缓冲区里把数据给CPU操作,如果别的线程要互相通信操作对方的数据,那只有线程1把自己缓冲区的数据复制到内存里,然后线程1再去内存里对内存这份数据进行读取:

而volatile的原理就是把该对象放到主内存里特意划分的内存屏障空间里,这块空间里的数据都被每个线程进行监听和实时更新:

修改volatile变量时会将修改后的值刷新到主内存中(里面的内存屏障区)去,因此此时其他线程它们自己的缓冲区里的变量值失效,因此当它们这些线程去读取该变量值时候就会重新去读取主内存中(里面的内存屏障区)刚刚更新的值。

但这种可见性也只是限定于原子性操作,就单纯的取值和赋值,像i++这种不是原子性操作的计算,用volatile修饰也没用,这点是要注意的,i++是复合操作,分三步的:
1)读取 i 的值
2)对 i 进行加1
3)将 i 的值写回到内存

中间是多了一步加1,所以volatile无法保证原子性,用代码实验一下:

public class LockRunnable implements Runnable{private volatile static int a = 0;private Object lockObject = new Object();@Overridepublic void run() {for (int i = 0; i < 100000; i++) {add();}}private void add() {a++;}public static void main(String[] args) {LockRunnable lockRunnable1 = new LockRunnable();LockRunnable lockRunnable2 = new LockRunnable();Thread thread1 = new Thread(lockRunnable1);Thread thread2 = new Thread(lockRunnable1);thread1.start();thread2.start();try {thread2.join();thread1.join();} catch (Exception e) {e.printStackTrace();}System.out.println(a);}
}

a变量用volatile去修饰,尽管这样,最终结果并不是我们预期的200000:

那么要让i++能并发起来不会有问题,而且又不会像synchronized那样有变成重量锁的风险,还有另一种方式,就是使用CAS。

4.CAS

CAS,Compare and Swap,比较并替换。CAS有三个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

当更新一个变量的值为新值的时候,只有当变量的预期值A和内存地址V中的实际值相同时,才会将内存地址V对应的值修改为新值B,下面来看使用了CAS这个例子:

线程1要更新值之前,线程2抢先一步将内存地址V中的变量值更新为2,然后线程1此时提交更新前会检查比较它自己的预期值A(1)和内存地址V中此时的实际值(已经被线程2更新为2)(因为CAS会使用volatile修饰变量所以能实时监听变量值),如果相同才会去更新刚刚自己的要更新的B值(2),很明显此时是不相同,所以不会进行修改,也就是修改失败。

当线程1下一次要修改值时,这次没其他线程抢先一步修改并更新,发现V中变量值跟A值是相同的,所以更新成功为2:

这就是CAS机制了,相信不难理解。

当然CAS也是有弊端的,那就是线程1是不知道线程2到底把这个变量值修改了多少次,假设线程2把值1修改成2后又修改成1,再更新,那线程1是不知道到底线程2有没有修改过这个变量值的,这就是ABA问题,可以使用AtomicStampedReference类解决,当修改过值后,因为该变量被AtomicStampedReference修饰,会记录一个时间戳的版本号,每次修改都会产生不同的版本号,从而识别出是否有修改过了。

public class Person {private int age;public static void main(String[] args) {Person person = new Person();person.age = 18;AtomicReference<Person> atomicReference  = new AtomicReference<>();atomicReference.set(person);}}

5.ReentrantLock

它能让我们自己来控制锁,可以说是完美解决了synchronized不可控的缺点了:

public class LockRunnable implements Runnable{private volatile static int a = 0;private Object lockObject = new Object();public static ReentrantLock lock = new ReentrantLock(true);@Overridepublic void run() {for (int i = 0; i < 100000; i++) {lock.lock();try {i++;} catch (Exception e) {e.printStackTrace();}finally {lock.unlock();}}}
}

我们不用synchronized修饰,使用ReentrantLock锁,构造的时候传true参数表示公平锁,然后调用lock.lock()表示锁住以下要同步的代码块,然后lock.unlock()表示释放锁。

ReentrantLock的原理其实是使用LockSupport.park来休眠和LockSupport.unpark取消休眠,然后还使用了CAS来同步多线程竞争锁对象的情况,当然,还有等待队列(双向链表实现)来确定公平性等,如果我们自己来仿照ReentrantLock来自定义锁,可以按照这样的思路:

public class MyReentrantLock {private AtomicBoolean isLock = new AtomicBoolean();public Queue<Thread> waitThreads = new ConcurrentLinkedDeque<>();public void lock() {Thread currentThread = Thread.currentThread();waitThreads.add(currentThread);while (currentThread != waitThreads.peek()|| !isLock.compareAndSet(false, true)) {LockSupport.park(this);//当前线程休眠,处于waiting}waitThreads.remove();}public void unlock() {isLock.set(false);LockSupport.unpark(waitThreads.peek());}
}

首先是定义了AtomicBoolean 类型的变量确保可见性和原子性,然后后面用它来调用compareAndSet()方法,这个方法就是使用了CAS,第一个参数表示旧的预期值A,第二个参数则表示要修改的新值B,这样isLock 变量不仅拥有了Atomic的优点,还使用CAS确保了线程同步,我们外部调用该锁也很方便:

@Overridepublic void run() {for (int i = 0; i < 100000; i++) {myReentrantLock.lock();try {i++;} catch (Exception e) {e.printStackTrace();}finally {myReentrantLock.unlock();}}}

跟ReentrantLock的调用一样。

6.线程池

说到并发怎么能不讲线程池呢,当一个线程start()之后,是在run方法里通过wait()、sleep()等方法来改变状态,而线程池是改不了线程的状态的,线程池的作用是用来管理这些线程何时开启start,还没开启的线程怎么排队,优先级是怎样的,线程池就是做这些工作。

下面来自定义一个线程池,首先定义一个接口,接口方法实现两个方法,加入Runnable对象和删除:

public interface IThreadPool {void add(Runnable runnable);void remove();
}

然后定义这个线程池:

public class ThreadPoolImpl implements IThreadPool{private static int WORK_NUM = 2;//线程池内的工作线程数为2private static PriorityBlockingQueue<Runnable> blockingQueue = new PriorityBlockingQueue<>();//线程队列@Overridepublic void add(Runnable runnable) {}@Overridepublic void remove() {}
}

首先实现IThreadPool接口,然后定义了一个固定线程(也就是该线程池里一直工作运行的线程)数量为2,还定义了一个权限队列(具有优先级PriorityBlockingQueue)blockingQueue 。接下来定义这个工作线程:

class ThreadPoolWorkerThread extends Thread{private boolean isRunning = true;@Overridepublic void run() {while (isRunning) {synchronized (ThreadPoolWorkerThread.class) {try {Runnable runnable = blockingQueue.take();//取出线程后执行它的run逻辑runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}}}}

线程池里的工作线程是为了从队列里取出线程然后进行它们各自的run方法,而这个过程是需要同步,所以使用了synchronized锁(根据之前分析的锁机制可以根据实际情况来优化)。那什么时候来开启这些工作线程,当然是外部在构造线程池的时候开启这些工作线程,所以ThreadPoolImpl 的构造方法:

public ThreadPoolImpl() {for (int i = 0; i < WORK_NUM; i++) {ThreadPoolWorkerThread thread = new ThreadPoolWorkerThread();thread.start();}
}

当外部Activity构造ThreadPoolImpl对象的时候就构造出线程池内部两个工作线程然后让它们启动起来,别忘了线程池的添加线程方法:

 @Overridepublic void add(Runnable runnable) {//这里可以根据实际要求设置一个添加数据的限制blockingQueue.add(runnable);}

正如注释所说那样,如果实际需要线程池的队列数量有限制,可以在此之前判断一下然后才进行添加。基本上整个线程池的创建思路就是这些,另外一些优化和补充的功能和细节则要根据实际开发需求来定,最后外部Activity要使用该线程池则如下:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);IThreadPool threadPool =  new ThreadPoolImpl();for (int i = 0; i < 10; i++) {threadPool.add(new Task("线程" + i));}}static class Task implements Runnable{private String name;public Task(String name) {this.name = name;}@Overridepublic void run() {}}
}

代码不难,相信大家都能理解,在构造线程池的时候,就已经让它里面的工作线程启动,然后不断轮询队列里的线程,而队列里的线程则通过外部调用者来添加进去,删除线程的方法也是外部去操纵线程池的remove方法。另外有一点要说明一下,虽然在工作线程里不断轮询去从队列里取线程的时候会有阻塞(加了锁),但因为工作线程本身也是子线程,所以不会阻塞到主线程(UI线程)了。

整个关于线程讲解系列就总结到这里,更多有趣的文章可以关注本人公众号:Pingred
欢迎大家来学习与讨论

豁然开朗篇:安卓开发中关于线程那些事(下篇)相关推荐

  1. 豁然开朗篇:安卓开发中关于虚拟机那些事

    彻底搞懂虚拟机这一块,看这一篇就够了 前言 作为豁然开朗篇的最终篇,本文要讲解的是虚拟机这块,因为在之前讲解内存与线程的时候,一直都会牵涉到虚拟机和指令集这块,所以,为了让大家再豁然开朗多一次,本文会 ...

  2. 安卓开发中遇到的奇奇怪怪的问题(三)

    本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. 距离上一篇 安卓开发中遇到的奇奇怪怪的问题(二)又过了半年了,转眼也到年底了,是时候拿出点干货了.这篇算是本年度个人印象最深的几 ...

  3. 安卓开发中的USB转串口通讯

    安卓开发中的USB转串口通讯 本文使用GitHub上开源的"hoho.android.usbserial"USB串口库.该库基于"Android USB Host API ...

  4. 安卓开发中非常炫的效果集合

    安卓开发中非常炫的效果集合 这几天开发的时候,想做一些好看而且酷炫的特效,于是又开始从网上收集各种特效资源.下面给大家一些我喜欢的把,附代码,喜欢的看源代码,然后加到自己项目去把!! 一个开源项目网站 ...

  5. 安卓开发中,release安装包安装后,打开app后再按home键,再次点击程序图标app再次重新启动的解决办法

    安卓开发中,release安装包安装后,打开app后再按home键,再次点击程序图标app再次重新启动的解决办法 在开发中我们一般都是直接AS上的安装(Run)按钮,直接安装到真机或模拟器上进行测试, ...

  6. Android安卓开发中图片缩放讲解

    安卓开发中应用到图片的处理时候,我们通常会怎么缩放操作呢,来看下面的两种做法: 方法1:按固定比例进行缩放 在开发一些软件,如新闻客户端,很多时候要显示图片的缩略图,由于手机屏幕限制,一般情况下,我们 ...

  7. Android Studio安卓开发中使用json来作为网络数据传输格式

    如果你是在安卓开发中并且使用android studio,要使用json来作为数据传输的格式,那么下面是我的一些经验. 一开始我在android studio中导入那6个包,那6个包找了非常久,因为放 ...

  8. Java开发中Netty线程模型原理解析!

    Java开发中Netty线程模型原理解析,Netty是Java领域有名的开源网络库具有高性能和高扩展性的特点,很多流行的框架都是基于它来构建.Netty 线程模型不是一成不变的,取决于用户的启动参数配 ...

  9. iOS开发UI篇—IOS开发中Xcode的一些使用技巧

    iOS开发UI篇-IOS开发中Xcode的一些使用技巧 一.快捷键的使用 经常用到的快捷键如下: 新建 shift + cmd + n     新建项目 cmd + n             新建文 ...

最新文章

  1. 通用Login功能自动化测试
  2. web第6次作业position
  3. 绿盟科技鸿蒙系统,华为 X 绿盟科技,打造“云原生安全新生态”
  4. 利用pyinstaller打包python3程序
  5. oracle数据库查询空间大小,Oracle查看数据库空间使用情况
  6. nekohtml 用法
  7. 深雁论坛GhostXP专业装机版 V3.0
  8. layui整合Echart
  9. 一个中科大差生的8年程序员工作总结
  10. JAVA中(PO,VO,TO,BO,DAO,POJO)分别是指什么
  11. Flash&Flex大全
  12. 盘古开源解析:数据防泄漏对于数据安全的重要性
  13. 云计算应用(上) -- 云计算应用概述
  14. Java中sort实现降序排序
  15. QGIS输出地图图片操作指引
  16. 总结陈丹琦博士论文(二):NEURAL READING COMPREHENSION AND BEYOND
  17. python随机图片api_【python】7个随机二次元图片api接口汇总(附网页调用示例)...
  18. Java、JSP物业管理信息系统毕业设计
  19. Java 10W字面经
  20. 石城天气预报软件测试,石城天气预报15天

热门文章

  1. 公司企业全球电商平台上云最佳实践解决方案
  2. HQL17 计算男生人数以及平均GPA
  3. Centos设置自带中文输入法
  4. linux nss升级,Linux系统升级
  5. 天源财富:XING Mobility与嘉实多合作 为EV提供先进的浸没式冷却电池系统
  6. 补充:爬虫技术成就了这些商业公司的
  7. 因未按要求完成整改,蛋壳公寓等23款APP遭工信部下架
  8. iOS开发 - 清除缓存
  9. (Java实现) 最大团问题 部落卫队
  10. 多商户商城系统功能拆解34讲-平台端营销-足迹气泡