并发编程

281 阅读23分钟

image.png

image.png

Concurrency与Parallellism( 并发与并行)

并行

是指两个或者多个事件在同一时刻发生;
是在多台处理器上同时处理多个任务
是在不同实体上的多个事件

并发

是指两个或多个事件在同一时间间隔发生。
并发是在同一实体上的多个事件。
并发是在一台处理器上“同时”处理多个任务。

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

image.png

Process与Thread (进程与线程)

说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义

线程是CPU调度和执行的的单位。

线程的创建

线程创建有四种方式 (Thread,Runnable,callable,线程池创建)

Thread

继承Thread类,重写run方法,创建线程对象 调用start()方法执行


public class TestThread1 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("哈哈{"+i+"}");
        }
    }

    public static void main(String[] args) {

        //创建线程对象
        TestThread1 testThread1 = new TestThread1();

        //调用satrt()执行
        testThread1.start();
        //主线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("嘻嘻{"+i+"}");
        }
    }
}

继承Thread类

子类继承Thread类具备多线程能力 启动线程:子类对象. start()

不建议使用:避免OOP单继承局限性

Runnable

实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类。调用start方法。


public class TestThread2 implements Runnable{
    @Override
    public void run() {
        //run 方法线程体
        for (int i = 0; i < 100; i++) {
            System.out.println("哈哈{"+i+"}");
        }
    }

    public static void main(String[] args) {
        //创建runnable的实现类
        TestThread2 testThread2 = new TestThread2();

        //创建线程对象,通过线程对象来开启我们的线程,代理
        new Thread(testThread2).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("嘻嘻{"+i+"}");
        }
    }
}

实现Runnable接口

实现接口Runnable具有多线程能力 启动线程:传入目标对象+Thread对象.start()

推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

Callable


public class TestCallable implements Callable<Boolean> {
    
        @Override
        public Boolean call() throws Exception {
 
            return true;
        }
    
        public static void main(String[] args) {
            //创建Callable对象实例
            TestCallable t1 = new TestCallable();
            TestCallable t2 = new TestCallable();
            TestCallable t3 = new TestCallable();
            //创建执行服务 执行3个
            ExecutorService executorService = Executors.newFixedThreadPool(3);
    
            //提交请求
            executorService.submit(t1);
            executorService.submit(t2);
            executorService.submit(t3);
    
            //关闭服务
            executorService.shutdown();
        }
    }
    
1.实现Callable接口,需要返回值类型2.重写call方法,需要抛出异常
3.创建目标对象
4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool()
5.提交执行:Future<Boolean> result1 = ser.submit(t1);
6.获取结果:boolean r1 = result1.get()
7.关闭服务:ser.shutdownNow();

线程池

程序的运行,本质:占用系统的资源!优化资源的使用!=> 池化技术

线程池的好处:

1、降低资源的消耗

2、提高响应的速度

3、方便管理。

线程复用、可以控制最大并发数、管理线程

image.png

三大方法

线程的创建规范(阿里巴巴开发手册):

image.png

Executors.newSingleThreadExecutor(); //单线程

只有一个线程会执行任务

Executors.newFixedThreadPool(N); //创建固定线程

有固定的线程数 执行任务,运行N个并发

Executors.newCachedThreadPool(); //可伸缩的

根据线程数来决定,遇强则强,遇弱则弱

七大参数

深入三大方法的源码

    //newSingleThreadExecutor
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    //newFixedThreadPool   
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
   //newCachedThreadPool 
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    

我们可以发现三大方法 new ThreadPoolExecutor() 这个对象

我们再深入这个对象,会发现7大参数

  public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                              int maximumPoolSize,//最大核心线程池大小
                              long keepAliveTime,//超时了没人调用你就会释放
                              TimeUnit unit,//超时单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//线程工程,创建线程的,一般不动
                              RejectedExecutionHandler handler//拒绝测略) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

理解如下图

用银行办理业务模拟线程池的执行 (正常情况下) image.png

假如人流量突然增加了很多 image.png

人流量过后

image.png

参数名称解释说明理解说明
int corePoolSize核心线程池大小每天固定的默认开启的银行窗口
int maximumPoolSize最大线程数,控制资源人流量过多所开启的窗口
long keepAliveTime释放空闲线程(空闲线程 = maximumPoolSize - corePoolSize)浏览量过后多久关闭窗口
TimeUnit unit时间单位时间单位
BlockingQueue workQueue阻塞队列银行候客区(可指定数量)
ThreadFactory threadFactory线程创建工厂,创建线程的,一般不动默认不管
RejectedExecutionHandler handler拒绝测略当银行爆满,如何处理人流

