并发编程基础汇总

文章目录


前言


随着计算机发展,一直存在一个核心矛盾,即**CPU、内存、IO设备的速度差异**。这三者的执行速度由快到慢依次为,CPU>内存>IO设备,为平衡这三者的速度差异,做出了如下三方面的努力:
  • CPU增加缓存,均衡与内存的速度差异
  • 操作系统增加进程、线程,以分时复用CPU,均衡与IO设备的速度差异
  • 编译程序对指令执行次序进行优化,更加高效的利用缓存

 

一、并发编程三要素

1.定义


  • 可见性:

    线程修改共享变量的结果,对其他线程可见

  • 原子性:

    一个或多个操作在CPU执行过程中不会被中断,要么全执行,要么全不执行

  • 有序性

    程序按照代码先后顺序执行

2.可见性


2.1 可见性问题

 在多核CPU中,不同的处理器执行不同的线程,每个处理器为了提高处理的效率,增加了独有的高速缓存,减少了与内存的交互。
 在特殊情况下,高速缓存与内存的变量存在不一致的情况,如下图所示,当线程1执行c+1操作,修改了变量c的值为1,但还未同步至内存时,线程2获取内存中的c=0,执行c+1操作时就出现了缓存不一致问题

 

在这里插入图片描述

2.2 可见性问题根源

 由上述分析可知,出现可见性问题的根源为,CPU高速缓存与内存间的缓存不一致问题
 

3.原子性


3.1 原子性问题

 为了提高CPU的执行效率,在等待IO处理时,允许CPU进行线程切换,即重新选择一个线程来执行,如下图所示。

 

在这里插入图片描述

 在编程语言中,一条语句可能会编译成多条CPU指令,即编程语言的语句与实际执行的CPU指令并非一一对应的关系,在多条指令执行的过程中,若出现了CPU调度线程切换,就会出现原子性问题。
 

3.2 原子性问题根源

 由上述分析可知,出现原子性问题的根源为,执行多个CPU指令时发生线程切换
 

4.有序性


4.1 有序性问题

 编译器为了提高执行效率,会对语句进行编译优化,即改变程序的执行顺序,但不改变程序最终执行的结果。 如new Object():

语句执行顺序如下:
分配内存------在内存上初始化对象-------引用地址赋值

经过编译器优化后:
分配内存------引用地址赋值----------------在内存初始化对象

 

4.2 重排序分类

  • 编译器重排序
    如果语句没有先后依赖关系,那么为了优化性能,编译器可以重新调整语句的执行顺序。

  • CPU指令重排序
    在指令级别,让没有依赖关系的多条指令并行执行。

  • CPU内存重排序
    CPU有自己的缓存指令的执行顺序和写入主内存的顺序没有完全一致。

 

4.3 有序性问题根源

 由上述分析可知,出现有序性问题的根源为,编译优化带来的指令重排序
 

二、并发底层解决方案(Java内存模型)

1.定义


Java内存模型(JMM)从使用者(程序员)的角度来看,可以理解为解决并发问题的工具,按需对指令重排序(有序性问题)、线程切换(原子性问题)、CPU缓存(可见性问题)进行禁用。

 

2.volatile


2.1 语义

 volatile主要用于解决变量可见性问题

  • 禁止CPU缓存
    写变量时立即刷新至主内存,其他线程缓存失效
  • 禁止重排序
    插入特定类型的内存屏障指令
  • 单个volatile变量的读写具有原子性
    如count++这类操作不具备原子性

2.2 作用范围

 修饰变量

 

3.synchronized


3.1 语义

 synchronized主要用于解决原子性问题,保证同一时间只能有一个线程访问,即禁用线程切换,本质是通过对临界区上锁,保护临界区内的共享资源。

  • 禁止CPU缓存
    加锁时从主内存取,解锁时刷新到主内存
  • 禁止线程切换
    线程间互斥

3.1 作用范围

  • 修饰代码块
    被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象

  • 修饰方法
    被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象

  • 修饰静态的方法
    其作用的范围是整个静态方法,作用的对象是这个类的所有对象

 

4.final


4.1 语义

 final修饰的内容在初始化完成后不可变,只要保证构造方法中未出现对象逸出(将this赋值给其他变量),则final是线程安全的

  • 禁用CPU缓存
    final 变量会被缓存在寄存器中而不需要去内存获取

  • 初始化可见性
    同时为保证线程安全,1.5以后对final关键字进行了增强,保证final变量的写先于final对象的读先于final变量的读,这一约束解决了之前存在的构造方法溢出的问题(构造方法不为原子,其他线程可能读取到初始化一半的对象)

