Java多线程专题笔记-下

Java编程

Java多线程专题笔记-上

Java多线程专题笔记-下


文章目录

一、Lock锁

1.Lock的子类​编辑

2.Lock的介绍

3.Lock的基本使用

4.Lock的API

二、ReentrantLock类

1.ReentrantLock类

2.公平锁和非公平锁

3.可重入锁与不可重入锁

4.AQS

三、ReentrantReadWriteLock类(重入锁、读写锁、独占锁\互斥锁)

四、同步锁的底层优化:锁升级

五、volatile关键字

        1.线程安全三要素:可见性、原子性、有序性

2.理解CAS和ABA问题

3.Volatile实现原子性

六、线程通信-消费者和生产者问题

1.同步代码块的线程通信

2.同步方法的线程通信

3.Lock锁的线程通信

4.Condition类

七、线程池

1.线程池:

2.线程池的好处

3.线程池的应用场合

4.续用线程池执行大量无返回值的Runnable命令

5.使用线程池执行大量的Callable任务(有返回值)

6.线程池API

7.ThreadPoolExecutor

八、ForkJoin框架

1.ForkJoin框架原理

2.工作窃取算法( work-stealing )

3.工作窃取算法的优点:

4.工作窃取算法的缺点︰

5.ForkJoin框架实例

九、其他线程同步类

1.JUC线程同步类 CountDownLatch(门栓类)

2.JUC线程同步类 CyclicBarrier(回环屏障)

3.JUC线程同步类 Semaphore(计数信号量)


一、Lock锁

1.Lock的子类

2.Lock的介绍

Lock锁可以实现synchronized的效果,即实现原子性、有序性和可见性。但它属于轻量级锁,可以手动获取锁和释放锁、可中断的获取锁、超时获取锁。
Lock只能给代码块上锁,synchronized可以给方法和代码块上锁。

Lock类实际上是一个接口,我们创建Lock锁,其实是实例化Lock的实现子类,用lock.lock来加锁,用lock.unlock来释放锁。在两者中间放置需要同步处理的代码。

3.Lock的基本使用

public class LockDemo01 {
    private Lock lock = new ReentrantLock(); //创建Lock锁

    public void testMethod(){
        lock.lock();//上锁
        try{
            for (int i = 0 ;i < 100;i++){
                System.out.println("ThreadName = " + Thread.currentThread().getName() + " " + (i + 1));
            }
        }finally {
            //经典写法:在finally中解锁,防止前面代码异常,解不了锁。
            lock.unlock();//解锁
        }
    }

    public static void main(String[] args) {
        LockDemo01 service = new LockDemo01();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
    }
}

4.Lock的API

lock()获取锁
tryLock只尝试获取一次锁
tryLock(long time, TimUnit unit)在一定时间内尝试获取锁
lockInterruptibly()一直尝试获取,直到得到中断命令interrupt()。
unlock​()解除锁

二、ReentrantLock类

1.ReentrantLock类

ReentrantLock类,可重入锁,单线程可以重复进入,默认是非公平锁。

Lock接口和ReentrantLock类的底层都是继承AbstractQueuedSynchronizer类。AQS是除了synchronized关键字之外的另一套锁机制。

获取锁
1)CAS操作抢占锁,抢占成功则修改锁的状态为1,将线程信息记录到锁当中,返回state=1
2)抢占不成功,tryAcquire获取锁资源,获取成功直接返回,获取不成功,新建一个检点插入到
当前AQS队列的尾部,acquireQueued(node)表示唤醒AQS队列中的节点再次去获取锁

释放锁
1)获取锁的状态值,释放锁将状态值-1
2)判断当前释放锁的线程和锁中保存的线程信息是否一致,不一致会抛出异常
3)状态只-1直到为0,锁状态值为0表示不再占用,为空闲状态
 

ReentrantLock的使用:

  Lock lock = new ReentrantLock();
  try{
      lock.lock();
  }finally{
      lock.unlock();
  }

2.公平锁和非公平锁