工作顺序

1、线程池创建,准备好core数量的核心线程,准备接受任务

2、新的任务进来,用core 准备好的空闲线程执行。

(1)、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队
列获取任务执行

(2)、阻塞队列满了,就直接开新线程执行,最大只能开到 max指定的数量

(3)、max都执行好了。Max-core数量空闲的线程会在keepAliveTime,指定的时间后自
动销毁。最终保持到core大小

(4)、如果线程数开到了max的数量,还有新任务进来,就会使用reject 指定的拒绝策
略进行处理

3、所有的线程创建都是由指定的 factory创建的。

根据理解 自定义线程池

// 创建了一个自定义线程池
  ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,
                10,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.AbortPolicy() //默认的淘汰策略
        );

四种拒绝策略

CallerRunsPolic

哪来的线程回哪去

AbortPolicy

如果队列满了,抛出 RejectedExecutionException 异常

关于 RejectedExecutionException 异常

什么时候会发生这个异常

1.线程池关闭以后,再次提交任务

2.提交线程的数量大于最大线程数+任务队列中排队的个数 (workQueue+max)
  注意:提交线程的数量大于正在处理业务的数量 才会抛出该异常

DiscardPolicy

队列满了,丢掉任务,不会抛出异常!

DiscardOldestPolicy

队列满了,尝试去和最早的竞争,也不会抛出异常

线程池中的四种工作队列

ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。

LinkedBlockingQueue

是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue

是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue

一个具有优先级的无限阻塞队列。

小结

线程池最大连接数如何定义?

1、CPU密集型,几核,就是几,可以保持cpu的效率最高!
获取cpu几何
Runtime.getRuntime().availableProcessors()

2、IO密集型。>判断你程序中十分耗IO的线程,
程序15个大型任务io十分占用资源!

实现接口和继承Thread类比较

接口更适合多个相同的程序代码的线程去共享同一个资源。
接口可以避免java中的单继承的局限性。
接口代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现 Runable或 Callable接口的线程,不能直接放入继承Thread的类

注意:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

Runnable和callable接口比较

相同点:
两者都是接口;
两者都可用来编写多线程程序;
两者都需要调用Thread.start()启动线程;

不同点:
实现Callable接口的线程能返回执行结果;
而实现Runnable接口的线程不能返回结果;
Callable接口的cal()方法允许抛出异常;
而Runnable,接口的run()方法的不允许抛异常;
实现Callable接口的线程可以调用Future.cancel取消执行,而实现Runnable.接口的线程不能

注意点:
callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,
此方法会阻塞线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!


线程的状态

/**
* 线程的状态有6个
*/
 public enum State {
          
       // 新生
        NEW,

       // 运行
        RUNNABLE,

       // 阻塞
        BLOCKED,

       // 等待  死死的等待
        WAITING,

       // 超时等待 过期不候
        TIMED_WAITING,

       // 终止
        TERMINATED;
    }


    

线程的生命周期

image.png

新建
new关键字创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值

就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
JVM为线程创建方法柱和程序计数器,等待线程调度器调度

运行
就绪状态的线程获得cPU资源,开始运行run()方法,该线程进入运行状态

阻塞
当发生如下情况时,线程将会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式I0方法,在该方法返回之前,该线程被阻塞
线程试图获得一个同步锁(同步监视器)﹐但该同步锁正被其他线程所持有。线程在等待某个通知( notify)
程序调用了线程的 suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

死亡
线程会以如下3种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束。
线程抛出一个未捕获的 Exception或 Error 。
调用该线程stop)方法来结束该线程,该方法容易导致死锁,不推荐使用。

线程安全问题

什么是线程安全

如果有多个线程同时运行同一个实现了Runnable,接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。

线程安全问题根本原因

多个线程在操作共享的数据;

操作共享数据的线程代码有多条;

多个线程对共享数据有写操作;

多线程下集合类不安全的措施

单线程下List集合类是完全ok的,但在多线程下可能就不太ok了;

public class ListTest {

