多线程编程核心技术

1、多线程基础知识

①、进程和线程的区别

线程:线程是进程当中独立运行的子任务。java

②、java.exe、javaw.exe和javaws.exe

 

      javaw.exe主要用于启动基于GUI的应用程序。web

    java.exe执行应用日志再在控制台显示输出与错误信息。编程

    javaws.exe是用来启动经过web来描述的项目,咱们须要一个jnlp文件,来描述javaws.exe须要运行的程序安全

③、Thread的start()方法和run()方法、sleep()方法

用start()方法启动线程之后,只是通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个线程让系统安排一个时间来调用Thread的run()方法,具备异步执行的效果。而run()不是异步的,而是同步执行的。Thread.sleep()会让当前线程阻塞,若是在主线程中经过start()调用时,调用的线程是main,若是直接用run()调用的话则直接是当前线程调用Thread-0多线程

④currentThread()方法

Thread.curredntThread()返回代码段在被那个线程调用的信息。并发

⑤、isAlive()

Thread.currentThread().isAlive() 当前线程是否处于活跃状态。异步

在自定义线程类时,若是线程类是继承java.lang.Thread的话,那么线程类就可使用this关键字去调用继承自父类Thread的方法,this就是当前的对象。ide

Thread.currentThread()能够获取当前线程的引用,通常都是在没有线程对象又须要得到线程信息时经过Thread.currentThread()获取当前代码段所在线程的引用。性能

http://www.javashuo.com/article/p-dkyiqfem-nh.html学习

⑥、sleep()

在指定的毫秒内让当前“正在执行的线程”休眠(暂停执行),这个“正在执行的线程”是指this.currentThread()返回的线程。

⑦、getId()

Thread.currentThread().getId();得到当前线程的id

⑧、中止线程的方法

(1)当run方法完成后线程终止

(2)使用stop方法强行终止线程,不推荐使用,和suspend及resume同样被废弃调了

(3)使用interrupt方法中断(线程不会真的中止)

    interrupt()仅仅是在当前线程中打了一个中止标记,并非真的中止线程,判断线程是否中止的方法

    1)this.interrupted():测试当前线程是否已是中断状态,并将标志状态清除为false。 是静态方法,能够经过Thread.interrupted()来进行判断

    2)this.isInterrupted():测试线程Thread对象是否已是中断状态,但不清楚状态标志。是非静态方法,经过对象进行判断。

⑨、如何真正中止线程,抛出异常

/**

 * @author 赵洪坤

 * @日期2018年6月6日

 */

public class ExtendThread extends Thread {

    @Override

    public void run() {

        super.run();

        try {

            for (int i = 0; i < 50000; i++) {

                if (this.interrupted()) {

                    System.out.println("已是中止状态了!我要退出");

                    throw new InterruptedException();

                }

                System.out.println("i=" + (i + 1));

            }

            System.out.println("for循环又继续了");

        } catch (InterruptedException e) {

            System.out.println("进入interrupt异常");

            e.printStackTrace();

        }

    }

}

/**

 * @author 赵洪坤

 * @日期2018年6月6日

 */

public class Main {

    /**

     * @author 赵洪坤

     * @日期2018年6月6日

     * @param args

     * @throws InterruptedException

     */

