JAVA基础补完之并发编程

来源:互联网 发布:js获取手机当前位置 编辑:程序博客网 时间:2024/05/17 11:58

前言

本篇后半部分针对java并发编程领域,需要一定JAVA基础

WHY JAVA并发编程?

为什么要学习Java并发编程的技术呢?我根本用不到啊,我会用IDE写好业务代码就ok啦
好吧,估计不少同学会有上面的想法,也许你现在看不到学习这方面的需求,但视野不能仅停留在每天的日常工作中,作为开发人员,一个手艺人要让自己的手艺跟得上时代的发展,尤其在这个时代。

确实确定要学习什么是一个非常困难又极为重要的问题,我的思路如下所示

  1. 找准自己的定位以及未来的目标,假如你的目标是成为整天一大堆猎头趋之若鹜的站在技术顶尖的人,拿着上百万的年薪,带着一大堆视你为神明的小弟,开发惠及全球一半人口的大型系统……
  2. 分析要达到这一点其他人都经历过那些阶段,具备什么能力
  3. 找到离你最近的那个看看那个阶段的要求是什么,然后看看自己有那些已经具备了,有哪些还没有
  4. 开始升级或改变目标。

目前我首要的目标是达到软件开发业界的先进对平,达到高级开发和架构师层次,薪资水平达到3W+,于是我去各大招聘网站查看并分析了高级开发和架构师职位的要求,同时查找BAT关于高级JAVA开发的面试经验,发现了一个共同点,有相当一部分都提到了JAVA的并发编程技术,并且我对其中涉及到的知识完全没有认识,所以目前锁定了JAVA并发编程这个技术点进行集中攻关。

学习资料与路径

由于在工作中用过多次并发编程的相关技术,但又发现缺乏进一步的了解,因此我直接跳过了基础部分的了解直接去看了看相关的视频教程,即(imooc)深入浅出Java多线程和(imooc)细说多线程之Thread VS Runnable ,看完之后做了几个例程,发现对于其中理论部分还是存在疑问意识又去看了看相关的书籍,于是看了《Java多线程编程核心技术》,这本书相对简单,有很多示例代码,比较适合初级人员学习,最后仔细阅读了《Java并发编程的艺术》,这本书相对与其他资料深度深了不少,从源码和CPU指令排序等角度解开了我之前很多困惑的地方。

学习路径如下所示:
1. (imooc)深入浅出Java多线程
2. (imooc)细说多线程之Thread VS Runnable
3. 《Java多线程编程核心技术》
4. 《Java并发编程的艺术》
我觉得这个顺序效果还不错,推荐给大家。

JAVA并发编程

并发编程的基础

JAVA多线程的创建

Java多线程的创建主要有两种方式对Thread类的继承与对Runnable接口的实现。实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。所以建议使用实现Runnable接口方式。

1.Thread类的集成

public class ThreadThread extends Thread{     public void run() {         for (int i = 0; i < 1500; i++) {           System.out.println(Thread.currentThread().getName() +  i);                      }      }     public static void main(String[] args) {         ThreadThread  ta= new ThreadThread ("A");         ThreadThread tb = new ThreadThread ("B");         ta.start();         tb.start();     }}

2.Runnable接口的实现

public class ThreadRunnable implements Runnable {        public void run() {                   for (int i = 0; i < 1500; i++) {                System.out.println(Thread.currentThread().getName() +  i);                        }        }        public static void main(String[] args) {            ThreadRunnable t1 = new ThreadRunnable();            Thread ta = new Thread(t1, "A");            Thread tb = new Thread(t1, "B");            ta.start();            tb.start();        }    }

锁的概念

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而在Java SE 5之后并发包中新增了Lock接口和实现类来实现锁的功能。
总的来说锁是个工具,用来避免在多线程中可能出现的问题。

常用关键字

在一般的多线程的开发中,都是使用volatilesynchronzed来实现的。
1.volatile

