《Java多线程编程核心技术》——多线程总结

《Java多线程编程核心技术》——多线程总结

  • 第1章 Java多线程技能
    • 进程和线程
    • 多线程的实现
    • 线程的状态
    • 常用API
    • 停止线程
    • 守护线程
  • 第2章 对象及变量的并发访问
    • 线程安全与非线程安全
    • 同步方法和同步代码块
    • 静态函数的锁
    • volatile关键字
  • 第3章 线程间通信
    • 等待/通知机制的实现
    • 生产者/消费者模式实现
    • join方法使用
    • 类ThreadLocal的使用
  • 第4章 Lock的使用
    • ReentrantLock类
    • 使用Condition实现等待/通知
  • 第6章 单例模式与多线程
    • 立即加载/饿汉模式
    • 延迟加载/"懒汉模式"
    • 静态内置类实现单例模式
    • 使用枚举enum数据类型实现单例模式
    • 单例模式存在的问题

第1章 Java多线程技能

本章主要介绍线程和进程的相关概念,多线程的实现和停止,以及Thread类中的核心方法,具体实现的代码可以参考《Java多线程编程核心技术》。

进程和线程

1.进程
一个可并发执行的具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。简单的说,进程就是一个程序的一次执行过程。

2.引入线程的动机和思路
操作系统采用进程机制使得多任务能够并发执行,提高了资源使用和系统效率。在早期操作系统中,进程是系统进行资源分配的基本单位,也是处理器调度的基本单位,进程在任一时刻只有一个执行控制流,这种结构称为单线程进程。单线程进程调度时存在进程时空开销大、进程通信代价大、进程并发粒度粗、不适合于并发计算等问题,操作系统引入线程机制来解决这些问题。线程机制的基本思路是,把进程的两项功能——独立分配资源和被调度分派执行分离开来,后一项任务交给线程实体完成。这样,进程作为系统资源分配与保护的独立单位,不需要频繁切换;线程作为系统调度和分派的基本单位会被频繁的调度和切换。

3.线程定义
线程是操作系统进程中能够独立执行的实体,是处理器调度和分派的基本单位。线程是进程的组成部分,每个进程内允许包含多个并发执行的线程。同一个进程中所有的线程共享进程的主存空间和资源,但是不拥有资源。
线程就是进程中的一个负责程序执行的一个控制单元(执行路径)。一个进程中可以有多个执行路径,称之为多线程。

4.进程和线程的区别

  • 定义方面:进程是程序在某个数据集合上的一次执行过程;线程是进程中的一个执行路径。
  • 角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是系统调度的单位。
  • 资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
  • 独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。

多线程的实现

实现多线程编程的方式主要有两种,一种是继承Thread类,另一种是实现Runnable接口。在这里插入图片描述
从jdk源码可以发现,Thread类实现了Runnable接口,他们之间是多态关系。其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为java语言的特性就是单根继承,所以为了多继承,完全可以实现Runnable接口的方式,一边实现一边继承。但是这两种方式创建的线程在工作时的性质是一样的,没有本质区别。

1.继承Thread类
(1)定义一个类继承Thread类。
(2)覆盖Thread类中的run方法。(方法run称为线程体)
(3)直接创建Thread类的子类对象创建线程。
(4)调用start方法,开启线程并调用线程的任务run方法执行。
注意:run()方法和start()方法的区别。start()方法来启动线程,run()方法当作普通方法的方式调用,程序还是顺序执行。

2.实现Runnable接口
(1)定义类实现Runnable接口
(2)覆盖接口中的run方法,将线程的任务代码封装到run方法中。
(3)通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread构造函数的参数进行传递。线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
(4)调用线程对象的start方法开启线程。

线程的状态

在Java当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。  
第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。  
第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。  
第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。  
第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。  
第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

《Java多线程编程核心技术》——多线程总结_第1张图片

线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪状态和运行状态之间的转换不受程序控制,而是由系统调度所决定,但是有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。【参考自《Java疯狂讲义》】

常用API

1、currentThread()方法:返回代码段正在被哪个线程调用。在书中强调了Thread.currentThread()和this的差异,可以参考博客Thread.currentThread()和this的差异。
2、isAlive()方法:判断当前线程是否处于活动状态。活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态。
3、sleep()方法:在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个正在执行的线程是指this.currentThread()返回的线程。
4、getId()方法:取得线程的唯一标识。
5、yield()方法:放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但是放弃时间不确定,有可能刚刚放弃,马上有获得CPU时间片。

