day14 线程池、原子性、并发工具类

目录

一、线程池

1.1 线程状态

1.2 线程池的基本原理

1.3 Executors默认线程池

1.4 创建指定上限的线程池

1.5 ThreadPoolExecutor(线程池执行器)

1.5.1 非默认拒绝策略

二、原子性

2.1 volatile

2.2 synchronized解决

2.3.1 volatile关键字不能保证原子性(同步锁可以)

2.4 AtomicInteger

2.4.1 AtomicInteger的构造方法

2.4.2 AtomicInteger的成员方法

2.4.3 AtomicInteger修改冰淇淋案例

2.4.4 AtomicInteger原理

2.5 synchronized和CAS的区别

三、并发工具类

3.1 Hashtable

3.2 ConcurrentHashMap

3.3 小结

3.4 ConcurrentHashMap1.7原理

3.5 ConcurrentHashMap1.8原理

3.6 CountDownLatch

3.7 Semaphore


一、线程池

1.1 线程状态

 如何验证线程的状态有几种?
    API中查询Thread.State内部类, 可以看到虚拟机中线程的六种状态
    
虚拟机中线程的六种状态?
    NEW                        创建对象
    RUNNABLE             start()
    BLOCKED               无法获得锁对象
    WAITING                 wait()
    TIMED_WAITING    sleep()
    TERMINATED         所有代码执行完毕

1.2 线程池的基本原理

问题: 为什么要用到线程池?

之前使用多线程存在类似的问题? -> 效率问题
    1. 用到线程就要创建
    2. 用完之后线程就消亡了


解决方案:

1. 创建一个池子(线程池), 刚开始是空的
2. 有任务要执行时, 才会创建线程对象, 任务执行完毕, 将线程对象归还给池子
3. 所有任务都执行完毕, 关闭连接池

1.3 Executors默认线程池

Executors类创建线程池的方法?
    1. newCachedThreadPool(); //创建线程池, 默认是空的, 最大容纳int的MAX_VALUE个线程
    2. newFixedThreadPool(int 最大容量);

代码示例

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //创建线程池服务对象,可以控制线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //通过线程池服务对象,创建线程
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "线程执行了");
        });
        Thread.sleep(1000);

        //通过线程池服务对象,创建线程
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "线程执行了");
        });
        //关闭线程池
        executorService.shutdown();
    }
}

1.4 创建指定上限的线程池

代码示例

public class Demo01 {
    public static void main(String[] args) {
        //创建线程池,最大容量为10
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //创建两个线程
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName()+"线程执行了");
        });

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName()+"线程执行了");
        });


        executorService.shutdown();
        
    }
}

如果查询默认的线程数量?
    1. debug查看, workers表示线程数量, 初始size=0
    2. 调用ThreadPoolExecutor对象的getPoolSize方法

1.5 ThreadPoolExecutor(线程池执行器)

核心线程数量, //不能小于0
最大线程数量,  //不能小于等于0, 最大数量 ?= 核心线程数量
空闲线程最大存活时间,  //不能小于0
时间单位, //时间单位
任务队列,  //不能为null
创建线程工厂, //不能为null
拒绝策略 //不能为null 

 默认任务拒绝策略

ThreadPoolExecutor.AbortPolicy(); 丢弃任务抛出异常(默认策略)

代码示例

public class Demo01 {
    public static void main(String[] args) {
        //创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2,//核心线程数量
                5,//最大线程数量
                2, //空闲线程最大存活时间
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<>(10),//阻塞队列,任务队列
                Executors.defaultThreadFactory(),//默认工程,自动创建线程对象
                new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略(停止)
                //拒绝时机,当提交的任务>池子中的最大线程数量+队列中的容量
                //拒绝策略,4种
        );

        //创建线程对象
        //当提交的任务小于等于(池子中的最大线程数量+队列中的容量)15
        for (int i = 1; i <= 15; i++) {
            pool.submit(new MyRunnable());
        }
        pool.submit(new MyRunnable());


        //释放资源
        pool.shutdown();