公平锁:按顺序排队一个一个获取锁。

非公平锁:各个线程相互竞争获取锁。默认是非公平锁。

创建公平锁:private Lock lock = new ReentrantLock(true);

3.可重入锁与不可重入锁

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

4.AQS

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,
即将暂时获取不到锁的线程加入到等待队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

三、ReentrantReadWriteLock类(重入锁、读写锁、独占锁\互斥锁)

Lock的读写规则:读读共享,读写互斥,写写互足

/**
 * 读写锁的基本使用
 * 读读共享,读写互斥,写写互足
 */
public class ReadWriterLockDemo {
    private ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
    private Lock readLock = rrwl.readLock(); //创建读锁
    private Lock writerLock = rrwl.writeLock(); //创建写锁

    //模拟读操作
    public void read(){
        String name = Thread.currentThread().getName();
        //上锁
        readLock.lock();
        System.out.println(name+" ---- 正在读取数据 -----");
        //模拟IO阻塞
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name+" ----- 读取结束 -----");
        //解锁
        readLock.unlock();
    }

    //模拟写操作
    public void writer(){
        String name = Thread.currentThread().getName();
        //上锁
        writerLock.lock();
        System.out.println(name+" ---- 正在写入数据 -----");
        //模拟IO阻塞
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name+" ----- 写入结束 -----");
        writerLock.unlock();
    }

    //主程序
    public static void main(String[] args) {
        ReadWriterLockDemo readWriterLockDemo = new ReadWriterLockDemo();

        //创建5个读线程并启动
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    readWriterLockDemo.read();
                }
            }).start();
        }

        //创建5个写线程并启动
        for (int i = 0; i < 5; i++) {
            //lamda表达式
            new Thread(
                    ()->{
                        readWriterLockDemo.writer();
                    }
            ).start();
        }
    }
}

四、同步锁的底层优化:锁升级

级别由高到低依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
普通对象在内存中的结构分为多部分,第一部分称为markwork,共64位。在对应锁对象的markword字段的低位字段标记锁的类型。
无锁状态:001
偏向锁状态:101
轻量级锁状态:00
重量级锁状态:10

五、volatile关键字

volatile:易变的,不稳定的意思。使用volatile修饰的变量,可以保证在多个线程之间的可见性,并且避免指令重排。但是无法保证操作的原子性。
 

1.线程安全三要素:可见性、原子性、有序性

可见性:
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。
原理:CPU修改变量,是在cache中修改的,每个CPU都有cache,所以别的CPU是看不见的,使用volatile,是将变量写在主存上,这样每个CPU都能看见。

原子性︰
原子是世界上的最小单位,具有不可分割性。非原子操作都会存在线程安全问题,需我们使用同步技术( sychronized )来让它变成一个原子操作。一条线程开始执行同步代码块的内容,就一定要全部执行完,否则不允许切换线程(不允许其他线程进来)。

有序性:
Java语言提供了volatile和synchronized 两个关键字来保证线程之间操作的有序性,volatile是因为其本身包含“禁止指令重排序”的语义,synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

synchronized可以保证可见性、原子性、有序性
volatile只能保证可见性和有序性,CAS可以保证原子性,volatile+CAS就是一种线程安全的方案,Lock底层使用的就是volatile+CAS。

2.理解CAS和ABA问题

CAS(Compare And Swap/Set):比较并交换,比较并修改。它的作用是,对指定内存地址的数据,校验它的值是否为期望值,如果是,就修改为新值,返回值表示是否修改成功。
ABA问题:当线程1读取数据A,想将其修改成B,但线程2已经读取并修成了C,线程3又读取修改成了A,这时候的A已经不是原来的A了,但线程1还是检验成了A,并将其修改成了B覆盖上去了。
解决方案:存储A的时候,在存储一个时间戳或者版本号,这样验证的时候既可以验证A,也可以验证A的时间戳或版本号。

使用原子类包装操作原子性
Java提供了一个非公开的类,sun.misc.UnSafe,来专门做操作底层的操作,它提供的方法都是native本地方法,它封装了一系列的原子化操作。

