【Java核心基础知识】05 - 多线程并发(4)

多线程知识点目录

多线程并发(1)- https://www.jianshu.com/p/8fcfcac74033
多线程并发(2)-https://www.jianshu.com/p/a0c5095ad103
多线程并发(3)-https://www.jianshu.com/p/c5c3bbd42c35
多线程并发(4)-https://www.jianshu.com/p/e45807a9853e
多线程并发(5)-https://www.jianshu.com/p/5217588d82ba
多线程并发(6)-https://www.jianshu.com/p/d7c888a9c03c

十一、Java阻塞队列原理

11.1 线程阻塞的两种情况:

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。


    情况1
  2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的为止,线程被自动唤醒。


    情况2

11.2 阻塞队列的主要方法

阻塞队列的主要办法
插入操作
  1. add(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回true,如果当前没有可用空间,则抛出IllegalStateException。如果该元素使NULL,则会抛出NullPointException异常。

  2. offer(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回true,如果当前没有可用空间,则返回false。

  3. put(E paramE) throws InterruptedException:将指定元素插入次队列中,将等待可用的空间(如果有必要),如果队列满了,则线程阻塞等待。

  4. offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

获取数据操作
  1. poll(time):取走BlockingQueue中排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。

  2. poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一单有数据可取,则立即返回队列中的数据。否则直到时间超时还没数据可取,返回失败。

  3. take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态,直到BlockingQueue有新的数据被加入。

  4. drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

11.3 Java中的阻塞队列

    1. ArrayBlockingQueue:由数据结构组成的有界阻塞队列。

这是一个基于数组的实现阻塞队列。它内部使用了一个数组来存储元素,按照先进先出(FIFO)的原则对元素进行排序,并且可以设置最大容量。当队列满时,如果有线程试图向队列中插入元素,线程将会被阻塞直到队列中有空间;当队列空时,如果有线程试图从队列中删除元素,线程将会被阻塞直到队列中有元素。
默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。

    1. LinkedBlockingQueue:由链表结构组成的有界阻塞队列。

这是一个基于链表的实现阻塞队列。它内部使用了一个链表来存储元素,按照先进先出(FIFO)的原则对元素进行排序。与ArrayBlockingQueue不同的是,LinkedBlockingQueue的大小可以动态增长。当队列满时,如果有线程试图向队列中插入元素,线程将会被阻塞直到队列中有空间;当队列空时,如果有线程试图从队列中删除元素,线程将会被阻塞直到队列中有元素。
而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

    1. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

这是一个支持优先级排序的阻塞队列,默认情况下元素采取自然顺序升序排列。它内部使用了一个优先级堆来存储元素,使得高优先级的元素总是先于低优先级的元素出队,需要注意的是不能保证同优先级元素的顺序。

    1. DelayQueue:使用优先级队列实现的无界阻塞队列。

这是一个使用优先级队列实现的阻塞队列。它内部使用了一个优先级堆来存储元素,但与PriorityBlockingQueue不同的是,DelayQueue中的元素只有当其指定的延迟时间到了,才能从队列中删除。

    1. SynchronizedQueue:不存储元素的阻塞队列。

这是一个不存储元素的阻塞队列。它的所有操作都是对另一个阻塞队列的操作进行同步,也就是说,它本身并不存储任何元素。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和 ArrayBlockingQueue。

    1. LinkedTransferQueue:由链表结构组成的无界阻塞队列。

这是一个基于链表的无界阻塞队列。它内部使用了一个链表来存储元素。与LinkedBlockingQueue不同的是,LinkedTransferQueue的大小可以动态增长且支持多生产者多消费者模式。

    1. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

这是一个基于链表的双向阻塞队列。它内部使用了一个链表来存储元素,支持在两端插入和删除元素。
双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法

十二、volatile关键字的作用

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。volatile变量具备两种特性:①变量可见性;②禁止重排。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile类型的变量是总是会返回最新写入的值。

  • ① 变量可见性
    其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程时可以立即获取的。
  • ② 禁止重排
    volatile禁止了指令重排。

比Synchronized更轻量级的同步锁

在访问volatile变量时不会执行加锁操作,因此也就不会执行线程阻塞,因此,volatile变量是一种比Synchronized关键字更轻量级的同步机制。volatile适合这种场景:一个变量被多个线程共享,现成直接给这个变量赋值。


不同类型变量读取

当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU Cache中。而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU Cache这一步。

使用场景

volatile变量的单次读/写操作可以保证原子性,如long和double类型变量,但是,不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile不能完全取代Synchronized,只有在一些特殊的场景下才能适用。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

  1. 对变量的写操作不依赖于当前值(i++依赖当前值,不能保证线程安全),或者说是单纯的变量赋值(boolean flag=true)
  2. 该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时,才能使用volatile。

十三、如何在两个线程之间共享数据

Java里面进行多线程通讯的主要方式就是共享内存的方式,共享内存主要的关注点有:①可见性;②有序性;③原子性。
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望坐到“同步”和“互斥”。有以下常规实现方法:

13.1 将数据抽象成一个类,并将数据的操作作为这个类的方法

将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以做到同步,只要在方法上加上“Synchronized”

public class MyThread {
    public static void main(String[] args) throws InterruptedException {
        MyData myData = new MyData();
        Runnable add = new AddRunnable(myData);
        Runnable dec = new DecRunnable(myData);
        for (int i = 0; i < 2; i++) {
            new Thread(add).start();
            new Thread(dec).start();
        }
    }
}

class MyData {
    private int j = 0;

    public synchronized void add() {
        j++;
        System.out.println("[add()]线程" + Thread.currentThread().getName() + "的j的值为:" + j);
    }

    public synchronized void dec() {
        j--;
        System.out.println("[dec()]线程" + Thread.currentThread().getName() + "的j的值为:" + j);
    }

    public int getData() {
        return j;
    }
}

class AddRunnable implements Runnable {
    private MyData myData;

    public AddRunnable(MyData data) {
        this.myData = data;
    }

    public void run() {
        myData.add();
    }
}

class DecRunnable implements Runnable {
    private MyData myData;

    public DecRunnable(MyData data) {
        this.myData = data;
    }

    public void run() {
        myData.dec();
    }
}

13.2 Runnable对象作为一个类的内部类

将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。

public class MyThread {
    public static void main(String[] args) throws InterruptedException {
        final MyData myData = new MyData();
        for (int i = 0; i < 2; i++) {
            new Thread(myData::add).start();
            new Thread(myData::dec).start();
        }
    }
}


class MyData {
    private int j = 0;

    public synchronized void add() {
        j++;
        System.out.println("[add()]线程" + Thread.currentThread().getName() + "的j的值为:" + j);
    }

    public synchronized void dec() {
        j--;
        System.out.println("[dec()]线程" + Thread.currentThread().getName() + "的j的值为:" + j);
    }

    public int getData() {
        return j;
    }
}

你可能感兴趣的:(【Java核心基础知识】05 - 多线程并发(4))