Java之多线程进阶

目录


一.上节内容复习

内容指路:Java之线程池

1.线程池的实现

1.阻塞队列保存要执行的任务

2.构造方法初始化线程的数量,不断扫描阻塞队列中的任务并执行

2.自定义一个线程池,构造方法的参数及含义

不推荐使用Executors工厂方法构建ExecutorService线程池对象,可能会存在浪费系统的资源的现象.可以自行new出来一个ExecutorService线程池对象

int corePoolSize  核心线程数,创建线程是包含的最小线程数
int maximumPoolSize  最大线程数,也叫临时线程数(核心线程数不够时,允许创建最大的线程数)
long keepAliveTime  临时空闲时长,超过这个时间自动释放
TimeUnit unit   空闲的时间单位,和keepAliveTime一起使用
BlockingQueue<Runnable> workQueue 用来保存任务的阻塞队列
ThreadFactory threadFactory  线程工厂,如何去创建线程
RejectedExecutionHandler handler  拒绝策略,触发的时机,当线程池处理不了过多的任务

3.线程池的工作原理

  • 当任务添加到线程池中时,先判断任务数是否大于核心线程数,如果不大于,直接执行任务
  • 任务数大于核心线程数,则加入阻塞队列
  • 当阻塞队列满了之后,会创建临时线程,会按照最大线程数,一次性创建到最大线程数
  • 当阻塞队列满了并且临时线程也创建完成,再提交任务,就会执行拒绝策略.
  • 当任务减少,临时线程达到空闲时长时,会被回收.

4.拒绝策略

AbortPolicy:直接拒绝任务的加入,并且抛出RejectedExecutionException异常

CallerRunsPolicy:返回给提交任务的线程执行

DiscardOldestPolicy:舍弃最老的任务

DiscardPolicy:舍弃最新的任务

5.为什么不推荐系统提供的线程池

1.无界队列

2.最大线程数使用了Integer.MAX_VALUE 

二.常见的锁策略

1.乐观锁和悲观锁

乐观锁:对运行环境处乐观态度,刚开始不加锁,当有竞争的时候才加锁

悲观锁:对运行环境处悲观态度,刚开始就直接加锁

2.轻量级锁和重量级锁

判断依据:消耗资源的多少.描述的实现锁的过程

轻量级锁:可以是纯用户态的锁,消耗的资源比较少

重量级锁:可能会调用到系统的内核态,消耗的资源比较多

3.读写锁和普通互斥锁

现实中并不是所有的锁都是互斥锁,互斥会消耗很多的系统资源,所以优化出读写锁

读锁:共享锁,读与读操作都能同时拿到锁资源

写锁:排它锁,读写,写读,写写不能同时拿到锁资源

普通互斥锁:synchronized,只要其中一个线程拿到锁资源,其他的线程就要堵塞等待.

4.自旋锁和挂起等待锁

自旋锁:不停的询问资源是否被释放,如果释放了可以第一时间获得锁资源

挂起等待锁:等待通知之后再去竞争锁,并不会第一时间获得锁资源

5.可重入锁和不可重入锁

可重入锁:对同一个锁资源可以加多次锁

不可重入锁:不可以对同一个锁资源加多次锁

6.公平锁和非公平锁

公平锁:先堵塞等待锁资源的线程先拿到锁资源

非公平锁:先争抢到锁资源的线程先拿到锁,没有先后顺序之说

  所有关于争抢的事情,大多是都是非公平的,这样可以提高系统效率.

三.synchronized实现的锁策略

  1. 既是乐观锁,又是悲观锁
  2. 既是轻量级锁,又是重量级锁                                                                                                   轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的
  3. 是普通互斥锁
  4. 既是自旋锁又是挂起等待锁
  5. 是可重入锁
  6. 是非公平锁


     

 四.CAS自旋锁

1.CAS

CAS:compare and swap,比较并交换

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

address:指的是内存地址

expectValue:期望值

swapValue:要交换的值

