java8_并行与并发

并行与并发

并发:多个任务可以在重叠的时间段内运行。
在这里插入图片描述

并行:多个任务可以同时运行。

在这里插入图片描述

1.基本概念

1.1 程序/进程/线程

程序:静态单元

进程:是执行程序的一次过程(动态),持有资源(共享内存,共享文件和线程),系统资源分配的单位 —— idea eclipse QQ

线程: CPU调度和执行的单位 线程是系统中最小的执行单元/同一进程中有多个线程/线程共享进程的资源 —— idea 源代码文本编辑、源代码编译、文本校验

​ 很多线程是模拟出来的(类似同时在做),真正的多线程是指有多个CPU,即多核,如果模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以有同时执行的错觉。

进程:班级 线程:班里的学生 学生是班级的最小单位,构成班级,共享桌椅等资源 互斥与同步

(1)线程是独立的执行路径

(2)在程序运行时,即使没有自己创建线程,后台也会有多个线程,例如:主线程,gc线程

(3)main()称之为主线程,为系统的入口,用于执行整个程序。

(4)在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统有关的,先后顺序不能人为干预。

(5)对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制。

(6)线程会带来额外的开销,如CPU调度时间,并发控制开销。

(7)每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2.线程创建

2.1 继承Thread类

继承Thread类,重写run()方法,调用start()开启线程。

    //继承Thread类
    public class MyThreadDemo1 extends Thread{    
        public void run(){  
            //dosomthing
        }
    }

    // 1.创建自定义线程实例 ——> 2.启动线程
    new MyThreadDemo1().start();

2.2 实现Runnable接口

实现Runnable接口,重写run()方法,执行过程需要放入Runnable接口实现类,调用start()方法开启线程。

    //1.重写run()方法,将Runnable交给一个线程 ——> 2.启动线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            //doSomthing
        }
    }, "lemon").start();

Q:为什么调用start()方法开启线程,不是run()方法?

(1)start()用于开启一个线程,使其处于就绪状态,一旦CPU得到时间片,就可以开始执行run()方法。

(2)run()是在线程里的,只是线程里的一个函数。

如果直接调用run()方法,其实就相当于调用了一个普通函数,必须等run()执行完,才会去执行其他,执行路径还是只有一条。

Q:为什么要用代理模式?目的:在不改变原有代码的前提下,实现一些其他功能

​ 代理模式分为静态代理和动态代理,两种代理从虚拟机加载类的角度来讲,本质上都是一样的,都是在原有类的行为基础上,加入多出的一些行为,甚至完全替代原有行为。

从静态代理的使用上来讲:

1.代理类一般持有一个被代理对象的引用

2.对于我们不关心的方法,全部委托给被代理对象处理,自己处理我们关心的方法

优势:在不改变目标对象的前提下,可以通过代理对象对目标对象功能扩展

缺点:代理对象只服务于一种类型的对象,如果要服务多类型的对象,必须要为每种对象都进行代理,静态代理在程序规模稍大的时候无法胜任。

java动态代理:通过反射来实现代理

2.3实现Callable接口

(重写call()方法,使用FutureTask—— Runnable和Fututre)

		//方式3.实现Callable接口  FutureTask类实现了RunnableFuture接口(Future + Runnable),
        //1)创建FutureTask实例,创建MyCallable实例
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {//call()方法有返回值 run()方法没有
                //doSomthing
                return "MyCallable接口执行完成!";
            }
        });
		//FutureTask指定的泛型需要与Callable接口的泛型一致,FutureTask实现了Runnable接口,因此可以将其传到Thread类中,通过Thread开启线程,执行task任务
        //2)创建Thread实例,执行FutureTask ——> 启动线程
        new Thread(futureTask,"lemon-Callable").start();
        //3)获取并打印执行结果
        try {
            String result = futureTask.get();
            System.out.println("执行结果是---"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

2.4使用线程池创建

(使用Executor框架)

	    //方式4:使用线程池创建线程
        //1)使用Executors获取线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //2)通过线程池对象获取线程并执行Runnable接口
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //doSomthing
            }
        });

3.线程的生命周期及常用方法等

3.1 线程生命周期

在这里插入图片描述

(1)新建状态

​ 线程对象被创建后,就进入了新建状态。此时它和其他Java对象一样,仅仅由Java虚拟机分配了内存,并初始化其成员变量值。

(2)就绪状态

​ 也被称为“可执行状态”。**线程对象被调用了该对象的start()方法,该线程处于就绪状态。**Java虚拟机会为其创建方法调用栈和程序计数器。处于就绪状态的线程,随时可能被CPU调度执行,取决于JVM中线程调度器的调度。

(3)运行状态

​ 线程获取CPU权限进行执行。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。线程只能从就绪状态进入到运行状态。

(4)阻塞状态

​ 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。**直到线程进入就绪状态,才有机会转到运行状态。**阻塞的情况分三种:
​ a.调用wait(),让线程等待某工作的完成,使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)。
​ b.线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
​ c.通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

