多线程 juc 锁相关总结

首先 锁是什么   我认为 锁 就像上厕所一样 一个位置 只能运行一个人进行 下一个人要想进去的话只能等先进去的那个人出来 才能再次尝试进去 。但是这里注意一下我们为了保证线程安全进行加锁但是我们也不能随意的加锁,需要去考虑加锁的锁的一个粒度。如果粒度过大也不行。最好还是使用代码块进行加锁因为其可以控制锁的粒度。

那多线程有什么作用?

  当我们执行一个比较大的任务时 并且可以把这个大的任务拆分成一些个小任务 而这些小任务之间 没有依赖关系 那么我们就可以使用多线程 让这个大任务中的小任务并发的去执行 这样对比与我们串行的去执行 效率会有所提升 那么java 中创建多线程的方式有那些

1. 继承 Thread 类  2 实现runable接口重写run方法 3 callable 接口重写call方法 4 使用线程池进行创建。

那么这些有什么 区别?

  因为java 只能单继承 (extend) 所以一般我会使用runable接口

 那runable 接口与callable接口有什么区别 ?

    runable接口重写run方法 没有返回值  callable 接口 重写call方法有返回值 runable 的run方法不能抛出异常 callable call方法可以  。 runable 接口 直接放在Thread 类的参数里  而 callable  对象需要用一个 funturnTask 来进行接收 并可以通过该类的get方法 能拿到callable call方法的返回值  代码如下 这里主要看看 callable接口

FutureTask<Runnable> future=new FutureTask(new CallableTest());
new Thread( future).start();
future.get();
class CallableTest implements Callable {

    @Override
    public Object call() throws Exception {
        return "hello";
    }
}

 线程 主要的一些方法 有 sleep 睡眠 多少秒  sleep 不会释放锁  还有join方法   谁里面有 join 方法 就需要 等待 调用join方法的线程执行结束才能继续执行。

  上面说java 只能单继承 但也不是绝对  在jdk1.8  接口 有了默认的方法体 比如 当 一个接口 a写了一个默认的方法  名字为test 接口B 也写了一个默认的方法 test a,b的默认方法方法名相同 现在类c实现a,b两个接口 此时就会有菱形继承的问题  这时必须让我们重写test方法 否则编译报错。

public interface A {

    public default  void AMethod(){
        System.out.println("A接口的默认方法");
    }
}
public interface B  {

    public default  void AMethod(){
        System.out.println("B接口的默认方法");
    }
}
public class C implements A,B {


    @Override
    public void AMethod() {
        
    }
}

java中 锁的功能分类 ,

      分为乐观锁与悲观锁,  乐观锁就是先执行任务 后加锁,  悲观锁 是先获取到锁之后 再去执行任务  ,所以 一般乐观锁适用于读多写少的 因为 冲突过多会浪费cpu的资源  而悲观锁 适合写多读少的。 乐观锁 Cas 比较并交换 。

 悲观锁 java 中  Synchronized ,reenterLock

    sync 有3 种方式进行加锁  

           1 同步代码块 ,2 在普通方法上加锁  ,3  在静态方法上进行加锁 

    那么他们锁的都是什么 ?

         sync 是对象锁  , 在普通方法上锁的是当前对象  。静态方法是 当前类对象 : 类锁。

         锁的相同的对象会互斥  ,但是 类锁 与 对象锁不互斥。 在 jdk1.6之前 sync 只有重量级锁  因为每次 加锁 与解锁 都需要进行上下文的切换 这很消耗资源  1.6 之后引入了 偏向锁 ,轻量级锁,  重量级锁。

     一个对象由对象头 ,示例数据  ,对其填充 ,组成   对象头又是由 markword 与类型指针组成(这个对象 所属的类) 。

    markword 里面主要存了gc的年龄 使用3位  所以是gc的年龄为15次  , hashcode值 ,加锁的情况 在 最后3位中 第一位是 是否偏向后两位 记录者锁的类型  101 偏向锁 00 轻量级锁 10 重量级锁。

为什么  调用对象的wait方法 必须写在代码块中 ?

      就是因为 sync 锁的是对象 wait方法也是对象的方法 所以必须写在代码快中  那 Thread 的sleep 方法不用在代码块中? 也是因为 睡眠 是针对的线程 在线程中就可以了 。

Thread 主要的方法 有:  wait 让当前线程等待 并释放获得的锁  必须由notify 进行唤醒  join  意思是让当前执行该方法的线程必须等待 调用join方法的线程先执行完才能进行向下执行   yield 让线程 让步 但是并不能保证下一次获取锁的线程不是执行该方法的线程

除了 sync 锁 还有reenterLock 锁

    它是 排他锁 即 互斥 还有共享锁 即多个线程都可以获得同一把锁

