多线程笔记

101 阅读8分钟

一笔记

1.java内存模型的设计是为了解决直接读写主内存效率低的问题。

2.volatile关键字可以保证被修饰的属性线程之间的可见性和****有序性无法保证对该属性非原子操作的原子性(如:i++)

3.对象没有被锁,在单线程情况下,不会并发,所以不会有并发问题。多线程情况下,就会出现并发问题。synchronized 很好的解决了并发同时访问公共数据照成的线程安全问题。

synchronized关键字,锁的是对象实例锁动作都做了什么?

  • 对象实例创建一个锁对象
  • 锁对象记录获取到当前锁对象线程的id
  • 锁对象记录并阻塞竞争锁的其他线程
  • 竞争到的线程持有锁对象

获取到锁的当前线程同步方法、同步代码块执行完成或调用被锁对象wait方法才释放锁(wait还有阻塞当前线程功效),让其他被阻塞线程同时竞争。调用被锁对象的notifyAll方法可以唤醒所有阻塞的线程同时去竞争锁。所以这个锁两个缺点,1.无法唤醒指定阻塞的线程,2,同时竞争无法保证等待线程获取锁的顺序-非公平锁

注意:线程的阻塞(wait)和唤醒(notifyAll)的API必须是被锁对象调用。否则会出现异常。sleep方法会阻塞当前线程,但不会释放锁

ReetrantLock类,可以通过它创建的Condition对象进行指定线程的阻塞和唤醒(阻塞的是持有这个Condition对象锁的线程,唤醒的也是持有的这个Condition对象锁的线程)。可以唤醒指定线程。构造方法可以设置公平锁保证线程获取锁的顺序,默认是非公平锁。

4.synchronized和ReetrantLock都是悲观锁。java提供了基于操作系统指令的乐观锁Unsafe。

5.java提供的原子类可以在不加锁的情况下保证线程安全。内部使用CAS算法。缺点:

  • 只能保证一个共享变量的原子性(不能保证一段代码原子性)
  • 如长时间自旋,会带来更大的内存开销
  • 都有ABA的问题,解决方式,加上版本号

注意:cas算法的原理并不能保证线程的执行顺序。

6.停止线程方式:1定义一个boolean值。2调用interrupt()。

7.队列