4.2 作用范围

  • 修饰类
    final 修饰类的时候代表这个类是不可以被扩展继承的,例如 JDK 里面的 String 类。

  • 修饰方法
    final 修饰方法的时候代表这个方法不能被子类重写。

  • 修饰变量
    final 修饰变量的时候,这个变量一旦被赋值就不能再次被赋值。

 

5.Happens-Before


5.1 定义

  Happens-Before是一套规则,定义了Java程序运行顺序的标准,意为前一个操作对后一个操作是可见的,或者前一个操作先行发生于后一个操作发生

5.2 程序次序原则

 在同一个线程内,程序执行的先后顺序按照代码顺序执行

5.3 传递性原则

 如果A先于B,B先于C,那么A先于C

5.4 volatile 变量规则

 volatile变量的写入先于volatile变量的读取,由于volatile变量在写入时会直接写入内存,故读取时可以保证读取到最新的数据

5.5 final变量规则

 final变量的写入先于final所在对象的读取先于final对象的读

5.6 锁定解锁规则

 锁的解锁先于锁的加锁,由于解锁后会强制刷新主存,下一次加锁时获取的是最新的数据

5.7 线程生命周期规则

 实际都是通过刷新主存实现可见性

  • start()
    A线程启动B线程的start方法,那么B能看到A线程启动前的任意操作

  • join()
    A线程调用B线程的join方法,那么在B执行完成后,A能看到B的执行结果

  • interrupt()
    A线程调用B线程的interrupt方法,那么在B能看到A线程中断前的任意操作

5.8 对象终结原则

 对象的初始化先于对象的finalize

 

三、并发基础

1.线程创建

1.1 继承Thread类


1.1.1 Thread类使用
public class ThreadTest {

    public static void main(String[] args) {
        //继承Thread
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        threadDemo1.start();
    }

    private static class ThreadDemo1 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread线程开始运行");
        }
    }
}


1.1.2 Thread类API

 Thread中获取对象信息:

方法名说明
getId()返回线程对象唯一标识符
getName()/setName()获取或设置线程对象名称
getPriority()/setPriority()获取或设置优先级(无法保证线程的执行顺序)
isDaemon()/setDaemon()获取或设置是否为守护线程(守护线程随主线程的关闭也会关闭,通常用于执行辅助任务,如垃圾收集器)
getState()获取线程对象状态

 

 Thread中线程通信相关:

方法名说明
interrupt()中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记
interrupted()判断目标线程是否被中断,但是将清除线程的中断标记
isinterrupted()判断目标线程是否被中断,不会清除中断标记
sleep(long ms)该方法将线程的执行暂停ms时间
join()暂停当前线程,执行加入的进程,加入进程执行完毕后,继续执行当前线程
yield()当前线程做出让步,主动放弃对CPU的占用,从运行状态转变为就绪状态,CPU从就绪状态的线程中随机选择一个线程执行(当前线程仍能被选中)

 
 Thread中其他方法:

方法名说明
setUncaughtExceptionHandler()设置未校验异常处理器
currentThread()Thread类的静态方法,返回实际执行该代码的Thread对象

 

1.2 实现Runnable接口


public static void main(String[] args) {

        //实现Runnable
        Thread threadDemo2 = new Thread(new ThreadDemo2());
        threadDemo2.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable简写线程开始运行");
            }
        }).start();

    }

    private static class ThreadDemo2 implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable线程开始运行");
        }
    }
}

 

1.3 实现Callable接口(有返回值)


 Callable与Runnable最典型的区别为:

  • Runnable 无返回值,Callable有返回值
  • Runnable不会抛出异常,Callable会抛出异常(可以实现自己的执行器并重载afterExecute()方法来处理异常)
  • Runnable是Java原生接口,Callable是JUC中的接口

public class ThreadTest {

    public static void main(String[] args) {
    
        //创建FutureTask的对象
        FutureTask<String> task = new FutureTask<>(new ThreadDemo3());

        //创建thread对象
        Thread thread = new Thread(task);
        thread.start();

        //简写
        new Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("简写callable开始运行");
                return "简写callable返回值";
            }
        })).start();
    }

    private static class ThreadDemo3 implements Callable<String> {

        @Override
        public String call() throws Exception {
            System.out.println("Callable线程开始运行");
            return "这是Callable线程调用的返回值";
        }
    }
}

 