3.Volatile实现原子性

public class VolatileDemo02 {
    //创建原子类的实例化
    volatile static AtomicInteger i = new AtomicInteger(0);
    
    public static void main(String[] args) {
        //执行10个线程,每个线程对1加10000下
        for (int j = 0; j < 10; j++) {
            new Thread(
                    ()->{
                        for (int k = 0; k < 10000; k++) {
                            //volatile修饰这个i,无法保证i的原子性;i++底层也会转换为多条机器指令。所以加上CAS保证原子性。
                            i.incrementAndGet(); //效果等于 i++
                        }
                    }
            ).start();
        }
        //休眠2秒,输出结果
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i);
    }
}

六、线程通信-消费者和生产者问题

线程通信的细节
细节1:进行线程通信的多个线程、要使用同一个同步监视器(product),还必须要调用该同步监视器的wait()、notify0. notifyAllO;

细节2:sleep()和wait()方法都会让出CPU,最大的区别是sleep()方法不会释放锁,wait()方法会释放锁。区别二,sleep()可以在任何地方用,wait()只能在同步代码块和同步方法中使用。

1.同步代码块的线程通信

        线程通信的前提条件:生产者和消费者线程都要加锁,而且要是同一把锁。

消费者和生产者线程通信的解释:生产者先判断仓库是否是空的,如果仓库是满的,就进入等待状态,并释放锁。如果仓库是空的,就生产商品,并将仓库状态设置为满,并唤醒消费者进行消费。
        消费会先判断仓库是否是满的,如果仓库是空的,就进入等待状态,并释放锁。如果仓库是满的,就消费商品,并将仓库状态设置为空,并唤醒生产者进行生产。

为什么没有设置唤醒的线程,就会唤醒消费者:
 对象锁设置的是商品,当线程启动时会创建一个等待队列,生产者和消费者会在队列中等待锁。
当生产者进行唤醒时,只有消费者在等待。当消费者进行唤醒时,只有生产者在等待。
当唤醒消费者后,生产者判断仓库是满的后,进入等待,让出CPU,消费者就进行消费,消费完后,唤醒生产者,消费者会判断仓库为空后进入等待状态。
 

1)商品类

public class Product {
    private String name; //馒头 玉米饼
    private String color; //白色 黄色
    private boolean flag; // 仓库状态

    public Product() {}
    public Product(String name, String color) {
        this.name = name;
        this.color = color;
        flag= false;
    }
    public String getName() {
        return name;
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

2)生产者线程

public class ProduceRunnable implements Runnable {
    private Product product;

    public ProduceRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        int num = 0;
        while (true) {
            synchronized (product) {
                //如果仓库已满,就等待
                if (product.isFlag()){
                    try {
                        product.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //生产商品,并输出
                if (num % 2 == 0) {
                    product.setName("馒头");
                    //模拟生产中断
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    product.setColor("白色");
                } else {
                    product.setName("玉米饼");
                    //模拟生产中断
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    product.setColor("黄色");
                }
                System.out.println("生产者线程生产商品:" + product.getColor() + product.getName());

                //修改仓库的状态:已满
                product.setFlag(true);
                //通知消费者消费
                product.notify();
            }
            num++;
        }
    }
}

3)消费者线程

public class ConsumeRunnable implements Runnable{
    private Product product;

    public ConsumeRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true){
            synchronized (product){
                //如果仓库为空,就等待
                while(!product.isFlag()){
                    try{
                        product.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                //消费商品
                System.out.println("消费者线程消费商品:"+product.getColor()+product.getName());
                //改变仓库状态:已空
                product.setFlag(false);
                //通知生产者生产
                product.notify();
            }
        }
    }
}

4)测试类

