跳到文章开头
  1. Theories/

Java-Concurrent

·3 分钟
并发编程
 ·  页面点击量:
目录

Java 并发编程
#

轻量级锁:
#

检查当前对象的mark word是否有被其他线程占用,假如没有就会在当前栈帧里面建立一个 锁记录,复制并存储当前对象的mark word 信息。

不像重量级锁需要向系统申请互斥量,

CAS(无锁算法)
#

img_3.png

显然,当cas失败的时候,说明有线程进入了这个同步代码块,这个时候,虚拟机将会检查 当前对象的mark word是否指向当前对象的栈帧,是的话就说明当前已经获得锁,否则不是-》退化为重量级锁(不可逆) 解锁时,同样使用CAS算法操作,同时唤醒挂起的线程

偏向锁
#

当某个锁频繁的被同一个线程获取,对轻量级锁进行优化,所以偏向锁专门为单个线程服务,此时,无须再进行CAS操作,当其他线程 又开始抢锁,偏向锁可能退化为轻量级锁。注意当调用对象的hashCode()方法,由于mark word 数据结构无法 保存hash值,偏向锁直接退化为轻量级锁。

锁的退化不可逆

锁消除和锁优化
#

代码块中有可能不会总是出现请求锁和释放锁的问题,(比如循环中加锁),此时锁为了优化出现锁消除和锁优化

Java内存模型(JMM)
#

img_6.png

主内存: 存放对象实例的部分 工作内存:虚拟机栈的部分,放入cpu的高速缓存里面。 自增操作不是由一个指令实现的!!!包括获取,修改和保存

class Test{
  private static volatile int sum=0;//成员变量,这样的变量必须是当前的对象所拥有的
  //加入了volatile关键字之后,线程之间可以感知彼此的值
  @Test
  void LockTestAdd() throws InterruptedException {
    //没有加锁的时候,由于线程之间不能感知对方的值,最终结构可能不会是200
    Thread th1 = new Thread(() -> {
      for (int i = 0; i < 100; i++)
        sum++;
    });

    Thread th2 = new Thread(() -> {
      for (int i = 0; i < 100; i++)
        sum++;
    });
    th1.start();
    th2.start();
    Thread.sleep(100);
    System.out.println(sum);
  }
}

重排序
#

由于JVM虚拟机和编译器的优化,指令可能出现重排序,在这种情况下可能出现不符合预期的情况

volatile关键字(无法保证原子性,但能保证可见性)
#

img_7.png

load和save操作,将主内存中的变量拷贝到本地,只对本地变量进行操作。

volatile实现原理就是改变成员变量时,save的工作区的变量无效。重新更新主内存中的变量值, volatile关键字会禁止指令重排序。

img_8.png

class VolatileTest{
  
private static volatile int sum=0;//成员变量,这样的变量必须是当前的对象所拥有的
    //加入了volatile关键字之后,线程之间可以感知彼此的值,但还是无法保证原子性操作,不能达到预期效果
    @Test
    void LockTestAdd() throws InterruptedException {
        //没有加锁的时候,由于线程之间不能感知对方的值,最终结构可能不会是200
        Thread th1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++)
                    sum++;
        });
       Thread th2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        });
        th1.start();
        th2.start();
        Thread.sleep(100);
        System.out.println(sum);
    }
}

JVM编译器下面的顺序执行:

Happens Before 原则(之前的对之后的可见)
#

程序次序规则,监视器锁原则,volatile关键字原则(写操作在读操作之前),

start(线程启动规则)
#

A线程用start调用B线程,那么A在B之前(A happens before B)

join线程加入规则
#

A线程join线程B,那么B在A之前(B happens before A)

程和线程的区别
#

程序软件 > 进程 > 线程

锁的框:Lock
#

为了代替传统的synchronized,notify,wait,notifyAll关键字,Lock接口->ReentrantLock可重入锁

img_9.png

可重入锁(排他锁)
#

多次加锁,其他线程想要得到锁需要把多次的锁释放才可以获取这把锁

当存在线程想要获取锁但是锁没有释放的时候,此时这些线程将会进入线程队列里面

公平锁和非公平锁(AQS)
#

img_10.png

公平锁始终保持先到先得到锁,非公平锁则是在等待队列中所有等待线程同时尝试获取锁,获取不到则再次进入等待队列

