JUC相关知识整理

视频链接:狂神说JUC:https://www.bilibili.com/video/BV1B7411L7tE

Synchronized 与 Lock 区别

  1. Synchronized是内置的java关键字,Lock是一个java类
  2. Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
  3. Synchronized会自动释放锁,Lock必须手动释放锁,如果不释放锁,会出现死锁
  4. Synchronized 线程1(获取锁)、线程2(等待), Lock锁不一定等待,可以通过tryLock() 尝试获取锁
  5. Synchronized是可重入锁,不可以中断,是非公平锁;Lock也是可重入锁,可以中断,默认是非公平锁,也可以设置成公平锁(自由度更高)
  6. Synchronized 适合锁少量代码同步问题;Lock适合锁大量的同步代码

8锁现象 弄懂锁是什么,怎么判断锁的是谁

  • 永远知道什么是锁,锁的是谁? synchronized锁的,要么是对象,要么是Class(模板,全局唯一)
  • 静态方法会在类加载的时候初始化
  • 如果两个线程中,两个对象,调用的都是普通同步方法,那么不需要相互等待。因为是两把锁,互不影响
  • 如果两个线程中,一个对象,调用的一个是普通同步方法,一个是普通非同步方法。(非同步方法不用等待同步方法的锁释放),互不影响
  • 如果两个线程中,调用的都是静态同步方法,那么锁的都是Class模板(全局唯一),不管调用方法的是一个对象,还是两个对象。都是同一把锁(Class模板)
  • 如果两个线程中,一个对象/两个对象,分别调一个静态同步方法,一个普通同步方法。那么相当于是两把锁,互不影响

Callable

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不同 call()
  4. 通过FutureTask作为适配类,中间转一道,来创建线程。
  5. futureTask.get() 获取到的结果可能会阻塞(线程执行耗时操作,需要等待执行完,才能拿到结果)
  6. futureTask.get() 获取到的结果会被缓存,以提高效率(两个线程执行,call()方法只执行一次)

学习方法

  1. 先会用
  2. 货比三家,寻找其他解决方案
  3. 分析源码,知其所以然

JUC中三大常用辅助类

CountDownLatch -- (减法计数器)

原理:每次线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行

  • new CountDownLatch(6); // 初始化线程计数器总数

  • countDownLatch.countDown(); // 计数器减1

  • countDownLatch.await(); // 等待计数器归零,然后向下执行

    /**
    * @author lixing
    * @date 2022-03-27 15:04
    * @description 减法计数器 为了保证多少个线程执行完毕后,才执行后续操作 (场景:多个学生都完成任务,回家了,才关闭教室门)
    */
    public class CountDownLatchDemo {
       public static void main(String[] args) throws InterruptedException {
           CountDownLatch countDownLatch = new CountDownLatch(6); // 总数是6
           for(int i=1; i<=6; i++){
               new Thread(()->{
                   System.out.println(Thread.currentThread().getName()+" go out");
                   countDownLatch.countDown(); // -1
               }, String.valueOf(i)).start();
           }
           countDownLatch.await(); // 等待计数器归零,然后向下执行
           System.out.println("close door");
       }
    }
    

CyclicBarrier -- (加法计数器)

原理:执行完指定个数的线程后,才会执行初始化定义的另一个线程

  • new CyclicBarrier(7, ()->{System.out.println("召唤神龙成功!");}); // 初始化定义线程计数到达7个,才执行指定线程

  • cyclicBarrier.await(); // 每次执行一个线程后执行等待,线程计数累加到7个,会执行指定线程

    /**
    * @author lixing
    * @date 2022-03-27 15:17
    * @description 加法计数器
    */
    public class CyclicBarrierDemo {
      public static void main(String[] args) {
          // 集齐七颗龙珠召唤神龙
          CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{
              System.out.println("召唤神龙成功!");
          }); // 代表需要执行完7个线程,才会执行第二个参数的线程“召唤神龙”
          for(int i=1; i<=7; i++){
              final int temp = i;
              new Thread(()->{
                  System.out.println(Thread.currentThread().getName()+"收集"+temp+"颗龙珠");
                  try {
                      cyclicBarrier.await();
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }).start();
          }
      }
    }
    