/**
 * 测试生产者线程和消费者线程:
 * 问题1:消费者消费的不是生产者的商品
 * 原因:生产者和消费者线程,商品数据没有共享。
 * 解决:不要在生产者类和消费者类中创建商品类,要通过构造方法或者Setter方法传递进去。
 *
 * 问题2:当生产中断时,生产的商品就会混乱,(黄色馒头,白色玉米饼)
 * 原因︰没有进行线程同步﹔商品生产没有生产完,切换到消费者线程开始消费
 * 解决:
 *   1.给生产者加锁,必须完整的生产一个商品吏释放锁
 *   2.也必须给消费者加锁,必须和生产者是同一把锁,生产者没有生产完毕,就没有释放锁,消费者及时获取了cPU了,也无法得到锁,从而无法消费
 *
 * 问题3:消费者和生产者没有交费,出现了供不应求或者供过于求
 * 原因:生产者和消费者之间没有进行通信
 *      仓库满,生产者应该等待;消费者消费完,应该通知生产者。
 *      仓库空,消费者应该等待;生产者生产完后,应该通知消费者。
 * 解决:进行线程通信
 *
 */
public class Test {
    public static void main(String[] args) {
        //创建商品类
        Product product = new Product();

        //创建一个生产者线程和消费者线程
        Runnable produceRunnable = new ProduceRunnable(product);
        Thread t1 = new Thread(produceRunnable);

        Runnable consumeRunnable = new ConsumeRunnable(product);
        Thread t2 = new Thread(consumeRunnable);

        //启动生产者和消费者线程
        t1.start();
        t2.start();
    }
}

2.同步方法的线程通信

1)商品类

public class Product {
    private String name; //馒头 玉米饼
    private String color; //白色 黄色
    private boolean flag; // 仓库状态

    public Product() {
        flag = false;
    }

    /**
     * 生产一个商品
     */
    public synchronized void produce(String name, String color) {
        //如果仓库已满,就等待
        if (isFlag()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //生产商品,并输出
        setName(name);
        //模拟生产中断
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        setColor(color);
        System.out.println("生产者线程生产商品:" + getColor() + getName());

        //修改仓库的状态:已满
        this.setFlag(true);
        //通知消费者消费
        this.notify();
    }

    /**
     * 消费一个商品
     */
    public synchronized void consume() {
        //如果仓库为空,就等待
        while (!isFlag()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消费商品
        System.out.println("消费者线程消费商品:" + getColor() + getName());
        //改变仓库状态:已空
        setFlag(false);
        //通知生产者生产
        this.notify();
    }


    public String getName() {
        return name;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

2)生产者线程

public class ProduceRunnable implements Runnable {
    private Product product;

    public ProduceRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        int num = 0;
        while (true) {
            if (num % 2 == 0) {
                product.produce("馒头","白色");
            }else{
                product.produce("玉米饼","黄色");
            }
            num++;
       }
    }
}

3)消费者线程

public class ConsumeRunnable implements Runnable{
    private Product product;

    public ConsumeRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true){
            product.consume();
        }
    }
}

4)测试类

public class Test {
    public static void main(String[] args) {
        //创建商品类
        Product product = new Product();

        //创建一个生产者线程和消费者线程
        Runnable produceRunnable = new ProduceRunnable(product);
        Thread t1 = new Thread(produceRunnable);

        Runnable consumeRunnable = new ConsumeRunnable(product);
        Thread t2 = new Thread(consumeRunnable);

        //启动生产者和消费者线程
        t1.start();
        t2.start();
    }
}

3.Lock锁的线程通信

之前实现线程通信时,是生产者和消费者在一个等待队列中,会存在本来打算唤醒消费者,却唤醒一个生产者的问题。Lock锁中的Condition可以创建不同的等待队列,让生产者和消费者线程在不同的队列中等待,实现精准唤醒操作。

1)商品类

public class Product {
    private String name; //馒头 玉米饼
    private String color; //白色 黄色
    private boolean flag; // 仓库状态
    private Lock lock = new ReentrantLock(); //创建Lock锁
    private Condition produceCondition = lock.newCondition(); //控制线程的等待与通知
    private Condition consumeCondition = lock.newCondition(); //控制线程的等待与通知

    public Product() {
        flag = false;
    }