具体实现:用期望值(expectValue)和内存中的值(&address)进行比较,如果内存中的值和期望值相等,用要交换的值(swapValue)覆盖内存中的值(&address).如果不等,什么都不做

2.用户态自旋实现自增操作

CAS用户态实现原子性

public class Demo01_CAS {
    public static void main(String[] args) throws InterruptedException {
        //原子整型
        AtomicInteger atomicInteger = new AtomicInteger();
        Thread thread = new Thread(() -> {
            //五万次自增操作
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();

            }
        });
        Thread thread2 = new Thread(() -> {
            //五万次自增操作
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();

            }
        });
        //启动线程
        thread.start();
        thread2.start();
        //等待两个线程执行完成
        thread.join();
        thread2.join();
        System.out.println(atomicInteger);
    }
}

3.CAS工作原理

线程1将主内存的value值加载到工作内存1中,工作内存1中var5=0(expectValue),然后线程1调离CPU,线程2调入CPU,此时主内存的value值加载到工作内存1中,工作内存2中var5=0(expectValue),然后线程2调离CPU,线程1调入CPU,此时进入到while循环判断,var5==&(var1+var2=1)(主内存中value的值),将主内存中的值赋值为swapValue(var5+1)返回true,线程1操作结束.

此时线程1调离CPU,线程2调入CPU,此时线程2工作内存中var5=0(expectValue),进入到CAS操作中,此时将&(var1+var2)=1与var5=0(expectValue)进行对比,发现不相同,返回false,之后重新将主内存中的value值加载到工作内存中,此时var5=1,进入到CAS操作中,此时将&(var1+var2)=1与var5=0(expectValue)进行对比,发现相同,将主内存中的值赋值为swapValue(var5+1=2)返回true,线程2操作结束.

两个线程的操作结束,没有发生线程不安全的现象,因此我们可以总结出:CAS操作通过不停的自旋检查预期值来保证了线程安全,while循环是在用户态(应用层)的层面上支持了原子性,所以比内核态的锁效率要高很多.

4.CAS实现自旋锁

轻量级锁,自旋锁的实现

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

owner:标记了哪一个线程竞争到了锁.

还是拿线程1和线程2举例,当线程1加锁,线程1执行lock()方法,while循环的CAS操作,此时owner=null,符合预期值,将owner赋值为线程1的信息,CAS方法返回true,while循环结束,加锁方法结束,此时线程2也执行lock()方法,进入到while循环CAS操作,owner此时保存线程1的信息,返回false,然后线程2一直执行while循环,直到线程1的内容执行完毕之后,执行unlock()方法,此时owner置为null,此时线程2的CAS操作owner符合预期值null,将owner赋值为线程2的信息,返回ture,结束while循环,线程2执行相应的操作,直到完毕.

5.CAS的ABA问题

ABA分别代表预期值的三种状态.

CAS的ABA状态可能会带来的问题:接下来我们看一个具体的场景

我的账户里面有2000块钱(状态A),我委托张三说:如果我忘给李四转1000块钱,下午帮我转一下,我在中午给李四转了1000块钱(状态B),但是随后公司发奖金1000到我的账户,此时我账户有1000块钱(状态A),张三下午检查我账户,发现我有2000块钱,于是又给李四转了1000块钱,此时就出现问题了,李四收到了两次1000元,不符合我们的需求了.

解决ABA问题:

给预期值加一个版本号.

在做CAS操作时,同时要更新预期值的版本号,版本号只增不减

在进行CAS比较的时候,不仅预期值要相同,版本号也要相同,这个时候才会返回true.

五.synchronized原理

通过以上锁策略学习可以知道,synchronized在不同的时期可能会用到不同的锁策略

1.锁升级

随着线程间对锁竞争的激烈程度不断增加,锁的状态不断升级.

查看锁对象的对象头信息

在pom.xml中导入依赖

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.16</version>
        </dependency>
public class Demo02_Synchronized {
    // 定义一些变量
    private int count;
    private long count1 = 200;
    private String hello = "";
    // 定义一个对象变量
    private TestLayout test001 = new TestLayout();