ReentrantLock lock=new ReentrantLock(false);//公平锁不一定总是保持公平。队列同步器
        Runnable runnable=()->{
            System.out.println(Thread.currentThread().getName() + "开始尝试获取锁资源");
            lock.lock();

            System.out.println(Thread.currentThread().getName() + "成功😺😺😺😺😺😺😺😺😺获取锁资源");
            lock.unlock();
        };

        for (int i = 0; i < 10; i++) {
            new Thread(runnable,"T"+i).start();
        }

读写锁:
#

除了可重入锁之外,还有一种类型的锁叫做读写锁,当然它并不是专门用作读写操作的锁, 它和可重入锁不同的地方在于,可重入锁是一种排他锁,当一个线程得到锁之后,另一个线程必须等待其释放锁,否则一律不允许获取到锁。而读写锁在同一时间,是可以让多个线程获取到锁的,它其实就是针对于读写场景而出现的。

读写锁维护了一个读锁和一个写锁,这两个锁的机制是不同的。

读锁:在没有任何线程占用写锁的情况下,同一时间可以有多个线程加读锁。

写锁:在没有任何线程占用读锁的情况下,同一时间只能有一个线程加写锁。

显然读锁是可以重复获取的但是写锁不能,当一个线程同时拥有写锁和读锁的时候,先申请读锁 然后释放写锁,此时其他的线程又可以获取读锁,只剩下读锁,此时称之为“锁降级”。 在仅仅持有读锁的时候去申请写锁,称为“锁升级”,这时候ReentryReadWriteLock类不支持。

队列同步器AQS(AbstractQueuedSynchronizer)
#

继承关系:Lock-> Sync -> AbstractQueuedSynchronizer

多线程并发环境下的ABA问题
#

//ABA问题的版本号解决方法
        AtomicReference<String> atomicReference=new AtomicReference<>("A");
        System.out.println(atomicReference.compareAndSet("a", "c"));
        String hello = "hello";
        String world = "world";

        Runnable r=()->{
            System.out.println(atomicReference.compareAndSet("A", "B"));
        };
        for (int i = 0; i < 100; i++) {
            new Thread(r).start();
        }

可以看到多个线程尝试去修改的时候出现了一个true

解决办法:添加版本号,每次修改的时候对版本号(stamp)进行修正

并发容器
#

比如在对链表添加元素的时候,还没有完成对链表扩容之前,其他线程插队,非法插入元素,就会造成数组越界的报错。

  • ConcurrentHashMap:在jdk1.7之前的实现方法,比如在原子类里面实现LongAdder具有压力分散的思想,提高了性能,选择将数据一段一段的存储,这样就减少了等待,当线程访问锁的时候只占用锁的一小部分
  • jdk8之后,实现方法是通过cas算法配合锁机制实现的。由于HashMap利用了哈希表,容量越大,加锁的粒度就会越细。
//原子类的测试操作
        //ABA问题的版本号解决方法
        CopyOnWriteArrayList<Object> objects = new CopyOnWriteArrayList<>();
        //读取不加锁,写数据需要加锁,所以性能还行
        //专用于多线程环境下的容器
        LinkedList<Object> list = new LinkedList<>();//没有发生并发异常
        ArrayList<Object> list1 = new ArrayList<>();
        ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
        HashMap<Integer, String> hashMap = new HashMap<>();
        Runnable r=()->{
            for (int j = 0; j < 100; j++) {
                int finalI=j;
                objects.add("aaa");
                map.put(finalI,"aaa");
                hashMap.put(finalI,"aaa");
            }
        };
        for (int i = 0; i < 100; i++) {
            new Thread(r).start();
        }
        TimeUnit.SECONDS.sleep(1);

阻塞队列(BlokingQueue)
#

