简单谈谈Juc并发编程——下

简单谈谈Juc并发编程——下

ReadWriteLock读写锁

ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入

读锁和写锁之间是互斥的,同一时间只能有一个在运行

读的时候可以被多个线程同时读

写的时候只能由一个线程来写

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author if
 * @Description: 读写锁:主要防止多个线程同时写与同时读写导致幻读
 * 读锁:共享锁
 * 写锁:独占锁
 * @Date 2021-11-06 上午 12:31
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache=new MyCache();

        //10个线程只做写入
        for(int i=1;i<=10;i++){
            final int temp=i;
            new Thread(()->{
                myCache.put(temp+"",temp);
            },"write"+i).start();
        }


        //10个线程只做读取
        for(int i=1;i<=10;i++){
            int temp=i;
            new Thread(()->{
                myCache.get(temp + "");
            },"read"+i).start();
        }

    }

    //自定义缓存
    static class MyCache{
        private volatile Map<String,Object> map=new HashMap<>();
        //读写锁,可以通过writeLock和readLock获取写锁和读锁后,再进行上锁
        private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();

        //写的时候,希望只能有一个线程操作
        public void put(String key,Object value){
            //从读写锁readWriteLock中获取写锁writeLock
            Lock writeLock = readWriteLock.writeLock();
            //写锁进行上锁操作
            writeLock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"写入key = "+key);
                map.put(key,value);
                System.out.println(Thread.currentThread().getName()+"写入完毕");
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                //写锁进行解锁操作
                writeLock.unlock();
            }

        }

        //读的时候,希望每个线程都可以读
        public void get(String key){
            Lock readLock = readWriteLock.readLock();
            readLock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"读取key = "+key+",value = "+map.get(key));
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                readLock.unlock();
            }
        }
    }
}
复制代码

阻塞队列BlockingQueue

什么时候会用到阻塞队列:多线程并发处理、线程池

和生产者消费者问题有点相似

  • 写入:如果队列满了,就必须阻塞等待
  • 取出:如果是队列是空的,必须阻塞等待生产

BlockingQueue的4组Api

方式 抛出异常 不抛异常且有返回值 阻塞等待 超时等待
添加 add() offer() put() offer(,,)
移除 remove() poll() take() poll(,)
判断队列首 element() peek - -

添加操作都会进行判空,所以不能放null

	checkNotNull(e);

	private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }
复制代码

1.抛出异常

设定好队列大小后,这些操作都会抛出异常

  • 队列满再添加:IllegalStateException: Queue full
  • 队列空再取出:NoSuchElementException
    //1、抛出异常
    public static void throwException(){
        //参数capacity表示队列大小
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));

        //此时队首为a
        System.out.println("blockingQueue.element() = " + blockingQueue.element());

        //队列大小设定为3,此时添加了第4个元素时会抛出异常
        //java.lang.IllegalStateException: Queue full
//        System.out.println(blockingQueue.add("d"));

        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());

        //清空队列后,再remove抛出异常java.util.NoSuchElementException
//        System.out.println(blockingQueue.remove());

        //清空队列后抛出NoSuchElementException
        System.out.println("blockingQueue.element() = " + blockingQueue.element());
    }
复制代码

这里的情况和普通LinkedList的队列的异常一模一样

我们查看一下ArrayBlockingQueue的源码

可以看到ArrayBlockingQueue调用了他的父类AbstractQueue的add()方法

而这个add方法调用的是offer方法,并在添加失败时主动抛出异常 (implement BlockingQueue的offer方法)

    public boolean add(E e) {
        return super.add(e);
    }

	
    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
复制代码

element方法也调用的是peek方法

为空手动抛异常

    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }
复制代码

2.不会抛出异常且有返回值

    //2.不抛出异常且有返回值
    public static void noExceptionAndReturn(){
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));

        //此时队首为a
        System.out.println("blockingQueue.peek() = " + blockingQueue.peek());

        //队列满返回false
        System.out.println(blockingQueue.offer("d"));

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());

        //队列空,返回null
        System.out.println(blockingQueue.poll());

        //队列空,无队首,返回null
        System.out.println("blockingQueue.peek() = " + blockingQueue.peek());
    }
复制代码