2.线程锁

2.1 定义


 锁是用于保护某一个资源在同一时间只有一个线程能访问的工具,核心就是共享资源的访问互斥。

 通用的加锁流程如下:

 
 

Created with Raphaël 2.2.0 加锁 临界区(一段代码,对共享资源进行了访问) 解锁

2.2 synchronized


 synchronized是Java语言原生实现的一种锁,里面封装了对临界区的加锁解锁操作。

 

2.2.1 修饰静态方法

 修饰静态方法时,该锁锁定的当前对象的所有实例(当前类的Class对象)

public class Test {
    synchronized static void method() {
        // 临界区  
    }
}

 等价于

public class Test {
    static void method() {
        synchronized (Test.class) {
            // 临界区
        }
    }
}

 

2.2.2 修饰非静态方法
public class Test {
    synchronized void method() {
        // 临界区  
    }
}

 等价于

public class Test {
    static void method() {
        synchronized (this) {
            // 临界区
        }
    }
}

 

2.2.3 修饰代码块

 synchronized也可以修饰一段代码块,相较整个方法都上锁,只上锁关键的共享资源的访问,要更加轻量级些。

public class Test {
    
    private final Object lock = new Object();
    
     void method() {
        //可以锁该类的所有实例 
        synchronized (Test.class) {
            // 临界区
        }

        //仅仅锁住当前实例
         synchronized (this) {
             // 临界区
         }

         //也可以把其他对象当做锁
         synchronized (lock) {
             // 临界区
         }
    }
}

 

2.3 锁的本质


2.3.1 锁和资源的关系
  • 锁与共享资源间的关系是1:N

在这里插入图片描述

如上图所示,当锁与资源1:1时,可以理解为,一把专用的钥匙只能开一个专用的门,当锁与资源1:N时,可以理解为一把万能钥匙可以开多个门

public class Test2 {
    
    private volatile int kitchen;
    private volatile int toilet;
    private volatile int bedroom;

    private final Object lock = new Object();

     void door1() {
        //厨房
        synchronized (lock) {
            // 临界区
            kitchen++;
        }
    }

    void door2() {
        //客厅
        synchronized (lock) {
            // 临界区
            toilet++;
        }
    }

    void door3() {
        //卧室
        synchronized (lock) {
            // 临界区
            bedroom++;
        }
    }
}

  • 锁即可以是其他对象也可以是资源本身
    例如上文中的Test2中,锁即为其他对象,当通过synchronized(this) {…}这种方式申明时,资源本身也成为了锁。

 

2.3.2 锁实现

 由于任何对象都可能成为锁,故在进行设计时,跟锁通信相关的方法如wait()、notify()等,在顶级父类Object类中定义,而非在Thread中。

 锁的实现原理为,在对象的头中存在一块数据Mark Word,该数据用于存放锁的标志位(记录当前对象的锁状态,如1已占用 0-未占用等)、占用该锁的thread ID(每个线程唯一标识符)及线程阻塞队列(记录未获取到锁的线程,依次排队阻塞等待)

 

3.线程通信

 线程中的wait()/notify()/notifyAll()必须依赖synchronized 进行加锁,加锁后才能使用,否则会抛出IllegalmoitorStateException异常,出现这个异常的实际原因是在synchronized 加锁后,会为当前加锁对象创建监视器,该监视器主要用于控制进程间同步及通信,只有在监视器内才可以完成通信的动作,这一整套体系也被称为管程,将在下面的章节详细介绍。

3.1 示例


 顾客在奶茶店买奶茶,如果有奶茶就直接购买,没奶茶排队:

public class MilkTeaShop {

    private volatile int milkTea = 0;

    public static void main(String[] args) {
        MilkTeaShop milkTeaShop = new MilkTeaShop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"准备买奶茶");
                milkTeaShop.customer();
                System.out.println(Thread.currentThread().getName()+"买到了");
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"准备买奶茶");
                milkTeaShop.customer();
                System.out.println(Thread.currentThread().getName()+"买到了");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                milkTeaShop.producer();
                System.out.println("奶茶做好了");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                milkTeaShop.producer();
                System.out.println("奶茶做好了");
            }
        }).start();
    }


    void customer() {
        synchronized (this) {
            //没奶茶,排队等待
            while (milkTea == 0) {
                System.out.println(Thread.currentThread().getName()+"开始排队");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            milkTea--;
        }

    }


    void producer() {
        //生产奶茶,叫等待的顾客来买
        synchronized (this) {
            milkTea++;
            notifyAll();
        }
    }
}

 执行效果如下:

Thread-0准备买奶茶
Thread-0开始排队
Thread-1准备买奶茶
Thread-1开始排队
奶茶做好了
Thread-1买到了
Thread-0开始排队
奶茶做好了
Thread-0买到了

 

3.2 wait


 加锁后调用wait()会使当前线程阻塞,等待其他线程使用notify()或notifyAll()唤醒,wait()在使用过程中有如下要点:

  • 执行wait()时,会先释放锁
    wait()执行流程如下,先释放持有的锁,阻塞等待被其他线程唤醒,唤醒后重新获得锁,完成剩下的操作后,退出synchronized 再次释放锁。这样做的原因是为了避免死锁,即当前线程获得锁后进入阻塞状态,其他线程永远无法获取到锁。

  • wait()阻塞后,会进入对象锁的等待队列,等待被唤醒
    这就意味着调用wait的对象,必须是锁对象,如果 synchronized 锁定的是 this,那么对应的一定是 this.wait(),如果是锁定的是object,必须调用object.wait()否则会抛出IllegalmoitorStateException异常

  • wait()会抛出中断异常InterruptedException
    关于相关的异常,将在线程中断中详细介绍

 

3.2 notify、notifyAll


 加锁后调用notify()会唤醒一个正在等待该对象锁的线程,notifyAll()会唤醒所有正在等待该对象锁的线程,在使用过程中有如下要点:

  • 执行notify()/notifyAll()时,唤醒的是处于对象锁等待队列中的线程
    即notify()/notifyAll()与wait的使用方式一样,需要调用跟锁的对象保持一致

  • notify()是随机唤醒一个线程,可能导致某些线程永远不会被唤醒
    例如有两份资源(A\B),线程1获取A,线程2获取B,线程3想获取A但被线程1占用,进入阻塞,线程4想要获取B被线程2占用进入阻塞,此时线程1执行完毕,调用notify随机唤醒了线程4,线程4依然无法获取资源B,继续阻塞,此时线程3就无法被唤醒了。

 

3.3 join


 A.join(),当前线程阻塞,待A线程执行完毕后唤醒当前线程继续执行

public static void main(String[] args) {
        System.out.println("主线程执行");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行完毕");
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程执行完毕");
    }
}

 执行结果为:

主线程执行
开始执行
执行完毕
主线程执行完毕

 

4.线程中断

4.1 interrupt


 在java中可以实现线程中断的方法为interrupt方法,意为中断轻量级阻塞(无法中断synchronized等重量级阻塞),只有如下方法可以响应该中断,并抛出InterruptedException异常

  • public static native void sleep(long millis) throws InterruptedException {…}
  • public final void wait() throws InterruptedException {…}
  • public final void join() throws InterruptedException {…}

 示例如下

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("sleep被中断");
                }
            }
        });
        thread.start();
        thread.interrupt();
    }

 

4.2 thread.isInterrupted()与Thread.interrupted()


 thread.isInterrupted()与Thread.interrupted()这两者都用于检测线程是否被中断,并返回一个boolean值,两者之间的区别如下

  • isInterrupted()是实例方法,interrupted是静态方法
  • isInterrupted()是调用对象的线程是否被中断过,interrupted()是当前线程是否被中断过
  • isInterrupted()不会清除线程的中断标记,interrupted会清除

 

public static void main(String[] args) {
        System.out.println("================Thread.interrupted()是否会清除中断标识");

        Thread thread1 = new InterruptedDemo1();
        thread1.start();
        thread1.interrupt();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("================isInterrupted是否会清除中断标识");
        Thread thread2 = new InterruptedDemo2();
        thread2.start();
        thread2.interrupt();
        System.out.println("线程2是否被中断过:"+thread2.isInterrupted());



        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("================抛出异常是否会清除中断标识");

        Thread thread3 = new InterruptedDemo3();
        thread3.start();
        thread3.interrupt();
    }

    private static class InterruptedDemo1 extends Thread{
        @Override
        public void run() {
            System.out.println("interrupted前thread是否被中断过:"+isInterrupted());
            System.out.println("interrupted调用结果,thread是否被中断过:"+Thread.interrupted());
            System.out.println("interrupted后是否被中断过:"+isInterrupted());
        }
    }

    private static class InterruptedDemo2 extends Thread{
        @Override
        public void run() {
            System.out.println("线程2是否被中断过:"+isInterrupted());
        }
    }

    private static class InterruptedDemo3 extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println();
                System.out.println("线程3是否被中断过:"+isInterrupted());
            }
        }
    }

 运行结果如下,由结果可知interrupted和抛出异常InterruptedException会清除中断标识,isInterrupted不会清除中断标识
 