    public static void main(String[] args) throws InterruptedException {
        // 创建一个对象的实例
        Object obj = new Object();
        // 打印实例布局
        System.out.println("=== 任意Object对象布局,起初为无锁状态");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        System.out.println("=== 延时4S开启偏向锁");
        // 延时4S开启偏向锁
        Thread.sleep(5000);
        // 创建本类的实例
        Demo02_Synchronized monitor = new Demo02_Synchronized();
        // 打印实例布局,注意查看锁状态为偏向锁
        System.out.println("=== 打印实例布局,注意查看锁状态为偏向锁");
        System.out.println(ClassLayout.parseInstance(monitor).toPrintable());

        System.out.println("==== synchronized加锁");
        // 加锁后观察加锁信息
        synchronized (monitor) {
            System.out.println("==== 第一层synchronized加锁后");
            System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
            // 锁重入,查看锁信息
            synchronized (monitor) {
                System.out.println("==== 第二层synchronized加锁后,锁重入");
                System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
            }
            // 释放里层的锁
            System.out.println("==== 释放内层锁后");
            System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
        }
        // 释放所有锁之后
        System.out.println("==== 释放 所有锁");
        System.out.println(ClassLayout.parseInstance(monitor).toPrintable());

        System.out.println("==== 多个线程参与锁竞争,观察锁状态");
        Thread thread1 = new Thread(() -> {
            synchronized (monitor) {
                System.out.println("=== 在线程A 中获取锁,参与锁竞争,当前只有线程A 竞争锁,轻度锁竞争");
                System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
            }
        });
        thread1.start();

        // 休眠一会,不与线程A 激烈竞争
        Thread.sleep(100);
        Thread thread2 = new Thread(() -> {
            synchronized (monitor) {
                System.out.println("=== 在线程B 中获取锁,与其他线程进行锁竞争");
                System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
            }
        });
        thread2.start();

        // 不休眠直接竞争锁,产生激烈竞争
        System.out.println("==== 不休眠直接竞争锁,产生激烈竞争");
        synchronized (monitor) {
            // 加锁后的类对象
            System.out.println("==== 与线程B 产生激烈的锁竞争,观察锁状态为fat lock");
            System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
        }
        // 休眠一会释放锁后
        Thread.sleep(100);
        System.out.println("==== 释放锁后");
        System.out.println(ClassLayout.parseInstance(monitor).toPrintable());

        System.out.println("===========================================================================================");
        System.out.println("===========================================================================================");
        System.out.println("===========================================================================================");
        System.out.println("===========================================================================================");
        System.out.println("===========================================================================================");
        System.out.println("===========================================================================================");

        // 调用hashCode后才保存hashCode的值
        monitor.hashCode();
        // 调用hashCode后观察现象
        System.out.println("==== 调用hashCode后查看hashCode的值");
        System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
        // 强制执行垃圾回收
        System.gc();
        // 观察GC计数
        System.out.println("==== 调用GC后查看age的值");
        System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
        // 打印类布局,注意调用的方法不同
        System.out.println("==== 查看类布局");
        System.out.println(ClassLayout.parseClass(Demo02_Synchronized.class).toPrintable());
        // 打印类对象布局
        System.out.println("==== 查看类对象布局");
        System.out.println(ClassLayout.parseInstance(Demo02_Synchronized.class).toPrintable());



    }

}
class TestLayout {

}

打印的信息:

无锁的状态(non-biasable)

 可偏向锁状态(biasable)

 已偏向锁状态(biased)

 当有一个线程参与竞争之后,就会升级成为轻量级锁(thin lock)

 继续创建线程参与锁竞争,那么就会升级为重量级锁

2.锁消除

在写代码的时候,程序员自己加synchronized来保证线程安全
如果加了synchronized的代码块只有读操作没有写操作,JVM就认为这个代码块没必要加锁,JVM运行的时候就会被优化掉,这个现象就叫做锁消除

简单来说就是过滤掉了无效的synchronized,从而提高了效率.JVM只有100%的把握才会优化

3.锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

四.JUC