3.阻塞等待

    //3、阻塞等待
    public static void blockWait() throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println("开始put");

        new Thread(()->{
            try {
                blockingQueue.put("a");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                blockingQueue.put("b");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                blockingQueue.put("c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //给1秒时间让线程插满队列
        TimeUnit.SECONDS.sleep(1);

        //此时队列已满,这个阻塞线程将被阻塞无法输出
        new Thread(()->{
            try {
                blockingQueue.put("d");
                System.out.println(Thread.currentThread().getName()+"put完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"阻塞线程").start();

        //救急线程将bq中take出一个元素,队列空一个位置,上一个阻塞线程可以完成put
        new Thread(()->{
            try {
                blockingQueue.take();
                System.out.println("再开一个"+Thread.currentThread().getName()+"take出来,阻塞线程能不能输出?可以");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"救急线程").start();

        System.out.println("主线程能不能输出?能输出,不影响,因为阻塞的是上上面的那个阻塞线程");
        System.out.println("如果在主线程put的话,这里一样会被阻塞\n====================");
    }
复制代码

因为put()take()用了Condition监视器,调用awaitsingle实现了精准睡眠和唤醒

下面是解析

成员变量condition

上文有讲到,这里不再赘述

private final Condition notFull;

put方法

while (count == items.length)队列满时,notFull.await();线程等待

enqueue方法中notEmpty.signal();唤醒阻塞的take线程

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
复制代码

take方法

while (count == 0)队列空时,notEmpty.await();线程等待

dequeue方法中,notFull.signal();唤醒阻塞的put线程

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }
复制代码

4.超时等待

和阻塞等待类比就很好理解了

阻塞等待就是会一直死等,直到有其他线程操作队列才有可能被唤醒

超时等待在设定的时间内会等待,超时则放弃

    //4、超时等待
    public static void outTimeWait() throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //用的还是offer和poll方法,不过这次是带参数的

        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");

        System.out.println("普通offer方法,立即放弃->"+blockingQueue.offer("d"));

        //等待3秒后,若还是阻塞则放弃
        //如果是不带参的方法会立即放弃
        System.out.println("带参offer方法,等待后放弃->"+blockingQueue.offer("d",3,TimeUnit.SECONDS));

        blockingQueue.poll();
        blockingQueue.poll();
        blockingQueue.poll();

        System.out.println("普通poll方法,立即放弃->" + blockingQueue.poll());

        //等待3秒后,还是阻塞则放弃
        System.out.println("带参poll方法,等待后放弃->" + blockingQueue.poll(3,TimeUnit.SECONDS));
    }
复制代码

我们来看看带参数offer源码

long nanos = unit.toNanos(timeout);获取了超时时间

if (nanos <= 0)判断计时是否结束

  • nacos<0,倒计时结束,return false;放弃等待直接返回(心灰意冷)
  • nacos>0,还在计时nanos = notFull.awaitNanos(nanos);,调用condition的awaitNanos继续计时等待(抱有希望)
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
复制代码

同步队列SynchronizedQueue

和其他的BlockingQueue不同,他不是用于储存元素

put了一个元素后就必须去take出来,不然就会等待(相当于只有1个空间的BlockingQueue?)

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description: 同步队列
 * 和其他的BlockingQueue不同,他不是用于储存元素
 * put了一个元素后就必须去take出来,不然就会等待(相当于只有1个空间的BlockingQueue?)
 * @Date 2021-11-06 下午 04:42
 */
public class SynchronizedQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"put a");
                synchronousQueue.put("a");

                System.out.println(Thread.currentThread().getName()+"put b");
                synchronousQueue.put("b");

                System.out.println(Thread.currentThread().getName()+"put c");
                synchronousQueue.put("c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"Thead-put").start();

        new Thread(()->{
            try {
                //给一点让他put的时间
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"take = " + synchronousQueue.take());

                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"take = " + synchronousQueue.take());

                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"take = " + synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread-take").start();
    }
}
复制代码

线程池

平时使用线程时需要创建、销毁。十分浪费资源和时间

池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我

线程池的好处

  1. 降低资源的消耗
  2. 提高响应的速度
  3. 方便管理

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

线程池:3大方法7大参数、**4种拒绝策略 **

阿里java规范中关于线程池写到

【==强制==】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式

这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明: Executors 返回的线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadPool
    • 允许的请求队列长度为Integer.MAX_VALUE, 可能会堆积大量的请求,从而导致OOM
  • CachedThreadPool ScheduledThreadPool
    • 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

OOM:out of memory内存溢出

Executors也是new的ThreadPoolExecutor,只是默认规定了一些参数

3大方法

1、单线程执行器SingleThreadExecutor

        //Single单个,创建单个线程处理
        ExecutorService service1 = Executors.newSingleThreadExecutor();
        try{
            for(int i=1;i<=5;i++){
                service1.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"在执行");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            service1.shutdown();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("========== service1已关闭 ==========");
        }
复制代码

执行结果

pool-1-thread-1在执行 pool-1-thread-1在执行 pool-1-thread-1在执行 pool-1-thread-1在执行 pool-1-thread-1在执行

2、固定线程池FixedThreadPool

        //Fix固定,根据参数nThreads,创建固定的线程池大小
        ExecutorService service2 = Executors.newFixedThreadPool(5);
        try{
            for(int i=1;i<=10;i++){
                service2.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"在执行");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            service2.shutdown();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("========== service2已关闭 ==========");
        }
复制代码

执行结果

pool-2-thread-2在执行 pool-2-thread-2在执行 pool-2-thread-1在执行 pool-2-thread-1在执行 pool-2-thread-1在执行 pool-2-thread-1在执行 pool-2-thread-1在执行 pool-2-thread-3在执行 pool-2-thread-4在执行 pool-2-thread-5在执行

3、缓存线程池CachedThreadPool

        //可伸缩的,遇强则强,遇弱则弱
        ExecutorService service3 = Executors.newCachedThreadPool();
        try{
            for(int i=1;i<=10;i++){
                //加入睡眠后发现全都是pool-3-thread-1在执行
                //推断可能是同时存在大量并发时,才会有多个线程入池
//                Thread.sleep(1);
                service3.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"在执行");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            service3.shutdown();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("========== service3已关闭 ==========");
        }
复制代码

执行结果

pool-3-thread-1在执行 pool-3-thread-2在执行 pool-3-thread-3在执行 pool-3-thread-4在执行 pool-3-thread-5在执行 pool-3-thread-7在执行 pool-3-thread-8在执行 pool-3-thread-9在执行 pool-3-thread-2在执行 pool-3-thread-6在执行

7大参数

查看源码可以发现其实调用Executor的这3个方法中,也是new的ThreadPoolExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
复制代码

只是他们默认给了一些固定的参数,例如maximumPoolSize=Integer.MAX_VALUE

俗话说得好,适合的才是最好的,有时候他默认给的参数并不一定是适合的,所以阿里java规范中让我们调用原生的线程池帮助类去创建线程池

	public ThreadPoolExecutor(int corePoolSize,		//初始核心线程池大小
                              int maximumPoolSize,	//最大线程池大小(核心线程不够,增加非核心线程)
                              long keepAliveTime,	//保持活跃时间(超时无调用的非核心线程则释放)
                              TimeUnit unit,		//超时时间的单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列(候客区)
                              ThreadFactory threadFactory,//线程工厂,用于创建线程的,一般用默认的
                              RejectedExecutionHandler handler)//拒绝策略,请求超出最大线程的承受且阻塞队列也占满时,将采用拒绝策略,默认为AbortPolicy(超出就不接收了并抛出异常)
复制代码

线程池运行原理简述

可以看到,请求打到线程池时,线程池首先根据初始化时创建的核心线程去处理请求,当核心线程都在使用时,接下来的请求将会放入阻塞队列中去

当核心线程都在处理,且阻塞队列占满时,会根据maximumPoolSize最大线程池大小继续创建非核心线程

keepAliveTimeunit决定了非核心线程能够在没有业务调用时的存活时间

非核心线程在keepAliveTime结束后会进行收回

如果所有线程(核心与非核心)都被取走使用,且阻塞队列也占满的情况下就会采取拒绝策略

这里默认的拒绝策略是AbortPolicy,超出承受范围就不接收,并抛出异常

    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

	//RejectedExecutionHandler类是接口类,有4种实现类,我们称之为4种拒绝策略
	public static class CallerRunsPolicy implements RejectedExecutionHandler
	public static class AbortPolicy implements RejectedExecutionHandler
	public static class DiscardPolicy implements RejectedExecutionHandler
	public static class DiscardOldestPolicy implements RejectedExecutionHandler
复制代码

代码简单实现

我们这初始化2个核心线程,最大线程数为5,超时时间为5秒,阻塞队列大小为3,默认的线程工厂和中止策略

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description: 调用原生ThreadPoolExecutor创建线程池
 * @Date 2021-11-07 下午 03:30
 */
public class MyPool {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            for(int i=1;i<=9;i++){
                threadPoolExecutor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在运行");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            threadPoolExecutor.shutdown();
        }
    }
}
复制代码

结果显示,此时都是2个核心线程在运转

因为5个请求有3个放入了阻塞队列中

pool-1-thread-1正在运行 pool-1-thread-2正在运行 pool-1-thread-1正在运行 pool-1-thread-2正在运行 pool-1-thread-1正在运行

此时我们将循环次数调到8次,可以看到已经创建多出了3,4,5线程,也就是根据最大线程数来决定创建的非核心线程(5-2=3),也就是说能够多创建出3个非核心线程来排忧解难

pool-1-thread-2正在运行 pool-1-thread-5正在运行 pool-1-thread-3正在运行 pool-1-thread-5正在运行 pool-1-thread-1正在运行 pool-1-thread-2正在运行 pool-1-thread-3正在运行 pool-1-thread-4正在运行

大家知道,我们最大有5个线程和队列中的3个位置,也就是一共可以并发8个请求,那我们将循环调到9次时会发生什么呢?

pool-1-thread-2正在运行 pool-1-thread-4正在运行 pool-1-thread-3正在运行 pool-1-thread-1正在运行 pool-1-thread-3正在运行 pool-1-thread-4正在运行 pool-1-thread-5正在运行 pool-1-thread-2正在运行 java.util.concurrent.RejectedExecutionException(一大段文字省略)

没错,就是当并发请求处理不过来的时候,我们选取的拒绝策略是AbortPolicy中止策略会直接拒绝请求并抛出异常RejectedExecutionException

4种拒绝策略

1、中止策略AbortPolicy

当并发请求处理不过来的时候,AbortPolicy中止策略会直接丢弃任务并抛出异常java.util.concurrent.RejectedExecutionException

2、调用者运行策略CallerRunsPolicy

哪来的回哪去

线程池表示:我没资源继续处理你的请求了,谁将你放进来的就让谁去执行你把

pool-1-thread-1正在运行 main正在运行 pool-1-thread-1正在运行 pool-1-thread-3正在运行 pool-1-thread-2正在运行 pool-1-thread-3正在运行 pool-1-thread-1正在运行 pool-1-thread-5正在运行 pool-1-thread-4正在运行

3、丢弃策略DiscardPolicy

资源不足,直接丢弃任务且不抛出异常,(直接摆烂,哪有任务,我怎么不知道?)

4、饱和策略DiscardOldestPolicy

丢弃线程中旧的任务,将新的任务添加

将最早进入队列的任务删除,之后再尝试加入队列

当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去

在rejectedExecution中先从任务队列中弹出最先加入的任务,空出一个位置,然后再次执行execute方法把任务加入队列

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
复制代码

最大线程数如何定义?

CPU密集型(CPU bound)

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多

此时,系统运作大部分的状况是CPU Loading100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound

CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间

线程数一般设置为:

**线程数 = CPU核数+1 **(现代CPU支持超线程,利用等待空闲)

IO密集型(I/O bound)

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

线程数一般设置为:

**线程数 = CPU总核心数 * 2 +1 **

java虚拟机的最大可用的处理器数量,决不会小于一个

Runtime.getRuntime().availableProcessors()
复制代码

也可以任务管理器->性能->cpu->逻辑处理器,查看

四大函数式接口

新时代的程序员需要掌握:lambda表达式、链式编程、函数式接口、Stream流式计算

什么是函数式接口

只有一个方法的interface接口

典型的就是Runnable和Callable接口了

可以看到他们都是被注解@FunctionalInterface标注的,这个注解直译也是叫函数式接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
复制代码

1、函数型接口Function

有输入T,和输出R

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
复制代码
        //普通调用,使用匿名内部类
        Function<String,String> function=new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        };
        System.out.println(function.apply("test"));

        //lambda
        Function<String,String> functionL= (str)->{return str;};
        System.out.println(functionL.apply("lambda"));
复制代码

甚至还能更简易

        //更简易
        Function<String,String> functionLS= str-> str;
        System.out.println(functionLS.apply("simple"));
复制代码

2、断定型接口Predicate

有输入,返回boolean

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
复制代码
        Predicate<Integer> predicate = new Predicate<Integer>(){
            @Override
            public boolean test(Integer num) {
                return num.equals(1);
            }
        };
        System.out.println(predicate.test(2));
        System.out.println(predicate.test(1));

        Predicate<Integer> predicateL= num-> num.equals(1);
        System.out.println("predicateL.test(1) = "+predicateL.test(1));
复制代码

3、消费型接口Consumer

只有输入,没有返回值

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
复制代码

sout甚至可以更加简化System.out::println

    Consumer<String> consumer= str-> System.out.println(str);
    consumer.accept("out");

	//极简版本
    Consumer<String> consumerS= System.out::println;
    consumerS.accept("out");
复制代码

4、供给型接口Supplier

只有返回值,没有参数

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
复制代码
        Supplier<String> supplier= ()-> "asd";
        System.out.println(supplier.get());
复制代码

Stream流式计算

stream流是io流?

**并不是!!!**这个是io流,是java.io包下的

而本处要讲的是Stream流,是java.util包下的

import java.io.InputStream;
import java.util.stream.Stream;
复制代码

什么是Stream流式计算?

大数据=计算+存储

存储:集合、mysql数据库。。。

计算:stream流

而这些流里面有很多很多的参数都是使用的函数式接口

话不多说,直接上代码

import java.util.Arrays;
import java.util.List;

/**
 * @Author if
 * @Description: stream流学习
 * 现在有5个用户进行筛选:
 * 1、ID必须是偶数
 * 2、年龄必须大于23岁
 * 3、用户名转为大写字母
 * 4、用户名字母倒着排序
 * 5、只输出一个用户
 * @Date 2021-11-07 下午 06:12
 */
public class Test01 {
    public static void main(String[] args) {
        User user1 = new User(1,"a",21);
        User user2 = new User(2,"b",22);
        User user3 = new User(3,"c",23);
        User user4 = new User(4,"d",24);
        User user5 = new User(6,"e",25);

        //将5个user转到list中
        List<User> list= Arrays.asList(user1,user2,user3,user4,user5);
        //计算交给stream流
        list.stream()
                //ID必须是偶数
                .filter(user-> user.getId()%2==0)
                //年龄必须大于23岁
                .filter(user-> user.getAge()>23)
                //用户名转为大写字母
                .map(user -> user.getName().toUpperCase())
                //用户名字母倒着排序(可简化为Comparator.reverseOrder())
                .sorted((u1,u2)->u2.compareTo(u1))
                //只输出一个用户
                .limit(1)
                .forEach(System.out::println);
    }
}
复制代码

ForkJoin分支合并计算

什么是ForkJoin?

ForkJoin在jdk1.7中,并行执行任务,提高效率,大数据量! 大数据:Map Reduce (把大任务拆分为小任务)

ForkJoin本质:分而治之

一个大任务分为了许多的小任务,最后将结果汇总得到解答

ForkJoin特点:工作窃取

当B线程更先完成时,A还未完成,那B就会去从A的任务里拿一些工作来做,帮助分担压力,提高效率

这里面维护的都是双端队列Dequeue

如何使用ForkJoin

最好是大数据量的情况下使用才可以提升效率,小数据量的情况下还不如直接for循环

直接上代码,感觉方式像递归一样,加入了forkjoin的工作队列的机制

有3种调用

  • public void execute(ForkJoinTask<?> task),直接调用,没有返回值
  • public ForkJoinTask submit(ForkJoinTask task),提交任务执行,返回得到ForkJoinTask类
    • public final V get(),返回的ForkJoinTask类的get方法可以得到执行结果
  • public T invoke(ForkJoinTask task),直接invoke得到执行结果
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

/**
 * @Author if
 * @Description: What is it
 * @Date 2021-11-07 下午 06:46
 */
public class ForkJoinDemo extends RecursiveTask<Long> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start=1L;
        long end=10_0000_0000L;
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(start,end);
        ForkJoinPool forkJoinPool=new ForkJoinPool();
        //执行任务,没有返回值
//        forkJoinPool.execute(forkJoinDemo);
        //提交任务,获得返回值ForkJoinTask类,然后根据这个类的get获取结果
//        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
//        System.out.println("submit.get() = " + submit.get());
        //采用invoke唤醒可直接得到返回值,比上一种方法更简便
        Long sum = forkJoinPool.invoke(forkJoinDemo);
        System.out.println("sum = " + sum);
    }

    private Long start;
    private Long end;
    private Long temp=10000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        //小于临界值时,采取普通循环
        if((end-start)<temp){
            long sum=0L;
            for(long i=start;i<=end;i++){
                sum+=i;
            }
            return sum;
        }
        //超过临界值时,采取fork-join方法
        long mid=(start+end)/2;
        //将其分成两份任务
        ForkJoinDemo fj1=new ForkJoinDemo(start,mid);
        //压入任务队列
        fj1.fork();
        ForkJoinDemo fj2=new ForkJoinDemo(mid+1,end);
        fj2.fork();
        //返回整合的结果
        return fj1.join()+fj2.join();
    }
}
复制代码

也可以使用stream并行流

        //使用stream并行流
        Long sum = LongStream.rangeClosed(start, end)
                .parallel()
                .reduce(0,Long::sum);
        System.out.println("sum = "+sum1);
复制代码

异步调用

我们采用了CompletableFuture类的两个方法,runAsync()supplyAsync()

public class CompletableFuture<T> implements Future<T>, CompletionStage<T>
复制代码

无返回值的异步调用

        //没有返回值的runAsync异步调用
        CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
            System.out.println("CompletableFuture.runAsync");
        });
        runAsync.get();

        System.out.println("==============");