================Thread.interrupted()是否会清除中断标识
interrupted前thread是否被中断过:true
interrupted调用结果,thread是否被中断过:true
interrupted后是否被中断过:false
================isInterrupted是否会清除中断标识
线程2是否被中断过:true
线程2是否被中断过:true
================抛出异常是否会清除中断标识

线程3是否被中断过:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.caizy.thread.Interrupt$InterruptedDemo3.run(Interrupt.java:60)

Process finished with exit code 0

 

5.线程终止

5.1 stop()、destory()


 线程可以通过stop()、destory()进行关闭,但官网不建议用这两种方式杀死线程,因为这两种方式只停止了线程,并未释放线程中的资源如网络资源、文件资源等。应等到线程执行完毕,释放资源后再退出

 

5.2 优雅关闭线程


 可以通过设置关闭标志位的方式中断线程,伪代码如下

 	//定义关闭的标识
    boolean running = true;
    while (running) {
        //执行线程逻辑,注意避免while循环内部阻塞,无法关闭的情况
    }
    //关闭线程
    running = false;

 

6.线程生命周期

6.1 线程状态


 线程的状态一共有五种,称为五态模型

  • 创建状态(NEW)
    当用 new 操作符创建一个线程的时候
  • 就绪状态(READY)
    调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
  • 运行状态(RUNNING)
    CPU 开始调度线程,并开始执行 run 方法
  • 阻塞状态(BLOCKED)
    线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等
  • 终止状态(TERMINATED)
    run 方法执行完或者执行过程中遇到了一个异常

6.2 JAVA线程状态


 JAVA对阻塞状态进行了一些修改,对应修改如下:

  • 合并运行状态(RUNNING)和就绪状态(READY)为RUNNABLE(可运行 / 运行状态)
    JVM 层面并不关心操作系统调度相关的状态,把这部分内容交给了操作系统
  • 扩充阻塞状态(BLOCKED),新增WAITING(无时限等待)、TIMED_WAITING(有时限等待)
    之所以扩充这两个状态,是为了对阻塞状态做细分,我们把阻塞状态(BLOCKED)称为重量级阻塞,不能被中断,WAITING(无时限等待)、TIMED_WAITING(有时限等待)为轻量级阻塞,可以被中断

 

6.3 线程状态转化


 线程的状态变化如上图所示,可以看出WAITING(无时限等待)、TIMED_WAITING(有时限等待)的区别为是否设置超时时间

在这里插入图片描述

 

四、并发通信(线程同步)

1.定义


  • 同步
    指线程之间通信、协作的机制,协调一个或多个线程按使用者预期的目标执行。

  • 互斥
    指同一资源在同一时间内只允许有一个线程访问,具有排他性

 由上述定义可知,同步的概念中已经包含了互斥相关的概念,可以理解为互斥是一种特殊的同步。

 

2.同步的方式


  • 控制同步
    控制任务的执行顺序

  • 数据访问同步
    控制共享变量同一时间只有一个线程访问

 

3.同步的机制


 目前主要有如下两种实现同步的机制,由于Java使用管程来实现对象的同步,故下文中将详细介绍管程的概念

  • 信号量(semaphore)
    定义一个变量记录线程状态或数量(信号),其他线程根据该变量判断自己应该执行的操作。

  • 监视器/管程(Monitor)
    通过对共享资源创建监视器,以达到管理共享资源及控制共享资源访问的效果的一种机制。

 

4 管程

4.1 定义


 管程的实质是对象监视器,封装了共享变量及对共享变量的操作,任何线程需要访问共享资源,需要排队进入监视器入口,监视器判断条件是否成立,若不成立继续回入口排队,成立则访问共享资源。

 Java实现中,给对象加锁的过程中会给该对象创建监视器,当其他线程访问该对象保护的共享资源时,会被监视器监管

 