java.util.concurrent 包的简称,JDK1.5之后对多线程的一种实现,这个包下的类都与多线程有关,提供了许多工具类.

1.Callable接口

也是描述线程任务的接口

Callable接口接口使用说明

public class Demo03_Callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 5; i++) {
                    sum += i;
                    TimeUnit.SECONDS.sleep(1);
                }
                return sum;
            }
        };
        //通过FutureTask来创建一个对象,这个对象持有Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //让线程执行好定义的任务
        Thread thread = new Thread(futureTask);
        thread.start();

        Integer result = futureTask.get();
        System.out.println("最终的结果为:" + result);
    }
}

打印结果:

Thread类没有关于Callable的构造方法,因此我们要借助FutureTask让Thread执行Callable接口定义的任务,FutureTask是Runnable的一个实现类,所以可以传入Thread的构造方法中.

Callable接口抛出异常演示:

public class Demo04_CallableException {
    public static void main(String[] args){
        // 先定义一个线程的任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 5; i++) {
                    sum += i;
                    TimeUnit.SECONDS.sleep(1);
                    throw new Exception("业务出现异常");
                }
                // 返回结果
                return sum;
            }
        };

        // 通过FutureTask类来创建一个对象,这个对象持有callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 创建线程并指定任务
        Thread thread = new Thread(futureTask);
        // 让线程执行定义好的任务
        thread.start();
        // 获取线程执行的结果
        System.out.println("等待结果...");
        Integer result = null;
        // 捕获异常
        try {
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            System.out.println("处理异常" + e.getMessage());
            e.printStackTrace();
        }
        // 打印结果
        System.out.println(result);
    }

}

打印结果:

Runnable和Callable接口的区别

1.Callable实现的是call()方法,Runnable实现的是run()方法

2.Callable可以返回一个结果,Runnable没有返回值

3.Callable要配合FutureTask一起使用.

4.Callable可以抛出异常,Runnabe不可以

2.ReentrantLock

本身就是一个锁,是基于CAS实现的纯用户态的锁

1.常用方法

lock()  tryLock()  unlock()

2.ReentrantLock类的使用

public class Demo05_ReentrantLock {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        //加锁
        lock.lock();
        //尝试加锁,死等
        lock.tryLock();
        //尝试加锁,有超时时间
        lock.tryLock(1, TimeUnit.SECONDS);
        //释放锁
        lock.unlock();
    }
}

 3.ReentrantLock模拟出现异常的处理

    //模拟出现异常的处理
    public static void exception() {
        ReentrantLock lock = new ReentrantLock();
        try {
            //加锁
            lock.lock();
            //需要实现的业务
            throw new Exception("出现异常");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();

        }
    }

4.ReentrantLock的公平和非公平锁

//公平锁
ReentrantLock lock = new ReentrantLock(true);

 //非公平锁

ReentrantLock lock = new ReentrantLock(false);

5.ReentrantLock的读写锁

public class Demo06_ReadWriteLock {
    public static void main(String[] args) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        //读锁,共享锁,读与读可以共享
        lock.writeLock();
        //写锁,排它锁,读写,读读,写读不能共存
        lock.readLock();
    }
}

