Java多线程进阶详解

1、卖票案例引入数据不安全问题

我们使用多线程模拟车站卖票,车站的票数一开始都是确定好的,多个窗口使用多个线程并发执行来模拟;

方式1:自定义类继承Thread类

public class MyTest1 {
    public static void main(String[] args) {
        sellTickets th1 = new sellTickets("线程-A");
        sellTickets th2 = new sellTickets("线程-B");
        sellTickets th3 = new sellTickets("线程-C");
        th1.start();
        th2.start();
        th3.start();
    }
}
public class sellTickets extends Thread {
    private static int tickets = 100;

    public sellTickets(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (true) {
            if (tickets >= 1) {
                try {
						/*售票时网络是不能实时传输的,
						总是存在延迟的情况,所以,在出售一张票以后,
						需要一点时间的延迟,
						这里增加的这行代码是为了更好的演示多线程的随机性与模拟网络延迟*/
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票");
            }
        }
    }
}

实验结果:
在这里插入图片描述
方式2:自定义类实现Runnable接口

public class MyTest2 {
    public static void main(String[] args) {
        sellRunnable runnable = new sellRunnable();
        Thread th1 = new Thread(runnable);
        Thread th2 = new Thread(runnable);
        Thread th3 = new Thread(runnable);

        th1.setName("线程-A");
        th2.setName("线程-B");
        th3.setName("线程-C");

        th1.start();
        th2.start();
        th3.start();
    }
}
public class sellRunnable implements Runnable {
    //这里不需要写成static,测试类只创建了一个任务
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets >= 1) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票");
            }
        }
    }
}

实验结果:
在这里插入图片描述
方式3:自定义类实现Callable接口

public class MyTest3 {
    public static void main(String[] args) {
        sellCallable<String> callable = new sellCallable<>();
        FutureTask task1 = new FutureTask(callable);
        FutureTask task2 = new FutureTask(callable);
        FutureTask task3 = new FutureTask(callable);

        Thread th1 = new Thread(task1,"线程-A");
        Thread th2 = new Thread(task2,"线程-B");
        Thread th3 = new Thread(task3,"线程-C");

        th1.start();
        th2.start();
        th3.start();
    }
}
import java.util.concurrent.Callable;
public class sellCallable<String> implements Callable<String> {

    private int tickets = 100;

    @Override
    public String call() throws Exception {
        while(true){
            if (tickets >= 1) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票");
            }
        }
    }
}

实验结果:
在这里插入图片描述

  • 为什么会出现0票或者负数票?

1、假设当th1线程这时候获得了CPU的执行权,这个时候假设票数=1了,因为我们在run()里面睡眠了50ms,在这50ms内,很有可能其他线程获得了CPU的执行权,
2、假设这时候th2获得了执行权,他也进入了while循环,判断此时的票数还是1,因此进入if循环,
3、假设这时候th1睡醒了,th2进入睡眠,th1执行了卖票语句,这时候票数变为0,
4、th2线程睡醒之后,也执行了卖票语句,这时候票数就变成了负数,或者同样情况下可能出现的0票;

在这里插入图片描述

  • 首先了解一下JVM 内存模型

主要目标:

定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

规则:

所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量;
在这里插入图片描述
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;

由于线程的原子性造成的;原子性指的是不可分割性,线程中对于卖票的操作不是原子性操作,首先,我们需要将三个线程都共享的数据票数从主存中读取到每个线程的工作内存,然后在每个线程的工作内存中进行修改,修改完再把这个数据写回到主存当中;

1、在这个过程当中,假设线程th1先获得CPU执行权,他读取到了票数4,
2、然后在自己的工作内存当中修改了票数为3,还没来得及刷回主存当中,
3、线程th2就抢走了CPU执行权,读取到了票数4,
4、它也改了票数为3,
5、这时候th1又抢回了CPU执行权,将票数3刷回主存,因此票数显示3;
6、这个时候th2抢走了cpu执行权,票数也显示了3;
最终造成显示相同票数的原因;
在这里插入图片描述

  • 经过上面的卖票案例,我们可以看到,多线程环境虽然提高了效率,提高了用户体验,但是出现了数据安全的问题,出现数据安全的问题主要是由于以下几点原因:

1、是不是一个多线程环境;
2、多个线程有没有共享数据;
3、有没有多条语句在操作共享变量;

2、同步代码块

  • 前两个条件在我们的卖票案例当中是不可缺少的,要想解决数据安全的问题,只能想办法打破第三个问题;我们可以把有可能出现数据安全问题的代码使用同步代码块进行包裹(锁住),让任意时刻只能有一个线程执行即可
  • 同步代码块的 语法格式
 synchronized(对象){
	//不能在括号了直接 new 对象 new 了就没效果要被同步的代码 ;
	//这个对象可以使用任意的Java对象代替;
}
  • 使用同步代码块包裹住可能出现数据安全问题的代码,保证在某一时刻只有一个线程可以进来这段代码并持有锁,它一旦持有了这把锁,等到执行完这段逻辑,才会释放锁,让线程们再次争抢CPU资源,来持有这把锁;
  • 具体实现:
-----自定义卖票类:
public class sellTickets extends Thread {
    private static int tickets = 100;
    //创建一个三个线程共同持有的一把锁对象
    private static Object obj = new Object();

    public sellTickets(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets >= 1) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票");
                }
            }
        }
    }
}
------测试类:
public class MyTest {
    public static void main(String[] args) {
        sellTickets th1 = new sellTickets("线程-A");
        sellTickets th2 = new sellTickets("线程-B");
        sellTickets th3 = new sellTickets("线程-C");
        th1.start();
        th2.start();
        th3.start();
    }
}

其中一次运行结果:
在这里插入图片描述

再也没有出现数据安全问题;
这里要注意:三个线程必须使用同一个锁对象,因为只有这样,才能保证某一时间段内只有一个线程可以持有这把锁,其中一个线程抢到了CPU执行权,锁住了门,其他线程只能等在门外,如果三个对象各自使用一把锁,就没有任何意义了;

深入理解synchronized关键字

1、什么是synchronized?

synchronized关键字可以实现一个简单的策略来防止线程干扰内存一致性错误,如果一个对象是对多个线程可见的,那么对该对象的所有读写都将通过同步的方式来进行;