4.2 管程模型(MESA模型)


 有关管程的模型,先后共出现了三种模型Hasen模型,Hoare模型,由于JAVA中实现的管程参考了MESA模型,故下面重点介绍MESA模型

在这里插入图片描述

  • 临界区
    保护共享资源,实现线程间互斥的代码块称为临界区,图中黑框部分为临界区,同时间内只能有一个线程进入临界区。
  • 入口
    通过对临界区加锁的方式,实现互斥访问
  • 入口等待队列
    未获取到锁的线程,阻塞后进入队列。
  • 条件变量
    按不同条件将线程归类
  • 条件变量等待队列
    属于同一类条件的线程,不满足条件变量定义的条件时,阻塞后进入该队列,满足条件后,重新进入入口等待队列

 

4.2 MESA模型实现


4.2.1生产者消费者模型

 生产者消费者模型,即有多个生产者生产物品存入队列,多个消费者消费物品,取出队列,队列实现的核心要点如下:

  • 一把锁
    生产的方法,与消费的方法需要加锁,由于操作的是同一个队列,故使用同一把锁
  • 两个条件
    队列满时阻塞生产者,队列不满时唤醒生产者;队列空时阻塞消费者,队列不空时唤醒消费者;

 
在这里插入图片描述

 

4.2.2 代码实现

 synchronized实现方法如下:


public class SynchronizedQueue<E> {


    private final Object[] items;


    private int takeIndex;


    private int putIndex;


    private int count;



    public SynchronizedQueue(int capacity) {
        items = new Object[capacity];

    }

    public synchronized void enqueue(E x) {
        while (count == items.length){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        items[putIndex] = x;
        if (++putIndex == items.length){
            putIndex = 0;
        }
        count++;
        notifyAll();
    }

    public synchronized E dequeue() {
        while (count == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        E item = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length){
            takeIndex = 0;
        }
        count--;
        notifyAll();
        return item;
    }
}



public class Main {

    public static void main(String[] args) {

        SynchronizedQueue<String> queue = new SynchronizedQueue<>(10);

        CustomerThread customerThread;
        for (int i = 0; i < 10; i++) {
            customerThread = new CustomerThread(queue);
            customerThread.start();
        }

        ProducerThread producerThread;
        for (int i = 0; i < 10; i++) {
            producerThread = new ProducerThread(queue);
            producerThread.start();
        }



    }


    private static class ProducerThread extends Thread {

        private final SynchronizedQueue<String> queue;


        private final Random random = new Random();

        public ProducerThread(SynchronizedQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            String name = "物品" + random.nextInt(100);
            System.out.println("线程"+Thread.currentThread().getName()+"开始生产物品"+name);
            queue.enqueue(name);
        }
    }


    private static class CustomerThread extends Thread {

        private final SynchronizedQueue<String> queue;


        public CustomerThread(SynchronizedQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            String name = queue.dequeue();
            System.out.println("线程"+Thread.currentThread().getName()+"开始消费物品"+name);
        }
    }
}





 

 该方法有一个明显弊端,会同时唤醒所有阻塞在队列中的消费者和生产者,为了对生产者阻塞队列及消费者阻塞队列做区分引入了Lock及Condition,代码如下:


public class LockQueue<E> {


    private final Lock lock;

    /**
     * 消费者阻塞队列
     */
    private final Condition notEmpty;


    /**
     * 生产者阻塞队列
     */
    private final Condition notFull;

    private final Object[] items;


    private int takeIndex;


    private int putIndex;


    private int count;


    public LockQueue(int capacity) {
        items = new Object[capacity];
        lock = new ReentrantLock();
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();

    }