6.根据不同condition休眠和唤醒操作

    /**
     * ReentrantLock可以根据不同的Condition去休眠或唤醒线程
     * 同一把锁可以分为不同的休眠或唤醒条件
     */
    private static ReentrantLock reentrantLock = new ReentrantLock();
    // 定义不同的条件
    private static Condition boyCondition = reentrantLock.newCondition();
    private static Condition girlCondition = reentrantLock.newCondition();

    public static void demo05_Condition () throws InterruptedException {
        Thread threadBoy = new Thread(() -> {
            // 让处理男生任务的线程去休眠
            try {
                boyCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 唤醒处理女生任务的线程
            girlCondition.signalAll();
        });

        Thread threadGirl = new Thread(() -> {
            // 让处理女生任务的线程去休眠
            try {
                girlCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 唤醒处理男生任务的线程
            boyCondition.signalAll();
        });



    }

7.ReentrantLock和synchronized的区别

1. synchronized 使用时不需要手动释放锁.ReentrantLock使用时需要手动释放.使用起来更灵活,但是也容易遗漏unlock.
2. synchronized在申请锁失败时,会一直等待锁资源.ReentrantLock可以通过trylock的方式等待一段时间就放弃.
3. synchronized是非公平锁, ReentrantLock 默认是非公平锁.可以通过构造方法传入一个true开启公平锁模式.
4. synchronized是一个关键字,是JVM内部实现的(
可能涉及到内核态).ReentrantLock是标准库的一个类,基于Java JUC实现(用户态实现)

3.原子类

JUC包下常见的原子类

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

 原子类是基于CAS操作实现的,因此比加锁的方式保证线程安全要高效的很多.

下面以AtomicInteger为例看一些方法

public class Demo07_Atomic {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        //相等于i++
        atomicInteger.getAndIncrement();
        System.out.println(atomicInteger);

        //相当于++i
        atomicInteger.incrementAndGet();
        System.out.println(atomicInteger);

        //相当于i--
        atomicInteger.getAndDecrement();
        System.out.println(atomicInteger);

        //相当于--i
        atomicInteger.decrementAndGet();
        System.out.println(atomicInteger);
    }
}

4.JUC四大并发工具类的使用

1.Semaphore  信号量

信号量:表示可用资源的数量,本质上就是一个计数器

时间案例:停车场展示牌,一共有100个车位,表示一共最多有100个车位可以使用

当有车开进去的时候,表示P操作(申请资源),可用车位就-1;当有车开出去的时候,表示V操作(释放资源),可用车位就+1,如果计数器已经为0了,这个时候还有车想进来,仅需要阻塞等待.

操作系统有PV操作

P操作表示申请资源,可用资源-1;

V操作表示释放资源,可用资源+1;

当没有可用资源的时候,其他线程就阻塞等待

Semaphore信号量的使用案例

public class Demo08_Semaphore {

    private static Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        //定义一个任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":申请资源");
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + ":获取到了资源");
                    //等待一秒,模仿处理业务
                    Thread.sleep(1000);
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + ":释放了资源");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        //创建10个线程
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(runnable);
            thread.start();
        }

    }
}

打印结果:

由结果可以看出,几乎所有的线程都在申请资源,但是信号量控制了最多三个可以申请到资源,因此最多有三个线程同时工作,其他的线程都处在阻塞等待的状态,等到申请到的资源的线程释放资源之后,其他阻塞等待的线程才可能申请到资源.

应用场景:需要指定有效资源个数(比如同时最多支持多少个并发执行),可以考虑使用Semaphore

2.CountDownLatch-闭锁

同时等待 N 个任务执行结束.

现实案例:相当于10个人赛跑,需要等待10个人都跑完之后,才能公布所有人的成绩.

public class Demo09_CountDownLatch {

    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + ":出发");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //执行完毕,计数减一
                countDownLatch.countDown();
                //走到这里说明这个线程已经结束
            }, "线程"+i);
            thread.start();
        }
        //等待所有的线程执行完成之后再继续执行下面的内容
        countDownLatch.await();
        System.out.println("所有的线程执行完毕");

    }
}

打印结果:

应用场景:把一个大任务分为几个小任务,或是等待一些前置资源,可以考虑使用CountDownLatch

3.CyclicBarrier-循环栅栏

通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

三个线程循环打印ABC

    private static int sharedCounter = 0;

    public static void main(String[] args) {
        // 打印的内容
        String printString = "ABC";
        // 定义循环栅栏
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
        });
        // 执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < printString.length(); i++) {
                    synchronized (this) {
                        sharedCounter = sharedCounter > 2 ? 0 : sharedCounter; // 循环打印
                        System.out.println(printString.toCharArray()[sharedCounter++]);
                    }
                    try {
                        // 等待 3 个线程都打印一遍之后,继续走下一轮的打印
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        // 开启多个线程
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();

    }

4.Exchanger-交换器