2、synchronized包括哪两个JVM重要的指令?

monitor entermonitor exit

3、synchronized锁的是什么?

  • 普通同步方法 ---------------> 锁的是当前实例对象;
  • 静态同步方法---------------> 锁的是当前类的Class对象;
  • 同步方法块 ---------------> 锁的是synchonized括号里配置的对象;

3、同步方法与静态同步方法

  • 上面我们看到了如何给代码块加一个锁,其实还有两种锁,可以直接将锁对象加在方法上面,根据方法是否为静态,这两种加锁的方法被称为:同步方法与静态同步方法

同步方法

  • 还是上面的卖票案例,这里使用实现Runnable接口的方式来创建线程;
-----测试类:
public class MyTest1 {
    public static void main(String[] args) {
        sellRunnable runnable = new sellRunnable();
        Thread th1 = new Thread(runnable,"线程-A");
        Thread th2 = new Thread(runnable,"线程-B");
        Thread th3 = new Thread(runnable,"线程-C");

        th1.start();
        th2.start();
        th3.start();
    }
}
----自定义卖票任务类:
public class sellRunnable implements Runnable{
    private static int tickets = 100;

    @Override
    public synchronized void run() {
        while (true) {
            if (tickets >= 1) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票");
            }
        }
    }
}

运行结果:
在这里插入图片描述

  • 同步方法默认的锁对象是this,代表当前对象,因为我们在测试类里面只new了一个sellRunnable 对象,三个线程使用一个任务对象,因此三个线程每次抢到CPU执行权之后,都是使用这个对象锁住这个方法;

静态同步方法

  • 卖票案例:
----测试类:
public class MyTest {
    public static void main(String[] args) {
        sellRunnable runnable = new sellRunnable();

        Thread th1 = new Thread(runnable,"线程A");
        Thread th2 = new Thread(runnable,"线程B");
        Thread th3 = new Thread(runnable,"线程C");

        th1.start();
        th2.start();
        th3.start();
    }
}
----自定义Runnable类:
public class sellRunnable implements Runnable{
    //这个票让三个线程共享
    static int piao = 100; //共享数据

    @Override
    public void run() {
        while (true) {
            //th1 th2 th3
            maiPiao();
            //th1 执行完了,出了同步代码块,就会释放锁。释放锁了之后,多个线程再去争抢CPU的时间片
        }
    }

    //静态同步方法:默认用的锁对对象,用的是当前类的字节码对象
    public static synchronized void maiPiao() {
        if (piao >= 1) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第:" + (piao--) + " 张票");
        }
    }
}

某一次运行结果:
在这里插入图片描述

避免了数据安全问题,这个时候sychronized默认持有锁对象是当前类的字节码文件对象,它属于类锁;

内置锁

1、每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁
2、线程进入同步代码块或同步方法的时候会自动获得该锁,在退出同步代码块或同步方法时会释放该锁;
3、获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法;
4、Java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去;
5、Java的对象锁和类锁:Java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的类锁是用于类的静态方法或者一个类的class对象上的
6、我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象, 所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
7、但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
8、 一个类的对象锁和另一个类的对象锁是没有关联的,当一个线程获得A类的对象锁时,它同时也可以获得B类的对象锁。

静态同步方法与同步代码块共同使用
  • 使用同步代码块与静态同步方法,共同模拟卖票案例;
----测试类:
public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();

        Thread th1 = new Thread(runnable,"线程A");
        Thread th2 = new Thread(runnable,"线程B");
        Thread th3 = new Thread(runnable,"线程C");

        th1.start();
        th2.start();
        th3.start();
    }
}
-----自定义类:
public class MyRunnable implements Runnable {
    public static int ticket = 100;
    int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                synchronized (MyRunnable.class) {
                    if (ticket >= 1) {
                        //模拟一下真实的售票环境,有网络延迟。
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第:" + (ticket--) + " 张票");
                    }
                }
            } else {
                sellTicket();
            }
            i++;
        }
    }

    private static synchronized void sellTicket() {
        if (ticket >= 1) {
            //模拟一下真实的售票环境,有网络延迟。
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第:" + (ticket--) + " 张票");
        }
    }
}

某一次执行结果:
在这里插入图片描述

  • 其实,类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁;

为什么要使用同步代码块

我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变量,再对这些变量做一些操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围;

同步的好处

同步的出现解决了多线程的安全问题;

同步的弊端

当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题;

4、Lock锁

Lock锁的概述

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,比较抽象,为了更清晰的表达如何加锁和释放锁,JDK 1.5以后,并发包新增Lock接口来实现锁功能

Lock接口提供 synchronized不具备的主要特性

特性描述
尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断的获取锁与sychronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁被释放
超时获取锁在指定的截止时间之前获取锁,如果截至时间到了仍然无法获取,则返回

重入锁ReentrantLock内幕

1、概述:

我们一般使用的是Lock锁的是是实现类:ReentrantLock
在这里插入图片描述

这个是 JDK @since 1.5 添加的一种颗粒度更小的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock锁的灵活度要远远大于前者;
继承关系描述
从类结构图看出,ReentrantLock 实现了 Lock 接口,ReentrantLock 只是 Lock 接口的一个实现而已。

java.util.concurrent.locks.Lock

它们都是java.util.concurrent 包里面的内容(俗称 JUC、并发包),也都是 JDK 1.5 开始加入的;

2、为什么叫重入锁

ReentrantLock,我们把它拆开来看就明了了,Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程

public void m() {
    lock.lock();
    lock.lock();
    try {
      // ... method body
    } finally {
      lock.unlock()
      lock.unlock()
    }
}

如示例代码所示,当前线程可以反复加锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导入锁不被释放;

试想一下,如果不设计成可重入锁,那自己如果反复给自己加锁,不是会把自己加死锁了吗

3、重入锁最重要的几个方法
在这里插入图片描述
(1)lock()

获取锁,有以下三种情况:

锁空闲:直接获取锁并返回,同时设置锁持有者数量为:1;
当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1;
其他线程持有锁:当前线程会休眠等待,直至获取锁为止;

(2)lockInterruptibly()

获取锁,逻辑和 lock() 方法一样,但这个方法在获取锁过程中能响应中断