public class Stage extends Thread {    public void run(){              ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();        ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();             //使用Runnable接口创建线程        Thread  armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋军");        Thread  armyOfRevolt = new Thread(armyTaskOfRevolt,"农民起义军");                //启动线程,让军队开始作战        armyOfSuiDynasty.start();        armyOfRevolt.start();               //舞台线程休眠,大家专心观看军队厮杀        try {            Thread.sleep(50);        } catch (InterruptedException e) {            e.printStackTrace();        }               //停止军队作战        //停止线程的方法        armyTaskOfSuiDynasty.keepRunning = false;        armyTaskOfRevolt.keepRunning = false;               try {            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        }               System.out.println("谢谢观看隋唐演义,再见!");    }    public static void main(String[] args) {        new Stage().start();    }}public class ArmyRunnable implements Runnable {    //volatile保证了线程可以正确的读取其他线程写入的值    volatile boolean keepRunning = true;    @Override    public void run() {        while(keepRunning){            //发动5连击            for(int i=0;i<5;i++){                System.out.println(Thread.currentThread().getName()+"进攻对方["+i+"]");            }        }        System.out.println(Thread.currentThread().getName()+"结束了战斗!");    }}

2.synchronized
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
使用方式 如下代码所示,若没有synchronized关键字,则for循环会在多个线程之间切换,而使用了该关键字,则保证全部循环完成后才会线程切换。

public class ThreadSync implements Runnable {    public void run() {        synchronized(this) {            for (int i = 0; i < 1500; i++) {                System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);            }        }    }    public static void main(String[] args) {        ThreadSync t1 = new ThreadSync();        Thread ta = new Thread(t1, "A");        Thread tb = new Thread(t1, "B");        ta.start();        tb.start();    }}

守护线程Deamon

Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

并发编程的进阶

线程的生命周期

本图来自网络
线程的生命周期
1.创建状态:
当用new操作符创建一个新的线程对象时,该线程处于创建状态。
处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。
2.可运行状态
执行线程的start()方法将为线程分配必须的系统资源,安排其运行,并调用线程体——run()方法,这样就使得该线程处于可运行状态(Runnable)。
这一状态并不是运行中状态(Running),因为线程也许实际上并未真正运行。    
3.阻塞状态:
线程调用wait()方法等待特定条件的满足;
线程输入/输出阻塞。
(返回可运行状态):
处于睡眠状态的线程在指定的时间过去后
如果线程在等待某一条件,另一个对象必须通过notify()或notifyAll()方法通知等待线程条件的改变;
如果线程是因为输入输出阻塞,等待输入输出完成。
4.消亡状态
当线程的run()方法执行结束后,该线程自然消亡。

多线程通信与控制

通过使用join,yeild,notify和wait等方法实现多线程的控制。
1.join
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。常见的使用方法有两种:
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
2.yield
该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。
3.notify与wait
通过notify与wait方法可以实现多线程间的任务调度,这也是最为常见的多线程调度方法。使用方法如下所示。
等待线程代码如下

package com.imooc.concurrent.base.notifyandwait;/** * Created by zhaoenwei on 2017/2/19. */public class MyThreadWait extends Thread {    private Object lock;    public MyThreadWait(Object lock) {        super();        this.lock = lock;    }    @Override    public void run() {        try {            synchronized (lock){                System.out.println("Start wait time ="+System.currentTimeMillis());                lock.wait();                System.out.println("end wait time ="+System.currentTimeMillis());            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

通知线程代码如下

package com.imooc.concurrent.base.notifyandwait;/** * Created by zhaoenwei on 2017/2/19. */public class MyThreadNotify extends Thread {    private Object lock;    public MyThreadNotify(Object lock) {        super();        this.lock = lock;    }    @Override    public void run() {            synchronized (lock){                System.out.println("Start notify time ="+System.currentTimeMillis());                lock.notify();                System.out.println("end notify time ="+System.currentTimeMillis());            }    }}

测试线程代码如下

package com.imooc.concurrent.base.notifyandwait;/** * Created by zhaoenwei on 2017/2/19. */public class ThreadTest {    public static void main(String[] args){        try{            Object lock=new Object();            MyThreadWait threadWait=new MyThreadWait(lock);            threadWait.start();            Thread.sleep(2000);            MyThreadNotify threadNotify=new MyThreadNotify(lock);            threadNotify.start();        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

高级锁Reentrantlock

JDK 1.5中新增了ReentrantLock类也可以实现wait/notify的线程通知作用,同时在其基础上扩展了嗅探锁定,多路分支通知等功能,同时也更易于使用。
下面就使用多路分支通知进行示例。
MyService代码

package com.imooc.concurrent.base.multicondition;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * Created by zhaoenwei on 2017/2/19. */public class MyService {    private Lock lock = new ReentrantLock();    public Condition conditionA = lock.newCondition();    public Condition conditionB = lock.newCondition();    public void awaitA() {        try {            lock.lock();            System.out.println("开始 awaitA时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());            conditionA.await();            System.out.println("结束 awaitA时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public void awaitB() {        try {            lock.lock();            System.out.println("开始 awaitB时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());            conditionB.await();            System.out.println("结束 awaitB时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public void singalAll_A() {        try {            lock.lock();            System.out.println("signalAll _A时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());            conditionA.signalAll();        } finally {            lock.unlock();        }    }    public void singalAll_B() {        try {            lock.lock();            System.out.println("signalAll _B时间为" + System.currentTimeMillis() + "NAME=" + Thread.currentThread().getName());            conditionB.signalAll();        } finally {            lock.unlock();        }    }}

ThreadA代码

package com.imooc.concurrent.base.multicondition;/** * Created by zhaoenwei on 2017/2/19. */public class ThreadA extends Thread{    private MyService service;    public ThreadA(MyService service) {        this.service = service;    }    public void run(){        service.awaitA();    }}

ThreadB代码

package com.imooc.concurrent.base.multicondition;/** * Created by zhaoenwei on 2017/2/19. */public class ThreadB extends Thread {    private MyService service;    public ThreadB(MyService service) {        this.service = service;    }    public void run() {        service.awaitB();    }}

主函数代码

package com.imooc.concurrent.base.multicondition;/** * Created by zhaoenwei on 2017/2/19. */public class ThreadRunLock {    public static void main(String[] args) throws InterruptedException {        MyService service=new MyService();        ThreadA a=new ThreadA(service );        ThreadB b=new ThreadB(service);        a.setName("A");        b.setName("B");        a.start();        b.start();        Thread.sleep(1000);        service.singalAll_A();    }}

输出如下所示
开始 awaitA时间为1487492110944NAME=A
开始 awaitB时间为1487492110944NAME=B
signalAll _A时间为1487492111944NAME=main
结束 awaitA时间为1487492111944NAME=A
说明通知只通知到了A而没有通知到B,多路分支通知有效。

并发编程的艺术

并发编程为什么容易出错

代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM和CPU可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。

多线程一定会快么

由于CPU是采用切换上下文的形式,切换多线程的任务,而切换上下文也会消耗一定时间,一般CPU美妙能够切换上千次上下文,所以要是为不适合的场景使用多线程编程不但可能无法提高效率,甚至可能形成处理的瓶颈。

如何减少上下文切换呢

  1. 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据是,可以用一些办法避免使用锁。
  2. CAS算法。使用Atomic包使用CAS算法来更新数据,不需要加锁。
  3. 使用最少线程。避免创建不需要的线程,若任务很少,但创建了很多处理,这样会有大量线程处于等待状态。
  4. 协程:在单线程里实现多任务的调度,在单线程里维持多个任务间的切换。

JAVA 内存模型JMM

在Java中,所有的实例域,静态域和数组元素都存储在对内存中,堆内存在线程之间共享。局部变量,方法定义参数和异常处理器参数不会在线程之间共享,他们不会有内存可见性问题,也不受内存模型的影响。Java线程之间的通讯有Java内存模型控制,JMM决定一个线程对共享变量的写入合适对另一个线程可见。
JMM定义了线程和主内存之间的关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程已读写变量的副本。本地内存是JMM的一个抽象概念并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
若线程A与线程B通信的话,一般包括如下来两个步骤
1.线程A把本地内存A中更新的共享变量刷新到主内存中
2.线程B到主内存中区读取线程A之前已经更新的共享变量。
而这两个步骤之间并不是原子性的,因此可能出现A将数据更新到主内存了,B还没更新自己的本地内存时,主内存被修改,导致B更新的不是A更新到 数据,从而产生数据异常。
因此,JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序提供内存可见性的保证。

happens-before原则

一般来讲原则如下

  1. 程序顺序规则:一个线程中的操作,happens-before于该线程中的任意后续操作;
  2. 监视器锁规则:对于一个锁的解锁,happens-before与随后对于这个锁的加锁;
  3. volatile变量规则:对于一个volatile域的写,happens-before与而难以后续这个volatile域的读;
  4. 传递性:若A happens-before B ,且B happens-before C,则A happens-before C;
    happens-before原则通过限制CPU与JVM对指令的重排,保证了单线程执行以及正确同步的多线程程序的执行的正确性。

CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。
本文就不详细讨论CAS问题,有兴趣的可以搜索一下。资料很多。

总结

要达到能够使用并发编程的程度至少要掌握以下内容

  1. 应了解多线程的创建方法
  2. wait/notify方法
  3. join方法
  4. volatile关键字
  5. synchronized关键字
    要进一步提高并发编程的水平,则应了解以下内容
  6. 线程的生命周期
  7. JAVA内存模型
  8. happens-before原则
  9. CAS
    同时结合日常的大量实践才能做到知其然也知其所以然
1 1
原创粉丝点击