	public static void main(String[] args) {

		List<String> list = new ArrayList<>();
        
		for (int i = 0; i < 30; i++) {
			new Thread(()->{
				list.add(UUID.randomUUID().toString().substring(0,5));
				System.out.println(list);
			},String.valueOf(i)).start();
		}

	}

}

运行上面代码发现会报 java.util.ConcurrentModificationException错,并发修改异常,会报错才是正常的,原因是因为当一条线程对 list 修改时,另一条线程进来了,先将调用list.add()方法,把modConut 版本号修改了,上一条线程比对版本号时不相等,所以快速失败了;

三种解决方法

第一种:使用 List<String> list = new Vector<>()
Vector(jdk1.0)出来的版本要比List(jdk1.2)早,Vector在所有方法都很粗暴的加了synchronize来保证线程安全;

第二种:使用List<String> list = Collections.synchronizedList(new ArrayList<>());
这个方法是集合工具类Collections提供的,其实它只是在集合类上简单封装了一下,
加了synchronized代码块

第三种:使用 List<String> list1 = new CopyOnWriteArrayList<>();
这个是并发包下提供的,按照字面上翻译就是写的时候复制,用了锁+数组拷贝+volatile实现的
意思是读的时候随便读,写的时候先加锁,然后复制一份出来写,写完再将复制出来的一份替换掉原来的那一份
这样就不会影响到原来的list

还有Map和Set的集合类,Collections集合工具类和并发包下也提供的对应线程同步的方法,原理和List是一模一样!

线程同步

针对多线程对写数据的操作,Java引入了7种线程同步机制

以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。为了保证每个经程都能正常执行共享资源操作。

同步代码块(synchronized)

同步方法(synchronized)

Lock(锁)

image.png

创建一把锁 Lock lock =new ReentrantLock(true)

image.png

true:公平锁,多个线程都有执行权,先来后到

false:非公平锁,独占锁,线程独占 只有一个线程可以获取到,可以插队(默认)

读写锁(readWriteLock)

读可以被多线程同时读,写的时候只能有一个线程去写

/**
 * 自定义缓存 不加锁
 */
class MyCache{
    private volatile Map<String,Object> map = new HashMap<>();

    public void put(String key,Object value){
        System.out.println(Thread.currentThread().getName()+"写入"+key);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+"写入成功");
    }

    public void get(String key){
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取成功");
    }
}

运行


    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(()->{
                myCache.put(String.valueOf(finalI),finalI);
            },String.valueOf(i)).start();

        }
    }

image.png

问题:写入的时候被插队了

加上读写锁

/**
 * 自定义缓存 加上读写锁
 */
class MyCache2{
    private volatile Map<String,Object> map = new HashMap<>();
    //读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //写入的时候只希望一个线程写
    public void put(String key,Object value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入成功");
        } finally {
            readWriteLock.writeLock().unlock();
        }

    }

    public void get(String key){
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取成功");
    }
}

运行


    public static void main(String[] args) {
        MyCache2 myCache = new MyCache2();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(()->{
                myCache.put(String.valueOf(finalI),finalI);
            },String.valueOf(i)).start();

        }
    }

image.png

解决了写入被插队的问题

特殊域变量(volatile)

Volatile 是Java虚拟机提供轻量级的同步机制

1,保证可见性

在理解可见性的前提是得先了解 java内存模型JMM

java内存模型(JMM)

JMM是一种不存在的东西,他是一个规范,一种约定,一个概率

关于JMM的一些同步约定:

1、线程解锁前,必须把共享变量`立刻`刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中
3,保证加锁和解锁是同一把锁

JMM内存模型8操作操作图解:

image.png

 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

