多线程进阶=>JUC并发编程

Posted dxj1016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程进阶=>JUC并发编程相关的知识,希望对你有一定的参考价值。

多线程进阶=>JUC并发编程

狂神视频JUC并发编程

准备环境:创建maven项目,修改项目配置为8,否则lambdas用不了。修改setting中的java compiler为8。
在这里插入图片描述
在这里插入图片描述

1、什么是JUC

  • JUC就是java.util.concurrent下面的工具包,专门用于多线程的开发。(源码+官方文档 面试高频问)
  • 在 Java 5.0 提供了 java.util.concurrent (简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

在这里插入图片描述
业务:普通的线程代码 Thread
Runable 没有返回值,效率相比Callable相对较低!
在这里插入图片描述
在这里插入图片描述

2、 线程和进程

参考之前的多线程

一句话概括线程和进程
进程是操作系统中的应用程序、是资源分配的基本单位,线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位

2.1、进程

进程: 在操作系统中运行的应用程序

一个进程往往可以包含多个线程,至少包含一个

在这里插入图片描述

2.2、线程

线程: 线程是CPU调度和执行的单位。开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)

  • 对于Java而言:Thread、Runable、Callable进行开启线程的。
  • Java默认有几个线程?2个线程! main线程、GC线程

提问?JAVA真的可以开启线程吗? 开不了的!

Java是没有权限去开启线程、操作硬件的,点开start的原码可以看到这是一个线程安全的,通过add加入线程组, 点开start0()方法发现这是一个native的一个本地方法,底层是C++。所以java是没有权限直接操作硬件的,由操作系统来开启线程。start()和strat0()方法的源码如下:

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	//这是一个C++底层,Java是没有权限操作底层硬件的
    private native void start0();

2.3、并发

并发编程:并发,并行

并发: 多线程操作同一个资源。

  • CPU 只有一核,只能操作一个线程,但是可以模拟出来多条线程。那么我们就可以使用CPU快速交替,来模拟多线程。看起来是同一时间操作多个线程操作同一个资源,其实本质还是一个线程执行,由于速度非常快,所以看起来是多个线程在执行。
  • 并发编程的本质:充分利用CPU的资源!

2.4、并行

并行: 多个人一起行走

  • CPU多核,多个线程可以同时执行。
  • 提高并行效率 我们可以使用线程池

获取cpu的核数

public class Test1 {
    public static void main(String[] args) {
        //获取cpu的核数
        //CPU密集型,IO密集型
                System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

2.5、线程的状态

6种状态,Thread.state查看源码如下:

public enum State {	
	//新生
    NEW,

	//运行
    RUNNABLE,

	//阻塞
    BLOCKED,

	//等待,死死等待
    WAITING,

	//超时等待
    TIMED_WAITING,

	//终止
    TERMINATED;
}

2.6、wait/sleep

1、来自不同的类

wait => Object

sleep => Thread

一般情况企业中使用休眠是:

TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s

2、关于锁的释放

  • wait 会释放锁;

  • sleep睡觉了,不会释放锁;

3、使用的范围是不同的

  • wait 必须在同步代码块中;

  • sleep 可以在任何地方睡;

4、是否需要捕获异常

  • wait和sleep都需要捕获异常
  • notify和notifyall都不需要捕获异常

3、Lock锁(重点)

3.1、传统的 synchronized

package com.marchsoft.juctest;

import lombok.Synchronized;

/**
 * Description:synchronized
 * 基本的卖票例子
 * 真正的多线程开发,公司中的开发
 * 线程就是一个单独的资源类,没有任何附属的操作
 * 1、属性,方法。
 **/

public class Demo01 {
    public static void main(String[] args) {
    //并发:多线程操作同一个资源类
        final Ticket ticket = new Ticket();
    //new Runnable()看源码可以发现他是一个@FunctionalInterface函数式接口,接口不能实例化,但是可以用匿名内部类实现他,但是这个比价繁琐,所以在jdk1.8,可以使用lambdas表达式(参数)->{代码}实现。
    /*
       new Thread(new Runnable(){
              @Override
              public void run(){
              
              }                     
          }).start();
    */
        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"C").start();
    }
}
// 资源类 OOP 
class Ticket {
     //属性、方法
    private int number = 30;

    //卖票的方式
    public void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
        }
    }
}

  • 代码开启了三个线程
  • 运行结果出现了不符合实际情况,卖出的票跟剩余的票和总数不对应,原因是多个线程可能争夺同一个资源
    在这里插入图片描述
  • 改变上面的代码,在sale方法加上synchronized,可以解决上述问题。
  • synchronized 本质:队列,锁
  • 这个锁的是对象或者class
    在这里插入图片描述

3.2、Lock(重点)

Lock接口

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

公平锁: 十分公平,必须先来后到~;