    /**
     * 生产一个商品
     */
    public void produce(String name, String color) {
        lock.lock();
        try{
            //如果仓库已满,就等待
            if (isFlag()) {
                try {
                    //让生产者进入生产者等待队列
                    produceCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //生产商品,并输出
            setName(name);
            //模拟生产中断
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            setColor(color);
            System.out.println("生产者线程生产商品:" + getColor() + getName());
            //修改仓库的状态:已满
            setFlag(true);
            // produceCondition.signal(); 通知一个消费者消费
            consumeCondition.signalAll(); //通知所有消费者消费
        }finally{
            lock.unlock();
        }

    }

    /**
     * 消费一个商品
     */
    public void consume() {
        lock.lock();
        try{
            //如果仓库为空,就等待
            while (!isFlag()) {
                try {
                    //让消费者进入消费者等待队列
                    consumeCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //消费商品
            System.out.println("消费者线程消费商品:" + getColor() + getName());

            //改变仓库状态:已空
            setFlag(false);

            //consumeCondition.signal(); 通知一个生产者生产
            produceCondition.signalAll(); //通知所有生产者生产
        }finally {
         lock.unlock();
        }
    }

    public String getName() {
        return name;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}

2)生产者线程

public class ProduceRunnable implements Runnable {
    private Product product;

    public ProduceRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        int num = 0;
        while (true) {
            if (num % 2 == 0) {
                product.produce("馒头","白色");
            }else{
                product.produce("玉米饼","黄色");
            }
            num++;
        }
    }
}

3)消费者线程

public class ConsumeRunnable implements Runnable{
    private Product product;

    public ConsumeRunnable(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true){
            product.consume();
        }
    }
}

4)测试类

public class Test {
    public static void main(String[] args) {
        //创建商品类
        Product product = new Product();

        //创建一个生产者线程和消费者线程
        Runnable produceRunnable = new ProduceRunnable(product);
        Thread t1 = new Thread(produceRunnable);

        Runnable consumeRunnable = new ConsumeRunnable(product);
        Thread t2 = new Thread(consumeRunnable);

        //启动生产者和消费者线程
        t1.start();
        t2.start();
    }
}

4.Condition类

Condition类的作用:能够更加精细的控制多线程的休眠与唤醒。 对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。 一个 Condition包含一个等待队列。一个Lock可以产生多个Condition,所以Lock拥有一个同步队列和多个等待队列。

Condition类的方法: 

await()将线程添加到等待队列中
signal()唤醒一个等待队列中的线程
signalAll()唤醒所有等待队列中的线程

七、线程池

1.线程池:

创建好多个线程,放入线程池中,使用时直接获取引用,不使用时放回池中。可以避免频繁创建销毁、实现重复利用

 

2.线程池的好处

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 提高线程的可管理性避免线程无限制创建、从而销耗系统资源,降低系统稳定性,甚至内存溢出或者CPU耗尽

3.线程池的应用场合

  •     需要大量线程,并且完成任务的时间短
  •     对性能要求苛刻
  •     接受突发性的大量请求

4.续用线程池执行大量无返回值的Runnable命令

public class RunnableTask {
    public static void main(String[] args) {
        //创建线程池
        //线程池只有一个线程∶可以节省创建线程的时间,保证一定会有一个
        ExecutorService pool = Executors.newSingleThreadExecutor();
        //线程池中有固定数量的线程
        //ExecutorService pool = Executors.newFixedThreadPool(10);
        //线程池中有可变数量的线程
        //ExecutorService pool = Executors.newCachedThreadPool();
        //Scheduled:线程池用来执行间隔执行(每个30分钟执行一次)或者延迟执行(半小时之后执行,12:00执行)的任务
        //ExecutorService pool = Executors.newScheduledThreadPool(10);

        //使用线程
        //使用线程池执行大量的Runnable命令
        for (int i = 0; i < 200; i++) {
            //将任务交给线程池执行
            pool.execute(new MyRunnable(i));
        }

        //关闭线程
        //pool.isShutdown(); //判断线程池有没有关闭
        //pool.shutdownNow(); //立刻关闭线程池,返回还没有执行完的线程
        pool.shutdown(); //关闭线程池
    }
}

