Java 高并发系列1-开篇

我们都知道在Android开发中, 一个Android程序启动之后会有一个主线程,也就是UI线程, 而网络加载数据, 本地文件长时间读写,图片压缩,等等,很多耗时操作会阻塞UI线程,到时ANR的产生,在Android 3.0 之后便不能在UI线程使用。 由此可见多线程的使用在Android开发中占地位是多么重要。

这个系列 我打算通过一个个的例子来说明多线程的基本概念,多线程的使用, 锁的使用, 并发容器, 线程池的使用,等等。

基本概念

  • 1.线程概念
  • 2.启动一个线程
  • 3.基本的线程同步

1. 线程概念

提到线程时,不得不提到进程。这里有两个问题,

  • 第一 什么是进程, 什么是线程?

我们首先了解一下什么是进程。进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可以被看作程序的实体,同样,它也是线程的容器。例如Mac 监控活动窗口中一个个的任务,这边是操作系统的运行单元,进程。 在Android系统中同样是这样,通过Android Device Monitor 我们可以看到一个进程列表,里面就是Android手机中运行的进程。进程就是程序的实体,是受操作系统管理的基本运行单元。这么说吧, 我们打开的一个又一个App 便是一个又一个应用进程,当然如果某个App做了多进程,该应用便有了两个进程。

先不说线程是什么, 这么说吧,我们使用的QQ浏览器,打开一个网页, 这个网页打开过程,有的加载文本,有的加载图片。这些子任务就是线程,是操作系统调度的最小单
元,也叫作轻量级进程。在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变
量等属性,并且能够访问共享的内存变量,这也就是我们这个系列要研究的对象。

  • 第二 为什么要用多线程?
  1. 充分利用系统资源,提升程序执行效率。就说现在的计算机,动不动就是八核CPU、 16核、32核的 ,如果使用单线程,多浪费资源,这么想可知,只要任务分配的合理,调度合理。 多个人干活肯定比单线程效率高。
  2. 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高
  • 第三 线程的状态

来一张价值连城的线程状态图

简单说一下,Java线程在运行的声明周期中可能会处于6种不同的状态

  1. New 新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作要做。
  2. Runnable 可运行状态。一旦调用start方法,线程就处于Runnable状态。一个可运行的线程可能正在
    运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
  3. Blocked 阻塞状态。表示线程被锁阻塞,它暂时不活动。
  4. Waiting 等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器
    重新激活它。
  5. Timed waiting 超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的
  6. Terminated 终止状态。表示当前线程已经执行完毕。导致线程终止有两种情况:第一种就是run方
    法执行完毕正常退出;第二种就是因为一个没有捕获的异常而终止了run方法,导致线程进入终止状态。

2. 启动一个线程

  • 一种就是 实现Runnable接口
    放入Thread 构造函数中, start 便可启动。 执行的事务便在run方法中执行。
  • 另一种便是实现Callable 接口
    使用方法和Runnable实现的方式一样,
    两者的区别就是,后者有返回值,前者没有返回值。

3. 基本的线程同步

对某个对象加锁。