复制代码

有返回值的异步调用

这里和ajax、axios有点相似,提交请求的业务后需要进行成功回调和失败回调

supplyAsync()方法的参数是一个函数式接口Supplier<U> supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
复制代码

就是说在其中执行业务逻辑

然后可以通过成功回调whenComplete失败回调exceptionally来决定业务的处理

代码如下,注释写的很清楚了,这里排列出来更方便查看

  • 成功回调whenComplete,无论是否异常都执行,所以判断t和u来决定代码逻辑
    • 参数t,正常的返回结果,t不为null则正常执行,t为null时则执行失败(出现异常)
    • 参数u,错误信息,u为null则正常执行,否则就是异常信息
    • 没有返回值(因为业务成功执行的返回值200在supplyAsync中已经写了)
  • 失败回调exceptionally,只有在出现异常时才会执行
    • 参数e,一般是Exception e
    • 有返回值(一般根据异常的不同决定不同的返回值)
        //有返回值的runAsync异步调用
        //返回一个CompletableFuture类对象,用get获取最终结果
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> {
            //执行的业务逻辑
            System.out.println("CompletableFuture.supplyAsync中执行业务逻辑");
//            int i=1/0;
            return 200;
        })
            //成功回调,无论是否异常都执行,所以判断t和u来决定代码逻辑
            .whenComplete((t, u) -> {
                //t不为null则正常执行
                if(!Objects.isNull(t)&&Objects.isNull(u)){
                    System.out.println("t = " + t);//正常的返回结果
                }else{
                    //u为null则正常执行,否则就是异常信息
                    System.out.println("异常信息u = " + u);//错误信息
                }
             })
            //失败回调,只有在出现异常时才会执行
            .exceptionally((e) -> {
                //这里的参数一般是Exception e
//                e.printStackTrace();
                System.out.println("异常回调->e.getMessage() = " + e.getMessage());
                return 400;
            });
        
        //获取  成功/异常  的结果
        System.out.println("result.get() = "+result.get());