    class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程执行了");
    }
}
//当提交的任务大于(池子中的最大线程数量+队列中的容量)15
        //报错
        /*
        for (int i = 1; i <= 16; i++) {
            pool.submit(new MyRunnable());
        }*/
        /*
        Exception in thread "main" java.util.concurrent.RejectedExecutionException:
        Task java.util.concurrent.FutureTask@4411d970[Not completed, task =
        java.util.concurrent.Executors$RunnableAdapter@3796751b[Wrapped task =
        com.默认线程策略.MyRunnable@67b64c45]] rejected from java.util.concurrent.ThreadPoolExecutor@6442b0a6[Running,
        pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0]

1.5.1 非默认拒绝策略

非默认任务拒绝策略
    ThreadPoolExecutor.DiscardPolicy(); 丢弃任务不抛出异常(不推荐)
        只能看到线程池最大线程数量 + 任务队列容量条结果, 没有报错提示
        
    ThreadPoolExecutor.DiscardOldestPolicy(); 抛弃队列中等待时间最久的,将当前任务加入
        只能看到线程池最大线程数量 + 任务队列容量条结果, 其中最后一条是最的任务
        
    ThreadPoolExecutor.CallerRunsPolicy(); 调用任务的run()绕过线程池执行
        只能看到线程池最大线程数量 + 任务队列容量条结果, main方法帮我们执行了其他任务
        

二、原子性

2.1 volatile

问题:女孩和男孩共同持有一笔资金,男孩某天拿走一部分,

需求:要求女孩让知道,金额变动

1. Money类

代码示例

public class Money {

    public static int money=100000;
}

2. Boy类

代码示例

public class Boy extends Thread{
    @Override
    public void run() {
        try {
            Thread.sleep(10);
            Money.money = 90000;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money=90000;
    }
}

3.Girl类

代码示例

public class Girl extends Thread{

    @Override
    public void run() {
        while (Money.money==100000){

        }
        System.out.println("金额变动");
    }
}

4. 测试类

代码示例

public class Demo {
    public static void main(String[] args) {
        Girl girl = new Girl();

        girl.setName("女孩");

        Boy boy = new Boy();
        boy.setName("男孩");

        girl.start();
        boy.start();
    }
}

问题:
    我们发现启动线程后没有任何结果
    原因是男孩线程修改了金额, 没有"告诉"女孩线程
    所以女孩线程, 在死循环中一直重复

问题详解:
    1. 堆内存是唯一的, 但是每一个线程都有自己的线程栈
    2. 每一个线程在使用堆里面的共享数据时, 都会拷贝一份到自己的[变量副本]中
如果一个线程修改了共享数据, 那么其他线程不一定能及时更新自己的[变量副本]为最新值

volatile解决:
    通过volatile关键字解决(修饰共享数据)
    强制要求每一次都会查看最新值

只需要在Money类中的成员变量位置加上volatile关键字

代码示例

public class Money {

    public static volatile int money=100000;
}

2.2 synchronized解决

1. Money类

代码示例

public class Money {

    public static Object lock = new Object();
    public static volatile int money = 100000;
}

2. Boy类

代码示例


public class Boy extends Thread{
    @Override
    public void run() {
        synchronized (Money.lock) {
            try {
                Thread.sleep(10);
                Money.money = 90000;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.Girl类

代码示例

public class Girl extends Thread{

    @Override
    public void run() {
        while (true){
            synchronized (Money.lock){
                if(Money.money!=100000){
                    System.out.println("金额发生变动");
                    break;
                }
            }

        }
    }
}

2.3 原子性

原子性:
    是指在一次或者多次操作过程中, 要么所有的操作全部执行(不受其他因素干扰), 要么全部不执行
    多个操作是一个不可分割的整体

代码示例

public class Demo {
    public static void main(String[] args) {
        MyAtomicThread mt = new MyAtomicThread();
        for (int i =1;i<=100;i++) {
            Thread t = new Thread(mt);
            t.start();

        }
    }
}

class MyAtomicThread implements Runnable {
    private int count = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + "送了" + count + "个冰淇淋");
        }
    }
}

代码运行结果可能错误! 不是100000,问题分析: 
count++不是一个原子性的操作, 每一步操作都有可能被抢执行权!
1.从共享数据中读取数据到本线程栈中
2.修改本线程栈中的变量副本
3.将本线程栈中的变量副本的值,赋值给共享数据

2.3.1 volatile关键字不能保证原子性(同步锁可以)

操作思路:
    1. 上述代码count++不具备原子性
    2. 使用volatile关键字试试能不能解决? -> 测试后发现不能

原因解析:
    volatile关键字只能保证当前线程栈中, 变量副本的值是新的
    但是不能保证count++操作的原子性 -> count++这里的三步还是有可能被抢走执行权
如何解决?
    1. 同步锁(效率低)
    2. AtomicInteger(效率高,线程安全)

2.4 AtomicInteger

AtomicInteger效率高,线程安全

2.4.1 AtomicInteger的构造方法

在JDK1.5之后提供了一个在util包下的comcurrent包下的atomic包(原子包)
里面提供对各种类的原子操作

AtomicInteger常用方法?
构造方法:
public AtomicInteger();
public AtomicInteger(int initialValue);
代码示例

public class AtomicInteger02 {
    public static void main(String[] args) {
        // 空参构造 public AtomicInteger();
        AtomicInteger ac = new AtomicInteger();
        System.out.println(ac); //0

        // 带参构造 public AtomicInteger(int initialValue);
        AtomicInteger ac2 = new AtomicInteger(10);
        System.out.println(ac2); //10
    }
}

2.4.2 AtomicInteger的成员方法

成员方法:
int get(); 获取值
int getAndIncrement(); 以原子方式将当前值+1, 返回自增前的值
int incrementAndGet(); 以原子方式将当前值+1, 返回自增后的值
int addAndGet(int data); 以原子方式将当前值和参数相加, 返回相加结果
int getAndSet(int value); 以原子方式设置为参数的值, 返回旧值
代码示例

public class AtomicInteger01 {
        public static void main(String[] args) {
            // 创建对象
            AtomicInteger ac = new AtomicInteger(10);
            // int get(); 获取值
            System.out.println(ac.get()); //获取值 10

            // int getAndIncrement(); 以原子方式将当前值+1, 返回自增前的值
            System.out.println(ac.getAndIncrement()); //返回自增前的值 10
            System.out.println(ac.get()); //10 + 1 = 11

            // int incrementAndGet(); 以原子方式将当前值+1, 返回自增后的值
            System.out.println(ac.incrementAndGet()); //11 + 1 = 12

            // int addAndGet(int data); 以原子方式将当前值和参数相加, 返回相加结果
            System.out.println(ac.addAndGet(20)); //返回相加结果 32

            // int getAndSet(int value); 以原子方式设置为参数的值, 返回旧值
            System.out.println(ac.getAndSet(50)); //返回旧值 32
            System.out.println(ac.get()); //设置为参数的值 50
        }
    }

2.4.3 AtomicInteger修改冰淇淋案例

代码示例

public class Demo {
    public static void main(String[] args) {
        MyAtomicThread mt = new MyAtomicThread();
        for (int i = 1;i<=100;i++) {
            Thread t = new Thread(mt);//或者给0
            t.start();

        }
    }
}

class MyAtomicThread implements Runnable {
   AtomicInteger ac =new AtomicInteger();

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
           int count = ac.incrementAndGet();//以原子方式将当前值+1, 返回自增后的值
           System.out.println(Thread.currentThread().getName() + "送了" + count + "个冰淇淋");
        }
    }
}

2.4.4 AtomicInteger原理

AtomicInteger原理: CAS + 自旋
    有三个操作数据 (内存值V, 旧值A, 要修改的值B)
    当旧值A == 内存值, 证明在当前线程操作时, 没有其他线程来过, 此时可以修改, 将V改为B
    当旧值A != 内存值, 证明在当前线程操作时, 有其他线程来过, 此时不能修改, 进行自旋
    自旋: 重新获取现在的最新内存值V, 继续进行上述判断

简单理解:
    在修改共享数据时, 将修改前的旧值记录下来
        如果现在的内存值, 和原来的旧值一样, 证明没有其他县城操作过内存值, 则可以修改内存值
        如果现在的内存值, 和原来的旧值不一样, 证明有其他线程操作过内存值, 则不能修改内存值
        继续获取最新的内存值, 再次进行上述操作(自旋)
      

2.5 synchronized和CAS的区别

synchronized和CAS的区别:

    1.相同点: 
        在多线程的情况下, 都可以保证共享数据的安全性
        
    2.不同点
        synchronized是从最坏的角度出发, 认为每次获取数据的时候, 别人都有可能修改, 所以每次操作共享数据之前, 都会上锁 (悲观锁),效率低
            
        CAS时候从乐观的角度出发, 认为每次获取数据的时候, 别人都不会修改, 所以不会上锁. 只不过在修改共享数据的时候, 检查一下别人有没有操作过这个数据 (乐观锁)
        如果操作了, 那么再次获取最新的值
        如果没有操作, 那么直接修改共享数据的值

三、并发工具类

3.1 Hashtable

HashMap是线程不安全的, 在多线程环境下会存在问题
为了保证线程安全, 我们可以使用Hashtable, 但是Hashtable效率低

Hashtable底层是哈希表结构, 由数组 + 链表组成
    数组默认长度16, 加载因子0.75 (存满12个要扩容)
    链表是当计算当前元素要存入的位置有元素时, 先判断内容
        如果一样则不存
        如果不一样老元素挂在新元素下, 形成链表 (哈希桶)

Hashtable效率低的原因:
    通过查看底层代码我们发现, Hashtable底层使用悲观锁(synchronized),来保证数据的安全性, 只要有线程访问,就会把整张表锁起来

代码示例

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //HashMap<String,String>hm = new HashMap<>();

        Hashtable<String,String> hm = new Hashtable<>();
        Thread t1 = new Thread(
                ()->{
                    for (int i = 0; i < 25; i++) {
                        hm.put(i+"",i+"");
                    }
                }
        );
        Thread t2 = new Thread(
                ()->{
                    for (int i = 25; i < 51; i++) {
                        hm.put(i+"",i+"");
                    }
                }
        );
        t1.start();
        t2.start();

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

        //为了t1和t2能把数据添加进去,这里让main线程休眠一会
        Thread.sleep(1000);

        for (int i = 0; i < 51; i++) {
            System.out.println(hm.get(i+""));
        }
    }
}

使用HashMap集合,会导致打印的结果出现null,

3.2 ConcurrentHashMap

ConcurrentHashMap<String,String> hm = new ConcurrentHashMap<>();

3.3 小结

HashMap: 线程不安全
Hashtable: 线程安全, 效率低 (底层悲观锁锁整张表)
ConcurrentHashMap: 线程安全, 效率高(分析JDK7和8的底层区别)

3.4 ConcurrentHashMap1.7原理

创建ConcurrentHashMap对象时:
    创建一个长度为16的大数组, 加载因子是0.75 (Segment[])
    创建一个长度为2的小数组, 将地址值赋值给0索引处, 其他索引位置都为null (HashEntry[])
    
添加元素时, 根据键的哈希值来计算出在大数组中的位置
    如果为null, 按照模板创建小数组
    创建完毕, 会二次哈希计算出在小数组中应存入的位置, 由于第一次都是null所以直接存入
        如果不为null, 会二次哈希, 计算出在小数组中应存入的位置
            如果小数组需要扩容, 则扩容为2倍 (存到索引1的地方)
            如果不需要扩容, 则会判断小数组当前索引位置是否为null
                如果为null代表没有元素, 直接存入
                如果不为null代表有元素, 则根据equals方法比较属性值
                    一样则不存
                    不一样则将老元素挂在新元素下, 形成链表 (哈希桶)
 
综上所述, 如果这个大数组Segment[]存满了, 就是一个16*16的大哈希表

为什么效率高?
    因为每一次操作只会锁小表 (小数组HashEntry[]), 不会锁大表
    所以在JDK1.7之前, 某一时刻最多允许16个线程同时访问
 

3.5 ConcurrentHashMap1.8原理

相关快捷键:
    Alt + 7: 底层中展示所有方法
    Ctrl + Alt + Shift + U: 底层中展示继承结构

ConcurrentHashMap在JDK1.8底层分析:
    结构: 哈希表 (数组 + 链表 + 红黑树)
    线程安全: CAS机制 + synchronized同步代码块
    
    1. 如果使用空参构造创建ConcurrentHashMap对象时, 则什么都不做 (查看空参构造及父类的空参)
    2. 在第一次添加元素时 (调用put方法时) 创建哈希表 (initTable方法)
        计算当前元素应存入的索引位置
            如果为null, 代表没有元素, 则通过CAS算法, 将本节点添加到数组中
            如果不为null, 代表有元素, 则利用volatile获得当前索引位置最新的节点地址
                挂在它下面, 形成链表, 链表长度大于等于8的时候, 自动转为红黑树
    3. 每次操作, 会以链表或者树的头结点为锁对象, 配合悲观锁(synchronized) 保证多线程操作集合时的安全问题

3.6 CountDownLatch

 CountDownLatch相关方法
    1. CountDownLatch(int count); 表示要等待的线程数量
    2. public void await(); 让线程等待
    3. public void countDown(); 表示当前线程执行完毕

countDownLatch应用场景
    让一条线程等待其他线程执行完毕后再执行

案例:等3个孩子吃完后,妈妈收拾碗筷

1. child1类

代码示例

public class Child1 extends Thread{
    //创建CountDownLatch的构造方法
    private CountDownLatch countDownLatch;

    public Child1(CountDownLatch countDownLatch) {
        this.countDownLatch=countDownLatch;
    }
    
    @Override
    public void run() {

        for (int i = 1; i <= 10; i++) {
            System.out.println(getName()+"在吃饺子,"+"吃了"+i+"个");

        }
        //当吃完时,通知线程执行完毕
        countDownLatch.countDown();
    }
}

2. child2类

代码示例

public class Child2 extends Thread{
    private CountDownLatch countDownLatch;
    public Child2(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName()+"在吃饺子,"+"吃了"+i+"个");

        }
        countDownLatch.countDown();

    }
}

3. child3类

代码示例


public class Chlid3 extends Thread {
    private CountDownLatch countDownLatch;

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

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            System.out.println(getName()+"在吃饺子,"+"吃了"+i+"个");
        }
        countDownLatch.countDown();
    }
}

4.Mother类

代码示例

public class Mother extends Thread {
    private CountDownLatch countDownLatch;

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

    @Override
    public void run() {
        try {
            //调用await方法,让这个线程等待,CountDownLatch设置的线程执行完毕后,走到这
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //try..catch执行完后,打印这句
        System.out.println("妈妈正在收拾碗筷");
    }
}

5.测试类

代码示例

public class Demo {
    public static void main(String[] args) {
        //创建CountDownLatch对象,设置要等待的线程数量,这里设置为3
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //创建Mother对象,将CountDownLatch作为参数传递进去
        Mother mother = new Mother(countDownLatch);

        //启动线程
        mother.start();

        //创建Child对象,将CountDownLatch作为参数传递
        Child1 c1= new Child1(countDownLatch);
        Child2 c2= new Child2(countDownLatch);
        Chlid3 c3= new Chlid3(countDownLatch);

        //启动线程
        c1.start();
        c2.start();
        c3.start();
    }
}

3.7 Semaphore

Semaphore构造方法
    Semaphore(int 最大通行数量)  常用
    Semaphore(int,boolean) 不常用

使用场景:

访问控制特定资源的线程数量 

需求:

        有50辆车子要通过路口,路口设置两个栏杆,每次只能通过2辆车,这两辆车在通过时,管理员需要发通行证,出了路口,要收回通行证。

1.当有两辆车在两个栏杆之间行驶时,外边的车辆不能进入。

2.有一辆车时,外边可以进入一辆,其余车辆等待进入

3.里面没车时,外边可以进两辆,其余车辆等待进入

代码示例

public class Demo {
    public static void main(String[] args) {

        //创建MyRunnable对象
        MyRunnable mr = new MyRunnable();

        //线程启动50次,就是有50辆车要通过
        for (int i = 1; i <=50; i++) {
            Thread t = new Thread(mr);
            t.start();
        }
    }
}

class MyRunnable implements Runnable {

    //1.需要有人管理这个通道 - 创建Semaphore对象,这个对象只能创建一次,所以要放在成员变量位置
    //参数里面写最大通行数量,在这里每次最多两辆车通过,所以设置为2
    private Semaphore semaphore = new Semaphore(2);

    @Override
    public void run() {

        // 2. 有车子进来, 发放通行证 - acquire();发
        try {
            semaphore.acquire();
            System.out.println("车辆获得通行证");
            //调用Thread的sleep方法,让车辆每次获得通行证后,等待0.5秒
            Thread.sleep(500);

        // 3. 有车子出去, 收回通行证 - release();收
            System.out.println("收回通行证");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 4. 如果通行证都发出去了, 那么只允许车子等待 - 自动完成
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值