线程池(看这一篇足够了)

目录

线程状态介绍:

线程池-基本原理

概述:

线程池的设计思路:

线程池-Executors默认线程池

线程池-Executors创建指定上限的线程池

自定义线程池

原子性:

Volatile-问题

Volatile解决

Synchronized解决

原子性

悲观锁和乐观锁

并发工具类

并发工具类-Hashtable

并发工具类-ConcurrentHashMap

总结 :

并发工具类-CountDownLatch

总结: 

并发工具类-Semaphore


线程状态介绍:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程状态被定义在了java.long.Thread.State枚举类中,State枚举类的源码如下:

线程池-基本原理

概述:

        提到池,大家第一反应是水池,水池就是盛谁的容器,那么线程池毫无疑问就是装有线程的容器了。

线程池存在的意义:

        系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点“舍本逐末”了。所以我们需要创建一个线程池来存放线程,用线程就去从线程池中取,用完就归还线程池

线程池的设计思路:

  1. 准备一个任务容器
  2. 一次性启动多个消费者线程
  3. 刚开始任务容器都是空的,所以线程都在wait
  4. 直到一个外部线程向这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒
  5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来

线程池-Executors默认线程池

newCachedThreadPool()直接上代码!!!

//static ExecutorService newCachedThreadPool()   创建一个默认的线程池
//static ExecutorService newFixedThreadPool(int nThreads)	创建一个指定最多线程数量的线程池
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //1.创建一个默认的线程池,池子中默认是空的,默认最多可以容纳int类型的最大值(2147483647)
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以帮我们创建线程池对象
        //ExecutorService --- 可以帮我们控制线程池
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        
        //让主线程睡2秒
        Thread.sleep(2000);

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

}

结果如下:

来来来,分析一下结果原因:(要仔细看结果的分析)

        首先我们创建了一个线程池,然后提交了第一个任务,第一个任务发现线程池里面是空的,于是就创建了一个线程来执行第一个任务,当第一个任务执行完成后,会把线程归还给线程池,此时池子里就有一条空闲的线程,当有第二个任务之后,第二个任务会从线程池中用那一条空闲的线程,此时,也是第一条线程执行任务,sleep的作用就是等待第一条线程执行完任务,然后归还给线程池,如果没有加sleep,那么第一条线程还没归还到线程池,第二条线程就开始执行任务了。

线程池-Executors创建指定上限的线程池

代码实现:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //1.参数不是初始值,而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize());
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        System.out.println(pool.getPoolSize());
        executorService.shutdown();
    }

结果如下:

这结果很好理解,

         pool.getPoolSize()就是获取连接池里现成的数量

自定义线程池

代码来上!

public class Demo {
    public static void main(String[] args){
        //参数1:核心线程数量            =不能小于0
        //参数2:最大线程数              =不能小于等于0,最大数量>=核心线程数
        //参数3:空闲线程最大存活时间    =不能小于0
        //参数4:时间单位                =时间单位
        //参数5:任务队列                =不能为null
        //参数6:创建线程工厂            =不能为null
        //参数7:任务的拒绝策略          =不能为null
        ThreadPoolExecutor  pool = new ThreadPoolExecutor(
                                    2,
                                    5,
                                    2, 
                                    TimeUnit.SECONDS,
                                    new ArrayBlockingQueue<>(1),
                                    Executors.defaultThreadFactory(),
                                    new ThreadPoolExecutor.AbortPolicy());
        pool.submit(()->{
            System.out.println(Thread.currentThread().getName()+"在执行了");
        });
        pool.submit(()->{
            System.out.println(Thread.currentThread().getName()+"在执行了");
        });
        pool.shutdown();
    }