生产者消费之模型:

 //测试阻塞队列实现生产者消费者模型
        BlockingQueue<Object> queue=new ArrayBlockingQueue<>(1);//窗口只能放一个菜,阻塞队列的容量
        Runnable supplier=()->{
          while (true){
              try {
                  String name = Thread.currentThread().getName();
                  System.err.println(time()+"生产者"+name+"正在准备餐品");
                  TimeUnit.SECONDS.sleep(3);
                  System.err.println(time()+"生产者"+name+"已出餐");
                  queue.put(new Object());
              }catch (InterruptedException e){
                  e.printStackTrace();
                  break;
              }
          }
        };

        Runnable consumer=()->{
            while (true){
                try {
                    String name = Thread.currentThread().getName();
                    System.out.println(time()+"消费者"+name+"正在等待餐品");
                    queue.take();
                    System.out.println(time()+"消费者"+name+"已取餐");
                    TimeUnit.SECONDS.sleep(4);
                    System.out.println(time()+"消费者"+name+"已吃完");
                }catch (InterruptedException e){
                    e.printStackTrace();
                    break;
                }
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(supplier,"supplier"+i).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(consumer,"consumer"+i).start();
        }
    }
    public static String time(){
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        return "["+format.format(new Date())+"]";
    }

常见的阻塞队列:ArrayBlokingQueue(有界缓冲阻塞队列),SynchronousQueue(无缓冲阻塞队列),LinkedBlokingQueue(无界带缓冲阻塞队列)

public boolean offer(E e) {
        Objects.requireNonNull(e);//检查当前的队列是否为空
        final ReentrantLock lock = this.lock;//对当前线程加锁
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();//当当前线程读取到队列为空时,由于take操作时阻塞的,需要挂起当前线程,等待直到队列里面有值
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

SynchronousQueue(没有任何容量,插入和取出是一一对应的)
#

SynchronousQueue<String> queue=new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(queue.take());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        queue.put("sss");

这是一个特殊的队列,内部维护了一个抽象类Transfer(put和take操作糅合在一起了), 里面有一个方法:transfer(E e,bolean timed,long nanos)直接通过生产者和消费者模型来实现数据的传递。 当新的元素put进去线程将会阻塞。直到元素被take。同时这个类维护了公平锁和非公平锁两种方法。

image.png

当当前队列里面有元素但是没有线程来取元素时候,就会开启自旋,当自旋一定次数之后还没有来取就会挂起

PriorityBlockingQueue优先队列
#

构造方法:PriorityQueue queue=new PriorityQueue(10,Integer::compare);

传入容量和函数逻辑(优先逻辑)

DelayQueue延时队列
#

特殊地方:存储的元素必须是继承自Delayed接口的类,同时元素类必须实现Delayed接口。

使用时可以实现延时出队,按照同时的时候按照优先级(自己实现)进行出队,这样就可以实现缓存了。

底层实现:

  public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
        implements BlockingQueue<E> {
    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();  //通过内部维护的优先队列来实现元素的存储,而不是本身进行存储
}

主要常见的阻塞队列总结如下:
#

  • ArrayBlokingQueue底层通过数组实现的阻塞队列,可以设置初始的容量,换句话容量是固定的
  • LinkedTransferQueue
  • SynchronousQueue要求入队和出队必须同时进行,一一对应,原因是内部维护了一个抽象类tansfer,需要等到消费者和生产者同时到齐才可以完成交接工作,支持公平和非公平
  • PriorityBlokingQueue优先队列,元素的获取顺序按照优先级决定
  • DelayQueue能够实现延迟获取元素,同样支持优先级,要求加入的元素必须继承Delayed接口

数据字典
#

常见的常用的数据,具有分级的特点,现在将比如省份县市地点的信息维护成一张表就可以是实现 据字典,通常具有id,parent_id这些字段,维护父级和子级的关系。

关于ElementUi的数据字典的显示方法就是检查hasChildren字段的值进行渲染

线程池
#

引入原因:频繁创建和销毁线程对系统资源的浪费十分严重,为了合理分配和调用系统资源,产生了线程池的技术

实现原理:将已创建的线程复用,利用池化技术,就像数据库连接池一样,我们也可以创建很多个线程,然后反复地使用这些线程,而不对它们进行销毁。

由于线程池可以反复利用已有线程执行多线程操作,所以它一般是有容量限制的,当所有的线程都处于工作状态时,那么新的多线程请求会被阻塞,直到有一个线程空闲出来为止,实际上这里就会用到我们之前讲解的阻塞队列。

image.png

相关文章

如何通过hugo搭建自己的博客
·1 分钟
杂记
科学上网
·1 分钟
杂记
对象间的关系
·1 分钟
Java SE