Exchanger一般用于两个工作线程之间交换数据。

5.线程安全的集合类

1.集合类线程不安全的现象

public class Demo10_List {
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; ++i) {
            int num = i;
            Thread thread = new Thread(() -> {
                list.add(num);
                System.out.println(list);
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println("=============");
        System.out.println(list);


    }
}

打印结果:

 我们可以看出报了并发修改异常的错误,而且每一次执行,报异常的位置不一样,也可能没有报异常.

如果我们在开发中遇到这个问题,我们要先考虑是不是集合类使用不恰当.也就是说我们使用了线程不安全的集合类,如何使用线程安全的集合类

2.使用线程安全的集合类

1.前面我们也学习过了Vector,HashTable,但是不推荐使用.不推荐使用,效率太低

只是方法加了synchronized

2.自己加synchronized和ReentrantLock进行加锁,也不推荐,效率还是很低.

3.通过工具类,创建一个线程安全的集合类  —不推荐

List<Object> list = Collections.synchronizedList(new ArrayList<>());

 源码分析:本质上和Vector一样,不过是将所有代码加上synchronized

 4.CopyOnWriteArrayList

它是JUC包下的一个类,使用的是一种叫写时复制技术来实现的

1.当要修改一个集合时,先复制这个集合的复本
2.修改复本的数据,修改完成后,用复本覆盖原始集合

优点:
在读多写少的场景下,性能很高,不需要加锁竞争.

缺点:
1.占用内存较多.是因为复制了一份新的数据进行修改

2.新写的数据不能被第一时间读取到.

在多线程环境下,如果使用集合类,优先推荐使用CopyOnWriteArrayList

6.线程安全的队列

1) ArrayBlockingQueue 基于数组实现的阻塞队列 2) LinkedBlockingQueue 基于链表实现的阻塞队列 3) PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列 4) TransferQueue 最多只包含一个元素的阻塞队列

7.多线程下使用哈希表

1.HashTable

线程安全的哈希表,但只是将方法简单的加上了synchronized修饰,效率很低,不推荐使用

2.HashMap

线程不安全的哈希表,单线程下使用不会报错,但是多线程环境下使用会产生线程不安全的问题

3.ConcurrentHashMap

多线程环境下推荐使用这个哈希表,并不是通过synchronized简单的加锁,与HashTable不同,而是通过JUC包下的ReentrantLock实现的加锁(基于CAS,纯用户态实现).

1.更小的锁粒度

HashTable的加锁方式:一个HashTable对象只有一把锁,当一个线程修改一个哈希桶的时候,其他线程无法修改任何一个桶的数据

ConcurrentHashMap的加锁方式:加锁的方式是synchronized,但是不是锁整个对象, 而是锁桶” (用每个链表的头结点作为锁对象)

2.只给写加锁,不给读加锁

3.共享变量用volatile修饰

4.充分利用CAS机制
比如size属性通过CAS来更新.避免出现重量级锁的情况.

5.对扩容进行了特殊优化
对于需要扩容的操作,新建一个新的Hash桶,随后的每次操作都搬运一些元素去新的Hash桶
在扩容没有完成时,两个Hash桶同时存在每次写入时只写入新的Hash桶.每次读取需要新旧的Hash桶同时读取.所有的数据搬运完成后,把老的Hash桶删除

8.死锁

死锁就是一个线程加上锁之后不运行也不释放僵住了,死锁会导致程序无法继续运行,是一个最严重的BUG之一.

死锁出现的场景:

1.一个线程一把锁

一个线程对一把锁加锁两次,如果是不可重入锁,就会产生死锁的现象,如果是可重入锁,就不会产生死锁的现象.

2.两个线程两把锁