那这里的锁 锁的对象是什么呢  ?

     这里锁的对象其实是  一个 status值  通过volatil 进行修饰的一个成员变量  

为什么需要用它进行修饰?

      因为 成员变量 是存储在堆当中的 即线程共享 那么多个线程共享该变量 那么肯定会有线程安全的问题  即 原子性, 可见性, 与有序性。   而 volatile 修饰变量时 可以保证可见性 与有序性 。 因为当用他修饰时  通过jmm 我们可以知道,  主内存 与工作内存 , 我们对变量的操作都是在工作内存中进行操作 然后返回到主内存当中 volatile 可以保证 ,当在工作内存操作完毕后 会主要刷到所有的工作内存  ,这样即可保证可见性 即每个线程都能立即看到变量的变化  有序性则通过内存配置完成的。

那么原子性呢  ?

     当我们进行加锁时 会使用cas 进行修改status的变量的值 这样即可保证原子性。

可重入 的实现呢?

      是通过对status 进行加1  里面还有存储着当前线程的成员变量  当一个线程进行 加锁时 首先判断 存储线程的变量与当前线程id 是否一样 如果一样则对status 进行加1 这样即可做到可重入  sync 实现可重入 也是类似操作

死锁是什么?

     死锁需要满足4个条件 第一个互斥:一个资源只能被一个线程获取。

                                       第二个占有且等待:占有对方需要的资源且不释放。

                                       第三个不可抢占:不可以强行获取对方已经占有的资源。

                                      第四个循环等待:即a线程占有a资源获取b资源 b线程占有b资源等待获取a资源 

例如下面死锁

  synchornized 发生死锁

 

 ReentrantLock 发生死锁

 

 发生死锁

 

  那么如何解决死锁?

           首先可以破坏占有且等待 我们可以对获取锁时设置过期时间 如果一定时间内没有获得到锁则不去获取了 ,然后也可以破坏循环等待 让加锁的顺序变得统一有序 都首先获取a资源 然后获取b资源即可解决。

     如何解决代码如下

设置获得锁的最长时间。

 

 

首先让加锁变得有序 通过对象的hashcode值进行比较

 

 结果没有发生死锁

 

那么共享锁 有什么

      1 信号量 Semaphore 信号量构造函数传入允许的线程数量  可以控制当前的线程数量

主要的方法有acquire(); 信号量减一 如果为0 不能减来了 则阻塞 等待, release() 数量 加一

写一个例题  比如控制2个线程依此打印 a,b    如 abababab   使用信号量进行控制