复制代码

执行结果

正常执行时

CompletableFuture.supplyAsync中执行业务逻辑 t = 200 result.get() = 200

执行失败时

CompletableFuture.supplyAsync中执行业务逻辑 异常信息u = java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero 异常回调->e.getMessage() = java.lang.ArithmeticException: / by zero

result.get() = 400

理解JMM

之前我们经常接触到JVM,Java Virtual Machine,Java虚拟机

那JMM又是什么呢?

Java Memory Model,java内存模型

它是一个不存在的模型,相当于一种概念、一种约定

关于JMM的一些同步的约定

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

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作, 而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程

我们来看下面两个线程对于主内存的操作

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的不可在分的

(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

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

  • 不允许read和load、store和write操作之一单独出现
    • 使用了read必须load使用了store必须write
  • 不允许线程丢弃他最近的assign操作
    • 工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量
    • 就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

Volatile

Volatile 是 Java 虚拟机提供轻量级的同步机制,是一个java的关键字,但是volatile 并不能保证线程安全性

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

1、保证可见性

我们来看看这一个问题代码

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description: What is it
 * @Date 2021-11-08 下午 06:11
 */
public class JmmTest {
    private static int num=0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(num==0){

            }
            System.out.println("线程结束");
        }).start();

        TimeUnit.SECONDS.sleep(1);

        num=1;
        System.out.println("num = "+num);
    }
}
复制代码