public class Demo11_DeadLock {
    public static void main(String[] args) {
        //定义两个锁对象
        Object locker1 = new Object();
        Object locker2 = new Object();

        //线程1先获取locker1,再获取locker2
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":申请locker1");
            synchronized (locker1) {
                System.out.println(Thread.currentThread().getName() + ":获取到了locker1");
                //模拟业务处理
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //获取locker2
                System.out.println(Thread.currentThread().getName() + ":申请locker2");
                synchronized (locker2) {
                    System.out.println(Thread.currentThread().getName() + ":获取到了locker2");

                }

            }
        });
        thread1.start();
        //线程1先获取locker2,再获取locker1
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":申请locker2");
            synchronized (locker2) {
                System.out.println(Thread.currentThread().getName() + ":获取到了locker2");
                //模拟业务处理
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //获取locker1
                System.out.println(Thread.currentThread().getName() + ":申请locker1");
                synchronized (locker1) {
                    System.out.println(Thread.currentThread().getName() + ":获取到了locker1");

                }

            }
        });
        thread2.start();
    }
}

打印结果:

 线程1获取到了locker1,线程2获取到了locker2,线程1又想获取到了locker2,而线程2又想获取locker1,但是这两个锁已经被占用,而且不会被释放,所以两个线程互相拿到了对象想要获取的锁,就产生了死锁的现象.

2.发生死锁的原因
1.互斥使用:锁A被线程1占用了,线程2就不能用了

2.不可抢占:锁A被线程1占用了,线程2不能主动把锁A抢过来,除非线程1主动释放

3.请求保持:有多把锁,线程1拿到了锁A之后,不释放还要继续再拿锁B

4.循环等待:线程1等待线程2释放锁,线程2要释放锁得等待线程3先释放锁,线程3释放锁得等待线程1释放锁…形成了循环关系
3.避免死锁解决方案
以上四条是形成死锁的必要条件,打破上面四条中的任何一条就可以,逐条分析一下
1.互斥使用:这个不能打破,这个是锁的基本特性

2.不可抢占:这个也不能打破,这个也是锁的基本特性

3.请求保持:这个有可能打破,取决于代码怎么写

4.循环等待:约定好加锁顺序就可以把破循环等待,t1.locker1 ->locker2,t2.locker2 -> locker1这个顺序造成了循环等待,如调整加锁顺序,就可以避免循环等待

现在举一个哲学家吃面的案例:一共有五个哲学家,一共5个筷子.

哲学家只做两件事情:吃面或者思考人生(阻塞等待).

 如果所有的哲学家都先拿右手的筷子,然后再尝试拿左手的筷子的时候,发现左手的筷子都被占用了,全部都会阻塞得不到筷子,所以就会产生死锁.

现在我们重新安排一下,就可以避免死锁的问题.

让每个哲学家先拿身边编号小的筷子,然后再拿身边编号相对大的筷子.

这样哲学家1和哲学家5就会先拿筷子1,比如哲学家1争抢到了筷子1(此时哲学家5处在阻塞等待状态),哲学家2拿到筷子2,哲学家3拿到筷子3,哲学家4可以拿到筷子4和筷子5,哲学家4吃完之后释放筷子,哲学家3,2,1依次吃面,最后哲学家5拿到筷子吃饭,就可以避免死锁的问题.

9.ThreadLocal

场景:多个班级根据各班的人数订制校服

public class Demo12_ThreadLocal {
    // 初始化一个ThreadLocal
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 多个线程分别去统计人数
        Thread thread1 = new Thread(() -> {
            // 统计人数
            int count = 35;
            threadLocal.set(count);
//            Integer value = threadLocal.get();
//            System.out.println(value);
            // 订制校服
            print();
        }, "threadNameClass1");

        Thread thread2 = new Thread(() -> {
            // 统计人数
            int count = 40;
            threadLocal.set(count);
//            Integer value = threadLocal.get();
//            System.out.println(value);
            // 订制校服
            print();
        }, "threadNameClass2");

        thread1.start();
        thread2.start();
    }

    // 订制校服
    public static void print() {
        // 从threadLocal中获取值
        Integer value = threadLocal.get();
        System.out.println(Thread.currentThread().getName() + " : 需要订制 " + value + "套校服.");
    }

}

相当于一个map,以当前线程作为key,值为value保存

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月21日
下一篇 2023年12月21日

相关推荐