Semaphore -- (信号量)

原理:初始通过信号量定义共享资源的个数,通过acquire()和release()方法实现多个共享资源的互斥使用。常用场景如:抢车位、并发限流

  • semaphore.acquire(); // 获得,如果已经满了,则等待被释放为止。

  • semaphore.release(); // 释放,会将当前信号量释放 +1,然后唤醒等待的线程。

    /**
    * @author lixing
    * @date 2022-03-27 15:33
    * @description 信号量  场景:抢车位! 6个车 3个车位  、 限流!
    */
    public class SemaphoreDemo {
      public static void main(String[] args) {
          // 信号量 => 停车位3个  线程数 => 车辆数6个
          Semaphore semaphore = new Semaphore(3);
          for(int i=1; i<=6; i++){
              new Thread(()->{
                  try {
                      semaphore.acquire(); // acquire() 得到
                      System.out.println(Thread.currentThread().getName()+"抢到车位");
                      TimeUnit.SECONDS.sleep(2);
                      System.out.println(Thread.currentThread().getName()+"离开车位");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }finally {
                      semaphore.release(); // release() 释放
                  }
              }, String.valueOf(i)).start();
          }
      }
    }
    

ReadWriteLock -- 读写锁

概念:更细粒度的锁,针对于写操作,一次只允许一个线程占有;针对于读操作,可以同时多个线程占有

/**
* @author lixing
* @date 2022-03-27 15:56
* @description 读写锁 ReadWriteLock
* 读-读 可以共存
* 读-写 不能共存
* 写-写 不能共存
* 独占锁(写锁)一次只能被一个线程占有
* 共享锁(读锁)多个线程可以同时占有
*/
public class ReadWriteLockDemo {
  public static void main(String[] args) {
      MyCacheLock myCacheLock = new MyCacheLock();
      // 写入
      for(int i=1; i<=5; i++){
          final int temp = i;
          new Thread(()->{
             myCacheLock.put(temp+"", temp+"");
          }, String.valueOf(i)).start();
      }
      // 读取
      for(int i=1; i<=5; i++){
          final int temp = i;
          new Thread(()->{
              myCacheLock.get(temp+"");
          }, String.valueOf(i)).start();
      }
  }
}