此时会输出“线程”结束的语句嘛?并不会

因为线程并不知道num已经被main线程改变了,工作内存中的num还是0,所以一直在循环

如果我们将num加上关键字volatile呢?

private static volatile int num=0;

可以看到,线程结束了

线程结束 num = 1

Process finished with exit code 0

2、不保证原子性

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:

  • 原子性(atomicity,或称不可分割性)
    • 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
  • 一致性(consistency)
    • 在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 隔离性(isolation,又称独立性)
    • 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以**防止多个事务并发执行时由于交叉执行而导致数据的不一致 **
      • 事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
  • 持久性(durability)
    • 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

我们看一下下面的代码

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author if
 * @Description: What is it
 * @Date 2021-11-08 下午 06:27
 */
public class VolatileTest {

    private static int num=0;
    private static volatile int vnum=0;

    public static void add(){
        num++;
        vnum++;
    }

    private static int snum=0;
    public synchronized static void sAdd(){
        snum++;
    }

    private static int lnum=0;
    private static Lock lock=new ReentrantLock();
    public static void lAdd(){
        lock.lock();
        try{
            lnum++;
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                    sAdd();
                    lAdd();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            //线程让出当前时间片给其他线程执行
            Thread.yield();
        }
        System.out.println("结束,普通的num = "+num+",加了volatile的vnum = "+vnum);
        System.out.println("结束,synchronized方法的snum = "+snum);
        System.out.println("结束,lock方法的lnum = "+lnum);
    }
}
复制代码