public class T {private int count = 10;private Object o = new Object();public void m() {synchronized(o) { /// 线程需要执行下边的代码块,就先需要获取o的锁count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}}}

如果要执行下边的代码,需要先去申请o这个对象, 堆内存中的这个对象,并不是指o 这个引用, 当然不是指,当o这个引用指向其他对象的时候,锁会变换。
当然如果还没有释放o这个锁,其他线程是没法获取到锁,没有执行权限,所以这也是互斥锁。

如果单单是为了作为一个锁而声明一个对象,就太浪费了。
第二种写法

public class T {private int count = 10;public void m() {synchronized(this) { // 任何线程执行下边的代码块,需要先获取this 对象count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}}}

有人说 synchronized 是锁定的代码块,其实锁定的是对象。

第三种情况

public class T {private int count = 10;public synchronized void m() {  这种加锁写法等同于第二种 synchronized(this) count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}}

当synchronized 关键字放在了static 静态方法上时候,

public class T {private static int count = 10;public synchronized static void m() { // 这种加锁方法等同于 synchronized(T.class) count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void mm() {synchronized(T.class) { // 当然这里不能使用synchronized(T.this)这种写法了, 原因很简单,因为这是静态方法,静态方法调用不需要对象的调用,更不需要使用T.this 这种写法了。count --;}}}

再看一下这个程序的输出

public class T implements Runnable {private int count = 10;public /*synchronized*/ void run() { count--;System.out.println(Thread.currentThread().getName() + " count = " + count);}public static void main(String[] args) {T t = new T();for(int i=0; i<5; i++) {new Thread(t, "THREAD" + i).start();}}}

这个程序的执行 结果可能是 9,8,7,6,5 当然执行一两次是没有什么问题的, 如果执行的次数很多,问题就会出现。 结果可能是 7,6,7,7,7

这种奇怪的问题稍微解释一下,就是这种情况, 五个线程同时执行没有加锁的一个代码块,执行步骤就是先减,后打印, 当第一个减完,还没来得及打印时候,第二个线程又减了一次,第二个线程还没来得及打印的时候,第三个线程又减了一次, 这时候第一个线程拿到cpu执行时间片,打出的结果就是7, 后续结果就是这么没有规律的打印了出来。

很显然并没有达到我们的预期,这个问题的解决方案就是加锁,synchronized关键字使得整个代码执行块具有了原子性。 其他线程只有等待减一并且打印完,释放了锁之后,后续线程才可以继续拿到锁,执行后续操作。

原子性可以理解为不可分割的代码执行块。

多线程与数据脏读

模拟银行代码的逻辑,银行账户。public class Account {String name;double balance;//  设置银行账户的姓名, 存款 public synchronized void set(String name, double balance) {this.name = name;/*try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}*/this.balance = balance;}public /*synchronized*/ double getBalance(String name) {return this.balance;}public static void main(String[] args) {Account a = new Account();new Thread(()->a.set("zhangsan", 100.0)).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(a.getBalance("zhangsan"));try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(a.getBalance("zhangsan"));}}

如果在写的过程中进行读操作,这时候就会出现数据的脏读。 当然这时候需要看自己的业务逻辑,
如果允许脏读,对数据的实时性没有要求则可以不做处理,仅对写过程进行加锁。 如果不允许脏读,则对读方法也进行加锁。

public class T {synchronized void m1() {System.out.println("m1 start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}m2();}synchronized void m2() {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m2");}
}

一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。 会在原来内存中堆内存的锁上+1
也就是说synchronized获得的锁是可重入的。


public class T3 {synchronized void m() {System.out.println("m start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m end");}public static void main(String[] args) {new TT().m();}
}class TT extends T3 {@Overridesynchronized void m() {System.out.println("child m start");super.m();System.out.println("child m end");}
}

重入锁的第二种情形
这个例子和上个例子是一样的
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
也就是说synchronized获得的锁是可重入的
这里是继承中有可能发生的情形,子类调用父类的同步方法

如果线程执行在有锁的代码块中抛出异常该如何?

看一条程序

public class T {int count = 0;synchronized void m() {System.out.println(Thread.currentThread().getName() + " start");while(true) {count ++;System.out.println(Thread.currentThread().getName() + " count = " + count);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}if(count == 5) {int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续}}}public static void main(String[] args) {T t = new T();Runnable r = new Runnable() {@Overridepublic void run() {t.m();}};new Thread(r, "t1").start();try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}new Thread(r, "t2").start();}}

在第五秒的时候 t1出现数学算术异常,抛出导致所持有的锁被释放, 同时线程t2获取锁继续执行。

注意: m方法内 如果在数据处理逻辑中执行了一半,抛出异常,锁被释放,而又没有对异常之后的数据进行回滚。 同时其他线程拎起这个原来处理过了一半的数据进行操作的话。 结果必定是不准确的,导致的后果也是灾难性的。

小节结论:

线程执行中抛出异常锁会被释放。 需要添加相关处理逻辑, try-cache

volatile 简单解释意思就是 瞬时的,透明的,临时的, 多个线程可见的。


public class T {/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别void m() {System.out.println("m start");while(running) {/*try {TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}*/}System.out.println("m end!");}public static void main(String[] args) {T t = new T();new Thread(t::m, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}t.running = false;}}

看一下这条程序的运行结果, 一共分两个线程, 一个是t1, 一个是主线程, t1 线程执行while 循环,主线程 修改running 变量。 尝试让t1跳出while 死循环。 结果却并没有让t1跳出死循环。
如果要解释这个现象, 我们需要简单的了解一下java 的内存模型, 简称JMM (java memory model)。

CPU执行区


线程T1 running 线程 T2 running … Tn running


主内存区


running = true ( volatile modify --> notify all thread update )


新的线程执行时,将running 从主内存中拷贝一份到CPU执行区的一个线程缓存区内, 由于CPU一直在执行, 并没有闲暇时间与主内存中的running 进行同步。 所以线程T1便一直处于死循环中。

另一种情况, 当线程while 的死循环中的睡眠代码块 解开之后, CPU便有了与主内存中running 进行了同步, 此时当线程醒来之后 便可以结束了。 ( 具体这是属于什么机制 我还不太懂, 需要进一步学习 |汗)

还有一种情况便是,将running 前加上volatile 关键字,让running 的每一次修改便通知执行线程, 从主内存中读取新的内容,更新缓冲区。

那么volatile 和synchronized 的区别是什么呢?


public class T {volatile int count = 0; /* synchronized */ void m() {for(int i=0; i<10000; i++) count++;}public static void main(String[] args) {T t = new T();List<Thread> threads = new ArrayList<Thread>();for(int i=0; i<10; i++) {threads.add(new Thread(t::m, "thread-"+i));}threads.forEach((o)->o.start());threads.forEach((o)->{try {o.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.count);}
}

看这一条程序, 虽然count 变量前加上了volatile 关键字,表示该字段的可见性。 但是结果可能是56739, 或者其他 ,但是肯定不会是十万。
由于使用volatile,将会强制所有线程都去堆内存中读取running的值

分析过程:

十条线程同时启动, 同时对主内存中的count 进行了修改操作, 同时从栈中拷贝了一份到自己线程的CPU缓存区内,进行+1 ,完了以后写回到主内存中 101 , 第二个线程也会把加完的结果101 覆盖。 第三条线程可能拿到的是101 ,加完的结果是102 ,第四条可能还是覆盖102, 至此问题便形成。

当然如果把synchronized 注释放开, 结果便是正确的。

当然如果使用系统提供的AtomicXXX 系列类提供的操作方法 也是可以的,当然这也是最优解。


public class T {/*volatile*/ //int count = 0;AtomicInteger count = new AtomicInteger(0); /*synchronized*/ void m() { for (int i = 0; i < 10000; i++)//if count.get() < 1000   当然如果这里添加了if判断之后, 这里就不具备了原子性, 很简单,因为判断过程中会有多个线程同时读取到一样的数值,从而造成问题。// AtomicXXX 这个东西的出现就是为了 代替 count++ 操作。  因为这个操作是原子性的,不可再分的。效率比synchronized高。 /具体实现方法应该是使用了最底层的方式。 不太懂希望有懂出来说说。 count.incrementAndGet(); //count++}public static void main(String[] args) {T t = new T();List<Thread> threads = new ArrayList<Thread>();for (int i = 0; i < 10; i++) {threads.add(new Thread(t::m, "thread-" + i));}threads.forEach((o) -> o.start());threads.forEach((o) -> {try {o.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.count);}}

小节结论:

volatile 只保证了可见性,不保证原子性 效率高 。

synchronized 既保证了可见性,又保证了原子性。 效率低

如果程序可以 请使用 AtomicXXX类进行原子操作代替synchronized。

可以阅读这篇文章进行更深入的理解volatile

http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

再看一条程序


public class T {int count = 0;synchronized void m1() {//do sth need not synctry {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}//业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁count ++;//do sth need not synctry {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}void m2() {//do sth need not synctry {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}//业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁//采用细粒度的锁,可以使线程争用时间变短,从而提高效率synchronized(this) {count ++;}//do sth need not synctry {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}
}

小节结论:

给只需要上锁的部分进行上锁,以减少线程争用时间,从而提高效率。

再看一条程序

public class T {Object o = new Object();void m() {synchronized(o) {while(true) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}}}public static void main(String[] args) {T t = new T();//启动第一个线程new Thread(t::m, "t1").start();try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//创建第二个线程Thread t2 = new Thread(t::m, "t2");t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会t2.start();}
}

锁定某对象o,如果o的属性发生改变,不影响锁的使用 但是如果o变成另外一个对象,则锁定的对象发生改变

小节小结:

锁定某对象o,对象o是在堆上面的, 并不是栈中对象o的引用。

应该避免将锁定对象的引用变成另外的对象

还应该避免使用字符串常量来作为锁对象,如下 s1 s2 都是字符串变量, m1 m2 锁定的却是同一个对象

public class T {String s1 = "Hello";String s2 = "Hello";void m1() {synchronized(s1) {}}void m2() {synchronized(s2) {}}}

好了, 啰里啰嗦,说了一大通,看的云里雾里。 其实我觉得如果能把代码拿出来 敲一下,跑一跑,应该就会明白使用多线程的妙处。 东西比较多,如果有什么不对的,请批评指正。 这篇就先说到这里,下篇我们再见。

Java 高并发系列1-开篇相关推荐