// 自定义缓存(加锁的)
class MyCacheLock{ 
    private volatile Map<String, Object> map = new HashMap<>(); 
    // 读写锁:更加细粒度的控制 
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 存,写入的时候,只希望同时只有一个线程写
    public void put(String key, Object value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+"写入ok");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 取,读,所有人都可以读
    public void get(String key){
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取ok");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}

BlockingQueue 阻塞队列

  • 写入:如果队列满了,就必须阻塞等待

  • 读取:如果队列是空的,必须阻塞等待生产

  • 阻塞队列有四组API [添加、移除、检测队首元素]

    1. 抛出异常:add()、remove()、element()
    2. 有返回值,不抛出异常:offer()、poll()、peek()
    3. 阻塞等待:put()、take()
    4. 超时等待:offer(,,)、poll(,)
SynchronousQueue 同步队列

没有容量,进去一个元素,必须等待取出来,才能再往里放入一个元素

  • SynchronousQueue和其他的BlockingQueue不一样,它不存储元素,put了一个元素,必须从里面take出来,否则不能再put进去值

线程池

3大方法、7大参数、4种拒绝策略

池化技术

  • 程序运行的本质:占用系统的资源。 优化资源的使用 => 池化技术(线程池、连接池、内存池、对象池...)
  • 池化技术:事先准备好一些资源,有人要用,就来拿,用完后还回来
  • 线程池的好处: 线程复用、可以控制最大并发数、管理线程
    1. 降低资源消耗
    2. 提高响应速度
    3. 方便管理

线程池三大方法

  • newSingleThreadExecutor(); // 单个线程
  • newFixedThreadPool(5); // 创建一个固定大小的线程池
  • newCachedThreadPool(); // 缓冲线程池,可伸缩,遇强则强,遇弱则弱

线程池七大参数

源码分析

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, // 约等于21亿
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


// 本质:ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  • corePoolSize:核心线程大小
  • maximumPoolSize:最大线程大小
  • keepAliveTime:空闲线程超时时间,超时了没有人调用就会被释放
  • timeUnit:超时时间单位
  • workQueue:阻塞队列
  • threadFactory:线程工厂,创建线程的,一般用默认值
  • handler:拒绝策略

阿里java开发手册中【强制】不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学能更加明确线程池的运行规则,规避资源耗尽的风险。 Executors返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool:允许的任务请求队列长度为Integer.MAX_VALUE,约等于21亿,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool 和 ScheduledThreadPool:允许创建的最大线程数为Integer.MAX_VALUE,约等于21亿,可能会创建大量的线程,从而导致OOM。

线程池四种拒绝策略

  1. ThreadPoolExecutor.AbortPolicy():丢弃任务,拒绝执行,并抛出RejectedExecutionException异常
  2. ThreadPoolExecutor.CallerRunsPolicy():由调用者(调用的线程)去执行
  3. ThreadPoolExecutor.DiscardPolicy():丢弃任务,不抛出异常
  4. ThreadPoolExecutor.DiscardOldestPolicy():抛弃最早进入队列的任务,然后将当前任务入队,再次执行

最大线程数如何设置

CPU密集型、IO密集型(调优)

  • CPU密集型:几核,就是几,可以保持cpu的效率最高 (并行执行)
    • Runtime.getRuntime().availableProcessors() -- 获取当前运行服务器的CPU核数
  • IO密集型:判断程序中十分耗IO资源的线程,只要大于这个线程数就可以了,一般 2*n,保证处理IO的线程阻塞时,其他线程可以正常执行

四大函数式接口

lambda表达式、链式编程、函数式接口、Stream流式计算

1.函数式接口:只有一个方法的接口 Function,有输入,有返回,类型可自定义

// 函数式接口Function 源码 ,输入参数类型T,返回参数类型R
@FunctionalInterface
public interface Function<T, R> {

/**
    * Applies this function to the given argument.
    *
    * @param t the function argument
    * @return the function result
    */
R apply(T t);
}
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用
// 例如:foreach(消费则类的函数式接口)
public static void main(String[] args) {
//        Function function = new Function<String, String>() {
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };
    Function function = (str) -> {return str;};
    System.out.println(function.apply("123"));
}

2.断定型接口:有一个输入参数,返回值只能是布尔值。

public static void main(String[] args) {
    // 判断字符串是否为空
//        Predicate<String> predicate = new Predicate<String>() {
//            @Override
//            public boolean test(String str) {
//                return str.isEmpty();
//            }
//        };
    Predicate<String> predicate = (str)->{return str.isEmpty();};
    System.out.println(predicate.test("adf"));
}

3.消费型接口:只有输入,没有返回值

public static void main(String[] args) {
//        Consumer<String> consumer = new Consumer<String>() {
//            @Override
//            public void accept(String str) {
//                System.out.println(str);
//            }
//        };
Consumer<String> consumer = (str)->{
    System.out.println(str);
};
consumer.accept("abc");
}

4.供给型接口:没有输入,有返回值

public static void main(String[] args) {
//        Supplier<Integer> supplier = new Supplier<Integer>() {
//            @Override
//            public Integer get() {
//                return 1024;
//            }
//        };
        Supplier<Integer> supplier = ()->{return 1024;};
        System.out.println(supplier.get());
    }

Stream流式计算 (必须掌握)

什么是流式计算

  • 大数据:存储+计算
  • 集合、MySQL的本质就是存储东西的,计算都应该交给流来操作。
/*
     * 题目要求:一分钟之内完成此题,只能用一行代码实现
     * 现在只有5个用户。,筛选:
     * 1. ID必须是偶数
     * 2. 年龄大于23岁
     * 3. 用户名转为大写字母
     * 4. 用户名字母倒着排序
     * 5. 只输出一个用户
     */
    public static void main(String[] args) {
        User u1 = new User(1, "a", 21);
        User u2 = new User(2, "b", 22);
        User u3 = new User(3, "c", 23);
        User u4 = new User(4, "d", 24);
        User u5 = new User(5, "e", 25);
        // 集合就是存储
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
        // 计算交给Stream流
        // lambda表达式、链式编程、函数式接口、Stream流式计算
        list.stream()
                .filter(u->{return u.getId()%2==0;}) // 过滤出ID为偶数的
                .filter(u->{return u.getAge()>23;}) // 过滤出年龄大于23
                .map(u->{return u.getName().toUpperCase();}) // 名字转换为大写字母
                .sorted((o1, o2)->{return o2.compareTo(o1);}) // 用户字母倒序
                .limit(1)
                .forEach(System.out::println);
    }

ForkJoin

什么是ForkJoin