可以看见,不管是普通的num还是加了volatile的vnum都是不是正确的20000结果

加了lock和synchronized的方法中的lnum和snum的结果是正确的20000结果

结束,普通的num = 19986,加了volatile的vnum = 19986 结束,synchronized方法的snum = 20000 结束,lock方法的lnum = 20000

因为volatile不保证原子性num++自增也不是一个原子性操作

反编译查看

我们使用javap -c VolatileTest.class命令反编译一下这个class文件查看一下

  public static void add();
    Code:
	//获取static的num放入操作栈顶
       0: getstatic     #2                  // Field num:I
    //把常量1放入操作栈顶
       3: iconst_1
    //当前操作栈顶中两个值相加并且把结果放入操作栈顶(num=num+1)
       4: iadd
    //操作栈顶的结果赋值给static的num
       5: putstatic     #2                  // Field num:I
       
    //下边是vnum的操作,和上面一样,这里不再赘述
       8: getstatic     #3                  // Field vnum:I
      11: iconst_1
      12: iadd
      13: putstatic     #3                  // Field vnum:I
      16: return
复制代码

原子类Atomic

那如果要求不使用synchronized和lock怎么保证原子性实现这个方法?

使用java.util.concurrent.atomic包下的原子类

这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!

代码举例

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author if
 * @Description: What is it
 * @Date 2021-11-08 下午 11:12
 */
public class AtomicTest {
    private static AtomicInteger num=new AtomicInteger(0);

    public static void add(){
        //自增并返回,CAS乐观锁
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        long startTime=System.currentTimeMillis();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        long endTime=System.currentTimeMillis();
        System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
        //获得num的值并输出
        System.out.println("num.get() = "+num.get());
    }
}
复制代码

结果正常达到20000且效率也不低

程序运行时间: 44ms num.get() = 20000

我们来看看之前的synchronized和lock的

程序运行时间: 46ms 结束,synchronized方法的snum = 20000

程序运行时间: 47ms 结束,lock方法的lnum = 20000

现在看起来三者效率都不错

当我们把循环次数提起来后

        for (int i = 0; i < 20000; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    //do something
                }
            }).start();
        }
复制代码

程序运行时间: 2136ms 结束,synchronized方法的snum = 200000000

程序运行时间: 6287ms 结束,lock方法的lnum = 200000000

程序运行时间: 3881ms num.get() = 200000000

可以看到是synchronized占优势,atomic其次,lock反而是时间最长的

3、禁止指令重排

什么是指令重排?

我们写的程序,计算机并不是按照你写的那样去执行的

为了性能考虑, 编译器和CPU可能会对指令重新排序

什么是指令重排:不影响结果的前提下,对某些指令优先执行,提高效率

源代码-->编译器优化的重排--> 指令并行也可能会重排--> 内存系统也会重排---> 执行

as-if-serial语义

不管怎么重排序,单线程程序的执行结果不能被改变