(3)tryLock()

从关键字字面理解,这是在尝试获取锁,获取成功返回:true,获取失败返回:false, 这个方法不会等待,有以下三种情况:

锁空闲:直接获取锁并返回:true,同时设置锁持有者数量为:1;
当前线程持有锁:直接获取锁并返回:true,同时锁持有者数量递增1;
其他线程持有锁:获取锁失败,返回:false;

(4)tryLock(long timeout, TimeUnit unit)

逻辑和 tryLock() 差不多,只是这个方法是带时间的。

(5)unlock()

释放锁,每次锁持有者数量递减 1,直到 0 为止。所以,现在知道为什么 lock 多少次,就要对应 unlock 多少次了吧。

(6)newCondition

返回一个这个锁的 Condition 实例,可以实现 synchronized 关键字类似 wait/ notify 实现多线程通信的功能,不过这个比 wait/ notify 要更灵活,更强大!


大概用法如下:

class X {

  private final ReentrantLock lock = new ReentrantLock();

  // ...

  public void m() {
    lock.lock();  // block until condition holds
    try {
      // ... method body
    } finally {
      lock.unlock()
    }
  }
}

加锁和释放锁都在方法里面进行,可以自由控制,比 synchronized 更灵活,更方便。但要注意的是,释放锁操作必须在 finally 里面,不然如果出现异常导致锁不能被正常释放,进而会卡死后续所有访问该锁的线程
synchronized 是重入锁吗?

4、synchronized 是重入锁吗

你可能会说不是,因为 ReentrantLock 既然是重入锁,根据推理,相反,那 synchronized 肯定就不是重入锁,那你就错了,答案是:yes,看下面的例子:

public synchronized void operation(){
    add();
}

public synchronized void add(){

}

operation 方法调用了 add 方法,两个方法都是用 synchronized 修饰的,add() 方法可以成功获取当前线程 operation() 方法已经获取到的锁,说明 synchronized 就是可重入锁。

5、卖票案例演示:

import java.util.concurrent.locks.ReentrantLock;

public class MyTest1 {
    public static void main(String[] args) {
        myLockThread th1 = new myLockThread("线程-1");
        myLockThread th2 = new myLockThread("线程-2");
        myLockThread th3 = new myLockThread("线程-3");

        th1.start();
        th2.start();
        th3.start();
    }
}
class myLockThread extends Thread{
    public static ReentrantLock lock=new ReentrantLock();
    public static int tickets=100;

    public myLockThread(String s) {
        super(s);
    }

    @Override
    public void run() {
        while (true){
            lock.lock();
            if(tickets>=1){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第:" + (tickets--) + " 张票");
            }
            lock.unlock();
        }
    }
}

某一次执行结果:
在这里插入图片描述

5、Java中类的线程安全与线程不安全的对比

之前在学习集合与自定义类的时候,提到有些类是属于线程安全效率低,而有些则是线程不安全效率高,这么说的原因是什么?

1、ArraryList 与 Vector 集合

前者是线程不安全,效率高;
后者是线程安全,效率低;
在这里插入图片描述

2、StringBuilder 与 StringBuffer 类

前者是线程不安全,效率高;
后者是线程安全,效率低;
在这里插入图片描述

由此可见,这些线程安全的类是因为他们的成员方法都加了关键字sychronized,保证了一段时间内只能有一个线程来执行这个方法里面的逻辑,也就保证了数据安全;

6、死锁现象

指的是两个或两个以上的线程,因为同时抢占CPU资源而出现的相互等待现象;
------------------------------------------------------------------------
代码演示:

-----工具类:
public class ObjectUtils {
    //创建两个静态的对象充当锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}
------自定义线程类:
public class MyThread extends Thread {
    boolean flag;

    public MyThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (ObjectUtils.objA) {
                System.out.println("true线程持有A锁");
                synchronized (ObjectUtils.objB) {
                    System.out.println("true线程持有B锁");
                }
            }
        } else {
            synchronized (ObjectUtils.objB) {
                System.out.println("false线程持有B锁");
                synchronized (ObjectUtils.objA) {
                    System.out.println("false线程持有A锁");
                }
            }
        }
    }
}
---测试类:
public class MyTest {
    public static void main(String[] args) {
        MyThread th1 = new MyThread(true);
        MyThread th2 = new MyThread(false);
        th1.start();
        th2.start();
    }
}

某一次执行结果:
在这里插入图片描述
另外一次执行结果:
在这里插入图片描述
出现了死锁现象,为什么会出现这样的现象:

1、true线程最先抢占了CPU资源,持有了A锁;
2、flase线程紧接着抢占了CPU资源,持有了B锁;
3、true线程等待false线程执行完毕释放B锁,false线程等待true线程执行完毕释放A锁,但是他们都处于互相等待状态,谁也不让谁,就出现了死锁现象;
4、这就相当于:中国人和美国人一起吃饭,中国人使用的筷子,美国人使用的刀和叉;中国人获取到了美国人的刀,美国人获取到了中国人的一根筷子,谁也不让谁,谁都吃不成饭;

8、多线程下的等待唤醒机制

在实际的软件开发过程中,经常会碰到如下场景:

  • 某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等);
  • 产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
    单单抽象出生产者和消费者,还够不上是生产者/消费者模式;
  • 该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介,生产者把数据放入缓冲区,而消费者从缓冲区取出数据;
  • 下图是对生产者消费者的理解图:
    1、你把信写好——相当于生产者制造数据
    2、你把信放入邮筒——相当于生产者把数据放入缓冲区
    3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
    4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
  • 这个生产者消费者模式也叫做等待——唤醒机制

在这里插入图片描述

优点

可能会有这样的疑问:这个缓冲区有什么用?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区干什么?其实这里面是大有讲究的,大概有如下一些好处;

1、解耦:

  • 假设生产者和消费者分别是两个类,如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合),将来如果消费者的代码发生变化,可能会影响到生产者;
  • 而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了;
  • 接着上述的例子,如果不存在包子(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。

2、支持并发(concurrency)

  • 生产者直接调用消费者的某个方法,还有另一个弊端;由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
  • 使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据,基本上不用依赖消费者的处理速度;其实当初这个模式,主要就是用来处理并发问题的;从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询),不管是哪种方法,都挺土的。