停止线程

在java中有以下三种方法可以终止正在运行的线程。
(1)使用退出标记,使线程正常退出,也就是当run方法完成之后线程终止。
(2)使用stop方法强行终止线程,但是不推荐使用这种方法,因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
(3)使用interrupt方法中断线程。需要注意的是interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真正的停止线程,需要与标记一起使用来停止线程。

在介绍如何停止线程之前,先看看Thread中提供的两种用于判断线程的状态是不是停止的两种方法interrupted和isInterrupted方法。

#jdk1.8源码
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);//其中方法里面的变量是ClearnInterrupted,即是否清除中断标志
}

interrupted():测试当前线程是否已经是中断状态,执行之后具有将状态标志置清除为false的功能。
isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除标志。

具体实现停止线程的操作可参考博客停止线程的三种方法,里面对使用interrupt方法中断线程做了详细说明。

守护线程

在java中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,当进程中不存在非守护线程则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当线程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

该方法必须在启动线程前调用。守护线程和其他的线程城在开始和运行都是一样的,轮流抢占cpu的执行权,结束时不同。正常线程都需要手动结束,对于后台线程,如果所有的前台线程都结束了,后台线程无论处于什么状态都自动结束。

第2章 对象及变量的并发访问

本章主要介绍Java多线程中的同步,如何使用synchronized关键字实现同步,以及volatile关键字的主要作用,volatile与synchronized的区别及使用情况的说明。下面是对这部分内容的理解、总结及一些扩展。

线程安全与非线程安全

非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是脏读,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理,不会出现脏读的现象。

线程安全问题产生的原因:1.多个线程在操作共享的数据。2.操作共享数据的线程代码有多条。当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。
解决思路就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕之后,其他线程才可以参与运算。

在java中同步代码块就可以解决这个问题。同步代码块的格式:

synchronized(对象){    
   需要被同步的代码;
}

象如同锁即(监视器),持有锁的线程可以在同步中执行。没有持有锁的线程即使获得cpu的执行权,也进不去,因为没有获取锁。

以去银行存钱的例子来说明这个问题:

/*
需求:两个储户,每个都到银行存钱每次存100,,共存三次。
*/

class Bank {
    private int sum;
    public  void add(int num) {
        sum = sum + num;
        try{Thread.sleep(10);}catch(InterruptedException e){}
        System.out.println("sum="+sum);
    }
}
class Cus implements Runnable {
    private Bank b = new Bank();
    public void run() {
        for(int x=0; x<3; x++) {
            b.add(100);
        }
    }
}
public class BankDemo{
    public static void main(String[] args) {
        Cus c = new Cus();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

#输出结果为:
sum=200
sum=200
sum=400
sum=400
sum=600
sum=600

出现这个结果的原因就是多个线程对共享变量sum进行操作的原因,可以对代码进行修改实现同步

#同步代码块
class Bank {
    private int sum;
    Object object=new Object();
    public  void add(int num) {
        synchronized (object){
            sum = sum + num;
            try{Thread.sleep(10);}catch(InterruptedException e){}
            System.out.println("sum="+sum);
        }
    }
}

#输出为
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600

方法体也是封装代码块,也可以在方法上添加synchronized关键字来实现同步操作,即使用同步方法来实现:

#同步方法
class Bank {
    private int sum;
    public synchronized void add(int num) {
            sum = sum + num;
            try{Thread.sleep(10);}catch(InterruptedException e){}
            System.out.println("sum="+sum);
    }
}
#输出为
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600

这里需要思考一个问题:同步代码块的监视器(锁)可以是任意对象,那同步方法的监视器是什么?

同步方法和同步代码块

同步函数使用的锁是this,可通过一下代码来验证。

class Ticket implements Runnable {
    private  int num = 100;
    boolean flag = true;
    public void run() {
        if(flag)
            while(true) {
                synchronized(this) {
                    if(num>0) {
                        try{Thread.sleep(10);}catch (InterruptedException e){}
                        System.out.println(Thread.currentThread().getName()+".....obj...."+num--);
                    }
                }
            }
        else
            while(true)
                this.show();
    }
    public synchronized void show() {
        if(num>0) {
            try{Thread.sleep(10);}catch (InterruptedException e){}
            System.out.println(Thread.currentThread().getName()+".....function...."+num--);
        }
    }
}

class SynFunctionLockDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        try{Thread.sleep(10);}catch(InterruptedException e){}
        t.flag = false;
        t2.start();
    }
}