    public static void main(String[] args) {

        try {

            ExtendThread extendThread = new ExtendThread();

            extendThread.start();

            Thread.sleep(200);

            extendThread.interrupt();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

⑩、stop暴力中止线程

(1)stop()中止线程至关于电脑拔掉电源,可能使一些清理邢的工做得不到完成。

(2)对锁的对象进行了“解锁”,致使数据得不到同步处理,出现数据不一致

⑪、suspend的暂停线程,resume从新开始线程

(1)缺点-使用不当,容易形成公共的同步对象的独占

(2)缺点-不一样步

⑫、yield方法

放弃当前cpu的资源,将它让给其它的任务去占用cpu的执行时间。但放弃的时间不肯定,有可能刚刚放弃,立刻又得到cpu时间片。      

⑬、setpriority()

设置线程执行的级别,级别越大越有可能先执行完,1-10个级别,但不是绝对的

⑭、守护线程

设置线程为守护线程thread。setDaemon(true); 守护线程是一种特殊的线程,它的特性有陪伴的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程是垃圾回收线程。

2、对象及变量的并发访问

①、方法内的变量不存在线程安全问题,由于方法内的变量私有的特性形成的

②synchronized方法与锁对象

关键字synchronized去的的锁都是对象锁,而不是把一段代码或者方法当作锁,因此那个线程先执行带synchronized关键字的方法,那个线程就持有该方法所属对象的锁Lock,那么其它线程只能呈等待状态,前提是多个线程访问的是同一个对象。若是多个线程访问多个对象,则JVM会建立多个锁。

调用关键字sychronized声明的方法必定是排队运行的。另外要紧紧记住“共享”这两个字,只有共享资源的读写访问才须要同步化,若是不是共享资源,那么根本就没有同步的必要。

③、脏读

(1)当A线程调用anyObeject对象加入synchronized关键字的X方法时,A线程就得到了X方法锁,更准确的讲,是得到了对象的锁,因此其余线程必须等A线程执行完毕才能够调用X方法,但B线程能够随意调用其余的非synchronized同步方法。

 (2)当A线程调用anyObject对象加入sychronized关键字的X方法时,A线程就得到了X方法所在的对象的锁,因此其余线程必须等A线程执行完毕才能够调用X方法,而B线程若是嗲用声明了synchronized关键字的费X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才能够调用。这时A线程已经执行了一个完整的任务,也就是说username和password这两个示例变量已经同时被赋值,不存在脏读的基本环境。

③synchronized锁重入

当存在父子类继承关系时,子类是彻底能够经过“可重入锁”调用父类的同步方法的。 

④、出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

⑤、同步不具备继承性

当父类的方法加上synchronized之后,子类继承父类的方法之后,子类也要加synchronized才能同步。

⑥、关于synchronized代码块同步

在使用同步synchronized(this)代码块时须要注意的时,当一个线程访问object的一个synchronized(this)同步代码块时,其余线程对同一个object中全部其余synchronized(this)同步diamante块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

⑦、锁的使用

    ①synchronized(this)代码块锁定当前对象,监视器为当前对象

        例:public void  doLong(){

                    synchronized(this){

                        ………………

                    }

            }

    ②将任意对象做为对象监视器

        例:public void doLong(){

                    synchronized(anything){

                        …………

                }

            }

 总结: synchronized同步方法和synchronized(this)同步代码块都能达到阻塞的状态,锁非this对象具备必定的优势:若是一个类中有不少个synchronized方法,这是虽然能实现同步,可是会受到阻塞,因此影响运行效率;但若是使用同步代码块锁非this对象,则异步的不与其余锁this同步方法争抢this锁,则可大大提升运行效率。可见使用“synchronized(非this对象x)同步代码块”格式进行同步操做时,对象监视器必须是同一个对象。若是不是同一个对象监视器,运行的结果就是异步调用了,就会有交叉运行。

③synchronized public static void printA(){

        ………………

    }

总结:关键字synchronized还能够应用在static静态方法上,若是这样写,那是对当前的*.java文件对应的Class类进行持锁。synchronized关键字加到static静态方法上是给Class类上锁,二synchronized关键字加到非static静态方法上是给对象上锁。

⑧、关于使用String做为锁产生的问题

    String常量池会形成不一样的变量,相同值时线程的锁会认为是同一把锁。使用对象锁能够改善这种情况。

⑨、死循环的举例

(1)同步方法间产生的死锁

            解决办法:

            

 

 

⑩、volatile

关键字volatile的做用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

(1)原先线程的运行模式

            

问题:

这种结构会形成私有堆栈中的值和公有堆栈中的值不一样步。

(2)使用volatile关键字的线程结构

            

            经过使用volatile关键字,强制的从公共内存中读取变量的值。

(3)关于synchronized和volatile的对比

1)关键字volatile是线程同步的轻量级实现,因此volatile性能确定比synchronized要好,而且volatile只能修饰于变量,而synchronized能够修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上获得很大提高,在开发中使用synchronized关键字的比率仍是比较大的

2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞

3)volatile能保证数据的可见性,但不能保证原子性(由于它不具有同步性,也就不具有原子性);而synchronized能够保证原子性,也能够间接保证可见性,由于它会将私有内存和公共内存中的数据作同步。

4)再次重申一下,关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。线程安全包含原子性和可见性两方面,java的同步机制都是围绕这两方面来确保线程安全的。 

(4)volatile非原子的特性

            volatile增长了实例变量在多个线程之间的可见性,可是它不具有同步性,因此也就不具有原子性。volatile主要使用的场合是在多个线程中能够感知实例变量被更改,而且能够得到最新的值使用,也就是用多线程读取共享变量时能够获取最新值使用。关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。若是修改实例变量中的数据,好比i++,也就是i=i+1,则这样的操做其实并非一个原子操做,也就是非线程安全的。操做过程:

        1)从内存中取出i的值

        2)计算i的值

        3)将i的值写到内存中

    在第2步计算值得时候,另一个线程也修改i的值,这个时候就会出现脏读数据。解决的办法就是使用synchronized关键字。 