3、支持忙闲不均

  • 缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中,等生产者的制造速度慢下来,消费者再慢慢处理掉。
  • 为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。

编写代码

1、我们使用Student类模拟缓存,使用getThread类模拟消费者,使用setThread类来模拟生产者;
2、首先需要明确,生产者消费者模型希望达到的效果是:生产一个学生对象,消费一个学生对象,也就是set线程的run()需要增加学生信息(比如设置 [“张三”,23]),get线程的run()需要获取学生信息 [获取到 “张三”,23这个学生对象],但是学生对象是new出来的,两个线程之间没办法共享这个数据,怎么才能共享呢?我们可以给两个线程提供一个有参构造,在测试类里面就创建好学生对象,然后将一个学生对象传递给两个线程,这样可以实现学生对象的资源共享

----测试类:
public class MyTest {
    public static void main(String[] args) {
        Student student = new Student();
        setThread th1 = new setThread(student);
        getThread th2 = new getThread(student);
        th1.start();
        th2.start();
    }
}
----生产者:
public class setThread  extends Thread {
    int i = 1;
    Student student;

    public setThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true) {
            //没有资源生产资源
            if (i % 2 == 0) {
                //生产资源
                student.name = "张三";
                student.age = 23;
            } else {
                //生产资源 th1
                student.name = "李四";
                student.age = 24;
            }
            i++;
        }
    }
}
-----消费者:
public class getThread extends Thread {
    private Student student;

    public getThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        //消费资源
        while (true) {
            //有了资源就消费
            System.out.println(student.name + "===" + student.age);
        }
    }
}
----学生类:
public class Student {
    public int age;
    public String name;
}

某一次运行结果:
在这里插入图片描述
另外一次运行结果:
在这里插入图片描述
3、上面的运行结果可以看出,程序出现了数据安全的问题,本来希望达到生产一个消费一个的效果没有出现,明明生产的是李四–24,张三–23,却出现了李四–23,张三–24的现象;因为线程一旦开启,就会具有随机性,有可能还没生产,就消费了,也有可能生产到一半,就被消费了,造成名字与年龄不匹配;
4、怎么解决数据安全的问题呢?可以给生产资源和消费资源的代码加sychronized锁,锁住这部分代码,保证在没有生产好资源的时候,不能消费资源;并且需要注意的是,我们生产和消费线程必须使用的同一个锁住,这样才能保证一段时间内只能执行一个线程的任务,这时候,他们共享资源学生对象可以充当这个锁对象;
5、但是这样写代码还是不行,因为我们生产者一次只能生产一个资源,但是由于线程的随机性,有可能生产了一个资源,但是却在重复消费这个资源;
6、我们怎么解决上面的问题呢?在生产线程里面,我们希望生产完成一个资源,消费一个资源;
作为生产者来说,我生产一个资源,就通知消费线程来消费这个资源;作为消费线程来说,我消了资源,我就等着,并且通知生产线程来生产;
这时候就需要用到等待——唤醒机制

Object 类中
void wait ()——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待

  • 作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入"预执行队列"中,并在 wait()所在的代码处停止执行,直到接到通知或中断为止。
  • 在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁。

void wait (long timeout)——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待;
void notify ()——唤醒在此对象监视器上等待的单个线程;

  • notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发出通知 notify,并让它等待获取该对象的对象锁。

void notifyAll ()——唤醒在此对象监视器上等待的所有线程;

  • notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

7、考虑如何判断是否已经生产好了学生对象,因为不能出现还没生产就消费了的现象,可以给学生对象加一个布尔类型的值作为标记,如果还没有生产或者是生产之后被消费了该学生资源,将标记置为false,并使得当前线程处于等待状态;如果资源已经生产好了,就去通知消费,也就是唤醒;这里使用共享资源Student资源让线程等待或者唤醒;

-----生产者:
public class setThread extends Thread {
    int i = 1;
    Student student;

    public setThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (student) {
                if (student.flag) {
                    //有资源,生产者线程就等待
                    try {
                        //th1
                        student.wait();//线程一旦等待,就要立马释放锁,在哪里等待,被唤醒后,就从哪里开始执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //没有资源生产资源
                if (i % 2 == 0) {
                    //生产资源
                    student.name = "张三";
                    student.age = 23;
                } else {
                    //生产资源 th1
                    student.name = "李四";
                    student.age = 24;
                }
                //有了资源通知消费线程去消费线程
                //修改标记
                student.flag = true;
                student.notify(); 
                //通知 唤醒之后,线程还得再次争抢时间片 th1
            }
            i++;
        }
    }
}
-----消费者:
public class getThread extends Thread {
    private Student student;

    public getThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        //消费资源
        while (true) {
            synchronized (student) {
                if (!student.flag) { //false
                    //消费线程没有资源,就等着
                    try {
                        student.wait(); //一旦等待,就会释放锁,在哪里等待,被唤醒后,就从这里执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //有了资源就消费
                System.out.println(student.name + "===" + student.age);
                //消费了就没有了
                //通知生成者线程去生产
                student.flag = false;//修改标记
                student.notify();
            }
        }
    }
}
----缓存:
public class Student {
    public int age;
    //定义一个标记,用来表示是否有资源。 false 表示没有资源, true表示有资源。
    public boolean flag = false;
    public String name;
}
-----测试类:
public class MyTest1 {
    public static void main(String[] args) {
        Student student = new Student();
        setThread th1 = new setThread(student);
        getThread th2 = new getThread(student);
        th1.start();
        th2.start();
    }
}

某一次运行结果:
在这里插入图片描述

  • 注意:wait()只有notify()才能唤醒它;
  • 在哪里等待,被唤醒后,就从这里执行;
  • 即使被唤醒之后,线程还是会去争抢资源,拿set线程来说,即使它被唤醒后还是会抢占CPU资源,但是由于我们的代码增加了判断条件,如果有资源就处于等待,这样的话,get线程总会强到资源去消费;
  • 线程一旦等待,就会立马释放锁;
  • 等待线程被唤醒之后,并不会立刻执行,因为唤醒线程还持有"该对象的同步锁",必须等到唤醒线程释放了"对象的同步锁"之后,等待线程才有可能获取到"对象的同步锁",进而继续执行;
  • wait()和notify()只有在同步中才有效;
  • 唤醒的意思就是让线程具备执行资格;
wait()和sleep()的区别
  • 共同点:都可以使得线程处于阻塞状态;
  • 不同点:
    sleep():设置时间量;
    wait():可以设置也可以不设置,如果不设置,一旦等待,就会一直休眠,不释放锁;

8、内存可见性问题