非公平锁: 十分不公平,可以插队;(默认为非公平锁)

package com.marchsoft.juctest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

package demo02;


import jdk.nashorn.internal.ir.CallNode;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test1 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源类
        Ticket2 ticket = new Ticket2();
       /*
        new Runnable()看源码可以发现他是一个@FunctionalInterface函数式接口,接口不能实例化,
        但是可以用匿名内部类实现他,但是这个比价繁琐,
        所以在jdk1.8,可以使用lambdas表达式(参数)->{代码}实现。
        */
       /*
       new Thread(new Runnable(){
              @Override
              public void run(){

              }
          }).start();
       */
        new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"A").start();
        new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"B").start();
        new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"C").start();
    }
}
// 资源类 OOP
// 不再使用synchronized实现多线程,而是使用Lock锁。
/*lock三部曲
1、new ReentrantLock();
2、lock.lock();
3、finally=>lock.unlock();
*/

class Ticket2 {
            //属性、方法
            private int number = 30;
            Lock lock = new ReentrantLock();
            //卖票的方式
            public  void sale() {
//                执行之前加锁,执行完毕解锁
                lock.lock();
//try-catch-finally:Ctrl+Alt+T
                try {
//                   业务代码
                    if (number > 0) {
                        System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//解锁
                }
            }
        }



3.3、 Synchronized 与Lock 的区别

  • Synchronized 内置的Java关键字,Lock是一个接口
  • Synchronized 无法判断获取锁的状态,Lock可以判断
  • Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!如果不释放锁,可能会遇到死锁
  • Synchronized 线程1(获得锁->阻塞)、线程2(一直等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
  • Synchronized 是可重入锁,不可以中断的,非公平的;Lock是接口,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
  • Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

锁是什么,如何判断锁的是谁?

4、生产者和消费者问题

面试的:单例模式、排序算法、生产者消费者问题,死锁问题。

4.1、Synchronzied 版本

package pc;
/**
 * 线程之间的通信问题:生产者和消费者问题
 * 线程交替执行 A B操作同一个变量 num=0
 * A num+1
 * B num-1
 */
public class ConsumeAndProduct {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}
//判断等待,业务,通知
class Data {//数字,资源类
    private int num = 0;
//只要是并发编程一定会有锁。
    // +1
    public synchronized void increment() throws InterruptedException {
        // 判断等待
        if (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 +1 执行完毕
        this.notifyAll();
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
        // 判断等待
        if (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 -1 执行完毕
        this.notifyAll();
    }
}
/*
执行结果:
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
 */

4.2、存在问题(虚假唤醒)

问题,如果有四个线程,会出现虚假唤醒
两个生产者线程,两个消费者线程,会虚线虚假唤醒,

  • if判断流水线状态为空时,线程被阻塞,这时if判断就完成了,线程被唤醒后直接执行线程剩余操作。
  • while判断流水线状态为空时,线程被阻塞,这时的while循环没有完成,线程被唤醒后会先进行while判断,然后执行后面的操作。
  • if只判断了一次,等待队列的多生产者被消费者唤醒后重新进入同步队列,这时候抢到锁,会从if下面的语句开始执行,相当于没了判断。
  • 为什么if改成while之后就不会出现虚假唤醒问题了,因为while会一直判断,线程没有唤醒就出不来,比消费者唤醒生产者,其它2两个线程都会争夺锁,如果其中一个生产者抢到锁了,那么他会从等待位置醒来,while的时候,就会先循环判断条件,如果不满足条件才会出来,否则还会继续等待,但是if就不一样了,if判断一次就结束了,所以从wait醒来的时候,早就过了if判断语句,因此他会执行wait方法下面的操作。

image-20200810224629273
在这里插入图片描述

虚假唤醒:就是当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功。

解决方式 ,if 改为while即可,防止虚假唤醒

package com.marchsoft.juctest;

package pc;
/**
 * 线程之间的通信问题:生产者和消费者问题
 * 线程交替执行 A B操作同一个变量 num=0
 * A num+1
 * B num-1
 */
public class ConsumeAndProduct {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}
//判断等待,业务,通知
class Data {//数字,资源类
    private int num = 0;
//只要是并发编程一定会有锁。
    // +1
    public synchronized void increment() throws InterruptedException {
        // 判断等待
        while (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 +1 执行完毕
        this.notifyAll();
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
        // 判断等待
        while (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 -1 执行完毕
        this.notifyAll();
    }
}
以上是关于多线程进阶=>JUC并发编程的主要内容,如果未能解决你的问题,请参考以下文章

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段

JUC并发编程 线程运行原理 -- 多线程 & 上下文切换

JUC并发编程 -- synchronized 原理进阶之轻量级锁 & 锁膨胀

JUC并发编程总结复盘

juc多线程编程学习

并发编程的基石——AQS类