编译器、runtime和处理器都必须遵守as-if-serial语义

处理器在进行指令重排的时候考虑:数据之间的依赖性

int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我们所期望的:1234 但是可能执行的时候回变成 2134 1324 
    不可能是 4123!因为y赋值依赖于x!
复制代码

只要加了volatile就可以避免指令重排

对于内存区的读写都加内存屏障:静止上下指令的顺序交换

作用:

  • 保证特定的操作的执行顺序!
  • 可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)

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

单例模式

关于单例模式的话,可以去看看我写的单例模式的笔记

理解CAS

什么是CAS?

CAS,compare and swap的缩写,中文翻译成比较并交换

CAS是CPU的并发原语,是操作系统层面的原子性操作

我们查看AtomicInteger类中有一个Unsafe

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
复制代码

大家知道,java不能直接操作系统,而是通过关键字native操作C++来操作系统底层

而这个Unsafe类就是java留的“后门”,可以直接操作系统的内存

在讲cas之前,我想先看看compareAndSet方法,交换并赋值

private volatile static AtomicInteger num=new AtomicInteger(0);
num.compareAndSet(1,2);
复制代码

就是当num的值为1时,将其替换为2,其实CAS的结果和这个比较相似

我们来看一下上一小节的AtomicInteger是怎么原子性自增的

    private volatile static AtomicInteger num=new AtomicInteger(0);

    public static void add(){
        //自增并返回
        num.getAndIncrement();
    }
复制代码

然后查看这个getAndIncrement方法

可以看到就是调用的Unsafe类的getAndAddInt方法

	private static final Unsafe unsafe = Unsafe.getUnsafe();    
	public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
复制代码

我们继续查看Unsafe类的getAndAddInt方法的源码

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
复制代码

也就是说我们调用了是将this(AtomicInteger对象),valueOffset(内存地址偏移值)和需要加的值i进行操作

简单来说其实也就是上一小节讲的

获取static的num放入操作栈顶

把常量1放入操作栈顶

当前操作栈顶中两个值相加并且把结果放入操作栈顶(num=num+1)

操作栈顶的结果赋值给static的num

样例的CAS源码的简单解释

根据var1实例对象与其的内存地址var2可以取出现在元素的值var5

然后循环调用CAS操作compareAndSwapInt,去比较var5的值是否尚未发生改变,如果还是原值,则交换成新值

这个我们也称其为自旋操作,或者叫自旋锁

比较当前工作内存中的值和主内存中的值

如果这个值是期望的,那么则执行操作!

如果不是就一直循环!

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
复制代码

如果根据var1var2取出来的值,还是与var5相同,那我就将var5替换为var5 + var4并返回

方法调用的native,也就是CAS原语了,(这里var4就是1)

CAS的缺点

  • 循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • ABA问题

什么是ABA问题?

在其他线程不知情的情况,来了一手狸猫换太子又换狸猫,但是别的线程并不知道这个被替换过,也不知道这个狸猫是否还是原来的那个狸猫

好比现在A=1

然后B线程调用cas(1,3)和cas(3,1)

先把1换为了3,再把3换为了1

对于线程A来说,A的值还是1,但是他可能并不是原来的那个1了

对于基本类型来说没有太大影响,因为指向常量池的位置

如果是引用类型来说,可能就有问题了,传递的值也许没变,可是对象变了!

ABA问题的解决方法

版本号的原子操作,详见下一节“原子引用”

原子引用

乐观锁的实现不仅只有CAS操作,还有一个版本号机制也可以实现

我们这里采用AtomicStampedReference类来实现版本号机制

需要注意的是:compareAndSet方法底层用的==判断相等,所以使用Integer的话只能使用缓存区间-128~127!

    static final int low = -128;
    static final int high;
	assert IntegerCache.high >= 127;

	public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
复制代码

代码实现

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author if
 * @Description: 版本号机制
 * 注意如果这里写Integer的话,只能采用-127~128的区间
 * 因为底层是采用的==判断!!!!!
 * expectedReference == current.reference
 *
 * @Date 2021-11-09 下午 04:33
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1, 1);

        new Thread(()->{
            //获取版本号
            int stamp = atomicInteger.getStamp();
            System.out.println("一开始的A - stamp = " + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A -> "+atomicInteger.compareAndSet(1, 2, stamp, (stamp + 1)));
            System.out.println("结束的的A - stamp = " + atomicInteger.getStamp());
            System.out.println("=======================");
        },"A").start();

        new Thread(()->{
            int stamp = atomicInteger.getStamp();
            System.out.println("B - stamp = " + stamp);
            System.out.println("B -> "+atomicInteger.compareAndSet(1, 2, stamp, stamp + 1));
            System.out.println("结束的的B - stamp = " + atomicInteger.getStamp());
            System.out.println("=======================");
        },"B").start();


        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最后getStamp = "+atomicInteger.getStamp());
        System.out.println("atomicInteger值 = "+atomicInteger.get(new int[]{atomicInteger.getStamp()}));
    }
}
复制代码

各种锁的理解

1、公平锁和非公平锁

这个咱们在学习Lock类时应该就接触到了

Lock lock=new ReentrantLock();