(5)死亡状态

​ 线程执行完了,因异常退出了run()方法或者直接调用该线程的stop()方法(容易导致死锁,现在已经不推荐使用),该线程结束生命周期。对于已经死亡的线程,无法再使用start方法令其进入就绪。

​ 处于Running或者Blocked状态的线程都有可能变成终止状态,原因类似如下:

​ a.线程运行正常结束

​ b.程序运行异常终止

​ c.JVM意外终止

3.2 常用方法

(1)线程休眠 — sleep():模拟延时,放大问题发生的可能性,要抛异常。每个对象都有一把锁,sleep不会释放锁。

​ 使正在执行的线程暂停,进入休眠等待状态,将CPU让给别的线程。(可以理解为:线程工作到一半休息了会,但它所占的资源并不会交还,因为线程在Sleep的时候可能是处于同步代码块的中间位置,如果此时把锁放弃,就违背了同步的语义,所以sleep时并不会放弃锁,等过了sleep时长后,可以确保后面的逻辑还在同步执行)

注:休眠时间结束,线程会返回到就绪状态,而不是立即开始运行。

(2)线程礼让 — yield()   运行状态 —> 就绪状态

​ 让当前正在执行的线程暂停,但不阻塞,等待CPU重新调度。(礼让不一定会成功,看CPU调度)

		new Thread(()->{
            for (int i = 0; i<10; i++){
                System.out.println("线程A -- > " + i);
                if (i == 2){
                    Thread.yield();//在多线程中意味着本线程愿意放弃CPU资源,也就是可以让出CPU资源。不过这只是给CPU一个提示,当CPU资源并不紧张时,则会无视yield提醒。
                }
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i<10; i++){
                System.out.println("线程B -- > " + i);
            }
        }).start();

和sleep()方法有点相似,都会使当前正在运行的线程暂停,**区别在于yield()方法不会阻塞线程,它只是将线程转换成就绪状态,**让系统的调度器重新调度一次,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。在实际中用的比较少。

(3)线程插队(合并线程 ) — join()

​ 当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被Join()方法加入的线程执行完后它才会继续。

​ 我们用它能够实现并行化处理。比如主线程需要做两件没有相互依赖的事情,那么可以起 A、B 两个线程分别去做。通过调用A、B 的 join 方法,让主线程 block 住,直到 A、B 线程的工作全部完成,才继续走下去。

//常用方法join():join()调用后,block的是调用线程,而不是被调用线程。
public class JoinClient {
    public static void main(String[] args) throws InterruptedException {
        Thread backendDev = createWorker("backed dev","backend coding");
        Thread frontendDev = createWorker("fronted dev","frontend coding");
        Thread tester = createWorker("tester","testing");//前后端开发完成之后,再开始测试工作

        backendDev.start();
        frontendDev.start();

//        //主线程会被block,直到backendDev和frontendDev线程都执行结束,才往下继续执行。
//        backendDev.join();
//        frontendDev.join();

        tester.start();

    }
    public static Thread createWorker(String role,String work){
        return new Thread(() -> {
            System.out.println("I finished "+ work + " as a "+ role);
        });
    }
}

(4)currentThread方法:静态方法,用于获取当前线程的实例。

Thread.currentThread();

拿到线程的实例后,可以获取Thread名称:

Thread.currentThread().getName();

此外,还可以获取线程ID:

Thread.currentThread().getId();

(5)setPriority方法:setPriority(int newPriority)

​ 此方法用于设置线程的优先级。每个线程都有自己的优先级数值,当 CPU 资源紧张的时候,优先级高的线程获得 CPU 资源的概率会更大。

​ 优先级范围:1~10,main()默认为5

优先级只是意味着调度概率的高低,并不是优先级低就不会被调用了。(CPU调度)

3.3 守护线程

​ 守护线程 : 后台操作日志,监控内存,垃圾回收等。

​ 守护线程进入Terminated状态有个特殊的方式:当JVM没有任何一个非守护线程时,所有守护线程都会进入Terminated状态,JVM退出。

​ 在 Java 中,当没有非守护线程存在时,JVM 就会结束自己的生命周期,而守护进程也会自动退出。守护线程一般用于执行独立的后台业务。比如 JAVA 的垃圾清理就是由守护线程执行。而所有非守护线程都退出了,也没有垃圾回收的需要了,所以守护线程就随着 JVM 关闭一起关闭了。

​ 当你有些工作属于后台工作,并且你希望这个线程自己不会终结,而是随着 JVM 退出时自动关闭,那么就可以选择使用守护线程。

​ **要实现守护线程只能手动设置,在线程 start 前调用 setDaemon 方法(默认为false——用户线程 可以设为true —— 守护线程)**Thread 没有直接创建守护进程的方式,非守护线程创建的子线程都是非守护线程。

4.线程安全问题

Q:什么是线程安全?