1. lock     (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
2. unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

3. read    (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
4. load     (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

5. use      (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值
就会使用到这个指令
6. assign  (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

7. store    (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
8. write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

1. 不允许readload、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

3. 不允许一个线程将没有assign的数据从工作内存同步回主内存

4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。
就是怼变量实施use、store操作之前,必须经过assign和load操作

5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

6. 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前
必须重新load或assign操作初始化变量的值

7. 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存

问题:线程B修改了值,但线程A不可见!如何解决? 问题体现:

/**
 * 当前有2个线程
 * 主线程 与 子线程
 */
public class Dome {
    //定义了 公共变量 a = 0
    private static Integer a = 0;
    public static void main(String[] args) throws InterruptedException {
        //开启一个子线程
        new Thread(()->{
            //在子线程里判断 a
            while (a==0){
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        //在主线程里修改 a
        a = 1;
        System.out.println(" a ="+a);
    }
}

image.png

解决: 加上volatile关键字在公共变量上

  private volatile static Integer a = 0;

image.png

所以得出 volatile 保证可见性这一结论

2, 不保证原子性

原子性:要么同时成功,要么同时失败,在操作的时候不可被改动

体现 volatile 不保证原子性

public class Dome2 {
    private volatile static Integer num = 0; //这里我们已经添加了volatile关键字
    
    public static void add(){
        num++;
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("num = "+num);
    }
}

image.png

那如何不加锁,保证原子性呢? 使用原子类Atomic来解决原子性问题

public class Dome2 {
    //原子类的 Integer
    private  static AtomicInteger num = new AtomicInteger();

    public static void add(){
        num.getAndIncrement(); //加1操作
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("num = "+num);
    }
}

image.png

3, 禁止指令重排

volatile可以避免指令重排:因为内存屏障。CPU指令。

作用:

1、保证特定的操作的执行顺序!

2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

Volatile是可以保持可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生

局部变量(ThreadLocal)

阻塞队列( LinkedBlockingQueue)

原子变量( Atomic )

Synchronized和 Lock区别 (6点区别)

synchronized是 java内置关键字,在jvm层面
Lock是个java类;

synchronized无法判断是否获取锁的状态
Lock 可以列断是否获取到锁;

synchronized 会自动释放锁(a线程执行完同步代码会释放锁﹔b 线程执行过程中发生异常会释放锁),
Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

使用synchronized关键字的两个线程1和线程2,
如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去
而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

synchronized的锁可重入、不可中断、非公平
Lock锁可重入、可判断、可公平(两者皆可)

synchronized锁适合代码少量的同步问题。
Lock锁适合大量同步的代码的同步问题

阻塞队列(BlockingQueue)

image.png

什么情况下我们会使用阻塞队列:多线程并发处理,线程池!

阻塞队列结构图 如下: image.png

学会使用队列 添加、移除

四组API

创建阻塞队列 ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(队列大小);

image.png

同步队列(SynchronousQueue)

put存,take取 SynchronousQueue<> synchronousQueue= new SynchronousQueue<>();

同步队列和其他的BLockingQueue 不一样, SynchronousQueue不存储元素put了一个元素,必须从里面先take取出来,否则不能在put进去值!

线程死锁

什么是死锁

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题就是死锁。 所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待)﹐若无外力作用,这些进程都将无法向前推进。

死锁产生的四个必然条件

互斥条件

进程要求对所分配的资源(如打印机〉进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

不可剥夺条件

进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

请求与保持条件

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程此时请求进程被阻塞,但对自己已获得的资源保持不放。

循环等待条件

存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求即存在一个处于等待状态的进程集合{PlI, P2,…, pn},其中pi等待的资源被Pi+1)占有〈 i=0,1,… , n-1),Pn等待的资源被Po占有,如图所示。

image.png

死锁处理

预防死锁

通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。

1.破坏“互斥”条件
“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。

2.破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。
即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:
一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
方法二:
要求每个进程提出新的资源申请前,释放它所占有的资源。
这样,一个进程在需要资源s时,须先把它先前占有的资源R释放掉,然后才能提出对s的申请,即使它可能很快又要用到资源R。

3.破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:
如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,
如果有必要,可再次请求这些资源和另外的资源。
方法二:
如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。
只有在任意两个进程的优先级都不相同的条件下,方法二才能防死锁。

4.破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,
但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

避免死锁

在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。

检测死锁

允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。

解除死锁

当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

线程通讯

为什么要线程通信

多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。

线程通讯方式

线程间通信常用方式如下:

Object 的 wait、notify、notifyAll

Object本地的final方法,无法被重写

wait:使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用
      当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态
notify:唤醒当前线程
notifyAll:唤醒所有线程

notify/notifyAll() 的执行只是唤醒沉睡的线程,并不会释放锁

wait 与 sleep的区别

1、来自不同的类
wait => Object
sleep => Thread

2、关于锁的释放
wait 会释放锁
sleep 睡觉了,抱着锁睡觉,不会释放!

3、使用的范围是不同的
wait 必须在同步代码块中使用
sleep 可以在任何地方使用

image.png

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

因为JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。 如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。

简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象

Condition 的await、signal、signalAll

    Object和Condition体眠唤醒区别:
    object wait()必须在synchronized(同步锁)下使用
    object wait()必须要通过nodify()方法进行唤醒

    condition await()必须和Lock(互斥锁/共享锁)配合使用
    condition await()必须通过signa1()方法进行唤醒

CountDownLatch (减法计数器)

用于某个线程A等待若干个其他线程执行完之后,它才执行

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        //指定线程数
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            int finalI = i;
            new Thread(()->{
                System.out.println("当前线程数 " + finalI);
                //线程数 -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();

        }
        //等计数器归零,然后再向下执行 否则 阻塞在这
        countDownLatch.await();

        System.out.println(" 线程执行完毕 " );
    }
}

countDownLatch.countDown(); 数量减1

countDownLatch.await(); 等计数器归零,然后再向下执行

每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行!

CyclicBarrier(加法计数器)

一组线程等待至某个状态之后再全部同时执行

public class CyclicBarrierTest {
    public static void main(String[] args) {
        //指定线程任务数量,当线程数量完成后 再开启一个新线程运行其他任务
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("所有任务执行完毕");
        });

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            new Thread(()->{
                System.out.println("当前线程任务" + finalI);

                try {
                    cyclicBarrier.await();  //等待 6个线程任务 执行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();

        }
    }
}

Semaphore (信号量)

用于控制对某组资源的访问权限


public class SemaphoreTest {
    public static void main(String[] args) {
        // 设定3个可通过的线程数量
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i <= 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire(); //得到权力
                    System.out.println(Thread.currentThread().getName()+"获得到通过权");
                    TimeUnit.SECONDS.sleep(2); //等待2秒
                    System.out.println(Thread.currentThread().getName()+"取消通过权");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release(); //等待2秒,释放权力
                }
            },String.valueOf(i)).start();
        }
    }
}

ForkJoin(分支合并)

ForkJoin在JDK1.7 ,并行执行任务!提高效率。在大数据量情况下!

image.png

ForkJoin 特点就是工作窃取 因为是双端队列,所以可以窃取

image.png

CompletableFuture 异步编排

实现了 Future

Future的类图结构 image.png Future 最大特点就是可以获得异步的结果,所以 CompletableFuture是可以有返回值的

创建异步对象(有四种)

runAsync:

CompletableFuture c = CompletableFuture.runAsync(Runnable runnable) 无返回值,无需线程池

CompletableFuture c = CompletableFuture.runAsync(Runnable runnable,executors) 无返回值,需要线程池

supplyAsync:

CompletableFuture c = CompletableFuture.supplyAsync(Runnable runnable) 有返回值,无需线程池

CompletableFuture c = CompletableFuture.supplyAsync(Runnable runnable,executors) 有返回值,需要线程池


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 创建线程池
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5,
                Runtime.getRuntime().availableProcessors(),
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );


        /**
        * 创建了一个有返回值 需要线程池的异步任务
        */
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println("异步线程 ---------> 运行处理");
            int a = 10/5;
            return a;
        },threadPoolExecutor)


        System.out.println(" 主线程 " );
        System.out.println(completableFuture.get()); 
        //completableFuture.get() 获取异步任务的执行结果,会进行阻塞,直到异步任务执行完成
    }

completableFuture.get()

获取异步任务的执行结果,会进行阻塞,直到异步任务执行完成

异步回调

whenComplete

whenComplete需要一个强加的消费型函数接口,需要传入2个参数 t,u

t:completableFuture的返回结果,如果没有返回结果则为null

u:completableFuture的异常信息,如果没有错误则为null

.whenComplete((t,u)->{
            System.out.println("t:"+t);
            System.out.println("u:"+u);
        })
        

exceptionally

exceptionally 需要一个供给型函数接口

异常捕获器,如果没有异常将不会触发,有错误的话就可以进行捕获并return

.exceptionally((e)->{
            System.out.println("错误信息为:"+e.getMessage());
            return 0;
            })

线程串行化

thenRunAsync 需要等待上一步任务完成

thenAcceptAsync 可以获取到上一步结果,没有返回值

多线程特性

多线程编程要保证满足三个特性:原子性、可见性、有序性。 原子性:保证所有操作都执行,不可被打断,要么都不止行

可见性:线程之间访问同一个变量时,要避免脏读现象。对单线程来说这个不存在

有序性:程序执行按照代码先后执行