代码通过两个线程来卖一百张票,设置flag标志来切换同步代码块和同步函数来卖票,最终实现卖票的数据同步,说明同步函数的锁就是this。如果卖票出现数据不同步问题就说明同步代码块和同步函数不是同一个监视器。

同步函数与同步代码块的区别
同步函数的锁是固定的this,同步代码块的锁是任意的对象。建议使用同步代码块。同步函数是同步代码块的简写形式,简写需要满足一定的条件:当同步代码块的锁是this的时候可以简写为同步函数,但是锁不是this的时候不能简写。

静态函数的锁

静态函数的锁绝对不是this,经过验证可以知道静态函数的锁是字节码文件对象。静态的同步函数使用的锁是该函数所属的字节码文件对象 可以用getClass方法获取(这里要注意getClass方法是非静态的,使用要谨慎),也可以用当前类名.class(class是静态属性)形式表示。

volatile关键字

volatile关键字的主要作用是使变量在多个线程间可见。具体的示例可以参考书上的代码。这里需要了解java的内存模型,可以参考博客JAVA并发编程:volatile关键字以及内存模型以及volatile关键字解析。

使用volatile关键字增加了实例变量在多个线程之间的可见性。但是volatile关键字不支持原子性(在上面的博客中也有说明)。下面将关键字synchronized和volatile进行一下比较:
(1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
(2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
(3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
(4)关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。

第3章 线程间通信

本章的重点是使用wait/notify实现线程间的通信,生产者/消费者模式的实现,方法join的使用以及ThreadLocal类的使用。

等待/通知机制的实现

方法wait的作用是使当前执行代码的线程进行等待,wait方法是Object类的方法。该方法用来将当前线程置入“预执行队列”中,并且在wait所在的代码处停止执行,直到接到通知或被中断为止。在调用wait方法之前,线程必须获得该对象的对象级别锁。即只能在同步方法或同步代码块中调用wait方法。在执行wait方法后,当前线程释放锁。在从wait返回前,线程与其他线程竞争重新获得锁。如果调用wait时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕获异常。

方法notify也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify时没有持有适当的锁,则抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify方法的线程将程序执行完,也就是退出synchronized代码块之后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁

等待唤醒机制涉及的方法总结:
1.wait();让线程处于阻塞状态,这时线程会释放执行资格和执行权,被wait的线程会被存储到线程池中2.notify();唤醒线程池中的一个线程(任意)
3.notifyAll();唤醒线程池中的所有线程这些方法都必须定义在同步中
因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。例如在a锁里wait必须在该锁里notify。由于锁可以是任意对象,故这些方法定义在Object类中。等待和唤醒必须是同一个锁。

生产者/消费者模式实现

1.一生产者一消费者
使用生产烤鸭和消费烤鸭的例子来模拟单生产者单消费者。设置标志来判断此时是需要生产还是消费,当标志false,说明资源为空,此时生产烤鸭,将标志置为true并且通知消费线程来消费资源,如果cpu的执行权还是切换到了生产者线程,此时标记为true,生产者线程就会进入阻塞状态,释放cpu的执行权和监视器(锁);如果消费线程获得cpu的执行权就会拿到相同的锁,判断标记为true就会消费资源,将标记置为false,并且通知生产线程生产,再次拿到cpu的执行权的话就会进入等待状态。
同步及等待唤醒机制使生产消费正确进行,得到生产一只烤鸭就消费一只烤鸭的结果。

class Resource {
    private String name;
    private int count=1;
    private boolean flag;
    public synchronized void set(String name){
        if (flag) try { wait();} catch (InterruptedException e) { e.printStackTrace(); }
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);
        flag=true;
        notify();

    }
    public synchronized   void out(){
        if (!flag)
            try { wait();} catch (InterruptedException e) { e.printStackTrace(); }//t2 t3
        System.out.println(Thread.currentThread().getName()+".......消费..."+name);//消费1
        flag=false;
        notify();
    }
}
class Producer implements Runnable {
    Resource r;
    Producer(Resource r){
        this.r=r;
    }

    @Override
    public void run() {
        while (true){
            r.set("烤鸭");
        }
    }
}
class Consumer implements Runnable{
    Resource r;
    Consumer(Resource r){
        this.r=r;
    }

    @Override
    public void run() {
        while (true){
            r.out();
        }
    }
}
class Run{
    public static void main(String[] args) {
        Resource r=new Resource();
        Producer p=new Producer(r);
        Consumer c=new Consumer(r);
        Thread t1=new Thread(p);
        Thread t2=new Thread(c);
        t1.start();
        t2.start();
    }
}
#输出结果
Thread-0...生产...烤鸭1
Thread-1.......消费...烤鸭1
Thread-0...生产...烤鸭2
Thread-1.......消费...烤鸭2
Thread-0...生产...烤鸭3
Thread-1.......消费...烤鸭3
Thread-0...生产...烤鸭4
Thread-1.......消费...烤鸭4
....

2.多生产者多消费者
还是使用上面的例子来模拟多生产者多消费者,会出现如下结果

class Run{
    public static void main(String[] args) {
        Resource r=new Resource();
        Producer p1=new Producer(r);
        Consumer c1=new Consumer(r);
        Producer p2=new Producer(r);
        Consumer c2=new Consumer(r);
        Thread t1=new Thread(p1);
        Thread t2=new Thread(c1);
        Thread t3=new Thread(p2);
        Thread t4=new Thread(c2);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

《Java多线程编程核心技术》——多线程总结_第2张图片
《Java多线程编程核心技术》——多线程总结_第3张图片
在IDEA里面出现的结果如上,会产生生产多个但是只有一个被消费的情况,或者生产一只但是被多次消费的情况。原因是notify方法是唤醒的是同一个锁的任意一个线程,如果生产线程1唤醒了生产线程2就会在唤醒处继续向下执行,而不会去判断标记,导致生产多次的结果,消费多次也是同理。根据分析我们将if换成while就会再次去判断标记,就不会出现这种情况。

class Resource {
    private String name;
    private int count=1;
    private boolean flag;
    public synchronized void set(String name){
        while (flag) try { wait();} catch (InterruptedException e) { e.printStackTrace(); }
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);
        flag=true;
        notify();

    }
    public synchronized   void out(){
        while (!flag)
            try { wait();} catch (InterruptedException e) { e.printStackTrace(); }//t2 t3
        System.out.println(Thread.currentThread().getName()+".......消费..."+name);//消费1
        flag=false;
        notify();
    }
}

修改之后的运行结果如下:
《Java多线程编程核心技术》——多线程总结_第4张图片
出现了死锁(假死)的情况,原因是使用while判断标记避免了多次生产或者多次消费的问题,但是由于notify只能唤醒一个线程,如果本方唤醒了本方,就会导致线程会全部进入阻塞状态。因此要唤醒所有阻塞线程将notify换成notifyAll方法即可。

class Resource {
    private String name;
    private int count=1;
    private boolean flag;
    public synchronized void set(String name){
        while (flag) try { wait();} catch (InterruptedException e) { e.printStackTrace(); }
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);
        flag=true;
        notifyAll();

    }
    public synchronized   void out(){
        while (!flag)
            try { wait();} catch (InterruptedException e) { e.printStackTrace(); }//t2 t3
        System.out.println(Thread.currentThread().getName()+".......消费..."+name);//消费1
        flag=false;
        notifyAll();
    }
}

解决多个线程都进入等待问题的思路就是唤醒对方的线程(就是生产者线程唤醒消费者线程,而不是唤醒生产者线程),而不是唤醒本方线程,由于没有这种方法就全部唤醒,已方线程醒了话会判断标记,不会出现多生产或者多消费的问题。

join方法使用

1、join使用场景及说明
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完之后再结束,比如子线程处理一个数据,主线程要取得这个数据的值,就要用到join方法。join方法的作用是等待线程对象销毁。

使用下面的例子来说明join方法:

class Demo implements Runnable {
    public void run() {
        for(int x=0; x<5; x++) {
            System.out.println(Thread.currentThread().getName()+"....."+x);
        }
    }
}
class  JoinDemo {
    public static void main(String[] args) throws Exception {
        Demo d = new Demo();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();
        t2.start();
        t1.join();//t1线程要申请加入进来,运行。临时加入一个线程运算时可以使用join方法。
        for(int x=0; x<5; x++) {
         System.out.println(Thread.currentThread().getName()+"....."+x);
        }
    }
}

《Java多线程编程核心技术》——多线程总结_第5张图片
有结果可以知道,Thread-0和Thread-1抢占cpu的执行权,但是main线程一定会在Thread-0线程执行完之后在执行。

总结:方法join的作用是使所属线程Thread-0正常执行run方法中的任务,而使当前线程main无限期的阻塞,等待线程Thread-0销毁之后再继续执行线程main后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized的区别是:join内部使用wait方法进行等待,而synchronized关键字使用的是“对象监视器”原理做同步。

2、join(long)与sleep(long)的区别
join(long)的内部是使用wait(long)实现的,两者的区别实际上就是wait(long)和sleep(long)的区别,前者释放锁,而后者不释放锁。

类ThreadLocal的使用

类ThreadLocal主要解决的就是每个线程绑定自己的值,使每个线程都有自己的变量,并且该变量对于其他线程而言是隔离的。ThreadLocal的应用以及原理可以参考博客ThreadLocal就是这么简单。

第4章 Lock的使用

主要介绍使用java5中Lock对象也能实现同步的效果,主要介绍ReentrantLock类以及Condition对象的使用。

ReentrantLock类

在java多线程中,可以使用synchronized关键字来实现线程之间的互斥,但在JDK1.5中新增了ReentrantLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,并且在使用上也比synchronized更加灵活。

Object object=new Object();
void show(){
    synchronized (object){
        code...//同步代码块或者同步函数对于锁的操作是隐式的
    }
}
Lock lock=new ReentrantLock();
void show(){
    lock.lock();//获取锁
    code...
    lock.unlock();//释放锁
    //jdk1.5之后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作

}

需要注意的是如果在需要同步的代码code处抛出异常的话,后面的释放锁的操作就不会执行,但是释放锁这个动作一定要完成,放在finally语句里。

使用Condition实现等待/通知

关键字synchronized与wait()和notify()/notifyAll()方法相结合实现等待/通知模式,类ReentrantLock也可以实现同样的功能,但需要借助Condition对象。Condition对象是JDK5中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度上更加灵活。
在使用notify()/notifyAll()方法进行通知时,被通知的线程是由JVM随机选择的。但是使用ReentrantLock结合Condition类是可以实现“选择性通知”;而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有线程都注册到它一个对象身上。线程开始notifyAll时,需要通知所有的WAITING线程,会出现相当大的效率问题。

Object类中的wait()、notify()、notifyAll()分别对应Condition类中的await()、signal()、signalAll()方法。
通过ReentrantLock和Condition类来实现第3章的多生产者多消费者烤鸭问题:

class Resource{
    private String name;
    private int count=1;
    private boolean flag;
    Lock lock=new ReentrantLock();
    Condition pro=lock.newCondition();
    Condition con=lock.newCondition();
    public  void set(String name){
        lock.lock();
        try {
            while (flag)
                try { pro.await(); } catch (InterruptedException e) { e.printStackTrace(); }
            this.name=name+count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);
            flag=true;
            con.signal();
        }finally {
            lock.unlock();
        }

    }
    public  void out(){
        lock.lock();
        try {
            while (!flag)
                try { con.await();} catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+".......消费..."+name);
            flag=false;
            pro.signal();
        }finally {
            lock.unlock();
        }
    }
}