我们来看看可重入锁ReentrantLock的构造器

   //默认创建非公平锁Nonfair
	public ReentrantLock() {
        sync = new NonfairSync();
    }
	//boolean参数为true创建公平锁Fair,反之创建非公平锁Nonfair
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
复制代码

公平锁:公平:需要先来后到 非公平锁:不公平:可以插队 (默认)

2、可重入锁

可重入:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

synchronized和ReentrantLock都是可重入的

  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

可重入锁的意义之一在于防止死锁

当然,有一次lock()也得要一次unlock(),即加锁次数和释放次数要一样

实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程

当计数为0时,认为锁是未被占有的,线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1

如果同一个线程再次请求这个锁,计数器将递增

每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放

现有阶段的锁默认都是可重入锁(也称递归锁)

如果要实现不可重入的效果,可以自己设置一个继承Lock的类

成员变量绑定一个线程,第一次调用将当前线程赋值给绑定线程,然后后续的调用lock时,去判断绑定线程是不是当前线程,如果当前线程就是绑定线程则给他wait,在unlock方法中就清除绑定线程即可

具体实现可以参考一下这篇博客blog.csdn.net/wb_zjp28312…

3、自旋锁

其实之前讲CAS的时候,就讲到了自旋操作

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
复制代码

“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环

“自旋”就是自己在这里不停地循环,直到目标达成

而不像普通的锁那样,如果获取不到锁就进入阻塞

非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。而自旋锁会不停地尝试

自旋锁的好处在于,自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销

可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源

4、死锁

什么是死锁

死锁:多线程下,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都讲无法推进下去

此时称系统处于死锁状态或系统产生了死锁,这些永远在相互等待的进程成为死锁进程

出现死锁的条件

  1. 必须是两个或者两个以上进程(线程)
  2. 必须有竞争资源

一张图带你看懂死锁!

代码示例

import java.util.concurrent.TimeUnit;

/**
 * @Author if
 * @Description: 死锁样例
 * @Date 2021-11-09 下午 06:54
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        Object lockA=new Object();
        Object lockB=new Object();

        new Thread(()->{
            synchronized (lockA){
                System.out.println(Thread.currentThread().getName()+"获取到A锁");

                try{
                    TimeUnit.SECONDS.sleep(1);
                }catch(Exception e){
                    e.printStackTrace();
                }

                synchronized (lockB){
                    System.out.println(Thread.currentThread().getName()+"获取到B锁");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"获取到B锁");

                try{
                    TimeUnit.SECONDS.sleep(1);
                }catch(Exception e){
                    e.printStackTrace();
                }

                synchronized (lockA){
                    System.out.println(Thread.currentThread().getName()+"获取到A锁");
                }
            }
        },"B").start();
    }
}
复制代码

代码应该很清晰明了了,中间sleep是因为怕一个线程同时抢了两把锁导致不成功

A抢到A锁进入睡眠,B抢到B锁进入睡眠,然后A唤醒后尝试获取B锁,当然获取不到啊,B也尝试获取A锁,当然也获取不到,需要的锁都在对方的手中,自然陷入死锁

A获取到A锁 B获取到B锁

如何排查死锁?

  • 定位进程号:
    • 在windows命令窗口,使用 jps -l  查看当前的java进程的pid,通过包路径很容易区分出自己开发的程序进程
  • 找到线程状态和问题代码:
    • 查看到pid,输入jstack -l 15528,15528是进程pid
# 查看进程
>jps -l
14720
1464 org.jetbrains.jps.cmdline.Launcher
15528 com.ifyyf.test.deadlock.DeadLockDemo
4040 org.jetbrains.idea.maven.server.RemoteMavenServer36
9176 sun.tools.jps.Jps

# 查看具体的错误信息(篇幅太长,随便挑一点)
>jstack -l 15528

Found one Java-level deadlock:
=============================
"B":
  waiting to lock monitor 0x0000000002e39fe8 (object 0x000000076b614298, a java.lang.Object),
  which is held by "A"
"A":
  waiting to lock monitor 0x0000000002e3c928 (object 0x000000076b6142a8, a java.lang.Object),
  which is held by "B"

Java stack information for the threads listed above:
===================================================
"B":
        at com.ifyyf.test.deadlock.DeadLockDemo.lambda$main$1(DeadLockDemo.java:42)
        - waiting to lock <0x000000076b614298> (a java.lang.Object)
        - locked <0x000000076b6142a8> (a java.lang.Object)
        at com.ifyyf.test.deadlock.DeadLockDemo$$Lambda$2/1078694789.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"A":
        at com.ifyyf.test.deadlock.DeadLockDemo.lambda$main$0(DeadLockDemo.java:26)
        - waiting to lock <0x000000076b6142a8> (a java.lang.Object)
        - locked <0x000000076b614298> (a java.lang.Object)
        at com.ifyyf.test.deadlock.DeadLockDemo$$Lambda$1/990368553.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
复制代码

完结撒花

本篇juc的学习并没有特别深入,可以说是简单入个门吧

本篇代码都放在我的gitee仓库了,需要的可以自取

猜你喜欢

转载自juejin.im/post/7031105166858420261