  • ForkJoin是在JDK1.7中提出的,对于大数据量,将任务拆成多个子任务,放入到队列中,通过多线程并行执行,将计算结果累加返回,提高效率。

    ForkJoin特性:工作窃取

  • 里面维护的是双端队列,一个线程的队列任务执行完了,会去窃取另一个线程上的任务来执行,以提升执行效率

    如何使用forkJoin

  1. new 一个 forkjoinpool,用它来执行

  2. 计算任务 forkjoinpool.execute(ForkJoinTask task)

  3. 计算类要继承 ForkJoinTask

    public class ForkJoinDemo {
     public static void main(String[] args) throws ExecutionException, InterruptedException {
         test1(); // 时间:272
         test2(); // 时间:166
         test3(); // 时间:150
     }
    
     // 常规计算方式
     public static void test1(){
         long sum = 0;
         long start = System.currentTimeMillis();
         for(long i=1; i<=10_0000_0000; i++){
             sum += i;
         }
         long end = System.currentTimeMillis();
         System.out.println("sum="+sum+" 时间:"+(end-start));
     }
    
     // forkjoin方式
     public static void test2() throws ExecutionException, InterruptedException {
         long start = System.currentTimeMillis();
         ForkJoinPool forkJoinPool = new ForkJoinPool();
         ForkJoinTask<Long> task = new ForkJoinDemo1(0, 10_0000_0000);
         ForkJoinTask<Long> submit = forkJoinPool.submit(task);
         long sum = submit.get(); // 获取线程执行结果的时候,会阻塞等待
         long end = System.currentTimeMillis();
         System.out.println("sum="+sum+" 时间:"+(end-start));
     }
    
     // stream并行流
     public static void test3(){
         long start = System.currentTimeMillis();
         long sum = LongStream.range(0, 10_0000_0000).parallel().reduce(0, Long::sum);
         long end = System.currentTimeMillis();
         System.out.println("sum="+sum+" 时间:"+(end-start));
     }
    }
    
    class ForkJoinDemo1 extends RecursiveTask<Long>{ 
        private long start; 
        private long end; 
        private long temp = 10000; // 临界值1w,超过1w就采用ForkJoin的方式执行
        public ForkJoinDemo1(long start, long end){
            this.start = start;
            this.end = end;
        }
    
    @Override
        protected Long compute() {
            if(end-start < temp){ // 小于临界值,采用常规方法计算
                long sum = 0;
                for(long i=start; i<=end; i++){
                    sum+=i;
                }
                return sum;
            }else{ // 超过临界值,采用forkJoin方式计算
                long middle = (start+end)/2; // 中间值
                ForkJoinDemo1 task1 = new ForkJoinDemo1(start, middle);
                task1.fork(); // 拆分任务,把任务压入线程队列
                ForkJoinDemo1 task2 = new ForkJoinDemo1(middle+1, end);
                task2.fork(); // 拆分任务,把任务压入线程队列
                return task1.join() + task2.join(); // 将求和结果累加
            }
        }
    
    }
    

JMM

请谈谈你对volatile的理解

  • volatile是jvm提供的轻量级同步机制
    • 保证可见性
    • 禁止指令重排
    • 不保证原子性

什么是JMM

  • JMM:java内存模型

  • 关于JMM的一些同步的约定:

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

  • 8种操作(四组操作)

    • lock(锁定):把主存中一个变量标识为线程独占状态
    • unlock(解锁):把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定
    • read(读取):把一个变量从主存中读出,传输到线程的工作内存中
    • load(载入):把read操作的变量放入到线程的工作内存中
    • use(使用):把工作内存中的变量传输给执行引擎
    • assign(赋值):把一个从执行引擎中接收到的值放入工作内存的变量副本中
    • store(存储):把工作内存中一个变量的值传输到主内存中
    • write(写入):把从工作内存传过来的变量值放到主存的变量中

volatile