图示演示volatile出现非线程安全的缘由:

         1)read和load阶段:从主存复制变量到当前线程工做内存

         2)use和assign阶段:执行代码,改变共享变量值

        3)store和write阶段:用工做内存数据刷新主存对应变量的值

在多线程环境中,use和assign是屡次出现的,但这一操做并非原子性,也就是在read和load以后,若是主内存count变量发生修改以后,线程工做内存中的值因为已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不一样步,因此计算出来的结果会和预期不同,也就出现了非线程安全问题。对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工做内存的值时最新的,例如线程1和线程2在进行read和load的操做中,发现内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的事变量读时的可见性问题,但没法保证原子性,对于多个线程访问同一个实例变量须要加锁同步。

总结:关键字synchronized能够保证在同一时刻,只有一个线程能够执行某一方法或某一代码块。它包含两个特征:互斥性和可见性。同步synchronized不只能够解决一个线程处于不一致的状态,还能够保证进入同步方法或同步代码块的每一个线程,都能看到由同一个锁保护以前的修改效果。学习多线程并发,要着重“外练互斥,内修可见”,这是掌握多线程,学习多线程并发的重要技术点。

3、线程间的通讯

①等待/通知机制的实

 

②方法wait()锁释放与notify()锁不释放

③锁释放的条件

④经过管道进行线程间通讯:字节流、字节流

⑤方法join的使用

⑥方法join与异常

父线程异常,子线程继续运行

⑦方法join(long)和sleep(long)的区别

⑧ThreadLocal的使用

⑨生产者/消费者模式

 

4、Lock的使用

①使用多个Condition实现通知部分线程:正确用法

②公平锁与非公平锁

5、单例模式

①当即加载/饿汉模式

②延迟加载/懒汉模式

③延迟加载的缺点

④解决方法

(1)解决方案一

 

(2)解决方案二

(3)解决方案三

(4)解决方案四

注意1:不加volatile的双检测是线程不安全的

1. 传统的单例模式

 

  你们都知道,单例模式主要分为:懒汉模式和饿汉模式。当咱们在使用单例模式时,考虑到延迟加载,懒汉模式确定是必须的。可是懒汉模式有一个很大的缺点,那就是线程不安全。咱们为了解决这个问题,发明了双重检查锁定的写法,以下:

 

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon(){

 

    }

 

    public static SingleTon getInstance(){

        if(instance == null){ // 1

            synchronized (SingleTon.class){

                if(instance == null){

                    instance = new SingleTon(); // 2

                }

            }

        }

        return instance;

    }

}

 

  如上代码所示,若是第一次检查instnace不为null的话,那么就不须要去获取锁来进行instance的初始化。所以,看上去能够大大的下降synchronized带来的性能开销。 

  双重检查锁定看上去很是的完美,可是倒是一个错误的优化。当一个线程在执行到1时,读取到instance不为null时,instance引用的对象可能尚未初始化完毕。

 

2.问题的根源

 

  在前面的代码中,instance = new SingleTon();是用来建立对象。这一行代码能够分解为以下三行伪代码:

 

memory = allocate(); //1.分配对象内存空间 

ctorInstance(memory); //2.初始化对象

instance = memory; //3.设置instance指向刚分配的内存地址

 

  由于2和3不存在数据依赖性,因此可能会被重排序。2和3重排序以后的执行顺讯可能以下:

 

memory = allocate(); //1.分配对象内存空间 

instance = memory; //3.设置instance指向刚分配的内存地址,注意,此时对象尚未被初始化

ctorInstance(memory); //2.初始化对象

 

  这里可能有人对重排序存在疑惑,若是想要理解什么是重排序,为何要重排序等等缘由,强烈推荐:方腾飞、魏鹏、程晓明三位老师的《Java 并发编程的艺术》。这里我就不对这部分进行展开,主要是本身太菜了,惧怕对这部分的解释很差。 

  咱们知道instance = new SingleTon()这一步可能会被重排序以后,如今咱们来看看什么状况下可以致使问题。假设有两个线程,ThreadA和ThreadB,这两个线程都在调用SingleTone的getInstance方法来获取一个SingleTon的对象。执行顺序可能出现以下状况: 

  因为单线程内须要遵照intra-thread semantics,从而能保证ThreadA的执行结果不会被改变(全部线程在执行Java程序必须遵照intra-thread semantics,而intra-thread semantics保证全部的重排序在单线程里内,程序的执行结果不会被改变)。可是当ThreadB在按照上图的顺序在执行时,ThreadB将看到一个尚未被初始化的SingleTon对象。 

  从咱们的程序代码中能够看出来,当instance = new SingleTon()发生了重排序,ThreadB在if(instance == null) 判断出false,接下来将访问instace所引用的对象,可是此时这个对象可能尚未被ThreadA初始化完毕。

 

  上表就是对上面的流程图的一个总结。咱们知道,最后ThreadB可能会返回一个为未初始化的对象。 

  在知道了问题的根源以后,咱们能够想出两个办法来实现线程安全的延迟初始化。 

  1.不容许2和3重排序。 

  2.容许2和3重排序,可是不容许其余线程“看到”这个重排序。

 