  1. java高并发系列 - 第1天:必须知道的几个概念

    java高并发系列-第1天:必须知道的几个概念 同步(Synchronous)和异步(Asynchronous) 同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后, ...

  2. [Java高并发系列(5)][详细]Java中线程池(1)--基本概念介绍

    1 Java中线程池概述 1.1 什么是线程池? 在一个应用当中, 我们往往需要多次使用线程, 这意味着我们需要多次创建和销毁线程.那么为什么不提供一个机制或概念来管理这些线程呢? 该创建的时候创建, ...

  3. Java高并发系列5-线程池

    Java高并发系列5-线程池 接上一篇Java并发系列4-并发容器我们继续 在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销.如果每次执行一个任务都需要开个新线程去执行, ...

  4. java高并发系列 - 第6天:线程的基本操作,必备技能

    新建线程 新建线程很简单.只需要使用new关键字创建一个线程对象,然后调用它的start()启动线程即可. Thread thread1 = new Thread1(); t1.start(); 那么 ...

  5. java高并发系列 - 第3天:有关并行的两个重要定律

    java高并发系列第3篇文章,一个月,咱们一起啃下java高并发,欢迎留言打卡,一起坚持一个月,拿下java高并发. 有关为什么要使用并行程序的问题前面已经进行了简单的探讨.总的来说,最重要的应该是处 ...