 结果如下:

现在我们讲讲ThreadPoolExecutor构造方法中的参数:
前四个参数不难理解,接下来就从参数5开始讲起:
任务队列:让任务在任务队列中等着,等有线程空闲了,再从这个队列中获取任务并执行
创建线程工厂:按照默认的方式创建线程对象,底层也是new Thread
拒绝策略:
①什么时候拒绝任务?
        当提交的任务 > 池子中最大的线程数量 + 队列容量

②如何拒绝?
下面有四种拒绝策略

//丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.AbortPolicy:
//丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardPolicy:
//抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.DiscardOldestPolicy:
//调用任务的run()方法绕过线程池直接执行。
ThreadPoolExecutor.CallerRunsPolicy:

原子性:

Volatile-问题

首先定义一个类,然后让其中一个线程去更改这个类里的成员变量,最后创建一个测试类运行,然后观察结果

public class Money {
    public static int money = 100000;
}
public class MyThread1 extends  Thread {
    @Override
    public void run() {
        while(Money.money == 100000){

        }

        System.out.println("零花钱已经不是十万了");
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Money.money = 90000;
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("可乐同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("猫儿同学");
        t2.start();
    }
}

最后发现控制台没有打印出“零花钱已经不是十万了”字段,那么我们来分析一下原因:

当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题,原因如下:

  1. 堆内存是唯一的,每一个线程都有自己的线程栈。
  2. 每一个线程在使用堆里面变量的时候,都先会拷贝一份到自己的变量副本中。
  3. 在线程中,每一次使用都是从变量副本中获取的。

Volatile解决

如何解决上述问题?
那么接下来用一个volatitle关键字解决,只需要在成员变量前面加入volatitle关键字进行修饰即可

volatitle关键字的作用:强制线程每次在使用的时候,都会看一下共享区域中最新的值

public class Money {
    public static volatile int money = 100000;
}

Synchronized解决

public class Money {
    public static Object lock = new Object();
    public static volatile int money = 100000;
}
public class MyThread1 extends  Thread {
    @Override
    public void run() {
        while(true){
            synchronized (Money.lock){
                if(Money.money != 100000){
                    System.out.println("零花钱已经不是十万了");
                    break;
                }
            }
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        synchronized (Money.lock) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Money.money = 90000;
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("可乐同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("猫儿同学");
        t2.start();
    }
}

最后控制台也可以把输出语句打印出来

synchronized解决:

  1. 线程获得锁
  2. 清空变量副本
  3. 拷贝共享变量最新的值到变量副本中
  4. 执行代码
  5. 将修改后变量副本中的值赋值给共享数据
  6. 释放锁

原子性

概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

首先说一下代码场景,然后再用代码实现:

        一个小男孩给小女孩送冰淇淋,一次送一百个,总共送一百次,那么最后小女孩应该收到一万个,那么直接代码实现↓↓↓

public class MyAtomThread implements Runnable{
    private int count = 0;      //送冰淇淋的数量
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count++;
            System.out.println("已经送了"+count+"个冰淇淋");
        }
    }
}
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread mt = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(mt).start();
        }
    }
}

注意:最后运行的结果可能不一样,需要多运行几次,多线程这一块儿不能保证每一次运行结果数据的唯一准确性

最后我们发现,少了一个冰淇淋,那么是什么原因呢?

那么我们需要从count++入手,那么我们来分析一下count++

//1.从共享数据中读取数据到本线程栈中
//2.修改本线程栈中变量副本的值
//3.会把本线程栈中变量副本的值赋值给贡献数据
count++;

 在执行上述三步过程中,cpu的执行权可能会被其它线程抢走,从而导致最终数据的问题

代码总结: count++ 不是一个原子性操作, 他在执行的过程中,有可能被其他线程打断

Volatile关键字:

        只能保证线程每次在使用共享数据的时候都是最新值,但是不能保证原子性,所以volatile也不能解决上述问题

解决办法:

  1. 可以给count++加锁
    public class AtomDemo {
        public static void main(String[] args) {
            MyAtomThread atom = new MyAtomThread();
    
            for (int i = 0; i < 100; i++) {
                new Thread(atom).start();
            }
        }
    }
    class MyAtomThread implements Runnable {
        private volatile int count = 0; //送冰淇淋的数量
        private Object lock = new Object();
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                //1,从共享数据中读取数据到本线程栈中.
                //2,修改本线程栈中变量副本的值
                //3,会把本线程栈中变量副本的值赋值给共享数据.
                synchronized (lock) {
                    count++;
                    System.out.println("已经送了" + count + "个冰淇淋");
                }
            }
        }
    }
  2. jdk5之后,提供了一个原子包Atomic

原子类:AtomicInteger

public AtomicInteger():	   			  //初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue):  //初始化一个指定值的原子型Integer