  • 看下面这样一个现象:
public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread th = new Thread(runnable);
        th.start();
        while (true){
            if(runnable.getFlag()){
                System.out.println("进来执行了!");
                break;
            }
        }
    }
}
public class MyRunnable implements Runnable {
    public boolean flag = false;

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {
        System.out.println("线程进来执行了!");
        flag = true;
    }
}

执行结果:
在这里插入图片描述

  • 这是为什么?明明线程已经进来执行了任务,这意味着他也修改了标记值为true,那为什么主线程认为这个标记值为false?
  • 这是由于内存可见性问题,Java的内存模型分为主存和每个线程都具有的工作内存,操作主存中的数据就需要将主存中的数据进行拷贝,修改完之后再刷回主存;
    在这里插入图片描述
  • 由于这个写回主存的时间具有不确定性,因此具有随机性,但是主程序很可能很早就读取了这个成员变量的值,造成了数据安全的问题;
  • 为了避免这个错误,我们可以给程序加一个sychronized关键字,保证程序顺利执行;

为什么可以顺利执行的原因:

  • Java程序在编为字节码文件的时候,代码的执行顺序有可能和写的顺序不一样,也就是不知道什么时候flag会写回主存,所以造成了上面的代码出现卡死;
  • 上面的程序加锁应该给读数据、写数据都要加锁;
  • Sychronized关键字还有一个作用是 确保变量的内存可见性,写数据的时候读数据线程去申请同一个锁,它的工作内存会被设置为无效,然后读线程会重新从主存中加载它要访问的变量到它的工作内存中;
  • 因此就保证了读完之后再写上去程序顺利执行;
public class MyRunnable implements Runnable {
    public boolean flag = false;

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {
    	synchronized (ObjectUtils.objA){
                System.out.println("线程进来执行了!");
        		flag = true;
        }
    }
}
public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread th = new Thread(runnable);
        th.start();
        while (true){
            synchronized (ObjectUtils.objA){
                if(runnable.getFlag()){
                    System.out.println("进来执行了!");
                    break;
                }
            }
        }
        //线程进来执行了!
		//进来执行了!
    }
}
  • 加入sychronized关键字也可以,但是这样一来程序可以顺利执行,但是这样造成效率一定会降低,有没有更好的办法?我们可以引入Volatile关键字;

9、Volatile关键字

1、Volatile关键字是轻量级的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,它保证了 内存的可见性问题
2、 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性;

代码示例:

-----测试类:
public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread th = new Thread(runnable);
        th.start();
        while (true) {
            if (runnable.getFlag()) {
                System.out.println("进来执行了!");
                break;
            }
        }
        //线程进来执行了!
		//进来执行了!
    }
}
----自定义任务类:
public class MyRunnable implements Runnable {
    public volatile boolean flag = false;

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {
        System.out.println("线程进来执行了!");
        flag = true;
    }
}

3、另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性;
4、volatile 关键字:相较于 synchronized 是一种较为轻量级的同步策略;
5、volatile 变量,用来确保将变量的更新操作通知到其他线程;
6、可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的"原子性操作"

为什么不能保证原子性问题

1、首先要了解的是,volatile可以保证可见性和顺序性,这些都很好理解,那么它为什么不能保证原子性呢?
2、首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了;
3、举例:
①线程A首先得到了 i 的初始值100,但是还没来得及修改,就阻塞了;
②这时线程B开始了,它也得到了 i 的值,由于 i 的值未被修改,即使是被volatile 修饰,主存的变量还没变化,那么线程B得到的值也是100;
③之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中;
④根据可见性的原则,这个主存的值可以被其他线程可见;
⑤问题来了,线程A已经读取到了 i 的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存;
所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

10、CAS算法

  • 看这样一个现象:我们创建一个任务,使得多个线程去执行这个任务;
class MyRunnable implements Runnable {
    int piao = 0;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(piao++);
        }
    }
}

public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread th1 = new Thread(runnable, "线程--A");
        Thread th2 = new Thread(runnable, "线程--B");
        Thread th3 = new Thread(runnable, "线程--C");

        th1.start();
        th2.start();
        th3.start();
    }
}

某一次执行结果:
在这里插入图片描述

  • 理论来说,不应该出现重复值,但是由于piao++操作是一个非原子性操作,因此出现了数据安全问题;
  • 如何解决呢?sychronized是一个重量级的,volatile不能保证原子性,这时候需要用到 CAS算法

1、CAS算法概述

  • CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
  • CAS 是一种无锁的非阻塞算法的实现;

2、CAS算法原理

  • CAS 包含了 3 个操作数:需要读写的内存值 V、进行比较的值 A、拟写入的新值 B;
  • 当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作;
  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁;
  • JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

3、图解:

在这里插入图片描述

上面的图可知,两个线程其实就是在比较谁将新值刷回主存更快,你读到的不一定是此时内存中的值;
假如有三个线程,都读取到了主存中的100作为预估值,并进行修改操作,修改为101,但是线程1刷回主存很快,将101写入主存,其他两个线程将预估值和主存中的值一对比,对不上,因此刷回失败;就是在比较谁刷的快;

4、应用
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

  • AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
  • AtomicIntegerArray 、 AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray

我们只需要把变量对应的类型换为上面的类型就好了,这些类型里面比如AtomicInteger 提供的自加、自减操作都可以确保原子性操作;

5、代码示例:
在这里插入图片描述

import java.util.concurrent.atomic.AtomicInteger;

class MyRunnable implements Runnable {
    AtomicInteger piao = new AtomicInteger(0);

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(piao.getAndIncrement());
        }
    }
}

public class MyTest {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread th1 = new Thread(runnable, "线程--A");
        Thread th2 = new Thread(runnable, "线程--B");
        Thread th3 = new Thread(runnable, "线程--C");

        th1.start();
        th2.start();
        th3.start();
    }
}

