多线程编程核心技术

一、多线程基础知识

①、进程和线程的区别

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

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

 

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

    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就是当前的对象。

Thread.currentThread()可以获取当前线程的引用,一般都是在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用。

https://blog.csdn.net/yezis/article/details/57513130

⑥、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); 守护线程是一种特殊的线程,它的特性有陪伴的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程是垃圾回收线程。

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

①、方法内的变量不存在线程安全问题,因为方法内的变量私有的特性造成的

②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不仅可以解决一个线程处于不一致的状态,还可以保证进入同步方法或同步代码块的每个线程,都能看到由同一个锁保护之前的修改效果。学习多线程并发,要着重“外练互斥,内修可见”,这是掌握多线程,学习多线程并发的重要技术点。

三、线程间的通信

①等待/通知机制的实

 

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

③锁释放的条件

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

⑤方法join的使用

⑥方法join与异常

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

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

⑧ThreadLocal的使用

⑨生产者/消费者模式

 

四、Lock的使用

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

②公平锁与非公平锁

五、单例模式

①立即加载/饿汉模式

②延迟加载/懒汉模式

③延迟加载的缺点

④解决方法

(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语句块了,这样就可以解决由线程同步带来的性能问题了。

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java多线程编程是指在Java语言中使用多个线程来同时执行多个任务,以提高程序的并发性能和响应速度。Java多线程编程PDF是一本介绍Java多线程编程的PDF文档,其中包含了Java多线程编程的基本概念、原理、技术和实践经验。该PDF文档可以帮助读者快速了解Java多线程编程相关知识,并提供实用的编程示例和案例分析,有助于读者掌握Java多线程编程核心技术和方法。 在Java多线程编程PDF中,读者可以学习到如何创建和启动线程、线程的状态和生命周期、线程间的通信与同步、线程池的使用、并发容器等相关内容。同时,该PDF文档还介绍了Java中的并发包(concurrent package)的使用和实现原理,以及多线程编程中的常见问题和解决方案。 通过学习Java多线程编程PDF,读者可以深入了解Java多线程编程的理论和实践,掌握多线程编程的核心知识和技能,提高自己的并发编程能力,为开发高性能、高并发的Java应用程序打下坚实的基础。同时,对于已经掌握多线程编程知识的读者来说,该PDF文档也能够帮助他们进一步巩固和扩展自己的多线程编程技能,提升自己的编程水平和竞争力。 总之,Java多线程编程PDF是一本全面介绍Java多线程编程的优秀文档,对于Java程序员来说具有很高的参考价值,可以帮助他们在多线程编程领域取得更好的成就。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿的十万个为什么

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值