//模拟线程任务
class MyRunnable implements Runnable{
    private int i;

    public MyRunnable(int i) {
        this.i = i;
    }
    @Override
    public void run() {
        System.out.println("任务开始:"+i);
        System.out.println("任务结束:"+i);
    }
}

5.使用线程池执行大量的Callable任务(有返回值)

import java.util.concurrent.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class CallableTask {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程池
        //线程池中有固定数量的线程
        ExecutorService pool = Executors.newFixedThreadPool(10);

        //使用线程
        //使用线程池执行大量的Callable命令(有返回值)
        List<Future> futureList = new ArrayList<>(); //储存返回的结果
        for (int i = 0; i < 200; i++) {
            //创建Callable任务
            Callable<Integer> callable = new MyCallable();
            //使用线程池执行有返回值任务
            Future<Integer> submit = pool.submit(callable);
            futureList.add(submit);
        }

        //输出结果
        for (int i = 0; i < 200; i++) {
            Future<Integer> task = futureList.get(i);
            int result = task.get();
            System.out.println(result);
        }

        //关闭线程
        pool.shutdown();
    }
}

//模拟Callable任务
class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return new Random().nextInt(10);
    }
}

6.线程池API

Executor:线程池顶级接口,只有一个方法
ExecutorService:真正的线程池接口

  •     void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
  •     Futuresubmit(Callable task):执行任务,有返回值,一般又来执行CallableVoid
  •     shutdown:关闭线程池

AbstractExecutorService :基本实现了ExecutorService的所有方法

ThreadPoolExecutor:默认的线程池实现类!
ScheduledThreadPoolExecutor:实现周期性任务调度的线程池
Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor) :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

7.ThreadPoolExecutor

ThreadPoolExecutor类的构造参数

public ThreadPoolExecutor(
    //线程池的核心线程数量
    int corePoolSize, 
    //线程池的最大线程量
    int maximumPoolSize,
    //线程的存活时间 
    long keepAliveTime, 
    //存活时间的单位:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS
    TimeUnit unit, 
    //等待队列的类型
    BlockingQueue<Runnable> workQueue, 
    //线程工厂
    ThreadFactory threadFactory, 
    //设置拒绝任务的策略CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy、AbortPolicy
    RejectedExecutionHandler handler 
)

RejectedExecutionHandler handler参数:
1、CallerRunsPolicy :如果发现线程池还在运行,就直接运行这个线程
2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去。
3、DiscardPolicy :什么也不做
4、AbortPolicy : java默认,抛出一个异常

八、ForkJoin框架

1.ForkJoin框架原理

Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架。
ForkJoin框架的底层使用的是ForkJoinPool线程池,ForkJoinPool线程池也是继承AbstractExecutorService,和ThreadPoolExecutor线程池是兄弟线程池。

 

2.工作窃取算法( work-stealing )

一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。
A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的
头部拿任务执行。

注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,
所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

3.工作窃取算法的优点:

利用了线程进行并行计算,减少了线程间的竞争。

4.工作窃取算法的缺点︰

1、如果双端队列中只有一个任务时,线程间会存在竞争。
2、窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列。

5.ForkJoin框架实例

public class SumTask extends RecursiveTask<Long> {
    private long start;
    private long end;
    private long step = 10000;