    public void enqueue(E x) {
        lock.lock();
        try {
            while (count == items.length) {
                try {
                    //阻塞生产者
                    notFull.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            items[putIndex] = x;
            if (++putIndex == items.length) {
                putIndex = 0;
            }
            count++;
            //不空唤醒消费者
            notEmpty.signal();

        } finally {
            lock.unlock();
        }

    }

    public E dequeue() {
        lock.lock();
        try {
            while (count == 0) {
                try {
                    //阻塞消费者
                    notEmpty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            E item = (E) items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length) {
                takeIndex = 0;
            }
            count--;
            //不满唤醒生产者
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}



public class Main2 {

    public static void main(String[] args) {

        LockQueue<String> queue = new LockQueue<>(10);

        CustomerThread customerThread;
        for (int i = 0; i < 10; i++) {
            customerThread = new CustomerThread(queue);
            customerThread.start();
        }

        ProducerThread producerThread;
        for (int i = 0; i < 10; i++) {
            producerThread = new ProducerThread(queue);
            producerThread.start();
        }



    }


    private static class ProducerThread extends Thread {

        private final LockQueue<String> queue;


        private final Random random = new Random();

        public ProducerThread(LockQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            String name = "物品" + random.nextInt(100);
            System.out.println("线程"+Thread.currentThread().getName()+"开始生产物品"+name);
            queue.enqueue(name);
        }
    }


    private static class CustomerThread extends Thread {

        private final LockQueue<String> queue;


        public CustomerThread(LockQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            String name = queue.dequeue();
            System.out.println("线程"+Thread.currentThread().getName()+"开始消费物品"+name);
        }
    }
}

 

五、并发问题

1.安全性问题


1.1 数据竞争

 存在多个线程同时对共享变量进行写入和读写,这种情况称为数据竞争。例如下面这个例子,库存为共享变量,多个线程同时对库存进行扣减

public class Stock{

    private int stock = 10;

    public  void sell() {
        stock -= 1;
    }


    public static class StockThread extends Thread{

        private final Stock stock;

        public StockThread(Stock stock) {
            this.stock = stock;
        }

        @Override
        public void run() {
            stock.sell();
        }
    }



    public static void main(String[] args) {
        Stock stock = new Stock();
        for (int i = 0; i < 100000; i++) {
            new StockThread(stock).start();
        }
        //等待计算结果
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终剩余库存为"+stock.stock);
    }

}

 
 运行结果如下,可以看到该结果是不正确的,同时可以看出stock -= 1;语句需要先获取stock的值+1,再设置stock的值,即存在先后顺序,后一个语句依赖前一个语句,也影响程序的执行结果,这种情况我们称为竞态条件(程序的执行结果依赖线程执行的顺序)

最终剩余库存为-99986

 
 要想解决该问题,最简单的解决方式是上锁,在sell()方法前加上synchronized关键字,运行结果如下:

最终剩余库存为-99990

 

2.活跃性问题

2.1 死锁


2.1.1 定义

死锁是一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象

在这里插入图片描述
 如图,线程1持有资源1的锁,线程2持有资源2的锁,线程1想获取资源B,则需要获取资源B的锁,但此时被线程1占用,变为阻塞状态,线程2想获取资源1,发现资源B的锁竞争不到,发生阻塞,此时两个线程都处于阻塞状态,无法被唤醒。

 

2.1.2 死锁的必要条件

 Coffman总结了死锁产生的必要条件,有如下四条,只有四条都满足,死锁才会发生

  • 互斥
    共享资源 X 和 Y 只能被一个线程占用
  • 占有并等待条件
    线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
  • 不可抢占
    其他线程不能强行抢占线程 T1 占有的资源
  • 循环等待
    线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

 

2.1.3 避免死锁

 避免死锁只需要破坏上述四条死锁产生的条件其中一条即可

2.1.3.1 死锁样例

 以转账为例,涉及两个资源,转出方和转入方,要想转账成功必须同时占有两个资源,就可能发生死锁

public class Account {

    /**
     * 余额
     */
    private int balance;

    /**
     * 转账
     *
     * @param target 对方账户
     * @param money  金额
     */
    public void transfer(Account target, int money) {
        //获取转入方的锁
        synchronized (this) {
            //获取转出方的锁
            synchronized (target) {
                if (balance >= money) {
                    balance = balance - money;
                    target.balance = target.balance + money;
                }
            }
        }
    }

}

 

2.1.3.2 破坏占有且等待

 一次性申请所有资源,就可以避免占有且等待的情况了,可以引入第三方资源分配器

public class Allocator {

   private static volatile Allocator instance = null;


   /**
    * 申请中列表
    */
   private final List<Account> applyingList = new ArrayList<>();

   private Allocator() {
   }

   /**
    * 单例
    * @return
    */
   public static synchronized Allocator getInstance() {
       if (instance == null) {
           synchronized (Allocator.class) {
               if(instance == null){
                   instance = new Allocator();
               }
           }
       }
       return instance;
   }

   public synchronized void apply(Account from, Account to) {
       while (applyingList.contains(from) || applyingList.contains(to)) {
           try {
               wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       applyingList.add(from);
       applyingList.add(to);
   }

   public synchronized void free(Account from, Account to) {
       applyingList.remove(from);
       applyingList.remove(to);
       notifyAll();
   }
}



public class Account {

   /**
    * 余额
    */
   private int balance;

   /**
    * 转账
    *
    * @param target 对方账户
    * @param money  金额
    */
   public void transfer(Account target, int money) {

       //申请资源
       Allocator.getInstance().apply(this,target);
       //获取转入方的锁
       try {
           synchronized (this) {
               //获取转出方的锁
               synchronized (target) {
                   if (balance >= money) {
                       balance = balance - money;
                       target.balance = target.balance + money;
                   }
               }
           }
       }catch (Exception e){
           e.printStackTrace();
       }finally {
           Allocator.getInstance().free(this,target);
       }
   }

}

 

2.1.3.3 破坏不可抢占

 破坏不可抢占条件只需要让线程主动释放资源即可,synchronized 无法实现主动释放资源,加锁后会进入阻塞状态,无法执行后续的操作。而lock.tryLock()可以实现,如果获取到锁就返回true,未获取到锁立即返回false,不进行阻塞。

 

2.1.3.4 破坏循环等待

 破坏循环等待只需要控制获取锁的顺序,为资源分配唯一标识符,按资源标识符的顺序大小来获取

public class Account2 {

  /**
   * 账户的唯一标识符
   */
  private int id;

  /**
   * 余额
   */
  private int balance;

  /**
   * 转账
   *
   * @param target 对方账户
   * @param money  金额
   */
  public void transfer(Account2 target, int money) {
      //比较账户ID的大小
      Account2 max = this;
      Account2 min = target;

      if (min.id > max.id) {
          max = target;
          min = this;
      }

      //获取转入方的锁,id小的先获取id大的后获取
      synchronized (min) {
          //获取转出方的锁
          synchronized (max) {
              if (balance >= money) {
                  balance = balance - money;
                  target.balance = target.balance + money;
              }
          }
      }
  }

}

 

2.1 活锁


活锁是指多个线程因对方的行为改变自己的状态,而导致陷入状态变更的无限循环,无法继续执行。 例如,路上有两个行人,行人A往左走,行人B也往左走,此时行人A看到行人B想避开,故往右走,行人B同样看到行人A想避开也往右走,此时就会出现无限循环的情况。解决该种情况最直观的就是随机采取行动,避免因对方的行动而行动。

 

2.3 饥饿


饥饿是指线程因无法获取到所需资源一直无法执行后续步骤的情况 ,例如设置线程的优先级,优先级低的线程有可能永远没机会执行。要解决饥饿问题可以采取三种解决方案:

  1. 保证资源充足
  2. 保证分配公平
  3. 避免线程长时间占有锁

 1,3种的使用场景有限,通常都是采用方案2的形式解决饥饿问题,如公平锁

 

3.性能问题

 想要提高并发程序的效率,可以从两方面入手提高程序并行度(软件效率)和提高CPU及IO设备利用率(硬件效率)
 

3.1 最佳线程数量


 在编程层面想提高硬件利用率,可以从确定合适的线程数量入手,由于实际IO及CPU利用率无法准确估算,通常要根据压力测试,来调试最佳线程数量,故下文中只是提供一种确定最佳线程的思路

 

3.1.1 CPU密集型(CPU计算较多)

 我们可以假设,在极端情况下,没有任何IO利用率,完全只执行CPU计算,那么最合理的线程数应等于CPU核数,若多于CPU核数会发生线程切换增加不必要的成本,通常会设置为

  • CPU 核数 +1

 +1是用来保证当线程因为偶尔的内存页失效或其他原因导致阻塞时,有多余的线程可以执行

 

3.1.2 IO密集型(IO执行较多)

 单核情况下,IO时间越长,CPU空闲时间越长,需要越多的线程来提升CPU的利用率,故IO耗时与线程数量成正比,CPU耗时与线程数量成反比,多核情况只需按CPU核数等比扩大即可,可推导最佳线程数为:

  • CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

 

3.2 阿姆达尔(Amdahl)定律


 该定律用于描述并行率与性能提升的能力,即并行度越高,性能越高

  • S=1/((1−p)+p/n)​​ 其中S代表性能,p代表并行率,n代表CPU核数

 可以通过如下两种方式,提高并发的并行度

  1. 无锁

 例如CAS(乐观锁)、TLS(线程本地存储)

  1. 减少锁的持有时间

 通过减小锁的粒度,可以实现更高的并行度,例如分段锁、读写锁

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值