运行结果:
在这里插入图片描述

11、匿名内部类开启线程

使用匿名内部类更简单的开启线程:
—方式1:

public class MyTest1 {
    public static void main(String[] args) {
        Thread th = new Thread() {
            @Override
            public void run() {
                System.out.println("这是一个线程");
            }
        };
        th.start();
        //这是一个线程
    }
}

—方式2:

public class MyTest1 {
    public static void main(String[] args) {
        Thread th = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是一个线程任务");
            }
        });
        th.start();
        //这是一个线程任务
    }
}

12、线程池

概述

1、程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互;
2、而使用线程池可以很好的提高性能,他本质就是一个存储一定数量线程对象的容器,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池;
3、线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用;
4、在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池;

创建一个线程池

1、 JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法

public static ExecutorService newCachedThreadPool():
根据任务的数量来创建线程对应的线程个数
public static ExecutorService newFixedThreadPool(int nThreads):
固定初始化几个线程
public static ExecutorService newSingleThreadExecutor():
初始化一个线程的线程池

2、这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:

在这里插入图片描述
线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程直到任务完成;get(long timeout,TimeUnit unit)可以设置超时时间;

3、使用步骤:

创建线程池对象
创建Runnable实例
提交Runnable实例
关闭线程池

4、为什么使用线程池

  • 几乎所有需要异步或者并发执行任务的程序都可以使用线程池;

降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。
提供线程可以管理性:可以通过设置合理分配、调优、监控。

5、线程池工作流程

(1)判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务,是->走下个流程。
(2)判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。
(3)判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,是->走下个流程。
(4)按照设置的策略来处理无法执行的任务。

6、代码示例:

import java.util.concurrent.*;

public class MyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程池对象
        ExecutorService pool = Executors.newCachedThreadPool();

        //提交任务,传递一个任务类的参数
        Future<?> future = pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是一个Runnable里面的任务");
            }
        });

        //创建一个任务类的参数
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("这是一个Callable里面的任务");
                return 20;
            }
        };

        //提交任务
        Future<Integer> submit = pool.submit(callable);
        //获取任务的返回值
        System.out.println("Callable里面的任务的返回值:" + submit.get());

        //关闭线程池
        pool.shutdown();

        /*这是一个Runnable里面的任务
        这是一个Callable里面的任务
        Callable里面的任务的返回值:20*/
    }
}

13、定时器

  • 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行;
  • 在Java中,可以通过Timer和TimerTask类来实现定义调度的功能;

Timer:
public Timer()
public void schedule(TimerTask task, long delay):
--------------------安排在指定的时间执行指定的任务
public void schedule(TimerTask task,long delay,long period);
--------------------task - 所要安排的任务。
--------------------delay - 执行任务前的延迟时间,单位是毫秒。
--------------------period - 执行各后续任务之间的时间间隔,单位是毫秒。
public void schedule(TimerTask task, Date time):
public void schedule(TimerTask task, Date firstTime, long period):

TimerTask:定时任务
在这里插入图片描述
public abstract void run()
public boolean cancel()

代码示例1:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;

public class MyTest1 {
    public static void main(String[] args) throws ParseException {
        //创建一个定时器
        Timer timer = new Timer();
        //创建一个定时任务
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("这是一个定时任务!");
            }
        };
        //定时执行一个任务
        timer.schedule(task,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-06-11 0:06:30"));
        //这是一个定时任务!
    }
}

代码示例2:

import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;

public class MyTest2 {
    public static void main(String[] args) throws ParseException {
        //创建一个定时器对象
        Timer timer = new Timer();
        //将定时器传递过去,方便在任务结束后关闭定时器
        MyTimerTsk task = new MyTimerTsk(timer);

        //第一次执行是在当前系统时间后的2s后,以后每隔1s之后执行一次任务
        //等3秒第一次执行任务,以后间隔1秒重复执行定时任务
        timer.schedule(task, 3000,1000);
    }
}

class MyTimerTsk extends TimerTask {

    private Timer timer;

    public MyTimerTsk(Timer timer) {
        this.timer = timer;
    }

    @Override
    public void run() {
        System.out.println("砰~爆炸了!");
        //任务执行完了,取消定时器
        //timer.cancel();可以选择执行一次任务后就关闭定时器
    }
}

定时删除一个文件

import java.io.File;
import java.util.Timer;
import java.util.TimerTask;

public class MyTest3 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        MyTimerTask task = new MyTimerTask(timer);

        //间隔3s之后执行该任务
        timer.schedule(task,3000);
        //删除文件完成
    }
}

class MyTimerTask extends TimerTask {
    private Timer timer;

    public MyTimerTask(Timer timer) {
        this.timer = timer;
    }

    @Override
    public void run() {
        delFile();
        System.out.println("删除文件完成");
        timer.cancel();
    }

    private void delFile() {
        File file = new File("D:\\桌面图标\\练习文件打包");
        delFiles(file);
    }

    private void delFiles(File file) {
        File[] files = file.listFiles();
        for (File f : files) {
            if (f.isFile()) {
                f.delete();
            } else {
                delFiles(f);
            }
        }

        file.delete();
    }
}

14、设计模式

面对对象程序设计原则

单一职责原则(single responsibility principle)

——优化代码的第一步

  • 小明写的图片加载框架中将各个功能拆分,将Image’Loader一分为二,ImageLoader只负责图片的加载逻辑,ImageCache只负责图片的缓存逻辑;这样ImageLoader的代码量少了,逻辑也清晰了
开闭原则(open close principle)

——让程序更稳定、更灵活

  • 软件中的对象(类、模块、函数)应该对扩展是开放的,但是对修改是封闭的。而遵守开闭原则的重要手段应该时通过抽象…;开闭原则指导我们,当软件需要变化时,应该“尽量”通过扩展的方式来实现变化,而不是修改已有的代码; 并不是绝对的,现实场景中会有修改源码的情况。
  • 小明的代码通过依赖注入(set方法)的方式设置缓存类型,比如内存缓存、sd卡缓存、双缓存;ImageLoader中声明Cache的接口类,set方法设置的对象都是Cache的子类;也就是多态;这样修改后,如果后期有其他缓存方式需要修改,完全不需要对源码修改,只需要添加一个新的Cache的实现类就可以,也就是对扩展开放,对修改是封闭的