第3章的代码一个锁上只能有一个监视器,这组监视器 既监视着生产者,又监视着消费者,意味着这组监视器能将生产者和消费者全部唤醒。因此我们通过Condition对象创建两个监视器,一组监视生产者,一组监视消费者,线程用的同一个锁,但是监视器不同,提高效率。

第6章 单例模式与多线程

立即加载/饿汉模式

立即加载/"饿汉模式"是在调用方法之前,实例已经被创建

public class Single {
    private static final Single single=new Single();
    private Single(){}
    public static Single getInstance(){return single;}
}

变量为静态变量,类加载的时候就会立即加载并且创建实例,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,因此饿汉模式实现单例模式是线程安全的,但是类加载的同时立即实例化对象,会造成资源的浪费。

延迟加载/“懒汉模式”

延迟加载就是在调用get()方法的时候实例才会被创建,常见的实现方式就是在get()方法中进行new实例化。延迟加载是在调用方法时实例才会被创建。

public class Single {
    private static  Single single=null;
    private Single(){}
    public  static Single getInstance(){
        if (single==null)
            single=new Single();
        return single;
    }
}

在多线程的情况下会创建出多个对象,为了保证在多线程情况下依然保证同步,需要添加同步操作。可以在方法上加synchronized关键字来实现,但是每个线程都需要同步执行获取实例的方法,执行效率非常的慢。可以采用DCL双检查锁机制来实现多线程环境中延迟加载单例设计模式。具体代码如下:

public class Single {
    private static volatile  Single single=null;
    private Single(){}
    public  static Single getInstance(){
        if (single==null){//解决懒汉式的效率问题
            synchronized (Single.class){//解决线程安全问题
                if (single==null)
                    single=new Single();
            }
        }
        return single;
    }
}

静态内置类实现单例模式

public class Single{
    //内部类方式
    private static class SingleHandler{
        private static  final Single single=new Single();
    }
    private Single(){}
    public static Single getInstance(){
        return SingleHandler.single;
    }
}

外部类没有static属性,则不会像饿汉式那样立即加载对象;只有真正调用getInstance()才会加载静态内部类,加载类时是线程安全的。instance是static final类型,保证了内存中只有一个这样的实例存在,而且只能被赋值一次,从而保证了线程安全;兼备了并发高效调用和延时加载的优势。

使用枚举enum数据类型实现单例模式

public enum Single {   
 //这个枚举元素,本身就是单例对象    
	 INSTANCE;   
	  //添加自己需要的操作   
	   public void singletonOperation(){
	   }           
}

枚举类实现单例实现过程简单;枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞,具体可以参考博客枚举线程安全以及序列化问题。

单例模式存在的问题

1.可以通过序列化和反序列化的破坏单例模式的唯一性(除枚举之外)。在反序列化时,ObjectInputStream 因为利用反射机制调用了 readObject --> readObject0 --> readOrdinary --> CheckResolve。在readOrdinady中调用了invokeReadResolve(),该方法使用反射机制创建新的对象,从而破坏了单例唯一性。具体可以参考博客序列化对单例模式的破坏。
解决办法就是在反序列化中使用readResolve()方法,反序列化时会自动调用这个方法,直接返回此方法指定的对象。
下面使用内部类的方式来演示序列化对单例模式唯一性的破坏以及解决办法