  1. 保证可见性
public class VolatileDemo {
    // 不加volatile,程序会死循环 
    // 加了volatile,可以保证可见性 
    private volatile static int num = 0;
    /**
    * 线程1和线程2都读取了主存中的num变量的值,其实是都拷贝了一份num=0到自己线程的工作内存中。
    * 1秒后线程1将num改成1,并写入主存中,但是线程2没有读取主存中更新的值,所以一直在循环体中
    **/
    public static void main(String[] args) throws InterruptedException { 
        // 主线程(线程1)
        // 线程2 对主内存的变化不可见,加了volatile后,num的值在主存中发生了变化,线程2中可以获取到最新的值 
        new Thread(()->{
            while(num == 0){

            }
        }).start(); 
        TimeUnit.SECONDS.sleep(1); 
        num = 1;
        System.out.println(num);
    }
}
  1. 不保证原子性
  • 原子性:不可分割

    /**
    * @author lixing
    * @date 2022-03-29 21:19
    * @description 不保证原子性,并发下 num++的操作可能被其他线程抢占
    */
    public class VolatileDemo2 {
    private volatile static int num = 0;
    public static void add(){
       num++; // 不是原子性操作
    }
    
    public static void main(String[] args) {
       // 理论上结果应该是20000
       for (int i = 1; i <= 20; i++) {
           new Thread(()->{
               for (int j = 0; j < 1000; j++) 
                   add();
               }
           }).start();
       }
       while(Thread.activeCount() > 2){ // main gc
           Thread.yield();
       }
       System.out.println(Thread.currentThread().getName() + " " + num); // 很难输入 20000
    }
    }
    

如何在不用lock和synchronized的情况下,保证原子性

  • 使用原子类 -- 这些类的底层都直接和操作系统挂钩,在内存中修改值。
// 原子类的Integer
private volatile static AtomicInteger num = new AtomicInteger(0);
public static void add(){ 
    num.getAndIncrement(); // AtomicInteger +1的方法 CAS
}
  1. 禁止指令重排

    什么是指令重排

    • 你写的程序,计算机并不是按照你写的那样去执行的
    • 源代码 --> 编译器优化的重排 ---> 指令并行也可能会重排 ---> 内存系统也会重排 ---> 执行
    • volatile修饰后,会在指令的上面和下面添加内存屏障,防止上面的指令和下面的指令交换位置

深入理解CAS

什么是CAS

  • CAS(ComparAndSet):比较并交换,比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操作,如果不是,则一直循环。

  • 缺点:

    • 循环会耗时
    • 一次只能保证一个共享变量的原子性
    • 存在ABA问题
public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(2022);
    // 期望、更新
    // public final boolean compareAndSet(int expect, int update)
    // 如果期望的值达到了,就更新;否则,不更新  CAS是CPU的并发原语
    atomicInteger.compareAndSet(2022, 2023);
    System.out.println(atomicInteger.get()); // 2023
}

ABA问题

  • 一个线程t1拿到了内存值为1,另一个线程t2也拿到了内存值为1,然后t2将1改为2,又改回1;对于t1,它看到的内存值依旧是1,认为没有做任何修改。所以t1可以操作成功
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2022);
// ---------- 线程t2 ----------
atomicInteger.compareAndSet(2022, 2023);
System.out.println(atomicInteger.get()); // 2023
atomicInteger.compareAndSet(2023, 2022);
System.out.println(atomicInteger.get()); // 2022
// ---------- 线程t1 ----------
atomicInteger.compareAndSet(2022, 8888);
System.out.println(atomicInteger.get()); // 8888
}

如何解决ABA问题 -- 引入原子引用 AtomicStampedReference,对应的思想:乐观锁

  • 通过带版本号的原子操作