    public SumTask(long start, long end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Long compute() {
        long sum = 0;
        if (end - start <= step) {
            for (long i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            //分解成两个任务
            long mid = (start + end) / 2;
            SumTask leftTask = new SumTask(start, mid);
            SumTask rightTask = new SumTask(mid + 1, end);
            //fork
            leftTask.fork();
            rightTask.fork();

            //join
            long leftSum = leftTask.join();
            long rightSum = rightTask.join();
            sum = leftSum + rightSum;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //方式1: 没有重复的利用多核的优势,只有一个核在拼命的工作,其他核在一旁吃西瓜
        long n = 1000000000L;
        long sum = 0;
        long start = System.currentTimeMillis();
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("使用循环计算结果∶" + sum + " "+(end-start));

        //方式2: 使用forkJoin框架
        // 1.创建一个线程池
        ForkJoinPool pool = new ForkJoinPool ( );
        // 2.创建一个任务
        SumTask sumTask = new SumTask (1,n) ;
        // 3.将任务交给线程池
        start = System.currentTimeMillis();
        ForkJoinTask<Long> task = pool.submit(sumTask);
        // 4.得到结果并输出
        long result = task.get();
        end = System.currentTimeMillis();
        System.out.println("使用forkJoin框架计算:"+result+" "+(end-start));
        // 5.关闭线程
        pool.shutdown();
    }
}



九、其他线程同步类

1.JUC线程同步类 CountDownLatch(门栓类)

在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join()来实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类,底层基于AQS。

CountDown是计数递减的意思,Latch是门栓的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等 Latch 数量递减到0的时候,就结束阻塞执行后续操作。
CountDown( ):减少Latch的计数,如果计数达到零,释放所有等待的线程。
await():导致当前线程等待,直到到Latch计数到零,或者被interrupt。

CountDownLatch实例

上门栓,等执行完了一个子线程就去除一个门栓,等全部门栓去除了,就释放锁。

public class CountDownLatchDemo {
    //创建门栓,两道
    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("----- 子线程1 -----");
            countDownLatch.countDown(); //解开一道门栓
        }).start();

        new Thread(()->{
            System.out.println("----- 子线程2 -----");
            countDownLatch.countDown(); //解开一道门栓
        }).start();

        countDownLatch.await();
        //当解开两道门栓,主线程开始执行
        System.out.println("----- 主线程 -------");
    }
}

2.JUC线程同步类 CyclicBarrier(回环屏障)

CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为О之后,再调用countDown()、await(将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。底层基于AQS。

Barrier:屏障,会等待线程数目满足指定数量后,冲破屏障,同时执行,

Cyclic:回环,冲破屏障后数量重置,开始下一轮线程的等待和冲破屏障。

实例

创建一道屏障,设置5个回环线程数量,当5个线程全部执行完,会推倒屏障(释放锁),等后面5个线程再次执行完,又一次推到屏障,再继续下一轮。

public class CyclicBarrierDemo {
    //创建回环屏障,将回环线程设置成2
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("task1 step1");
            //等待有两个线程执行完,才能执行该线程
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            System.out.println("task1 step2");
            //等待有两个线程执行完,才能执行该线程
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            System.out.println("task1 step3");
        }).start();

        new Thread(()->{
            System.out.println("task2 step1");
            //等待有两个线程执行完,才能执行该线程
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            System.out.println("task2 step2");
            //等待有两个线程执行完,才能执行该线程
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            System.out.println("task2 step3");
        }).start();
    }
}

3.JUC线程同步类 Semaphore(计数信号量)

CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是递增的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore 也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。

常用方法如下:

  • release():释放许可证,将其返回到信号量,可用许可证的数量增加一个
  • acquire(int n):从该信号量获取给定数量的许可证,数量不足就阻塞等待

实例

每执行完线程一次,就释放许可证,当许可证达标就能执行下一个回环的线程。

public class SemaphoreDemo {
    //创建计数信号线程,并初始许可证为2
    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("----- 子线程1 -----");
            semaphore.release(); //释放许可证,将其返回到信号量,可用许可证的数量增加一个
        }).start();

        new Thread(()->{
            System.out.println("----- 子线程2 -----");
            semaphore.release(); //释放许可证,将其返回到信号量,可用许可证的数量增加一个
        }).start();

        //从该信号量获取给定数量的许可证,数量不足就阻塞等待
        semaphore.acquire(4);
        System.out.println("----- 主线程 -------");
    }
}

________________________________________________________________________

###

我见青山多妩媚,料青山见我应如是。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值