Semaphore semaphoreA = new Semaphore(1);
Semaphore semaphoreB = new Semaphore(0);
new Thread(()->{
    try {
        while (count<6){
            semaphoreA.acquire();
            System.out.println("A");
            semaphoreB.release();

            count++;
        }

    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},"A").start();

new Thread(()->{
    while (count<6){
        try {
            semaphoreB.acquire();
            System.out.println("B");
            semaphoreA.release(1);
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


},"B").start();

       2.计数器 CountDownLatch 构造方法传入一个数字 主要有2个方法一个countDown()

减一的方法 await 方法 阻塞等待直到 数字为0 时

一般用于 主线程 等待 多个线程执行完毕 创建2个线程 一个排队 一个看病 互不影响 主线程需要等待 知道计数器为0时 打印结束 计数器可以控制多个线程异步执行任务 最终可以阻塞等待所有线程执行完毕后

class one implements Runnable{

    protected CountDownLatch countDownLatch;

    public one(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        System.out.println("开始排队"+ LocalDateTime.now());
        try {
            Thread.sleep(5000);
            System.out.println("排队成功"+ LocalDateTime.now());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            countDownLatch.countDown();
        }
    }
}

class two implements Runnable{

    protected CountDownLatch countDownLatch;

    public two(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        System.out.println("开始看病"+ LocalDateTime.now());
        try {
            Thread.sleep(2000);
            System.out.println("看病结束"+ LocalDateTime.now());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            countDownLatch.countDown();
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    //联系 countdownLatch     只有等待的线程 都释放了 才会释放
    CountDownLatch countDownLatch = new CountDownLatch(2);
    new Thread(new one(countDownLatch)).start();
    new Thread(new two(countDownLatch)).start();

    countDownLatch.await();
    System.out.println("结束"+LocalDateTime.now());

3.栅栏  跟计数器类似  只不过 他是 相互等待    比如下面 创建3个线程 线程的名字依此为 1  2 3 让他们3个线程 依此打印 (只有当第三个线程打印后才能打印第一个)

 //测试栅栏
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);


        new Thread(()->{
            for (int i = 0; i < 8; i++) {

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (i%3==0){
                    System.out.println("第一个栅栏第"+i+"次执行了");
                }

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {

                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i < 8; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (i%3==1){
                    System.out.println("第二个栅栏第"+i+"次执行了");
                }
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {

                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i < 8; i++) {
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (i%3==2){
                    System.out.println("第三个栅栏第"+i+"次执行了");
                }
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {

                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

线程池

   线程池的好处 主要是可以帮助我们管理线程 ,当我们需要使用线程时可以直接使用 不需要自己创建与销毁 这样可以加快效率 类似的池化技术有很多 比如字符串常量池  因为我们平常使用最多的就是字符串 所以避免频繁的创建 所以有一个字符串常量池。

线程池的创建 可以通过  Executors 进行创建 里面可以创建4种不同的线程池

也可以通过

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
} 进行创建一个线程 里面有7个参数  核心线程数,最大线程数,非核心线程数的最大存货时间,时间单位,阻塞队列,线程工程,以及最后的 当队列以及非核心线程数满了之后的拒绝策略。

 之前看阿里的开发手册中明确提出了不允许使用Executors 进行创建线程池。

为什么不允许呢?

       看一下executors 的源码 我们可以看到 我们使用Executors 创建一个固定的线程池其实还是通过 new ThreadPoolExecutor()进行创建   可以看到其阻塞队列用的时LinkedBlockingQueue() 再点进去可以看到这个阻塞队列的大小 是Integer.Max_Value 这个值是比较大的 如果我们的堆内存不足 那么可能会发送内存溢出

 为什么这个值很大 就会发送内存溢出呢?

    首先来说一下线程池的一个执行流程  首先当一个任务进来了 首先会判断核心线程数是否满了  如果没有满则创建核心线程数  如果满了 会去看阻塞队列是否满了 如果满了才会去创建非核心线程数  如果没有满则会加入到阻塞队列中等待  。也不会执行拒绝策略 一直往阻塞队列放 所以会发送内存溢出。

java8 还有一个很好用的一个技术   异步编排 比如我们需要执行一个大的任务时,我们可以把这些任务分为一些个小的任务 这些小的任务中有的有依赖关系 有的没有 如果我们单纯的使用线程池去实现会比较麻烦。异步编排就可以很好的解决这个问题

CompletableFuture.supplyAsync() 参数传递一个线程 线程里执行自己的逻辑  该方法有返回值

CompletableFuture.runAsync() 参数传递一个线程 线程里执行自己的逻辑  该方法无返回值

    然后针对有返回值的也有2种方法 一个可以拿到返回值继续做处理的 一种拿不到返回值 自己做处理   就比如当我们 在电商场景中获得一个商品的详情页 时 开始通过id 进行查找到商品的信息 第二部我们就可以通过第一步返回的值 进一步去处理 这样就有一个先后的顺序 也就是异步编排。

CompletableFuture<String> stringCompletableFuture = supplyAsync1.thenApply(c -> { System.out.println("拿到了supplyAsync1 的结果:"+c); return c + " then apply"; }); 有返回结果

supplyAsync1.thenRun(()->{ System.out.println("拿不到 supplyAsync1 返回的结果"); }); 没有返回结果

当我们需要 上面的全部执行完之后再返回时 有一个join方法 可以让这些线程 相互等待 直到都完成之后。 如下 。

//测试 completable
static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd: HH-mm-ss");

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //有返回值  第一个
    CompletableFuture<String> supplyAsync1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync1 有返回值:" + LocalDateTime.now().format(dateTimeFormatter));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {


        }
        return "apply1:";
    });
    //当 supplyAsync1 执行完之后 执行
    supplyAsync1.whenComplete((a, e) -> {
        System.out.println("执行结束返回结果" + a);
    });

    // 拿到  supplyAsync1 的返回结果 继续执行自己的任务 也可以返回结果
    CompletableFuture<String> stringCompletableFuture = supplyAsync1.thenApply(c -> {
        System.out.println("拿到了supplyAsync1 的结果:"+c);
        return c + " then apply";
    });

    supplyAsync1.thenRun(()->{
        System.out.println("拿不到 supplyAsync1 返回的结果");
    });





    //第二个
    CompletableFuture<String> supplyAsync2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync2" + LocalDateTime.now().format(dateTimeFormatter));
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return "apply2:";
    });



    CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(supplyAsync1, supplyAsync2,stringCompletableFuture);
    voidCompletableFuture.join();    //同一时刻返回 会等待所有的执行完之后才返回
    System.out.println("................................"+LocalDateTime.now().format(dateTimeFormatter));
    System.out.println(supplyAsync1.get());
    System.out.println(supplyAsync2.get());
    System.out.println(stringCompletableFuture.get());
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值