ArrayBlockingQueue队列:

  • 长度必须指定,因为内部使用数组维护数据(永远不会扩容

  • 如果数组放满就会阻塞任何放数据线程,直到有空位子才会唤醒放数据线程继续放。

  • 如果数组为空就会阻塞任何取数据线程,直到有数据才会取数据线程继续取。

  • 内部使用单个ReetrantLock对并发安全性做了处理,用await和signal做了阻塞和唤醒。

  • 只有先进先出一种模式(首次都是从队首入队)。

注意:多线程存数据的顺序需要开发者自己控制线程的到达顺序

LinkedBlockingQueue队列:

  • 长度可以不指定,因为内部使用List链表维护数据。

  • 指定长度不会自动扩容,不指定长度容量是Int最大值

  • 放满就会阻塞任何放数据线程,直到有空位子才会唤醒放数据线程继续放。如果不指定长度可以一直放。

  • 为空就会阻塞任何取数据线程,直到有数据才会取数据线程继续取。

  • 内部使用两个ReetrantLock对并发安全性做了处理(存取分离,提高性能),用await和signal做了阻塞和唤醒。

  • 只有先进先出这一种模式(队尾入队,对首出队。使用链表特性完成先进先出,存取分离的关键)

注意:多线程存数据的顺序需要开发者自己控制线程的到达顺序

SynchronousQueue队列:

  • 内部没有维护队列
  • 只要放入就阻塞放入线程,直到有线程来取,才唤醒线程A
  • 只要取出就阻塞取出线程,直到有线程放入数据,才能唤醒取出线程
  • 内部使用CAS+自旋做到并发安全
  • 可以指定先进先出的队列效果,也可以指定后进先出的栈效果

注意:多线程存数据的顺序需要开发者自己控制线程的到达顺序

 LinkedTransferQueue队列

  • 内部维护List链表数据
  • 放入可以选择阻塞或者不阻塞放入线程
  • 取出就不阻塞取出线程,取不到才阻塞线程,直到检测到有数据放入,才唤醒取出线程
  • 内部使用CAS+自旋做到并发安全
  • 只有先进先出对列效果

注意:多线程存数据的顺序需要开发者自己控制线程的到达顺序

PriorityBlockingQueue队列

  • 内部维护一个数组(小堆型二叉树)
  • 会自动扩容
  • 放入数据,不阻塞线程
  • 取出,如果没有取到回阻塞线程。直到有数据放入,才唤醒取线程。
  • 按照开发者实现Comparable接口中的逻辑去排序。
  • 取出顺序是按照开发者定义的规则取出,没有先进先出或者后进后出这样写死的规则。

LinkedBlockingDeque队列

和LinkedBlockingQueue的特性一样,不过可以选择是队首入队或者队尾入队。也可以选择队首出队或者队尾出队。就是存的时候可以两头存,取得时候也可以从两头取。内部只有一把锁。

PriorityQueue队列

和PriorityBlockingQueue基本是一样的,就是缺少阻塞功能的队列。也是按照开发者定义的规则出队。

DelayQueue队列

内部使用了PriorityQueue队列去维护数据。入队不阻塞。出队为空阻塞,不为空检查堆顶元素是否过期,过期取出来。没过期先不取出来去阻塞。是按照优先级去做阻塞,并非按照时间去阻塞(所以有过期元素)。当然你可以优先级按照时间的逻辑去处理。就会变成按时间去阻塞。

二实战

假如有这样一个场景:时时绘制静态心电,每秒等间隔返回6个包。每个包25个心电值(每个包都来自不同线程)。在用户测试的时候我们:0.006秒(1/25*6)在子线程去获取一个缓存的数据,做心电绘制。--这是一个典型的生产消费者模型。这里我们为了巩固上边的笔记,一个一个去选择如何去处理这个模型。

2.1使用集合Vector去缓存数据

取数据去绘制伪代码

Thred{
   while (true) {
                //如果有数据才取
                if (mWaveData.size > 0) {
                    //获取一个数据,当前方法执行时获取锁,执行完成释放锁
                    val data: Int = mWaveData.removeAt(0)
                      ...
                    if (mWaveData.size > 25) {
                        //阻塞线程
                        Thread.sleep(6)
                    } else {
                        Thread.sleep(9)
                    }
                } else {
                    //否则阻塞线程
                    Thread.sleep(500)
                }
              }
     }.start()

存数据的伪代码

mWaveData.addAll(data)

这里的代码可以应对上诉场景。因为数据包是等间隔返回(0.16秒返回一个包,并且开发者赌在两个包返回也就是0.32秒内执行完成移除操作)。假如在移除数据的时候一下子来了两个包,这两个包都会阻塞。当锁释放的时候,因为synchronized是非公平锁,就有可能导致后来的数据先放入缓存,先来的数据后放入缓存。这样的程序是不健壮的。

2.2使用ArrayBlockingQueue队列

数组队列需要指定长度,我们并不能确定用户测试多长,不满足。假定我们确定返回的数据长度,用数组队列addAll去添加数据。这里就需要注意我们在创建数组队列的时候设定为公平锁,保证每个包的数据按照循序去插入。如果使用该队列,会有两个问题:1取和存数据使用同一个锁。性能不是最优。2.addAll添加数据行为不确定,很可能阻塞导致在添加的时候报错。

2.3使用LinkedBlockingQueue队列

链表队列需要不指定长度,用户不确定测试多长时间,可以满足。用队列addAll去设置数据。

链表内部使用两个锁,存取独立。但是他没提供设置公平锁的方式,还是解决不了在存数据的时候,一下子又来两个数据包。因为是非公平锁,导致数据插入顺序颠倒。不健壮。并且addAll添加数据行为不确定,有可能部分失败。

2.4使用SynchronousQueue队列

取一个阻塞,存一个阻塞。没有维护队列,根本不能满足需求。

2.5使用****LinkedTransferQueue队列

问题1:addAll添加数据行为不确定,有可能部分失败。问题2:数据同时来无法控制线程执行顺序。

其他的队列不在分析,都是不满足需求。

都不满足怎么办?那就自己写一个生产消费者模型。

public class WaveContainer {
    //缓存数据的集合
    LinkedList<Integer> list = new LinkedList<Integer>();
    //集合容量
    private final static int CAPACITY = Integer.MAX_VALUE;
    //锁对象
    private ReentrantLock lock = new ReentrantLock(true);
    //放数据的锁
    private final Condition putCondition = lock.newCondition();
    //取数据的锁
    private final Condition takeCondition = lock.newCondition();


    public void put(int[] wave) {
        try {
            //加锁
            lock.lock();
            while (list.size() == CAPACITY) {
                // 如果容器已满,阻塞
                putCondition.await();
            }
            for (int value : wave) {
                list.add(value);
            }
            // 数据放入成功,通知取线程
            takeCondition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public Integer take() throws InterruptedException {
        Integer wave;
        try {
            //加锁
            lock.lock();
            while (list.isEmpty()) {
                takeCondition.await();
            }
            wave = list.removeFirst();
            // 消费后通知生产者生产波形
            putCondition.signal();
        } finally {
            lock.unlock();
        }
        return wave;
    }


    public void clear() {
        lock.lock();
        list.clear();
        lock.unlock();
    }


}

该类还是使用一个锁,如果想要进一步提升性能,可以仿照链表队列设置两个锁。存取分离。这里不再提供。