里氏替换原则(Liskov Substitution Principle)

——构建扩展性更好的系统

  • 依赖继承、多态这两大特性,简单的说就是任何使用父类的地方都可以使用子类代替;
  • 上面小明通过提取Cache接口,使用依赖注入的方法设置缓存类型,传入Cache的子类(内存、sd卡、双缓存)的这种方式就是里氏替换原则。
依赖倒置原则(Dependence Inversion Principle)

——让项目拥有变化的能力

  • 是指一种特定的解耦形式,是指高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。
  • 上面小明通过提取Cache接口,使用依赖注入的方法设置缓存类型,传入Cache的子类(内存、sd卡、双缓存)的这种方式就是里氏替换原则;并且这样的话不会像修改前期那样,在ImageLoader中直接创建ImageCache对象,ImageLoader过度依赖低层次模块(ImageCache);将其抽取成接口,使用依赖注入的方式,这样就是依赖倒置原则。
接口隔离原则(InterfaceSegregation Principles)

——系统有更高的灵活性

  • 定义:不要强制破坏程序实现不想实现的原则;
  • 目的:系统解开耦合,从而容易重构、更改和重新部署
  • 小明设计的ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他一概不管,这就使得缓存功能的具体实现对ImageLoader隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。
迪米特原则(Law of Demeter)

——更好的扩展性

  • 定义:一个类应该对自己需要耦合或者调用的类知道的最少,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖着只需要知道它需要的方法即可,其他的一概不用管。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另外一个类的影响也越大。——高内聚,低耦合
  • 案例:房屋、中介、租客;改前 租客通过中介获取房屋资源,自己筛选符合条件的房屋;修改后租客直接调用中介的找房方法(传输价格和空间条件),中间根据条件找到合适的房屋返给租客就ok;

设计模式概述

  • 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编写、代码设计经验的总结。
  • 使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性以及代码的结构更加清晰.
  • 设计模式分类

创建型模式(创建对象的): 单例模式抽象工厂模式、建造者模式、工厂模式、原型模式;
行为型模式(对象的功能): 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
结构型模式(对象的组成): 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。

简单工厂模式

  • 简单工厂模式概述:举个例子,我现在有一个手机对象,它里面有构造方法也有打电话方法,但是,这样设计不好,又要创建该对象,又要调用功能,应该把创建对象分离出来;
  • 简单工厂模式又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例;
  • 优点: 使用静态工厂模式的优点是实现责任的分割,该模式的核心是工厂类,工厂类含有必要的选择逻辑,可以决定什么时候创建哪一个产品的实例,而客户端则免去直接创建产品的责任,而仅仅是消费产品。也就是说静态工厂模式在不改变客户端代码的情况可以动态的增加产品,明确了类的职责;
  • 缺点:这个静态工厂类负责所有对象的创建,如果有新的对象增加,或者某些对象的创建方式不同,就需要不断的修改工厂类,不利于后期的维护;
class AniamlFactory {
	//私有构造
    private AniamlFactory() {
    }

    public static Animal getAnimal(String name) {
        if ("cat".equals(name)) {
            return new Cat();
        } else if ("dog".equals(name)) {
            return new Dog();
        } else if ("tiger".equals(name)) {
            return new Tiger();
        } else {
            return null;
        }
    }
}

abstract class Animal {
    public abstract void eat();
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}

class Tiger extends Animal {
    @Override
    public void eat() {
        System.out.println("老虎吃肉");
    }
}

public class MyTest {
    public static void main(String[] args) {
        Animal an = AniamlFactory.getAnimal("cat");
        an.eat();
        an = AniamlFactory.getAnimal("dog");
        an.eat();
        an = AniamlFactory.getAnimal("tiger");
        an.eat();
        /*猫吃鱼
        狗吃骨头
        老虎吃肉*/
    }
}

工厂模式

  • 工厂模式概述:该模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现;
  • 优点:客户端不需要在负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性;
  • 缺点: 需要额外的编写代码,增加了工作量;
public class MyTest1 {
    public static void main(String[] args) {
        Animal an = new DogFactory().createAnimal();
        an.eat();
        an = new CatFactroy().createAnimal();
        an.eat();
    }
}

abstract class Animal {
    public abstract void eat();
}

interface BigFactory {
    //创建产品的方法
    Animal createAnimal();
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

class CatFactroy implements BigFactory {
    @Override
    public Animal createAnimal() {
        return new Cat();
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}

class DogFactory implements BigFactory{
    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}

单例模式

  • 保证一个类的对象在内存中只有一个,不能创建很多对象;
  • 首先我们必须私有对象的构造,然后在该对象所在的类内提供静态方法,以便于我们获取该类的对象;

1、懒汉式

public class MyTest {
    public static void main(String[] args) {
        Student student = Student.getStudent();
        Student student1 = Student.getStudent();
        System.out.println(student == student1);
        //true
    }
}

class Student {
    public static Student stu = null;

    private Student() {

    }

    //多线程环境下能保证这个方法的一定是创建一个对象吗?
    //加上synchronized保证多线程环境下的使用也是单列的
    public synchronized static Student getStudent() {
        if (stu == null) {
            stu = new Student();
        }
        return stu;
    }
}

2、饿汉式

public class MyTest1 {
    public static void main(String[] args) {
        Teacher tea1 = Teacher.getTeacher();
        Teacher tea2 = Teacher.getTeacher();
        System.out.println(tea1 == tea2);
        //true
    }
}

class Teacher {
    public static Teacher tea = new Teacher();

    private Teacher() {

    }

    public static Teacher getTeacher() {
        return tea;
    }
}

实际开发中我们使用饿汉式;Java中有一个类,Runtime,他就使用了饿汉式;
在这里插入图片描述
这个类可以执行一些DOS命令;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

public class MyTest2 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        MyTimerTask task = new MyTimerTask(timer);

        timer.schedule(task,2000);
    }
}
class MyTimerTask extends TimerTask{

    private Timer timer;

    public MyTimerTask(Timer timer) {
        this.timer = timer;
    }

    @Override
    public void run() {
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec("calc");
            //关机的命令为:shutdown \s \t 0
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("任务执行完成!");
        timer.cancel();
    }
}