3.解决方案

 

  前面解释了双重检查锁定问题的根源,而且列出了两种解决思路。这里,咱们将对这两种思路进行展开。

 

(1).基于volatile的解决方案

 

  这个解决方案是很是的简单,只须要将咱们以前的那个instance变量使用volatile关键字来修饰就好了。以下:

 

public class SingleTon {

    private volatile static SingleTon instance = null;

    private SingleTon(){

 

    }

 

    public static SingleTon getInstance(){

        if(instance == null){ //1

            synchronized (SingleTon.class){

                if(instance == null){

                    instance = new SingleTon(); //2

                }

            }

        }

        return instance;

    }

}

 

  是否是很是的简单?固然咱们这里的目的固然不是简单的实现解决方案,而是详细的解释为何须要这样作。

 

memory = allocate(); //1.分配对象内存空间 

ctorInstance(memory); //2.初始化对象

instance = memory; //3.设置instance指向刚分配的内存地址

 

  因为instance是volatile变量,因此上面的代码中3至关因而对volatile变量进行写的操做,也就是所谓的volatile写。根据《Java 并发编程的艺术》的P43,咱们知道对于一个volatile写,编译器会在volatile写的前面加入一个StoreStore内存屏障,用来防止前面的普通写与下面的volatile写进行重排序;在volatile写的后面加入一个StoreLoad屏障,主要是防止上面的volatile写与下面的可能有的volatile读/写进行重排序。以下图: 

  从而咱们能够得出,instance = new SingleTon()指令执行顺序图: 

  因此,咱们能够得出,只要instance被volatile修饰了,2和3就不能重排序。能够得出新的时序图: 

 

  这样,咱们经过上面解决方案中第一个方案来保证了线程安全的延迟加载。

 

注意2:单例为何要双重检查null

public class SingleTon {

    privatestatic SingleTon singleTon = null;  

    publicSingleTon() {

       // TODOAuto-generated constructor stub

    }

    publicstatic SingleTon getInstance(){

       if(singleTon == null) {

          synchronized(SingleTon.class) {

             if(singleTon == null) {

                singleTon =new SingleTon();

             }

          }

       }

       returnsingleTon;

    }

 

}

 

考虑这样一种状况,就是有两个线程同时到达,即同时调用getInstance() 方法,

此时因为singleTon== null ,因此很明显,两个线程均可以经过第一重的 singleTon== null ,

进入第一重 if语句后,因为存在锁机制,因此会有一个线程进入 lock 语句并进入第二重 singleTon== null ,

而另外的一个线程则会在lock 语句的外面等待。

而当第一个线程执行完new SingleTon()语句后,便会退出锁定区域,此时,第二个线程即可以进入lock 语句块,

此时,若是没有第二重singleTon== null 的话,那么第二个线程仍是能够调用 new SingleTon()语句,

这样第二个线程也会建立一个SingleTon实例,这样也仍是违背了单例模式的初衷的,

因此这里必需要使用双重检查锁定。

细心的朋友必定会发现,若是我去掉第一重singleton == null ,程序仍是能够在多线程下无缺的运行的,

考虑在没有第一重singleton == null 的状况下,

当有两个线程同时到达,此时,因为lock 机制的存在,第一个线程会进入 lock 语句块,而且能够顺利执行 new SingleTon(),

当第一个线程退出lock 语句块时, singleTon 这个静态变量已不为 null 了,因此当第二个线程进入 lock 时,

仍是会被第二重singleton == null 挡在外面,而没法执行 new Singleton(),

因此在没有第一重singleton == null 的状况下,也是能够实现单例模式的?那么为何须要第一重 singleton == null呢?

这里就涉及一个性能问题了,由于对于单例模式的话,newSingleTon()只须要执行一次就 OK 了,

而若是没有第一重singleTon == null 的话,每一次有线程进入getInstance()时,均会执行锁定操做来实现线程同步,

这是很是耗费性能的,而若是我加上第一重singleTon == null 的话,

那么就只有在第一次,也就是singleTton ==null 成立时的状况下执行一次锁定以实现线程同步,

而之后的话,便只要直接返回Singleton 实例就 OK 了而根本无需再进入 lock语句块了,这样就能够解决由线程同步带来的性能问题了。