  6. Java高并发系列---第1天(概念)

    同步(Synchronous)和异步(Asynchronous) 同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为. 异步方法调用更像一个消息传 ...

  7. Java高并发系列 — AQS

    只懂volatile和CAS是不是可以无视concurrent包了呢,发现一个好链接,继续死磕,第一日: 首先,我承认很多时候要去看源码才能更好搞懂一些事,但如果站在巨人肩膀上呢?有了大概思想源码看还 ...

  8. shell 获取命令执行结果_java高并发系列 第31天:获取线程执行结果,这6种方法你都知道?...

    这是java高并发系列第31篇. 环境:jdk1.8. java高并发系列已经学了不少东西了,本篇文章,我们用前面学的知识来实现一个需求: 在一个线程中需要获取其他线程的执行结果,能想到几种方式?各有 ...

  9. java高并发与多线程汇总(一):基础知识(上)

    java高并发与多线程汇总 往期文章推荐:   java高并发与多线程汇总(一):基础知识(上)   java常见面试考点(四十二):序列化与反序列化   java常见面试考点(四十三):泛型   j ...

最新文章

  1. 好程序员web前端技术分享媒体查询
  2. 提升平面设计思维能力的实用技巧
  3. Python标准库:itertools迭代器函数
  4. MapReduce不同进度的Reduce都在干什么?
  5. 获取32R的图像的直方图的一个算法
  6. redhat linux7.0的安装
  7. 浏览器内核与web标准
  8. 【华为云技术分享】关于Linux下Nginx的安装及配置
  9. VS2013 生成sqlite3动态连接库
  10. 网页鼠标点击特效案例收集
  11. ArcGIS的 高斯-克吕格 投影坐标系
  12. [2019IEEE Transactions on Cybernetics ] Asymptotic Soft Filter Pruning for Deep Convolutional Neural
  13. 新机购入 戴尔成就5000
  14. 阿里副总裁玄难:藏经阁计划研发大规模知识构建技术首次披露
  15. [C++杂谈]:MFC中使用excel2007读写excel表格
  16. ORACLE读取XML
  17. Lesson 12 Goodbye and good luck 再见,一路顺风
  18. Android系统完整的启动流程
  19. 勃林格殷格翰与Lifebit合作识别全球传染病暴发;百济神州和Shoreline Biosciences达成合作 | 医药健闻...
  20. office2010卸载记录

热门文章

  1. 【SAAS】同城+速送+跑腿+约车+快狗+家政+车检
  2. 数学向量 java,数学向量和旋转(Topdown java game dev – physics problem)
  3. OpenCV程序效率优化方法1
  4. 想进入互联网领域发展,是否该从电子信息工程专业考研到计算机
  5. 滴滴快车奖励政策,高峰奖励,翻倍奖励,按成交率,指派单数分级(3月20日)...
  6. r语言svr模型_使用R语言建立一个决策树回归模型
  7. 北京最新建筑八大员(电气)考试题库及答案
  8. 传智健康day01 项目概述和环境搭建
  9. 一文看懂,python抓取m3u8里ts加密视频及合成、多线程、写入的问题
  10. nvidia命令不可用linux,Linux服务器重启后nvidia-smi无法使用的解决方法