多线程学习小结

最近学了下多线程,做了一些笔记,整理如下。

在这里插入图片描述

死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,无外力作用,它们都将无法推进下去。此时称系统处于死锁状态

下面就是一个死锁的例子,线程1持有资源1,线程2持有资源2,他们同时都想申请对方的资源,这两个线程就会互相等待而进入死锁状态。

    //资源1
    private static final Object resources1 = new Object();
    //资源2
    private static final Object resources2 = new Object();

    public static void main(String[] args) {

        new Thread(()->{
            synchronized (resources1){
                System.out.println(Thread.currentThread().getName()+"get resources1");
                try {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"waiting get resources2");
                    synchronized (resources2){
                        System.out.println(Thread.currentThread().getName()+"get resources2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"死锁线程1").start();

        new Thread(()->{
            synchronized (resources2){ //正常运行只需把resources2改为resources1
                System.out.println(Thread.currentThread().getName()+"get resources2");
                try {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"waiting get resources1");
                    synchronized (resources1){//正常运行只需把resources1改为resources2
                        System.out.println(Thread.currentThread().getName()+"get resources1");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"死锁线程2").start();
    }

产生死锁必须具备以下条件:

  1. 互斥条件:该资源任意一个时刻只有一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对以获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在为使用完之前不能被其他线程强行剥夺,只有自己使用完后自动释放。
  4. 循环等待条件:若干线程之间形成一种首尾相接的循环等待的资源关系。
    如何避免产生死锁:就是破坏产生死锁的条件之一。

volatile关键字

把变量声明为volatile,这就告诉JVM,这个变量是不稳定的,每次使用它到主存中进行读取。volatile关键字主要作用就是保证变量的可见性还有一个作用是防止重排序。通俗一点来讲就是一个变量被volatile修饰,则Java可以确保所有线程看到这个变量的值是一致的。如果某个线程修改其值,那么其他线程立马可以看到更新,这就是内存可见性。
原理:通过屏障指令
不能保证数据的原子性:解决方法synchronized,ReentrantLock,AtomicInteger

ReentrantLock可重入锁

ReentrantLock属于JUC的locks包,是一种递归无阻塞的同步机制,其内部Sync继承了AQS队列同步器

synchronized和volatile区别

  • volatile关键字是线程同步的轻量级实现,所以性能比synchronized好,但volatile只能修饰变量,synchronized可以修饰方法和代码块。
  • 多线程访问volatile关键字不会发生阻塞,而synchronized可能发生阻塞。
  • volatile保证数据的可见性,但不能保证数据的原子性。synchronized都保证了。
  • volatile主要解决变量在多个线程中的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

synchronized和ReentrantLock异同

1.相同点:Lock可以完成synchronized的所有功能。

2.不同点:Lock比synchronized语义更精准性能更好,而且不强制要求一定要获取锁。synchronized会自动释放锁,而Lock需要手动释放。具体的来说有以下几点:

  • 含义不同

synchronized是关键字,属于JVM层面的,Lock是JUC包下的,是jdk1.5后api层面的锁。

  • 使用方法不同

synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock需要用户手动释放锁,没有手动释放可能导致死锁

  • 等待是否可以中断

synchronized不可以被中断,除非抛出异常或者运行完成。ReentrantLock可以中断,一种是通过tryLock(long time,TimeUtil util),另一种是lockInterruptibly () 放代码块中,调用interrupt()方法进行中断。

  • 加锁是否公平

synchronized是非公平锁,ReentrantLock默认是非公平锁,可以在构造方法中的加入Boolean参数进行设置,true代表公平锁。

synchronized关键字加到static静态方法和synchronized(class)代码块都是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁。

JUC之AQS(AbstractQueuedSynchronizer)队列同步器

  • 他是构建锁和其他同步组件的基础框架,是一个抽象类
  • state状态 AQS维护了一个volatile int类型的同步状态。当state>0表示已经获得了锁,state=0时表示释放了锁
  • 资源共享方式:独占(ReenntrantLock) 和 共享(Simaphore/CountDownLatch)
  • CLH队列,FIFO队列,AQS依赖他完成同步状态的管理。

JUC之CAS(Compare and Swap)

  1. 介绍 :比较并替换
  2. 原理 :三个参数,一个当前内存的值V,旧的预期值A,即将更新的值B,当且仅当旧的预期值A等于当前内存值V相同时,将内存值修改为B并返回true。
  3. 整个过程中根本就没有获取锁,释放锁的操作,是硬件层面的原子操作。
  4. JUC下atomic类都是通过CAS来实现的,Unsafe是CAS核心类,其里面方法是native修饰的。
  5. 多CPU的CAS的处理 总线加锁 缓存加锁
  6. 缺陷一:如果CAS一直不成功,自旋CAS长时间不成功,会给CPU带来很大的开销,可以限制自选次数。缺陷二:只能保证一个共享变量原子操作,如多个共享变量就只能加锁。缺陷三:ABA问题,CAS检查没有发生值改变但实际发生了变化。解决方案,加版本号,1A,2B,3A。可以通过AtomicStampedReference来解决。

JUC之atomic包

atomic包提供一系列了原子操作的类,而它的原子性实现原理则是基于CAS(compare and swap)技术。就拿AtomicInteger为例:

  1. AtomicInteger 内部使用 CAS 原子语义来处理加减等操作,CAS 全称 Compare And Swap(比较与交换),通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新。CAS 是内部是通过 Unsafe 类实现,而 Unsafe 类的方法都是 native 的,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作。
  2. 缺点: 循环开销大。如果 CAS 失败,会一直尝试。如果 CAS 长时间不成功,会给 CPU 带来很大的开销;只能保证单个共享变量的原子操作,对于多个共享变量,CAS 无法保证;存在ABA问题。

ABA问题 常见于数据更新的场景,CAS需要在操作时检查内存变化,没有变化才会更新内存值。但如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但实际上是变化的。

解决方案:ABA一般都是通过版本号机制来解决的,A-B-A变成了1A-2B-3A

JUC之工具类

CyclicBarrier(同步屏障)

当指定数量的线程到达后才取消等待,执行后面的方法。

package com.lank.heima.week1;
import java.util.concurrent.CyclicBarrier;

/**
 * @author lank
 * @date 2020/4/4 15:25
 * @desc 同步屏障
 *       模拟五个运动员比赛跑步,
 *       只有当五个运动员都准备好
 *       后开始比赛
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        for (int i =0;i<5;i++){
            new Thread(new Ath(cyclicBarrier,"线程"+i)).start();
        }

    }

    static class Ath implements Runnable{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Ath(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name+"已就位");
            try {
                cyclicBarrier.await();
                System.out.println(name+"到达终点");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果如下:

线程0已就位
线程1已就位
线程2已就位
线程3已就位
线程4已就位
线程4到达终点
线程1到达终点
线程3到达终点
线程2到达终点
线程0到达终点
CountDownLatch(倒计数)

用给定的计数初始化CountDownLatch。由于调用了countDown()方法,所以当前计数到达零之前,await()方法一直阻塞。之后会释放所有等待线程,await的所有后续调用会立即返回。这种现象只会出现一次,需要多次重置计数可以使用CyclicBarrier。

package com.lank.heima.week1;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;

/**
 * @author lank
 * @date 2020/4/5 16:02
 * @desc 倒计数器
 *  接力跑,当起点运动员到达接力点后接力运动员开始跑
 */
public class CountDownLatchDemo {
    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for (int i =0;i<5;i++){
            CountDownLatch countDownLatch = new CountDownLatch(1);
            //起点运动员
            new Thread(new Ath(cyclicBarrier,countDownLatch,"起点运动员"+i)).start();
            //接力运动员
            new Thread(new Ath(countDownLatch,"接力运动员"+i)).start();
        }

    }

    static class Ath implements Runnable{
        private CyclicBarrier cyclicBarrier;
        private String name;
        private CountDownLatch countDownLatch;

        //起点运动员
        public Ath(CyclicBarrier cyclicBarrier,CountDownLatch countDownLatch, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.countDownLatch = countDownLatch;
            this.name = name;
        }

        //接力运动员
        public Ath(CountDownLatch countDownLatch, String name) {
            this.countDownLatch = countDownLatch;
            this.name = name;
        }

        @Override
        public void run() {
            //判断是否为起点运动员
            if (cyclicBarrier!=null){
                System.out.println(name+"已就位");
                try {
                    cyclicBarrier.await();
                    System.out.println(name+"到达接力点");
                    //到达接力点,countDownLatch-1
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }else {//为接力运动员
                System.out.println(name+"已就位");
                try {
                    countDownLatch.await();
                    System.out.println(name+"到达终点");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

运行结果:

起点运动员0已就位
接力运动员1已就位
起点运动员1已就位
接力运动员0已就位
起点运动员2已就位
接力运动员2已就位
起点运动员3已就位
接力运动员3已就位
起点运动员4已就位
接力运动员4已就位
起点运动员4到达接力点
起点运动员0到达接力点
起点运动员1到达接力点
起点运动员2到达接力点
接力运动员1到达终点
接力运动员4到达终点
接力运动员0到达终点
起点运动员3到达接力点
接力运动员2到达终点
接力运动员3到达终点
Semaphore (信号量)

Semaphore维护了一个信号量许可集,当信号量中有可用的许可时线程能获得该许可,否则线程必须等待,直到有可用的许可为止。

package com.lank.heima.week1;

import java.util.concurrent.Semaphore;

/**
 * @author lank
 * @date 2020/4/5 16:28
 * @desc Semaphore停车场案例
 */
public class SemaphoreDemo {

    public static void main(String[] args) {
        //3个车位停车场
        Parking parking = new Parking(3);
        for (int i=0;i<5;i++){
            new Thread(new Car(parking)).start();
        }
    }

    //停车场
    static class Parking{
        //信号量
        private Semaphore semaphore;

        public Parking(int count) {
            semaphore = new Semaphore(count);
        }

        public void park(){
            //获取信号量
            try {
                semaphore.acquire();
                long time = (long) (Math.random()*10+1);
                System.out.println(Thread.currentThread().getName()+"进入停车场,停车"+time+"秒......");
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName()+"开出停车场......");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放信号量
                semaphore.release();
            }
        }
    }
    static class Car implements Runnable{
        Parking parking;

        public Car(Parking parking) {
            this.parking = parking;
        }

        @Override
        public void run() {
            //进入停车场停车
            parking.park();
        }
    }
}

运行结果如下:

Thread-1进入停车场,停车1......
Thread-0进入停车场,停车4......
Thread-1开出停车场......
Thread-2进入停车场,停车7......
Thread-3进入停车场,停车9......
Thread-0开出停车场......
Thread-4进入停车场,停车1......
Thread-4开出停车场......
Thread-2开出停车场......
Thread-3开出停车场......

为什么要使用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

如何使用线程池

工具类 Executors 提供了静态工厂方法以生成常用的线程池:

  1. newSingleThreadExecutor:创建一个单线程的线程池。如果该线程因为异常而结束,那么会有一个新的线程来替代它
  2. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值,一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(默认 60 秒不执行任务)的线程。当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  4. newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
  5. Executors也提供自定义的线程构造方法。

JUC之并发容器

ConcurrentHashMap

jdk1.8以前,ConcurrentHashMap采用分段锁的概念。1.8以后,利用CAS+Synchronized来保证并发安全问题,底层采用数组+链表+红黑树的存储结构。

HashMap

jdk1.7 数组+链表 将新值放入链表的表头(在多线程下可能造成死循环) 先扩容后插入新值

jdk1.7 ConcurreentHashMap 由一个个Segemnt(分段锁)组成,Segment通过继承ReentrantLock进行加锁,每次加锁操作的是一个segment,这就保证每个segement安全。默认16个,初始化后不可扩容,Segment内部的数组可以扩容。

jdk1.8 数组+链表+红黑树,当链表中的元素超过8个,链表会转为红黑树(提高查询效率),先插入新值后扩容

jdk1.8 ConcurrentHashMap CAS+Synchronized,底层采用数组+链表+红黑树的存储结构。

总结
  1. HashMap线程不安全,ConcurrentHashMap是线程安全的,仅仅是指对容器操作的时候是安全的
  2. ConcurrentHashMap的get方法不涉及锁
  3. ConcurrentHashMap允许一边遍历一边更新
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值