模板设计模式

需求: 计算一个for循环执行的时间

  • 模版设计模式概述:就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现;
  • 优点: 使用模版方法模式,在定义算法骨架的同时,可以很灵活的实现具体的算法,满足用户灵活多变的需求;
  • 缺点: 如果算法骨架有修改的话,则需要修改抽象类;
abstract class CalcClass {
    public void calc() {
        //算法骨架
        long start = System.currentTimeMillis();
        calcMethod();
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "毫秒");
    }

    public abstract void calcMethod();
}

class CalcFor extends CalcClass {
    @Override
    public void calcMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("abc");
        }
    }
}

class CopyFile extends CalcClass {

    @Override
    public void calcMethod() {
        System.out.println("复制文件");
    }
}
-----测试类:
public class MyTest3 {
    public static void main(String[] args) {
        CalcClass js = new CalcFor();
        js.calc();
        js = new CopyFile();
        js.calc();
        /*abc
        abc
        abc
        abc
        abc
        耗时:1毫秒
        复制文件
        耗时:0毫秒*/
    }
}

装饰者设计模式

  • 概述: 装饰模式就是使用被装饰类的一个子类的实例,在客户端将这个子类的实例交给装饰类,是继承的替代方案;
  • 优点:使用装饰模式,可以提供比继承更灵活的扩展对象的功能,它可以动态的添加对象的功能,并且可以随意的组合这些功能。
  • 缺点: 正因为可以随意组合,所以就可能出现一些不合理的逻辑。
interface Phone {
    void call();
}

class BZPhone implements Phone {
    private Phone phone;

    //你要包装哪个类,把那个类传过来
    public BZPhone(Phone phone) {
        this.phone = phone;
    }

    @Override
    public void call() {
        this.phone.call();
    }
}

class GamePhone extends BZPhone {
    public GamePhone(Phone phone) {
        super(phone);
    }

    @Override
    public void call() {
        super.call();
        System.out.println("游戏功能");
    }
}

class IPhone implements Phone {
    public void call() {
        System.out.println("打电话");
    }
}

class MusicPhone extends BZPhone {
    public MusicPhone(Phone phone) {
        super(phone);
    }

    @Override
    public void call() {
        super.call();
        System.out.println("听歌的功能");
    }
}

class VideoPhone extends BZPhone {
    public VideoPhone(Phone phone) {
        super(phone);
    }

    @Override
    public void call() {
        super.call();
        System.out.println("看视频的功能");
    }
}
----测试类:
public class MyTest4 {
    public static void main(String[] args) {
        Phone iPhone = new IPhone();
        iPhone.call();
        System.out.println("=====================");
        MusicPhone musicPhone = new MusicPhone(iPhone);
        musicPhone.call();
        System.out.println("====================");
        VideoPhone videoPhone = new VideoPhone(iPhone);
        videoPhone.call();

        System.out.println("=============================");
        VideoPhone videoPhone1 = new VideoPhone(new MusicPhone(iPhone));
        videoPhone1.call();
        System.out.println("============================");
        MusicPhone musicPhone1 = new MusicPhone(new VideoPhone(iPhone));
        musicPhone1.call();

        System.out.println("====================");
        GamePhone gamePhone = new GamePhone(new MusicPhone(iPhone));
        gamePhone.call();
        System.out.println("===========================");
        GamePhone gamePhone1 = new GamePhone(new MusicPhone(new VideoPhone(iPhone)));
        gamePhone1.call();
        /*打电话
         =====================
         打电话
         听歌的功能
         ====================
         打电话
         看视频的功能
         =============================
         打电话
         听歌的功能
         看视频的功能
         ============================
         打电话
         看视频的功能
         听歌的功能
         ====================
         打电话
         听歌的功能
         游戏功能
         ===========================
         打电话
         看视频的功能
         听歌的功能
         游戏功能*/
    }
}

观察者设计模式

  • 观察者 = 订阅者 + 发布者;
  • 案例: 找猎头找工作

岗位类 求职者 猎头(注册方法,注销方法,发布方法)

import java.util.ArrayList;

public class Hunter {
    //定义两个集合,用来装求职者,和工作岗位
    private ArrayList<JobSeeker> jobSeekers = new ArrayList<>();
    private ArrayList<Job> jobs = new ArrayList<>();

    //注册工作岗位
    public void addJob(Job job) {
        jobs.add(job);
        //工作岗位过来之后,通知求职者
        notifyJobSeeker(job);
    }

    private void notifyJobSeeker(Job job) {
        for (JobSeeker jobSeeker : jobSeekers) {
            System.out.println(jobSeeker.getName() + "你好!有一份工作:" + job.getJobName() + "薪资:" + job.getSal() + "欢迎你前去面试");
        }
    }

    //注册求助者
    public void addJobSeeker(JobSeeker jobSeeker) {
        jobSeekers.add(jobSeeker);
    }

    //注销
    public void removeJobSeeker(JobSeeker jobSeeker) {
        jobSeekers.remove(jobSeeker);
    }

}
public class Job {
    private String jobName;
    private double sal;

    public Job() {
    }

    public Job(String jobName, double sal) {
        this.jobName = jobName;
        this.sal = sal;
    }

    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public double getSal() {
        return sal;
    }

    public void setSal(double sal) {
        this.sal = sal;
    }
}
public class JobSeeker {
    private String name;

    public JobSeeker() {
    }

    public JobSeeker(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class MyTest5 {
    public static void main(String[] args) {
        //观察者 = 订阅者 + 发布者
        JobSeeker zs = new JobSeeker("张三");
        JobSeeker ls = new JobSeeker("李四");
        JobSeeker ww = new JobSeeker("王五");

        //在猎头出注册
        Hunter hunter = new Hunter();
        hunter.addJobSeeker(zs);
        hunter.addJobSeeker(ls);
        hunter.addJobSeeker(ww);

        Job job = new Job("Java开发工程师", 8000);
        hunter.addJob(job);
        System.out.println("=====================");
        Job job2 = new Job("前端开发工程师", 18000);
        hunter.addJob(job2);
        System.out.println("========================");
        //注销
        hunter.removeJobSeeker(ww);
        Job job3 = new Job("运维工程师", 5000);
        hunter.addJob(job3);
    }
}
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值