int get():   			 		//获取值
int getAndIncrement():      	//以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():     		//以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):		//以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):   	//以原子方式设置为newValue的值,并返回旧值。
import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomThread implements Runnable {
    //private volatile int count = 0; //送冰淇淋的数量
    //private Object lock = new Object();
    AtomicInteger ac = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //1,从共享数据中读取数据到本线程栈中.
            //2,修改本线程栈中变量副本的值
            //3,会把本线程栈中变量副本的值赋值给共享数据.
            //synchronized (lock) {
//                count++;
//                ac++;
            int count = ac.incrementAndGet();
            System.out.println("已经送了" + count + "个冰淇淋");
           // }
        }
    }
}
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();

        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

AtomicInteger原理 : 自旋锁 + CAS 算法

CAS算法:

有3个操作数(内存值V, 旧的预期值A,要修改的值B)

当旧的预期值A == 内存值 此时修改成功,将V改为B

当旧的预期值A!=内存值 此时修改失败,不做任何操作

并重新获取现在的最新值(这个重新获取的动作就是自旋)

(这里就简简单单的说一下这些,毕竟算法作者也不太熟,不过后面我会出算法的文章,把我经历的一些事情分享给大家)

悲观锁和乐观锁

synchronized和CAS的区别 :

相同点:在多线程情况下,都可以保证共享数据的安全性。

不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)

cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

如果别人修改过,那么我再次获取现在最新的值。

如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

并发工具类

并发工具类-Hashtable

Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

并发工具类-ConcurrentHashMap

ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

总结 :

  1. HashMap是线程不安全的。多线程环境下会有数据安全问题
  2. Hashtable是线程安全的,但是会将整张表锁起来,效率低下
  3. ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。
  4. JDK7的底层是数组+链表  JDK8的底层是数组+链表+红黑树,当链表的长度>=8时,会自动转换成红黑树

并发工具类-CountDownLatch

public CountDownLatch(int count)     //参数传递线程数,表示等待线程数量
public void await()                  //让线程等待                       
public void countDown()              //当前线程执行完毕                 

        首先介绍个代码背景:妈妈需要等待两个孩子吃完饺子然后收拾碗筷,此时两个孩子就是两个线程,当两个线程执行完然后说一声吃完了,然后妈妈线程去执行

上代码

import java.util.concurrent.CountDownLatch;

public class ChileThread1 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread1(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}
import java.util.concurrent.CountDownLatch;

public class ChileThread2 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread2(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}
import java.util.concurrent.CountDownLatch;

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

    @Override
    public void run() {
        //1.等待
        try {
            //当计数器变成0的时候,会自动唤醒这里等待的线程。
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //2.收拾碗筷
        System.out.println("妈妈在收拾碗筷");
    }
}
import java.util.concurrent.CountDownLatch;

public class MyCountDownLatchDemo {
    public static void main(String[] args) {
        //1.创建CountDownLatch的对象,需要传递给四个线程。
        //在底层就定义了一个计数器,此时计数器的值就是3
        CountDownLatch countDownLatch = new CountDownLatch(2);
        //2.创建四个线程对象并开启他们。
        MotherThread motherThread = new MotherThread(countDownLatch);
        motherThread.start();

        ChileThread1 t1 = new ChileThread1(countDownLatch);
        t1.setName("小明");

        ChileThread2 t2 = new ChileThread2(countDownLatch);
        t2.setName("小红");

        t1.start();
        t2.start();

    }
}

结果如下:

总结: 

  1. CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
  2. await():让线程等待,当计数器为0时,会唤醒等待的线程
  3. countDown(): 线程执行完毕时调用,会将计数器-1。

并发工具类-Semaphore

使用场景 :

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

        比如要下/上高速的汽车,需要过收费站,那么收费站允许一次最多两辆车一起进去/出来,进去时需要许可证,出来时需要收回许可证

直接上代码:

public class MyRunnable implements Runnable{
    //1.获得管理员对象,
    private Semaphore semaphore = new Semaphore(2);
    @Override
    public void run() {
        try {
            //2.获得通行证
            semaphore.acquire();
            //3.开始行驶
            System.out.println("获得了通行证开始行驶");
            Thread.sleep(2000);
            System.out.println("归还通行证");
            //4.归还通行证
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MySemaphoreDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        for (int i = 0; i < 100; i++) {
            new Thread(mr).start();
        }
    }
}

结果如图所示:

 最后的结果绝对不会出现同时获得/归还两个及以上的通行证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值