​ 当多个线程调用访问某个方法时,不论你通过怎样的调用方式,或者线程如何交替执行,在主线程中不用做任何同步,这个类的结果行为都是设想的正确。

​ 但往往是:多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。

注:并发编程的三大特性

(1)原子性:某系列的操作步骤要么全部执行,要么都不执行。

**例如:**变量的i-- 分为三个步骤:从内存中读取出变量 i 的值 —> 将 i 的值减1 —> 将减1后的值写回内存。因此,i-- 或 i++不具备原子性,因为有可能当某个线程执行到了第2步时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。

​ 竞态条件是指,在多线程的情况下,由于多个线程执行的时序不同。第 2、3 步操作依赖于第1步的检查,而第一步的检查结果并不能保证在执行 2、3 步的时候依旧有效。这是因为其它线程可能在你在执行完第一步时已经改变了剩余次数。此时 2,3 步依旧会按照已经失效的检查结果继续执行,那么线程安全问题就出现了。

如果在需要保证原子性的一组操作中,有竞态条件产生,那么就会出现线程安全的问题。我们可以通过为原子操作加锁或者通过原子变量来解决。

(2)可见性:一个线程对变量进行了修改,另一个线程能够立刻读取到此变量的最新值。

a.线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。

b.不同线程无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

cpu数据访问如下:CPU 会先从主存中复制数据到缓存,CPU 在计算的时候就可以从缓存读取数据了,在计算完成后再把数据从缓存更新回主存。这样在计算期间,就无须访问主存了,速度大大提升。

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
  ShowVisibility showVisibility = new ShowVisibility();
  Thread thread = new Thread(showVisibility);
  thread.start();
  //给线程启动的时间
  Thread.sleep(500);
  //更新flag
  showVisibility.flag = true;
  System.out.println("flag is true, thread should print");
  Thread.sleep(1000);
  System.out.println("I have slept 1 seconds. I guess there was nothing printed");
}
private static class ShowVisibility implements Runnable{
  //运算时把flag从主存中拿到了自己线程的缓存中,此后就一直从缓存中读取flag的值,即使main线程修改了flag的值,缓存中的并未更新
  private Boolean flag = false;
  @Override
  public void run() {
      while (true) {
          if (flag) { //flag为true——打印信息
              System.out.println(Thread.currentThread().getName()+":"+flag);
          }
      }
  }
}

(3)有序性:有序性是在多线程的情况下,确保 CPU 不对我们需要保证顺序性的代码进行重排序的。我们可以通过 sychronized 或者 volatile 来确保有序性。

代码在执行阶段,并不一定和编写顺序一致

​ 指令重排序:CPU 为了提高运行效率,可能会对编译后代码的指令做一些优化,这些优化不能保证 100% 符合你编写代码在正常编译后的顺序执行。但是一定能保证代码执行的结果和按照编写顺序执行的结果是一致的。指令重排序的优化,仅仅对单线程程序确保安全。如果在并发的情况下,程序没能保证有序性,程序的执行结果往往会出乎我们的意料。

例:

​ 线程A执行的代码中的最后一行:initialized = true 有可能重排序到了 processConfig方法调用的前面执行。这就意味着:配置信息还未成功初始化,但是initialized变量已经被设置成true了。那么就导致线程B的while循环“提前”跳出,拿着一个还未成功初始化的配置信息去干活(doSomethingWithConfig方法)

线程A:
在这里插入图片描述
线程B:
在这里插入图片描述

5.线程同步(解决方式)

5.1 什么是线程同步?

​ 线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团,而不是同时进行操作。线程同步的目的就是避免线程“同步”执行,线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏,避免发生线程安全问题。 (线程安全就是指程序按照你的代码逻辑执行,并始终输出预定的结果)

5.2 何时需要线程同步?

​ 线程同步发生在**多个线程操作同一份资源 **—— 并发时

注意:多线程&&共享资源&&变量(需要修改的)

5.3 实现方式

**(1)Synchronized:**把共享的资源操作放在Synchronized定义的区域内,便为这些操作加了同步锁。

​ 在 synchronized 代码块中的代码在多线程中会同步执行,同步执行的意思就是——排队。(对共享资源的访问我们要保证同步,否则就会出现问题。)

​ synchronized 作用域中的代码为同步执行的,也就是并发的情况下,执行到对同一个对象加锁的synchronized 代码块时,为串行执行的。synchronized 可以确保可见性,在一个线程执行完synchronized 代码后,所有代码中对变量值的变化都能立即被其它线程所看到。由于 synchronized 关键字会使得代码串行执行,这就丧失了多线程的优势。并且 synchronized 关键字的使用也有相应成本。所以我们代码中能不用 synchronized 就不用。当不得不用的时候,需要尽量控制 synchronized 代码块中的代码行数。

锁对象,是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时会将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。

​ 同步代码块/同步方法:

​ 小括号里的对象是可以是任意的对象。这个对象相当于是同步代码块的看门人,每个对其 synchronized 的线程,它都会记录下来,然后等到同步代码块没有线程执行的时候,它就会通知其它线程来执行同步代码块。

同步代码块中的锁对象可以是任意类型的对象,但多个对象线程共享的锁对象必须是唯一的。

//方式一:同步代码块
	private Object obj = new Object();//创建一个锁对象 充当锁的钥匙(同步代码块使用)

    @Override
    public void run() {
        while (true){
            synchronized (obj){ //代码块 都会去拿obj,但obj只有一个,只有一个线程可以拿到,代码执行完,才会释放obj
                if(ticketNum > 0){
                    //有票,让线程睡眠10毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //打印当前售出的票数和线程名,电影票数减一
                    String name = Thread.currentThread().getName();
                    System.out.println("线程:" +name+"销售电影票:"+ticketNum--);
                }
            }

        }
    }

	// 方式二:同步方法
    @Override
    public void run() {
        while (true){
            saleTicket();//调用同步方法
        }
    }

    //同步方法与同步代码块类似,也是有一个锁对象
    // 若同步方法是static的,则锁对象对当前所在类的类对象(Ticket.class);若不是静态方法,则锁对象是调用当前方法的对象实例(this)
    private synchronized void saleTicket(){
        if(ticketNum > 0){
            //有票,让线程睡眠10毫秒
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //打印当前售出的票数和线程名,电影票数减一
            String name = Thread.currentThread().getName();
            System.out.println("线程:" +name+"销售电影票:"+ticketNum--);
        }
    }

注:

1、选用一个锁对象,可以是任意对象;
2、锁对象锁的是同步代码块,并不是自己;
3、不同类型的多个 Thread 如果有代码要同步执行,锁对象要使用所有线程共同持有的同一个对象
4、需要同步的代码放到大括号中。需要同步的意思就是需要保证原子性、可见性、有序性中的任何一种或多种。不要放不需要同步的代码进来,影响代码效率。

**(2)volatile :**使变量在多个线程间可见,不保证原子性,修饰的变量不会被指令重排序优化。

a. volatile修饰符相当于告诉虚拟机该域可能会被其他线程更新,强制每个线程取主存中更新的值,能够实现可见性。

b. volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

c. volatile关键字修饰的变量不会被指令重排序优化。

​ volatile变量固然方便,但是存在着限制,volatile修饰的变量,并不能保证是原子操作的,所以多处理器操作数据时,会导致数据重复。所以volatile关键字通常被当作完成、中断的状态的标识使用。

synchronized与volatile实现同步的比较

(1)volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法。

(2)volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

(3)synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

**(3)Lock:**锁。在javaSE5.0中新增了一个java.util.concurrent包来支持同步。需要者可根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。ReentrantLock类是可重入(重复进入)、互斥、实现了Lock接口的锁,它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。

​ ReentrantLock 的设计思想是通过 FIFO 的队列保存等待锁的线程。通过 volatile类型的 state 保存锁的持有数量,从而实现了锁的可重入性。而公平锁则是通过判断自己是否排队成功,来决定是否去争抢锁。

公平锁和非公平锁:

​ synchronized 是非公平锁,也就是说每当锁匙放的时候,所有等待锁的线程并不会按照排队顺去依次获得锁,而是会再次去争抢锁。ReentrantLock 相比较而言更为灵活,它能够支持公平和非公平锁两种形式。只需要在声明的时候传入 true。

lock():在获取锁时,如果拿不到,会一直处于等待状态,直到拿到锁。

trylock():有Boolean返回值,如果没有拿到返回,就返回false,停止等待。

使用锁时,必须在finally中调用unlock()释放锁。

	//方式三:同步锁
    // 创建锁对象,必须是lock子类
    private Lock lock = new ReentrantLock(true);//是否为公平锁 true--是公平锁(多个线程都公平拥有执行权) false--非公平(独占锁),只有一个线程能拿到锁,false是默认值

    @Override
    public void run() {
        while (true){
            lock.lock(); //lock()与unlock()是必须成对调用的,否则会出现死锁
            try{
                if(ticketNum > 0){
                    //有票,让线程睡眠10毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //打印当前售出的票数和线程名,电影票数减一
                    String name = Thread.currentThread().getName();
                    System.out.println("线程:" +name+"销售电影票:"+ticketNum--);
                }
            }finally { //无论try里面执行成功与否,都必然会执行finally
                lock.unlock();//解锁
            }

        }
    }

synchronized与Lock的对比

(1)Lock是显示锁(需要手动开启和关闭)synchronized是隐式锁,出了作用域会自动释放

(2)Lock只有代码块锁,synchronized有代码块锁和方法锁