public static void main(String[] args) {
// 第二个参数,相当于版本号
// 注意:如果泛型是包装类型,注意对象的引用问题
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(34, 1);
// 让线程1先执行
new Thread(()->{
    int stamp = atomicStampedReference.getStamp(); // 获得版本号
    System.out.println("a1=>"+stamp); // a1=>1
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 模拟SQL中乐观锁的操作,更新操作的时候,将数据版本号+1
    System.out.println(atomicStampedReference.compareAndSet(34, 35, atomicStampedReference.getStamp(),
            atomicStampedReference.getStamp() + 1)); // true
    System.out.println("a2=>"+atomicStampedReference.getStamp()); // a2=>2
    System.out.println(atomicStampedReference.compareAndSet(35, 34, atomicStampedReference.getStamp(),
            atomicStampedReference.getStamp() + 1)); // true
    System.out.println("a3=>"+atomicStampedReference.getStamp()); // a3=>3
}, "a").start();
new Thread(()->{
    int stamp = atomicStampedReference.getStamp(); // 获得版本号
    System.out.println("b1=>"+stamp); // b1=>1
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 由于线程a执行了两次更新操作,版本号由1变成了3,与期望值不同了,所以这里修改失败。与乐观锁的原理相同
    System.out.println(atomicStampedReference.compareAndSet(34, 88, stamp, stamp + 1)); // false
    System.out.println("b2=>"+atomicStampedReference.getStamp()); // b2=>3
}, "b").start();
}

Unsafe类

// 源码
/**
* java无法操作内存,但是java可以通过native方法来调用c++,c++可以操作内存
* Unsafe类就相当于java的一个后门,可以通过这个类操作内存
**/
// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset // 获取内存地址偏移值
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

自旋锁

自旋锁的底层就是采用CAS的原理实现的,只要未达到期望值,就一直循环判断(自旋),直到达到期望值,进行更新

// java原子类 AtomicInteger的+1操作,底层就是CAS原理,自旋锁
atomicInteger.getAndIncrement();
// 底层源码
public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}
// var1 ==> this var2 ==> valueOffset var4 ==> 1public final int getAndAddInt(Object var1, long var2, int var4) { int var5; // 自旋锁 (基于CAS原理) do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

各种锁的理解

  1. 公平锁、非公平锁
  • 公平锁:非常公平,不能插队,必须先来后到
  • 非公平锁:非常不公平,可以插队(默认都是非公平锁,效率高)
public ReentrantLock(){
sync = new NonfairSync();
}

public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}
  1. 可重入锁
  • 某个线程获取到某个锁,可以再次获取锁,而不会出现死锁

public class LockDemo {
  public static void main(String[] args) {
      Phone5 phone5 = new Phone5();
      new Thread(()->{phone5.sms();}, "a").start();
      new Thread(()->{phone5.call();}, "b").start();
  }
}

class Phone5{ 
    Lock lock = new ReentrantLock();
    public void sms(){
        lock.lock(); // 细节问题:lock锁必须配对,否则会死在里面
        try {
            System.out.println(Thread.currentThread().getName()+" sms");
            call(); // 这里也有锁
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void call(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+" call");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
  1. 自旋锁: 基于CAS实现

  2. 死锁

/**
* @author lixing
* @date 2022-03-30 23:14
* @description 死锁 ==> 两个线程,一个占据资源1,一个占据资源2;现在他俩都想竞争对方持有的资源,造成一种相互等待的僵局
*/
public class DeadLockDemo {
private static final Object RESOURCE1 = "resource1"; // 资源1
private static final Object RESOURCE2 = "resource2"; // 资源2

public static void main(String[] args) {
    new Thread(()->{
        synchronized(RESOURCE1){
            System.out.println(Thread.currentThread().getName()+"获取资源1");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (RESOURCE2){
                System.out.println(Thread.currentThread().getName()+"获取资源2");
            }
        }
    }, "t1").start();
    new Thread(()->{
        synchronized(RESOURCE2){
            System.out.println(Thread.currentThread().getName()+"获取资源2");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (RESOURCE1){
                System.out.println(Thread.currentThread().getName()+"获取资源1");
            }
        }
    }, "t2").start();
}
}

如何解决死锁

  1. 使用 jps -l 定位进程号
  2. 使用 jstack 进程号 查看进程中的堆栈信息,分析死锁问题
  3. 比较low的方法:查看日志
全部评论

相关推荐

11-14 16:13
已编辑
重庆科技大学 测试工程师
Amazarashi66:不进帖子我都知道🐮❤️网什么含金量
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务