public class Single implements Serializable {
    private static final long serialVersionUID=888L;
    //内部类方式
    private static class SingleHandler{
        private static final Single single=new Single();
    }
    private Single(){}
    public static Single getInstance(){
        return SingleHandler.single;
    }

    //反序列化时会自动调用这个方法,返回方法里面指定的对象
//    private Object readResolve(){
//        return SingleHandler.single;
//    }
}
class SaveAndRead {
    public static void main(String[] args) {
        try {
            Single single = Single.getInstance();
            FileOutputStream fosRef = new FileOutputStream(new File(
                    "single.txt"));
            ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
            oosRef.writeObject(single);
            oosRef.close();
            fosRef.close();
            System.out.println(single.hashCode());
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        try {
            FileInputStream fisRef = new FileInputStream(new File(
                    "single.txt"));
            ObjectInputStream iosRef = new ObjectInputStream(fisRef);
            Single single = (Single) iosRef.readObject();
            iosRef.close();
            fisRef.close();
            System.out.println(single.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

在这里插入图片描述
将注释打开之后返回的结果如下:

《Java多线程编程核心技术》——多线程总结_第6张图片

2.反射也会破坏单例模式的唯一性(不包含枚举)

public class Single {
    //内部类方式
    private static class SingleHandler{
        private static final Single single=new Single();
    }
    private Single(){}
    public static Single getInstance(){
        return SingleHandler.single;
    }

}
class ReflectDemo{
    public static void main(String[] args) throws Exception {
        Class<Single> clazz=(Class<Single>)Class.forName("Test.Single");
        Constructor<Single> c=clazz.getDeclaredConstructor();
        c.setAccessible(true);//访问私有的构造方法,跳过权限校验
        Single single1=c.newInstance();
        Single single2=c.newInstance();
        System.out.println(single1==single2);
    }
}

#输出结果为false

解决方法是可以在构造方法中手动抛出异常控制

public class Single {
    //内部类方式
    private static class SingleHandler{
        private static final Single single=new Single();
    }
    private Single(){
        if (SingleHandler.single!=null){
            throw new RuntimeException();
        }
    }
    public static Single getInstance(){
        return SingleHandler.single;
    }

}

这时候如果利用反射区实例化对象就会抛出异常。

你可能感兴趣的:(java基础,多线程)