(3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

(错误,java 1.8对synchronized做了大量优化,两者相差无几,根据业务场景选择)

5.4 死锁

​ 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块中同时拥有“两个以上对象的锁”时,就可能会发生“死锁”问题。

产生死锁的四个必要条件:

(1)互斥条件:一个资源每次只能被一个进程使用。

(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3)不剥夺条件:进程已获得的资源,在未使用完成之前,不能进行剥夺。

(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

6 线程通信

(1)阻塞与非阻塞:强调的是程序在等待调用结果(消息,返回值)时的状态.

阻塞:线程挂起,不做任何事情,等待

非阻塞:不会等待,继续做别的事情,会检测loop,看任务线程是否完成

阻塞与非阻塞是相对于线程是否被阻塞。

(2)同步与异步:强调的是消息通信机制(相对于操作结果来说,是否等待结果返回

同步:主动请求并等到I/O操作完毕(走到这必须等到一个结果,可以loop检测)

异步:主动请求数据后便继续处理其他任务,随后等待I/O操作完毕的通知(子线程会自己主动告知已经做完)

生产者消费者模式:这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件:

(1)对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。

(2)对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费

(3)在生产者消费者问题中,仅有synchronized是不够的

​ a. synchronized可阻止并发更新同一个共享资源,实现了同步

​ b. synchronized不能用来实现不同线程之间的消息传递(通信)

Java提供了几个方法解决线程之间的通信问题:均是Object类的方法,调用者都是同步锁对象,都只能在同步方法或者同步代码块中使用,否则会抛出异常。

方法名作用
wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

​ 从概念上初步了解 wait/notify。原本 RUNNING 的线程,可以通过调用 wait 方法,进入BLOCKING 状态。此线程会放弃原来持有的锁。而调用notify 方法则会唤醒 wait 的线程,让其继续往下执行。

​ 在多线程开发中最基本的同步方式就是通过synchronized 关键字来实现。其实 synchronized 所使用的对象,只是用来记录等待同步操作的线程集合。他相当于一位排队管理员,所有线程都要在此排队,并接受他的管理,他说谁能进就可以进。另外他维护了一个 wait set,所有调用了 wait 方法的线程都保存于此。一旦有线程调用了同步对像的 notify 方法,那么wait set 中的线程就会被 notify,继续执行自己的逻辑。

在这里插入图片描述

​ 这也解释了为什么 synchronized 的对象并不一定是共享资源对象。这个对象只是看门人,确保同步代码块中的代码只有一个线程能够进入执行,但这个看门的工作并不一定要共享资源对象来做。任何对象都可担当此工作。

​ 需要注意的是,我们对哪个对象做了 synchronized 操作,那么就只能在同步代码块中使用此对象进行 wait 和 notify 的操作。这也很好理解,只有当看门人在听你讲话时,他才能按你的要求去做事情。我们只有获得了和同步对象的对话权,这个对象才能听此线程的命令。无论是请求加入 wait set 还是要通知 wait set 中的线程出来,均是如此。

A线程中:执行一段逻辑后将A线程放入punishment对象的wait set中,并且A线程会释放持有的锁。

synchronized (punishment){
//do something
punishment.wait();
//continue to do something
}

B线程:执行此段代码,会notify在punishment对象的wait set中的一个线程,将其弹出。例如此时A线程在wait set中,那么A线程将被弹出。被被弹出的 A 线程会在获取 CPU 资源后继续执行 wait 方法后面的逻辑。

synchronized (punishment){
//do something
punishment.nofity();
//continue to do something
}

notifyAll():唤醒所有在此对象的wait set 上的线程。而获得锁的线程是否真的需要做什么工作是由自己控制的。

(如果学生线程先抢到 CPU 资源,但是由于作业列表为空,他又会选择 wait 进入 wait set。此时他会释放锁。而老师线程此时会获得锁,在看到作业列表为空后,则会添加新的作业。通过 wait/notifyAll 让多个线程交互,同时通过共享资源的状态,各线程控制自己的逻辑。这样的程序称之为状态驱动程序。也就是说是否真的执行逻辑,是由状态值所决定的。如果状态不满足,即使被 notify 了,也会再次进入 wait set。)

关于wait set

  1. 每个对象除具有关联的监视器外,还具有关联wait set。
  2. 首次创建对象时,其wait set为空。将线程添加到wait set中或从wait set中删除线程的基本操作是原子的。wait set完全通过方法操作Object.wait,Object.notify和Object.notifyAll
  3. 所有的对象都会有一个wait set,用来存放调用了该对象wait方法之后进入block状态线程
  4. 线程被notify之后,不一定立即得到执行
  5. 线程从wait set中被唤醒顺序不一定是FIFO,关于顺序,JVM规范中并没有给出,各个虚拟机的厂商有各自的实现
  6. 线程被唤醒后,必须重新获取锁,但是JVM内部会进行地址恢复,直接继续上次线程后续的逻辑

•Sleep() VS wait()

1.首先 sleep 方法是Thread类中的静态方法,他的作用是使当前线程暂时睡眠指定的时间,可以不用放在synchronized方法或者代码块中,但是 wait 方法是Object类的方法,它是使当前线程暂时放弃监视对象的使用权进行等待,必须要放在synchronized方法或者代码块

2.调用线程的sleep 方法当前线程会让出CPU给其他线程执行,但不会释放锁,依然占用锁的使用权,但是 wait方法会释放锁。

3.sleep方法到睡眠指定时间后会自动回复到可运行状态,但是wait方法需要另外一个拥有锁的对象调用 notify 方法进行唤醒,否则将一直挂起。

7 Future

​ 之前我们讲解的 Thread 和 runnable,实现多线程的方式是新起线程运行 run 方法,但是 run 方法有个缺陷是没有返回值,并且主线程也并不知道新的线程何时运行完毕。

​ Future 持有要运行的任务,以及任务的结果。主线程只要声明了 Future 对象,并且启动新的线程运行他。那么随时能通过 Future 对象获取另外线程运行的结果。

​ Future:未来结果,代表线程任务执行结束后的结果,获取线程执行结果的方式是通过get()方法获取的。

FutureTask<String> cookTask = new FutureTask<String>(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(3000); //休眠3秒
        return "事件1完成!!";
    }
});
        //Java 8 lambda表达式简化代码
//        FutureTask<String> cookTask = new FutureTask<String>( ()->{
//            Thread.sleep(3000); //休眠3秒
//            return "事件1完成!!";
//        } );


LocalDateTime time1 = LocalDateTime.now(); //记录此刻时间
System.out.println("准备完成事件1......");
new Thread(cookTask).start();

System.out.println("准备完成事件2......");
Thread.sleep(2000); //休眠2s
System.out.println("事件2完成!!");

//(1)若cookTask运行的线程已经结束了,可直接取到运行结果
//(2)若还没有执行结束,则主线程阻塞,直到能取得运行结果
// -- 采用了多线程并发,所以执行时间应该等于耗时最长的那个任务,若为单线程串行执行,则是两者之和。
String Lunch = cookTask.get();

//        //get(Long,TimeUnit):阻塞固定时长,如果在阻塞时长范围线程未执行结束,则抛出异常
//        String Lunch = null;
//        try {
//            Lunch = cookTask.get(500, TimeUnit.MILLISECONDS);
//        } catch (TimeoutException e) {
//            e.printStackTrace();
//            System.out.println("超时...");
//        }
System.out.println("得到结果:" + Lunch );

LocalDateTime time2 = LocalDateTime.now();

System.out.println(" 一共花费了"+ Duration.between(time1,time2).toMillis() / 1000 +"秒!");

​ FutureTask的构造方法中传入了Callable的实现。

​ Thread构造方法需要传入Runnable的实现,而FutureTask实现是实现了Runnable接口的。FutureTask的"run()"方法实际执行的是Callable的call()方法。

​ 注:我们应该在真正需要使用Future对象的返回结果时再去调用get()方法,这样才能充分利用并发的特性来提升程序性能。

​ Future是一个接口,而FutureTask是他的实现。FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable与Future。(这也是为什么它能作为参数传入Thread构造方法的原因)

在这里插入图片描述

方法作用
get():V阻塞等待线程执行结束,并得到结果
get(Long,TimeUnit):V阻塞固定时长,等待线程执行结束后的结果,如果在阻塞时长范围线程未执行结束,则抛出异常
isDone()查看线程是否结束,任务是否完成,call()执行完线程就结束
cancel(boolean):boolean用于尝试取消任务,参数表示是否中断执行中的线程,true-中断
isCancelled():boolean返回任务在完成前是否已经被取消

8 线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

好处:

(1)提高响应速度(减少了创建新线程的时间)

(2)降低资源消耗(重复利用线程池中的线程,不需要每次都创建)

(3)便于线程管理

​ 如:corePoolSize:核心池的大小

​ maximumPoolSize:最大线程数

​ keepAliveTime:线程没有任务时最多保持多长时间后会终止

9.1 Executor && ExecutorService && Executors

1.Executor:线程池的顶级接口

​ 定义方法:void execute(Runnable) 用于处理任务的一个服务方法:调用者提供Runnable接口的实现,线程池通过线程执行这个Runnable,服务方法无返回值,因为Runnable接口中的run()方法无返回值。

2.ExecutorService 是Executor接口的子接口(JDK5.0起提供了线程池相关的API :ExecutorService 和 Executors)

​ 提供了一个新的服务方法:submit() 有返回值Fututre,submit()方法提供了overload()方法,其中:参数类型为Runnable的,不需要提供返回值;参数类型为Callable,可提供线程执行后的返回值

(1)ExecutorService:真正的线程池接口。常见的子类:ThreadPoolExecutor

​ void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable

​ Future submit(Callable task):执行任务,有返回值,一般用来执行Callable

​ void shutdown():关闭连接池

线程池状态:

Running:正在执行中的活动状态

ShuttingDown:正在关闭状态,“优雅关闭”:一旦进入这个状态,线程池不会再接收新的任务,处理所有已接收的任务,处理完毕后,关闭线程池

Terminated:已经关闭状态

3.Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

​ 为Executor线程池提供工具方法,可以快速提供若干种线程池,如固定容量的、无限容量的、容量为1的各种线程池。

​ 线程池是一个进程级的重量级的资源,默认生命周期和JVM一致,当开启线程后,只到JVM关闭为止,是其默认的生命周期,如果手动调用shutdown()方法,那么线程池执行所有任务后,自动关闭。

(1)FixedThreadPool:容量固定的线程池,即活动状态和线程池容量有上限的线程池。所有线程池中都有一个任务队列,使用的是BlockingQueue作为任务载体,当任务数量大于线程池数量的时候,没有运行的任务就保存在任务队列里,当线程有空闲时,自动从队列中取出任务执行。

​ 默认容量上限:Integer.MAX_VALUE

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  

(2)CachedThreadPool:缓存线程池:如果线程池中的线程数量不满足任务执行,创建新的线程。每次有新任务无法及时处理时,都会创建新线程。当线程池中的线程空闲时长达到一定临界值(默认60s)时,自动释放线程。

​ 应用场景:内部应用或者测试应用,测试的时候,尝试得到硬件或软件的最高负载量,用于提供FixedThreadPool容量指导。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

(3)ScheduledThreadPool:定长线程池,用于定时/周期完成线程任务

	ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
	scheduledThreadPool.scheduleAtFixedRate(new Runnable() {  
	public void run() {  
 	System.out.println("delay 1 seconds, and excute every 3 seconds");  
	}  
}, 1, 3, TimeUnit.SECONDS);//要执行的任务,第一次执行任务的延时,多次任务执行的间隔,时间单位

(4)SingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

	ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  

(5)newWorkStealingPool(这个是在jdk1.8出来的)会更加所需的并行层次来动态创建和关闭线程。它同样会试图减少任务队列的大小,所以比较适于高负载的环境。同样也比较适用于当执行的任务会创建更多任务,如递归任务。适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中

8.1 Fork/Join框架

​ ForkJoinPool 自 Java 7 引入。它和 ThreadPoolExecutor 都继承自 AbstractExecutorService,实现了 ExecutorService 和 Executor 接口。ForkJoinPool 用来把大任务切分为小任务,如果切分完小任务还不够小(由你设置的阈值决定),那么就继续向下切分。经过切分后,最后的任务是金字塔形状,计算完成后向上汇总。

在这里插入图片描述

​ 如果一个任务足够小,那么执行任务逻辑。如果不够小,拆分为两个独立的子任务。子任务执行后, 取得两个子任务的执行结果进行合并。

​ ForkJoinPool 通过 submit 执行 ForkJoinTask 类型的任务。ForkJoinTask 是抽象类,有着不同的子类实现。比较常用的是如下两种:
1、RecursiveAction,没有返回值;
2、RecurisiveTask,有返回值。
​ 此外 submit 方法还可以执行 Callable 和 Runnable 的接口实现。

例:计算1~10000的和,将任务拆分为100个,每个任务计算100个数字之和。

public class Task extends RecursiveTask<Integer> {
        private static final int THRESHOLD = 100; //递归任务大小力度为100
        private int from;
        private int to;
        public Task(int from, int to) {
            super();
            this.from = from;
            this.to = to;
        }
        @Override
        protected Integer compute() {//重写compute()
            if (THRESHOLD > (to - from)) {//判断任务的大小在范围内,若已经在,则进行计算
                return IntStream.range(from, to + 1)
                        .reduce((a, b) -> a + b)
                        .getAsInt();
            } else {//否则继续拆解任务
                int forkNumber = (from + to) / 2;
                Task left = new Task(from, forkNumber);
                Task right = new Task(forkNumber + 1, to);
                left.fork(); //fork操作:将当前任务放入线程池中执行
                right.fork();
                return left.join() + right.join();//join取得执行结果进行合并
            }
        }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();//通过静态方法生命一个ForkJoinPool
        ForkJoinTask<Integer> result = forkJoinPool.submit(new Task(1,10000));//通过submit提交Task,调用Task中的compute(),通过对任务的拆解以及对任务计算结果的合并,得出1~10000的和

        System.out.println("计算结果为:"+result.get());//通过Task的get()方法获取计算结果
        forkJoinPool.shutdown();//关闭线程池
    }

​ ForkJoinPool 中的每个线程都维护自己的工作队列。这是一个双端队列,既可以先进先出,也可以先进后出。简单来说就是队列两端都可以做出队操作。当每个线程产生新的任务时(比如说调用了fork 操作),会被加入到队尾。线程工作的时候会从自己维护的工作队列的 top 做出队操作(LIFO),取得任务来执行。线程还会去其它线程任务队列窃取任务,此时是从其它队列的 base取得任务(FIFO)。如下图所示:
在这里插入图片描述

fork():、fork 方法中会判断如果当前线程不是 ForkJoinWorkerThread,则把任务加入 submission queue。否则加入自己的工作队列中。submission queue (提交队列)没有关联的线程,是所有线程都可以执行的任务队列。

join():join()方法中,自己任务没有执行完,则取得自己任务队列中的队列执行,如果发现在即的任务已经没有了,则会去窃取其他线程的任务来执行。

​ ForkJoinPool通过任务窃取,使得任务的执行更为高效。

​ ForkJoinPool 为我们拆分大任务再汇总小任务计算结果提供了很好的支持。它很适合执行计算密集型的任务。但是如果你的任务拆分逻辑比计算逻辑还要复杂,ForkJoinPool 并不能为你带来性能的提升,反而会起到负面作用。因此需要结合自己的场景来选择使用。

9 Java8 并行流

​ 并发设计是指将一个任务分解为多个能同时运行的独立操作,换言之,并发应用程序由若干独立执行的进程组成。如果存在多个处理单元,就可以并行的实现这些并发任务,但是性能是否会因此提升将视情况而定。

对于java而言,并行在默认情况下将任务分解为多个子任务,每个子任务被分配给通用fork/join线程池并执行,最后将所有结果合并在一起。

​ 在Java8中,请求并行流只需进行一次方法调用。

9.1 将顺序流转换为并行流

​ 默认情况下,Java创建的流是顺序流,可使用Collection接口定义的stream或parallelStream,也可使用BaseStream接口定义的sequential或parallel方法创建顺序流或者并行流。

(1)创建并行流/顺序流:isParallel方法可以判断流是否采用并行方式执行。

		Boolean isParallelStream1 = Stream.of(2,3,5,4).isParallel();//判断是否为并行流
        System.out.println(isParallelStream1);//false

        List<Integer> numbers = Arrays.asList(3,4,2,1);
        Boolean isParallelStream2 = numbers.parallelStream().isParallel();//parallelStream默认返回并行流
        System.out.println(isParallelStream2);//true

        Boolean isParallelStream3 = Stream.of(2,3,5,4).parallel().isParallel();//现有流上使用parallel()返回并行流
        System.out.println(isParallelStream3);//true

        Boolean isParallelStream4 = Stream.of(2,3,5,4).sequential().isParallel();//sequential()返回顺序流
        System.out.println(isParallelStream4);//false

(2)并行流到顺序流的切换

例:将所有数字倍增,然后排序

​ 由于倍增是无状态且无关联的,可采用并行操作,然后排序本质上是属于顺序操作。用peek()方法显示进行处理的线程名称,结果为main线程完成了所有处理。流在到达终止表达式之前不做任何操作,即在到达终止表达式之前才会评估流的状态

        List<Integer> numbers = Arrays.asList(3,1,4,3,6);
        List<Integer> nisParallel = numbers.parallelStream() //请求并行流
                .map(n -> n * 2)//倍增
                .peek(n -> System.out.println(Thread.currentThread().getName()+"线程 -- "+n))//打印线程名
                .sequential()//转为顺序流
                .sorted()
                .collect(Collectors.toList());
        System.out.println(isParallelStream5);

​ 如果确有必要以并行方式处理部分流,而已顺序方式处理流的其他部分,建议使用两个单独的流。

​ (3)Stream API 可以方便的将顺序流转为并行流,不过性能提升与否需要视情况而定。在java8中,并行流默认使用通用fork/join线程池来分发任务,无论是将任务分解为多个子任务,还是将所有子任务的结果合并为最终输出,都会为fork/join线程池引入管理开销。

​ 为了使额外的开销物有所值,应在满足以下要求时再使用并行流:

​ a.数据量较大

​ b.每个元素的处理比较耗时

​ c.数据源易于分解

​ d.操作是无状态且无关联

(4)默认情况下,通用线程池大小等于JVM上可用的处理器数量,它由Runtime.getRuntime().availableProcessors()确定。

将通用线程池大小设置为大于可用的CPU核心数无助于性能的提升。

		// 检查 CPU 多少核?
        System.out.println("CPU核数: "+Runtime.getRuntime().availableProcessors());

        //设置全局fork/join池线程数,但是一般没用到,Parallelism标志用于指定并行级别,为非负整数。
        System.setProperty("java.util.concurrent.ForkJoinPool.common.Parallelism","8");


		//自定义ForkJoinPool:ForkJoinPool类定义了一个构造函数,他传入一个整数作为并行级别。我们可以借此创建有别于通用线程池的自定义线程池,并将任务提交给它
		ForkJoinPool pool = new ForkJoinPool(15); //实例化一个大小为15的ForkJoinPool
        ForkJoinTask<Long> task = pool.submit( //提交Callable<Long>作为任务
                () -> LongStream.rangeClosed(1,3_000_000)
                .parallel()
                .sum());

        Long total = 0L;
        try {
            total = task.get();//执行并等待回复
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }
        System.out.println("pool size:"+pool.getPoolSize()); //打印pool size:15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值