Java之多线程进阶

目录

一.上节内容复习

1.线程池的实现

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

3.线程池的工作原理

4.拒绝策略

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

二.常见的锁策略

1.乐观锁和悲观锁

2.轻量级锁和重量级锁

3.读写锁和普通互斥锁

4.自旋锁和挂起等待锁

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

6.公平锁和非公平锁

三.synchronized实现的锁策略

 四.CAS自旋锁

1.CAS

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

3.CAS工作原理

4.CAS实现自旋锁

5.CAS的ABA问题

五.synchronized原理

1.锁升级

2.锁消除

3.锁粗化

四.JUC

1.Callable接口

2.ReentrantLock

1.常用方法

2.ReentrantLock类的使用

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

4.ReentrantLock的公平和非公平锁

5.ReentrantLock的读写锁

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

7.ReentrantLock和synchronized的区别

3.原子类

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

1.Semaphore  信号量

2.CountDownLatch-闭锁

3.CyclicBarrier-循环栅栏

4.Exchanger-交换器

5.线程安全的集合类

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

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

6.线程安全的队列

7.多线程下使用哈希表

1.HashTable

2.HashMap

3.ConcurrentHashMap

8.死锁

9.ThreadLocal


一.上节内容复习

内容指路: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了。

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保存

猜你喜欢

转载自blog.csdn.net/qq_64580912/article/details/130668183