并发编程入门---JUC - 上【随笔】

JUC - 上

基本概念

什么是 JUC

并发编程入门---JUC - 上【随笔】_第1张图片

线程与进程

Java 无法开启线程,start() 方法调用本地方法 start0() 开启线程,底层是 C++,java 无法直接操作硬件。

public synchronized void start() {
     
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
     
            start0();
            started = true;
        } finally {
     
            try {
     
                if (!started) {
     
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
     
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();	

并行与并发

并发(多线程操作同一个资源)

  • CPU 单核,模拟出来多条线程,快速执行,快速交替

并行(多个人一起行走)

  • CPU 多核,多个线程可以同时执行
// 获取 CPU 核数
Runtime.getRuntime().availableProcessors()

线程的几种状态

 public enum State {
     
        
     	// 新生
        NEW,

        // 运行
        RUNNABLE,

        // 阻塞
        BLOCKED,

        // 等待,死死的等
        WAITING,

        // 超时等待,过期不候
        TIMED_WAITING,

        // 终止
        TERMINATED;
    }

wait / sleep 区别

  • 来自不同的类,wait => Object sleep =>Thread
  • 关于锁的释放:wait 会释放锁,sleep 不会释放锁
  • 使用范围不同:wait 必须在同步代码块中,sleep 可以在任何地方

Lock 锁(重点)

传统 synchronized
使用案例

同步方法

public class Test {
     

    public static void main(String[] args) {
     
        Ticket ticket = new Ticket();

        new Thread(() -> {
     
            for (int i = 0; i < 40; i++) {
     
                ticket.sale();
            }
        }, "A").start();

        new Thread(() -> {
     
            for (int i = 0; i < 40; i++) {
     
                ticket.sale();
            }
        }, "B").start();

        new Thread(() -> {
     
            for (int i = 0; i < 40; i++) {
     
                ticket.sale();
            }
        }, "C").start();
    }
}

/**
 * 资源类
 */
class Ticket {
     

    private Integer num = 30;

    public synchronized void sale() {
     
        if (num <= 0) {
     
            return;
        }
        System.out.println(Thread.currentThread().getName() + "卖出了第: " + (num--) + "票,剩余: " + num);
    }
}

同步代码块

class Drawing implements Runnable {
     
    /**
     * 账户
     */
    private Account account;
    /**
     * 取出的钱
     */
    private int drawingMoney;
    /**
     * 手上的钱
     */
    private int nowMoney;

    public Drawing(Account account, int drawingMoney) {
     
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
     
        synchronized (account) {
     
            if (account.money < drawingMoney) {
     
                System.out.println("银行卡 " + account.cardName + " 余额不足,无法取款");
                return;
            }

            try {
     
                Thread.sleep(1000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }

            account.money -= drawingMoney;
            nowMoney += drawingMoney;
            System.out.println(Thread.currentThread().getName() + " 从银行卡 " + account.cardName + " 取款 " + drawingMoney);
            System.out.println(Thread.currentThread().getName() + " 身上的 money 为: " + nowMoney);
            System.out.println("银行卡" + account.cardName + "余额为: " + account.money);
        }
    }
}
对象锁(monitor)机制

下面让我们看看 synchronized 的具体底层实现,以上面的 Drawing 为例,切换到 Drawing.class 所在目录,使用 javap -v [class文件名] 查看字节码文件,如下:

并发编程入门---JUC - 上【随笔】_第2张图片
并发编程入门---JUC - 上【随笔】_第3张图片

注意上面使用橙色方框框住的部分,这也是添 synchronized 关键字之后独有的。执行同步代码块后,首先要执行的 monitorenter 指令,退出的时候执行 monitorexit 指令。使用 synchronized 进行同步,其关键就是必须要获取对象监视器 monitor,当线程获取 monitor 后才能继续往下执行,否则就只能等待。这个获取过程是互斥的,即:同一时刻只有一个线程能够获取到 monitorsynchronized 是可重入锁,即:在同一锁程中,线程不需要再次获取同一把锁。简单来说就是,在调用一个同步方法时,这个同步方法又调用了另一个同步方法,且两个同步方法是同一把锁,那么当线程获取到第一个同步方法的锁后,调用第二个同步方法时由于是同一把锁,将不再需要再获取一次锁。

另外注意:

  • 每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一

  • 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法

对象,对象监视器、同步队列以及执行线程之间关系如下:

并发编程入门---JUC - 上【随笔】_第4张图片

由图可知:任意线程对 Object 的访问,首先要获取 Object 的监视器;如果获取失败,该线程就会进入同步队列,线程变为 阻塞(BLOCKED) 状态;如果获取成功,则执行,线程执行完成后,释放 Object 监视器;在同步队列中的线程就会有机会重新获取该监视器。

Lock 接口

并发编程入门---JUC - 上【随笔】_第5张图片
并发编程入门---JUC - 上【随笔】_第6张图片
并发编程入门---JUC - 上【随笔】_第7张图片

公平锁:十分公平,先来后到

非公平锁:十分不公平,可以插队(默认)

public class ReentrantLockTest {
     

    public static void main(String[] args) {
     
        Ticket2 ticket2 = new Ticket2();
        new Thread(()->{
     
            for (int i = 0; i < 40; i++) {
     
                ticket2.sale();
            }
        }, "A").start();

        new Thread(()->{
     
            for (int i = 0; i < 40; i++) {
     
                ticket2.sale();
            }
        }, "B").start();

        new Thread(()->{
     
            for (int i = 0; i < 40; i++) {
     
                ticket2.sale();
            }
        }, "C").start();
    }
}

/**
 * Lock 三步曲
 * 1、Lock lock = new ReentrantLock(); 创建锁
 * 2、lock.lock();  加锁
 * 3、finally => lock.unlock();  释放锁
 */
class Ticket2 {
     

    private Integer num = 30;

    private Lock lock = new ReentrantLock();

    public void sale() {
     
        //加锁
        lock.lock();
        try {
     
            if (num <= 0) {
     
                return;
            }
            System.out.println(Thread.currentThread().getName() + "卖出了第: " + (num--) + "票,剩余: " + num);
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            // 释放锁
            lock.unlock();
        }
    }
}
Synchronized 与 Lock 区别
  • Synchronized 是内置的 Java 关键字,Lock 是一个 Java 类
  • Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  • Synchronized 会自动释放锁,Lock 必须手动释放锁,不释放锁会造成死锁
  • Synchronized 若是线程 A 获取锁后进入阻塞状态,线程 B 会一直傻傻的等待线程 A;Lock 锁就不一定会等待下去
  • Synchronized 是可重入、不可以中断的、非公平的锁;Lock 可重入、可以判断锁、非公平(默认,可以自己设置)的锁
  • Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码。

生产者消费者问题

传统生产者与消费者问题

生产者和消费者问题 Synchronized

public class SynchronizedTest {
     

    public static void main(String[] args) {
     
        Data data = new Data();
        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.increment();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.decrement();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "B").start();
}

/**
 * 判断等待,业务,通知
 */
class Data {
     

    private int num = 0;

    /**
     * +1
     */
    public synchronized void increment() throws InterruptedException {
     
        if (num != 0) {
     
            // 等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        // 通知其它线程 +1 完成
        this.notifyAll();
    }

    /**
     * -1
     */
    public synchronized void decrement() throws InterruptedException {
     
        if (num == 0) {
     
            // 等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        // 通知其它线程 -1 完成
        this.notifyAll();
    }
}

运行以上代码,看结果貌似没什么问题,打印的信息也正常。

并发编程入门---JUC - 上【随笔】_第8张图片

这时我们在 main 方法中再启动两个线程,修改如下:

 new Thread(()->{
     
     try {
     
         for (int i = 0; i < 10; i++) {
     
             data.increment();
         }
     } catch (InterruptedException e) {
     
         e.printStackTrace();
     }
 }, "C").start();

new Thread(()->{
     
    try {
     
        for (int i = 0; i < 10; i++) {
     
            data.decrement();
        }
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
}, "D").start();

启动方法,查看打印结果:可以看到出现了预期之外的结果。

并发编程入门---JUC - 上【随笔】_第9张图片

这时就产生了,虚假唤醒 的问题;这时我们需要将 this.wait(); 的条件判断用 while 来替换 if 即可解决这个问题,修改如下:

   public synchronized void increment() throws InterruptedException {
     
        while (num != 0) {
     
            // 等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        // 通知其它线程 +1 完成
        this.notifyAll();
    }

    /**
     * -1
     */
    public synchronized void decrement() throws InterruptedException {
     
        while (num == 0) {
     
            // 等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        // 通知其它线程 -1 完成
        this.notifyAll();
    }

重新运行 main 方法,查看控制台打印结果:可以看到,打印的结果恢复正常。

并发编程入门---JUC - 上【随笔】_第10张图片

个人总结:用 if 判断的话,唤醒后线程会从wait 之后的代码开始运行,但是不会重新判断 if 条件,直接继续运行 if 代码块之后的代码,而如果使用 while 的话,也会从 wait 之后的代码运行,但由于使用的是 while,所以唤醒后会重新判断循环条件,如果不成立再执行 while 代码块之后的代码块,成立的话继续 wait

结合案例来说:拿两个线程 AC 来说,比如 A 先执行,执行时调用了 wait 方法,那它会进入等待,同时会释放锁;接着 C 获得锁并且也执行 wait 方法,两个执行 加操作 的线程一起进入了等待状态,等待被唤醒。此时 减操作 的线程中某一个线程执行完毕,并且唤醒了这两个 加线程,那么这两个 加线程 就会执行(不会一起执行)。其中 A 获取了锁并加1,执行完毕后 C 再执行;如果是 if 的话,那么 A 修改完 num 后,B 不会再去判断 num 的值,直接会给 num+1。如果是 while 的话,A 执行完之后,B 还会去判断 num 的值,由于条件不满足 B 不会执行。

注意:this.wait(); 需要放在 while 中,防止 虚假唤醒 问题。

并发编程入门---JUC - 上【随笔】_第11张图片

JUC 版的生产者与消费者问题

并发编程入门---JUC - 上【随笔】_第12张图片

我们可以使用 Lock 替代 Synchronized,使用 Condition 中的 await 与 signal 方法替代wait 与 notify 方法。

并发编程入门---JUC - 上【随笔】_第13张图片

以上为 JDK1.8 中关于 Condition 接口的介绍,结合 Lock 的官方使用案例如下:

class BoundedBuffer {
     
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     
     lock.lock(); try {
     
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
      lock.unlock(); }
   }

   public Object take() throws InterruptedException {
     
     lock.lock(); try {
     
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
      lock.unlock(); }
   }
 } 

代码实现:

public class LockTest {
     

    public static void main(String[] args) {
     
        Data2 data = new Data2();

        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.increment();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "JUC-A").start();

        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.decrement();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "JUC-B").start();

        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.increment();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "JUC-C").start();

        new Thread(()->{
     
            try {
     
                for (int i = 0; i < 10; i++) {
     
                    data.decrement();
                }
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }, "JUC-D").start();
    }
}

/**
 * 判断等待,业务,通知
 */
class Data2 {
     

    private int num = 0;
    /**
     * 创建 Lock 锁
     */
    private Lock lock = new ReentrantLock();
    /**
     * 创建 Condition 对象
     */
    private Condition condition = lock.newCondition();

    /**
     * +1
     */
    public void increment() throws InterruptedException {
     
        lock.lock();
        try {
     
            while (num != 0) {
     
                // 等待
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "==>" + num);
            // 通知其它线程 +1 完成
            condition.signalAll();
        } catch (InterruptedException e) {
     
            throw new InterruptedException();
        } finally {
     
            lock.unlock();
        }
    }

    /**
     * -1
     */
    public void decrement() throws InterruptedException {
     
        lock.lock();
        try {
     
            while (num == 0) {
     
                // 等待
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "==>" + num);
            // 通知其它线程 -1 完成
            condition.signalAll();
        } catch (InterruptedException e) {
     
            throw new InterruptedException();
        } finally {
     
            lock.unlock();
        }
    }
}

运行以上案例,可以看到控制台输出的结果正常:

并发编程入门---JUC - 上【随笔】_第14张图片

但是 Condition 的功能只有如此了吗? **任何一个新的技术,绝对不是仅仅只是覆盖了原有的技术,它具有相应的优势与补充。**以上的虽然能够正常运行,但是可以看到线程的调用完全是没有顺序的,如:AB、AD、CB …,如果我们想要线程按照我们指定的顺序有序的执行,那么需要怎么做呢?下面我们将介绍使用 Condition 实现精准通知唤醒。

Condition 实现精准通知唤醒
public class ConditionTest {
     

    public static void main(String[] args) {
     
        Data3 data3 = new Data3();

        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.executeA();
            }
        }, "ConditionA").start();

        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.executeB();
            }
        }, "ConditionB").start();

        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.executeC();
            }
        }, "ConditionC").start();
    }
}

class Data3 {
     

    private Lock lock = new ReentrantLock();

    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    /*
       1=> 线程A,2=> 线程B,3=> 线程C
     */
    private int flag = 1;

    public void executeA() {
     
        lock.lock();
        try {
     
            while (flag != 1) {
     
                conditionA.await();
            }
            flag = 2;
            System.out.println(Thread.currentThread().getName() + "====>AAAAAAAA");
            conditionB.signal();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }

    public void executeB() {
     
        lock.lock();
        try {
     
            while (flag != 2) {
     
                conditionB.await();
            }
            flag = 3;
            System.out.println(Thread.currentThread().getName() + "===>BBBBBBBB");
            conditionC.signal();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }

    public void executeC() {
     
        lock.lock();
        try {
     
            while (flag != 3) {
     
                conditionC.await();
            }
            flag = 1;
            System.out.println(Thread.currentThread().getName() + "===>CCCCCCCCC");
            conditionA.signal();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

以上指定的线程执行顺序是:线程A ==> 线程B ==> 线程C,启动 main 方法,可以看到控制台打印的信息:

并发编程入门---JUC - 上【随笔】_第15张图片

8 锁现象

8锁,就是关于锁的 8 个问题,首先创建一个基础的演示模块;接下来我们将逐个演示 8 锁问题。

public class Lock8Test {
     

    public static void main(String[] args) {
     
      
    }
}

class Phone {
     
	/**
     * synchronized 方法锁的是方法的调用者(对象)
     */
    public synchronized void sendSms() {
     
        System.out.println(Thread.currentThread().getName() + "===>发送短信");
    }

    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName() + "===>打电话");
    }
}
问题一:标准情况下,两个线程先打印 发送短信 还是 打电话

修改 main 方法,实例化一个 Phone 对象,并创建两个线程,分别调用两个同步方法,两个线程中间添加一个延时。

Phone phone = new Phone();

new Thread(()->{
     
    phone.sendSms();
}, "LOCK8-A").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone.call();
}, "LOCK8-B").start();

启动 main 方法运行程序,从打印结果可以看到,先打印的是 发送短信;无论我们运行多少次,先打印的都是 发送短信

并发编程入门---JUC - 上【随笔】_第16张图片

解释:之所以每次都是先打印 发送短信,是因为 sendSms()call() 都是 synchronized 标注的同步方法,锁的是方法的调用者(即 phone 实例对象),两个方法都是用的同一个锁。由于 sendSms() 方法所在的线程先启动,并且中间做了延时保证 sendSms() 方法先获得锁,所以 sendSms() 方法先执行,先打印出 发送短信

问题二:sendSms() 延时4秒,两个线程先打印 发送短信 还是 打电话?

修改 sendSms() 方法,添加延时处理,具体修改如下:

public synchronized void sendSms() {
     
    try {
     
        TimeUnit.SECONDS.sleep(4);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "===>发送短信");
}

重新执行 main 方法,可以看到:即便是我们在 sendSms() 方法中,添加了延时 4 秒的处理,先打印出来的依然是 发送短信

并发编程入门---JUC - 上【随笔】_第17张图片

解释:原因与问题一基本一致,由于 sendSms() 方法先获得锁,所以 sendSms() 方法先执行;必须要等 sendSms() 方法执行完成后释放锁,call() 方法才能获取锁并执行,所以 发送短信 先打印。

问题三:增加一个普通方法!两个线程先打印 发送短信 还是 hello?

修改 Phone 类,增加一个普通方法 hello(),具体如下:

  	/**
     * 这里没有锁,不是同步方法,不受锁的影响
     */
    public void hello() {
     
        System.out.println(Thread.currentThread().getName() + "===>hello");
    }

修改 main 方法

Phone phone = new Phone();

new Thread(()->{
     
    phone.sendSms();
}, "LOCK8-A").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone.hello();
}, "LOCK8-C").start();

运行 main 方法,可以看到:延时了 1 秒,先打印的是 hello 消息。

20201215225533462

解释:这是由于 hello() 方法是普通方法不是同步方法,不受锁的约束,可以与同步方法 sendSms() 同时执行,且由于 sendSms() 方法做了 4 秒的延时,执行时间远大于 hello() 方法,所以先打印出来的是 hello 消息。

问题四:两个对象,两个同步方法,发送短信 还是 打电话?

修改 main 方法,创建两个 Phone 对象与两个线程,并且分别调用 sendSms()call() 方法:

Phone phone = new Phone();
Phone phone2 = new Phone();

new Thread(()->{
     
    phone.sendSms();
}, "LOCK8-A").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone2.call();
}, "LOCK8-D").start();

运行 main 方法,查看打印结果,可以看到先打印的是 打电话

并发编程入门---JUC - 上【随笔】_第18张图片

解释:前面也介绍了,同步方法锁的是方法的调用者,即 Phone 的实例对象;由于 phonephone2 是两个实例对象,所以这里有两把锁,phone.sendSms() 获得的是 phone 的锁,phone2.call() 获得的是 phone2 的锁;由于是不同的锁,所以两个方法可以同时执行,而由于 call() 方法执行时间短,所以先打印 打电话

问题五:增加两个静态的同步方法,只有一个对象,先打印 发送短信 还是 打电话?

修改 Phone 类,新增两个静态同步方法:

public static synchronized void sendSmsStatic() {
     
    try {
     
        TimeUnit.SECONDS.sleep(4);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "===>发送短信 static");
}

public static synchronized void callStatic() {
     
    System.out.println(Thread.currentThread().getName() + "===>打电话 static");
}

修改 main 方法,创建两个线程执行这两个方法,并查看打印结果:先打印 发送短信

注意:这里为了演示同一个对象,使用的是实例对象调用的静态方法,实际开发中还是以 类名.静态方法 的方式调用静态方法

Phone phone = new Phone();

new Thread(()->{
     
    phone.sendSmsStatic();
}, "LOCK8-E").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone.callStatic();
}, "LOCK8-F").start();

并发编程入门---JUC - 上【随笔】_第19张图片

解释:由于 sendSmsStatic()callStatic() 是静态同步方法,而静态同步方法锁的是 Class,一个类只有一个 class 对象,即 Phone.class 模板;sendSmsStatic()callStatic() 锁的都是 Phone.class 对象,所以在执行方法时,由于 sendSmsStatic() 先执行,先获得锁,所以必须等 sendSmsStatic() 执行完毕释放锁,才能执行 callStatic() 方法,所以先打印 发送短信

问题六:增加两个静态的同步方法,两个对象,先打印 发送短信 还是 打电话?

静态同步方法就用之前的,修改 main 方法并执行,执行结果:先打印 发送短信

Phone phone = new Phone();
Phone phone2 = new Phone();

new Thread(()->{
     
    phone.sendSmsStatic();
}, "LOCK8-E").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone2.callStatic();
}, "LOCK8-G").start();

并发编程入门---JUC - 上【随笔】_第20张图片

解释:可以看到,即便是用了不同的实例对象调用 sendSmsStatic()callStatic(),先打印的还是 发送短信;这是因为静态同步方法锁的是 Class 对象,而不是实例对象,一个类只有一个 Class 对象,所以先得到锁的先执行,与实例对象无关。

问题七:一个静态的同步方法,一个普通同步方法,一个对象,先打印 发送短信 还是 打电话?

修改 main 方法,具体修改如下:

Phone phone = new Phone();
    
new Thread(()->{
     
    phone.sendSms();
}, "LOCK8-A").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone.callStatic();
}, "LOCK8-F").start();

执行 main 方法,可以看到执行结果:先打印 打电话

并发编程入门---JUC - 上【随笔】_第21张图片

解释:sendSms() 是同步方法,锁的是调用方法的实例对象,即 phonecallStatic() 方法是静态同步方法,锁的是 Class 对象,即 Phone.class;两个方法所需的是不同的锁,可以同时执行,而由于 callStatic() 方法执行的时间短,所以先打印 打电话

问题八:一个静态的同步方法,一个普通同步方法,两个对象,先打印 发送短信 还是 打电话?

修改 main 方法并执行,查看执行结果:先打印 打电话

Phone phone = new Phone();

new Thread(()->{
     
    phone.sendSms();
}, "LOCK8-A").start();

try {
     
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
     
    e.printStackTrace();
}

new Thread(()->{
     
    phone2.callStatic();
}, "LOCK8-G").start();

并发编程入门---JUC - 上【随笔】_第22张图片

解释:原因与 问题七 基本相同,由于两个方法锁的对象不是同一个对象,所以执行时间短的先打印。

总结:

普通同步方法锁的是调用方法的类的实例对象静态同步方法锁的是类的 Class 对象,一个类只有一个 Class 对象。若方法所需要的锁不是同一个锁,则可以同时执行;若是同一个锁,则需要等上一个方法执行并释放锁,才能执行下一个方法。类的实例对象的锁可以有多个,类的 Class 对象的锁只有一个。实例对象的锁 与 Class 对象的锁可以同时使用。

集合类不安全

List 不安全

并发下 ArrayList 是不安全的

public class ListTest {
     

    public static void main(String[] args) {
     

        /**
         * 并发下 ArrayList 是不安全的 ConcurrentModificationException
         * 解决方案:
         * 1、List list = new Vector<>();
         * 2、List list = Collections.synchronizedList(new ArrayList<>());
         * 3、List list = new CopyOnWriteArrayList<>();
         * CopyOnWrite 写入时复制,是计算机程序设计领域的一种优化策略;在写入的时候避免覆盖,造成数据问题。
         */
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
     
            new Thread(()->{
     
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

运行以上案例,可以看到控制台打印了 ConcurrentModificationException 并发修改异常;显然,在并发情况下,ArrayList 并不是线程安全的。解决办法:

  • 使用 Vector 替代 ArrayList
  • 使用 Collections.synchronizedList() 方法将 ArrayList 转换成线程安全的
  • 使用 CopyOnWriteArrayList 替换 ArrayList

个人建议是使用 CopyOnWriteArrayList 因为它的效率更高;下面我们将对这三种方式进入分析:

一、Vector ,通过源码我们可以看到 Vector 是通过 synchronized 方法来实现的线程安全。

并发编程入门---JUC - 上【随笔】_第23张图片

二、Collections.synchronizedList(new ArrayList<>()) ,通过源码我们可以看到,Collections 是通过 synchronized 同步代码块 来保证线程安全。

并发编程入门---JUC - 上【随笔】_第24张图片

三、CopyOnWriteArrayList,通过源码我们可以看到,CopyOnWriteArrayList 是通过 Lock 锁,以及 CopyOnWrite(写入时复制)来保证线程安全与数据安全。

并发编程入门---JUC - 上【随笔】_第25张图片

总结:对比以上三种实现,由于执行效率 Lock 锁 > 同步代码块 > 同步方法,所以我们选择用 CopyOnWriteArrayList 来解决线程安全问题。

Set 不安全

与前面介绍的 List 一样,Set 也存在线程安全问题,同样也可通过 CollectionsCopyOnWriteArraySet 来解决线程安全问题,由于原理差不多,这里就不做过多介绍了。

public class SetTest {
     

    public static void main(String[] args) {
     
        /**
         * 1、Set set = Collections.synchronizedSet(new HashSet<>());
         * 2、Set set = new CopyOnWriteArraySet<>();
         * HashSet 本质是 HashMap 的键
         */
        Set<String> set = new HashSet<>();
        for (int i = 1; i <= 10; i++) {
     
            new Thread(()->{
     
                set.add(UUID.randomUUID().toString().substring(0, 7));
                System.out.println(set);
            }).start();
        }
    }
}

以上个人建议使用 CopyOnWriteArraySet

值得注意的是,HashSet 的本质是 HashMap 的键的集合。

并发编程入门---JUC - 上【随笔】_第26张图片

public HashSet() {
     
    map = new HashMap<>();
}

public boolean add(E e) {
     
    return map.put(e, PRESENT)==null;
}
Map 不安全

同样 Map 也存在线程安全问题

public class MapTest {
     

    public static void main(String[] args) {
     
        /**
         * 1、Collections.synchronizedMap(new HashMap<>());
         * 2、 Map map = new ConcurrentHashMap<>();
         */
        Map<String, String> map = new HashMap<>();
        for (int i = 1; i <= 10; i++) {
     
            new Thread(()->{
     
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 7));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

解决方法:

  • 使用 Collections.synchronizedMap() 方法将 HashMap 转换成线程安全的
  • 使用 ConcurrentHashMap 替换 HashMap

Callable

并发编程入门---JUC - 上【随笔】_第27张图片

通过查看 JDK1.8 的文档,我们可以发现 Callable 有以下特点:Callable 有返回值、Callable 可以抛出异常、Callable 运行的是 call() 方法;

那么我们如何使用 Callable 创建一个线程呢?

首先我们介绍下 Thread、Runnable、FutureTask、Callable 之间的关系,如下图:

并发编程入门---JUC - 上【随笔】_第28张图片

一、通过查看 JDK 文档我们可以发现,Thread 可以使用Runnable 对象作为构造方法的参数来创建一个线程。

并发编程入门---JUC - 上【随笔】_第29张图片

二、同样我们通过 JDK 文档可以查看到 Runnable 接口有一个 FutureTask 实现类。

并发编程入门---JUC - 上【随笔】_第30张图片

三、同样通过 JDK 文档我们可以看到 FutureTask 可以使用 Callable 作为构造参数构造一个 FutureTask 对象。

并发编程入门---JUC - 上【随笔】_第31张图片

综合以上:我们可以把 Callable 作为构造参数传入FutureTask 创建一个 FutureTask 实例对象;而 FutureTask 又是 Runnable 接口的实现,所以我们可以将 FutureTask 实例对象作为构造参数传入 Thread 创建一个线程。编码如下:

public class CallableTest {
     

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

        /**
         * new Thread(new Runnable()).start();
         * new Thread(new FutureTask()).start();
         * new Thread(new FutureTask(new Callable())).start();
         */
        FutureTask<String> futureTask = new FutureTask<String>(()->{
     
            TimeUnit.SECONDS.sleep(5);
            System.out.println("自定义 Callable 实现");
            return "Test";
        });
        new Thread(futureTask, "A").start();

        // get() 方法可能会产生阻塞(在 call() 方法执行比较耗时时会阻塞,等待返回结果)
        String result = futureTask.get();
        System.out.println(result);

        // 结果会被缓存,效率高
        new Thread(futureTask, "B").start();
        System.out.println("----------");
    }
}

运行以上代码,查看控制台打印信息:

并发编程入门---JUC - 上【随笔】_第32张图片

可以发现两个细节:

  • 结果会被缓存,效率高:通过运行程序我们可以看到,启动了两个线程,但是只打印出来一条提示信息;

  • 获取返回值的 get() 方法可能会阻塞等待:通过运行程序我们可以看到,在通过 get() 方法获取 Callable 返回值时,主线程进入阻塞等待状态;在线程 A 执行完成返回结果后,主线程才继续执行 输出打印信息。为了验证这个,我们可以注释掉获取并输出返回值的代码,再运行程序:

  //        String result = futureTask.get();
  //        System.out.println(result);

并发编程入门---JUC - 上【随笔】_第33张图片

可以看到,控制台先打印 ------,即证明:这时主线程并不会阻塞等待线程 A 的执行结果。

常用的辅助类(必会)

CountDownLatch

倒计时、减法计数器;

并发编程入门---JUC - 上【随笔】_第34张图片

使用示例:

public class CountDownLatchTest {
     

    public static void main(String[] args) throws InterruptedException {
     
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
     
            new Thread(()->{
     
                System.out.println("学生 " + Thread.currentThread().getName() + " 离开");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();
        System.out.println("学生全部离开,关门");
    }
}

以上示例中:一个教室中共有 6 个学生,当 6 个学生都离开教室后,执行关门操作;

原理:

countDownLatch.countDown(); // 数量 -1

countDownLatch.await(); // 等待计数器归零,然后再向下执行

每次有线程调用 countDown() 数量 -1,当计数器变为 0 ,countDownLatch.await() 就会被唤醒,继续执行。

CyclicBarrier

加法计数器

并发编程入门---JUC - 上【随笔】_第35张图片

使用示例:

public class CyclicBarrierTest {
     

    public static void main(String[] args) {
     
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
     
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
     
            final int temp = i;
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName() + " 收集了 " + temp + "颗龙珠");
                try {
     
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "等待结束");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
     
                    e.printStackTrace();
                }
            }, "akieay-" + i).start();
        }
    }
}

运行程序,打印结果如下:

并发编程入门---JUC - 上【随笔】_第36张图片

以上示例中:我们创建了一个数值为 7 的加法计数器,当数值增加到 7 的时候,执行我们设置的 CyclicBarrier 的第二个构造参数中的任务;当任务执行完成后,会唤醒处于等待状态中的 7 个线程,并继续往下执行;每执行一次 cyclicBarrier.await() 加法计数器自增 1。

Semaphore

信号量计数器

并发编程入门---JUC - 上【随笔】_第37张图片

使用示例:

public class SemaphoreTest {
     

    public static void main(String[] args) {
     
        // 线程数量:停车位、限流
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
     
            new Thread(()->{
     
                //得到
                try {
     
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 抢到车位");
                    TimeUnit.SECONDS.sleep(4);
                    System.out.println(Thread.currentThread().getName() + " 离开车位");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } finally {
     
                    //释放
                    semaphore.release();
                }
            }, "akieay" + i).start();
        }
    }
}

运行程序,结果如下:

20201217104151729

以上示例中,限制信号量为 3,即当前面三个线程获取到 信号量时,后续的线程必须等待;等待前面的线程执行完成后释放信号量,后续的线程才能获信号量并继续执行。

原理:

semaphore.acquire(),获得,假如已经满了,则等待,等待被释放为止。

semaphore.release() 释放,会将当前信号量释放 + 1,然后唤醒等待的线程。

使用场景:多个共享资源互斥时使用、并发限流,控制最大的线程数等。

读写锁

并发编程入门---JUC - 上【随笔】_第38张图片

读的时候,允许多个线程同时读;写的时候,只允许一个线程同时写。

以下是一个没加锁的示例:

public class ReadWriteLockTest {
     

    public static void main(String[] args) {
     
        MyCache myCache = new MyCache();

        // 写操作
        for (int i = 1; i <= 6; i++) {
     
            final int temp = i;
            new Thread(()->{
     
                myCache.put(temp + "", "akieay" + temp);
            }, String.valueOf(i)).start();
        }

        //读操作
        for (int i = 1; i <= 5; i++) {
     
            final int temp = i;
            new Thread(()->{
     
                Object o = myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
     

    private volatile Map<String, Object> map = new HashMap<>();

    /**
     * 存储,写
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
     
        System.out.println(Thread.currentThread().getName() + " 写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写入Ok");
    }

    /**
     * 读取,读
     * @param key
     * @return
     */
    public Object get(String key) {
     
        System.out.println(Thread.currentThread().getName() + " 读取" + key);
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取Ok");
        return value;
    }
}

运行程序,查看控制台打印的信息:可以看到写入操作是无序的,存在着多个线程同时写入的情况,还存在着读写同时进行的情况;这种问题再实际开发中是必须避免的,下面我们会使用读写锁解决以上问题。

并发编程入门---JUC - 上【随笔】_第39张图片

新建一个添加了读写锁的缓存,并将 main 方法中的 MyCache 对象修改为 MyCacheLock 对象:

class MyCacheLock {
     

    private volatile Map<String, Object> map = new HashMap<>();
    /**
     * 读写锁:更加细粒度的控制
     */
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 存储,写入的时候,只希望同时只有一个线程写
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
     
        lock.writeLock().lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + " 写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入Ok");
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.writeLock().unlock();
        }
    }

    /**
     * 读取,读取的时候,所有人都可以读
     * @param key
     * @return
     */
    public Object get(String key) {
     
        lock.readLock().lock();
        Object value = null;
        try {
     
            System.out.println(Thread.currentThread().getName() + " 读取" + key);
            value = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取Ok");
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.readLock().unlock();
        }

        return value;
    }
}
MyCacheLock myCache = new MyCacheLock();

重新运行应用程序,查看控制台打印信息:可以看到写入的顺序已经保证,在一个线程写入的时候不会存在另一个线程同时写入的情况;读取依然可以多个线程同时读取,保证了效率。

并发编程入门---JUC - 上【随笔】_第40张图片

总结:独占锁(写锁)一次只能被一个线程占有,共享锁(读锁)多个线程可以同时占有;ReentrantReadWriteLock 拥有比 ReentrantLock 更加细粒度的控制,写锁保证了不会存在并发修改的问题,读锁与 volatile 又保证了并发线程能同时读取到有效且正确的数据。

读-读 可以共存、读-写 不能共存、写-写 不能共存

阻塞队列

BlockingQueue【阻塞队列】

并发编程入门---JUC - 上【随笔】_第41张图片

阻塞队列

写入:如果队列满了,就必须阻塞等待

取:如果队列是空的,必须阻塞等待生产

并发编程入门---JUC - 上【随笔】_第42张图片

四组 API
方法 抛出异常 有返回值 阻塞 等待 超时 等待
添加 add(E e) offer(E e) put(E e) offer(E e, long timeout, TimeUnit unit)
移除 remove() poll() take() poll(long timeout, TimeUnit unit)
检测队首元素 element() peek() ---- ----
第一组【抛出异常】

add(E e)remove()element()

	/**
     * 抛出异常
     */
    public static void test1() {
     
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.add("A"));
        System.out.println(arrayBlockingQueue.add("B"));
        System.out.println(arrayBlockingQueue.add("C"));
        // 抛出异常:IllegalStateException: Queue full
//        System.out.println(arrayBlockingQueue.add("D"));

        System.out.println("++++++++++++++++++++++++++++");
        System.out.println(arrayBlockingQueue.element());
        System.out.println("++++++++++++++++++++++++++++");

        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        // 抛出异常:NoSuchElementException
//        System.out.println(arrayBlockingQueue.remove());
    }
  • add(E e):在队列尾部插入元素。若队列还存在可用的空间,则插入成功,返回 true;若队列已满,则抛出异常 IllegalStateException
  • remove():获取并删除此队列首部元素。 若队列中还存在元素,则删除并返回此队列的首部元素;若队列为空,则抛出异常 NoSuchElementException
  • element():获取但不删除此队列首部元素。若队列中还存在元素,则返回此队列首部元素;若队列为空,则抛出异常 NoSuchElementException
第二组【不会抛出异常】

offer(E e)poll()peek()

	/**
     * 有返回值,不抛出异常
     */
    public static void test2() {
     
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.offer("A"));
        System.out.println(arrayBlockingQueue.offer("B"));
        System.out.println(arrayBlockingQueue.offer("C"));
        // false 不抛出异常
        System.out.println(arrayBlockingQueue.offer("D"));

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

        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
        // null 不抛出异常
        System.out.println(arrayBlockingQueue.poll());
    }
  • offer(E e):在队列尾部插入元素。若队列还存在可用的空间,则插入成功,返回 true;若队列已满,则插入失败,返回 false。
  • poll():获取并删除此队列首部元素。 若队列中还存在元素,则删除并返回此队列的首部元素;若队列为空,返回 null。
  • peek():获取但不删除此队列首部元素。若队列中还存在元素,则返回此队列首部元素;若队列为空,则返回 null。
第三组【阻塞 等待】

put(E e)take()

	/**
     * 阻塞 等待
     */
    public static void test3() throws InterruptedException {
     
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        arrayBlockingQueue.put("A");
        arrayBlockingQueue.put("B");
        arrayBlockingQueue.put("C");
        // 若队列已满,则一直阻塞,等待队列空出空间
//        arrayBlockingQueue.put("D");

        System.out.println(arrayBlockingQueue.take());
        System.out.println(arrayBlockingQueue.take());
        System.out.println(arrayBlockingQueue.take());
        // 若队列已空,则一直阻塞,等待获取元素
//        System.out.println(arrayBlockingQueue.take());
    }
  • put(E e):在该队列的尾部插入指定的元素,如果队列已满,则会阻塞等待空间变为可用【一直阻塞等待】。
  • take() :获取并删除此队列的首部元素。如果队列中存在元素,则获取并删除队列首部元素;若队列为空,则一直等待,直到获取并删除首部元素。
第四组【超时 等待】

offer(E e, long timeout, TimeUnit unit)poll(long timeout, TimeUnit unit)

 /**
     * 阻塞 超时
     * @throws InterruptedException
     */
    public static void test4() throws InterruptedException {
     
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.offer("A", 2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.offer("B", 2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.offer("C", 2, TimeUnit.SECONDS));
        // 插入元素,若队列已满,等待两秒,若两秒内队列产生了可用空间,则插入元素,返回 true,否则插入失败,返回 false。
        System.out.println(arrayBlockingQueue.offer("D", 2, TimeUnit.SECONDS));

        System.out.println("----------------------");
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        // 获取并删除队列头,若存在元素则删除,若不存在则等待两秒,若两秒内插入了元素,则获取并删除队列头,否则返回 null。
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
    }
  • offer(E e, long timeout, TimeUnit unit):在该队列的尾部插入指定的元素。若队列存在可用空间,则插入成功,返回 true;若队列已满,则等待指定超时时间,若指定超时时间内队列产生了可用空间,则插入元素,返回 true,否则插入失败,返回 false。
  • poll(long timeout, TimeUnit unit):获取并删除队列头。若存在元素,则获取并删除队列头;若不存在,则等待指定超时时间,若指定超时时间内插入了元素,则获取并删除队列头,否则返回 null。
SynchronousQueue【同步队列】

并发编程入门---JUC - 上【随笔】_第43张图片

SynchronousQueue 与其它 BlockingQueue 不同,它没有内容;每个插入操作必须等待另一个线程相应的删除操作,反之亦然,SynchronousQueue 只能同时存在一个元素。

示例:

public class SynchronousQueueTest {
     

    public static void main(String[] args) {
     
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();

        new Thread(()->{
     
            for (int i = 1; i <= 3; i++) {
     
                try {
     
                    System.out.println(Thread.currentThread().getName() + " put " + i);
                    blockingQueue.put(i + "");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"Provider").start();

        new Thread(()->{
     
            for (int i = 1; i <= 3; i++) {
     
                try {
     
                    TimeUnit.SECONDS.sleep(3);
                    String take = blockingQueue.take();
                    System.out.println(Thread.currentThread().getName() + " take " + take);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "Consumer").start();
    }

}

运行示例,查看控制台打印信息,可以发现:每次 put 插入一个元素后,必须等待另一个线程 take 取出这个元素,才能继续 put 其它元素,否则会进入等待。

并发编程入门---JUC - 上【随笔】_第44张图片

线程池(重点)

池化技术

程序的运行,本质:需要占用系统资源。为了优化资源的使用,我们可以使用池化技术。池化技术:事先准备好一些资源,在需要使用时直接获取,用完之后需要返还。示例如:数据库连接池、线程池、内存池等。

线程池的好处:

  • 降低资源的消耗。频繁的创建和销毁线程是非常耗费资源的,使用线程池可以避免线程的频繁创建,同时也提高了线程的复用。

  • 提高响应速度。使用线程池,在需要一个线程执行任务的时候,可以直接从线程池中获取线程,而不用手动创建,节省了线程的创建时间。

  • 方便管理。使用线程池可以对线程进行统一的管理,可以控制最大并发数等。

三大方法
  • Executors.newSingleThreadExecutor():创建一个单个线程的线程池。
  • Executors.newFixedThreadPool(int nThreads):创建一个固定大小的线程池。 在任何时候,最多nThreads 个线程将处于主动处理任务。 如果所有线程都处于执行状态,这时来了新的任务,则这些任务将进入等待队列中直到线程可用。
  • Executors.newCachedThreadPool():创建一个有伸缩性的线程池。 当池中线程全被占用,没有可用的线程时,将创建一个新的线程并将其添加到该池中。 未使用时长达到六十秒的线程,将被终止并从线程池中删除。
public class ThreadPoolTest {
     

    public static void main(String[] args) {
     
        // 单个线程的线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 创建一个固定大小的线程池
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 创建一个可伸缩的,遇强则强,遇弱则弱的线程池
//        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
     
            for (int i = 0; i < 20; i++) {
     
                // 使用线程池后,使用线程池来创建线程
                threadPool.execute(()->{
     
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            // 程序结束,线程池用完,关闭线程池
            threadPool.shutdown();
        }
    }
}

可分别运行创建的三种线程池,查看控制台打印信息:可以看到 单个线程池 中始终只有一个线程执行,固定线程池中 最多有 5 个线程同时执行,可伸缩的线程则随着任务的执行逐渐增加线程的数量。

并发编程入门---JUC - 上【随笔】_第45张图片

注意:根据 阿里巴巴开发手册 的规范,线程池需要使用 ThreadPoolExecutor 来创建,规避 OOM 造成资源耗尽的风险。

七大参数
源码分析
public static ExecutorService newSingleThreadExecutor() {
     
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newFixedThreadPool(int nThreads) {
     
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
     
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

// 本质 ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize,   // 核心线程池大小
                              int maximumPoolSize,  // 最大线程池大小
                              long keepAliveTime,  // 超时时间,超过该时间没被调用就会释放
                              TimeUnit unit,  // 超时时间单位
                              BlockingQueue<Runnable> workQueue,  // 阻塞队列
                              ThreadFactory threadFactory,  // 线程工厂,创建线程的,一般不动
                              RejectedExecutionHandler handler // 拒绝策略) {
     
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

可以看到通过 三大方法 创建线程的操作,底层也是调用 ThreadPoolExecutor 来创建线程池,只不过指定了不同的参数;所以,我们创建线程池时建议使用 ThreadPoolExecutor,自己根据需要指定相关参数。

并发编程入门---JUC - 上【随笔】_第46张图片

七大参数详解

ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

参数介绍:

  • corePoolSize:核心线程池大小
  • maximumPoolSize: 最大线程池大小
  • keepAliveTime:超时时间,超过该时间没被调用就会释放
  • unit:超时时间单位
  • workQueue:阻塞队列
  • threadFactory:线程工厂,创建线程的,一般不动
  • handler:拒绝策略

线程池的最大负载为:最大线程池大小 + 阻塞队列大小

当新的任务提交到线程池时,线程池首先检测 核心线程池 中是否有空闲的线程;若有,则将任务交给其执行;若没有,则检测 阻塞队列 是否已满;若没满,则将任务添加到 阻塞队列;若已满,则查看线程池中线程数是否达到 最大线程池大小 ,若没有达到,则创建新的线程执行任务,若已经达到,则按指定的 拒绝策略 拒绝任务。

编码测试
public class ThreadPoolExecutorTest {
     

    public static void main(String[] args) {
     
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try {
     
            for (int i = 0; i < 5; i++) {
     
                threadPool.execute(()->{
     
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            // 关闭线程池
            threadPool.shutdown();
        }
    }
}

线程池线中资源的使用优先级为:核心线程 > 阻塞队列 > 其它线程;即:任务首先都交给核心线程执行,若 核心线程使用完了,则添加到阻塞队列,若阻塞队列也满了,就在最大线程数范围内创建新的线程,若线程数达到了最大线程数,则按指定的拒绝策略拒绝。

此时的线程数为 5 个,正好满足 核心线程池大小 + 阻塞队列大小 ,所以不需要额外创建线程;执行打印的结果也是两个线程,如下:

并发编程入门---JUC - 上【随笔】_第47张图片

此时,将线程数修改为 6,重新执行打印结果如下:可以看到启动了 3 个线程

并发编程入门---JUC - 上【随笔】_第48张图片

修改为 8 再执行,查看打印信息:可以看到这时启动了 5 个线程

并发编程入门---JUC - 上【随笔】_第49张图片

修改为 9 再执行,由于线程池的最大负载为 8,此时已经超出了最大负载,多余的任务将按照指定的拒绝策略拒绝(任务满了,多余的任务不做处理,抛出异常)

并发编程入门---JUC - 上【随笔】_第50张图片

四种拒绝策略
  • ThreadPoolExecutor.AbortPolicy():任务满了,多余的任务不做处理,抛出异常
  • new ThreadPoolExecutor.CallerRunsPolicy():任务满了,哪个线程抛过来的任务,返回给哪个线程执行。
  • new ThreadPoolExecutor.DiscardPolicy():任务满了,丢掉任务,不会执行也不会抛出异常。
  • new ThreadPoolExecutor.DiscardOldestPolicy():任务满了,尝试和最早的竞争,竞争成功则执行,竞争失败则丢弃,也不会抛出异常。

并发编程入门---JUC - 上【随笔】_第51张图片

一、ThreadPoolExecutor.AbortPolicy() 这种策略前面案例已经做了演示就不重复做了。

并发编程入门---JUC - 上【随笔】_第52张图片

二、将前面案例的决绝策略修改为 new ThreadPoolExecutor.CallerRunsPolicy() ,再启动程序,查看打印结果如下:可以看到多余的任务返回给了主线程执行。

并发编程入门---JUC - 上【随笔】_第53张图片

三、将拒绝策略修改为 new ThreadPoolExecutor.DiscardPolicy() ,再启动应用程序,并查看打印信息:可以看到只执行了8次,多余的任务没有执行也没有抛出异常,直接丢弃掉了。

并发编程入门---JUC - 上【随笔】_第54张图片

四、将拒绝策略修改为 new ThreadPoolExecutor.DiscardOldestPolicy() ,它是第三种策略的改进版,比第三种更人性化,当任务已满时,会与最早的竞争,成功则执行,失败则抛弃。

启动应用程序,并查看打印信息:可以看到只执行了8次,说明竞争失败任务被抛弃了。

并发编程入门---JUC - 上【随笔】_第55张图片

小结与扩展

最大线程池大小(maximumPoolSize) 如何设置:

CPU 密集型:几核就是几,可以保持 CPU 的效率最高!获取 CPU 核数的方法:Runtime.getRuntime().availableProcessors()

IO 密集型:一般为 (程序中十分耗 IO 的线程的数量) * 2,即:若有 15 个十分耗费 IO 资源的线程,则设置为 30

四大函数式接口(必须掌握)

函数式接口:只有一个方法的接口,如:

@FunctionalInterface
public interface Runnable {
     
    public abstract void run();
}

并发编程入门---JUC - 上【随笔】_第56张图片

Function【函数型接口】

函数型接口,有一个输入参数,有一个返回值

并发编程入门---JUC - 上【随笔】_第57张图片

使用示例:

public class FunctionTest {
     

    public static void main(String[] args) {
     
        // 工具类,将输入的字符串转化为大写字符串
//        Function function = new Function() {
     
//            @Override
//            public String apply(String s) {
     
//                return s.toUpperCase();
//            }
//        };

        // lambda 表达式简化
        Function<String, String> function = (str)->{
     
            return str.toUpperCase();
        };
        System.out.println(function.apply("akieay.com"));
    }
}

以上使用函数型接口创建了一个 将字符串转化为大写字符串 的转换器。

Predicate【断定型接口】

断定型接口:有一个输入参数,返回值只能是 布尔值。

并发编程入门---JUC - 上【随笔】_第58张图片

使用示例:

public class PredicateTest {
     

    private static Pattern PATTERN_MOBILE = Pattern.compile("^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$");

    public static void main(String[] args) {
     
        // 判断字符串是否是手机号
//        Predicate predicate = new Predicate() {
     
//            @Override
//            public boolean test(String mobile) {
     
//                Matcher matcher = PATTERN_MOBILE.matcher(mobile);
//                return matcher.matches();
//            }
//        };

        // lambda 表达式简化
        Predicate<String> predicate = (mobile)->{
     
            Matcher matcher = PATTERN_MOBILE.matcher(mobile);
            return matcher.matches();
        };

        System.out.println(predicate.test("1234567890"));
        System.out.println(predicate.test("18692264704"));
    }
}

以上使用断定型接口创建了一个 判断给定字符串是否是手机号 的判断器。

消费型接口与供给型接口

Supplier:供给型接口,没有参数,只有返回值

Consumer:消费型接口,没有返回值,只有参数

并发编程入门---JUC - 上【随笔】_第59张图片

使用示例:

public class SupplierConsumerTest {
     

    public static void main(String[] args) {
     
        // 使用供给型接口定义一个生产 ID 的ID生成器
        Supplier<String> supplier = () -> {
     
            return UUID.randomUUID().toString().replace("-", "");
        };

        // 使用消费型接口定义一个 uuid 消费器
        Consumer<String> consumer = (uuid) -> {
     
            System.out.println("uuid: " + uuid);
        };

        // 消费者 消费 生产者生产的 UUID
        consumer.accept(supplier.get());
    }
}

以上使用 供给型接口 创建了一个 uuid 生成器,使用 消费型接口 创建了一个 uuid 消费者。

Stream 流式计算

官方文档

并发编程入门---JUC - 上【随笔】_第60张图片

方法介绍
修饰符和类型 方法和描述
boolean allMatch(Predicate predicate) 返回此流的所有元素是否与提供的谓词匹配。
boolean anyMatch(Predicate predicate) 返回此流的任何元素是否与提供的谓词匹配。
static Stream.Builder builder() 返回一个 Stream的构建器。
R collect(Collector collector) 使用 Collector对此流的元素执行 mutable reduction Collector
R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) 对此流的元素执行 mutable reduction操作。
static Stream concat(Stream a, Stream b) 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。
long count() 返回此流中的元素数。
Stream distinct() 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。
static Stream empty() 返回一个空的顺序 Stream
Stream filter(Predicate predicate) 返回由与此给定谓词匹配的此流的元素组成的流。
Optional findAny() 返回描述流的一些元素的Optional如果流为空,则返回一个空的Optional
Optional findFirst() 返回描述此流的第一个元素的Optional如果流为空,则返回一个空的Optional
Stream flatMap(Function> mapper) 返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。
DoubleStream flatMapToDouble(Function mapper) 返回一个 DoubleStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。
IntStream flatMapToInt(Function mapper) 返回一个 IntStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。
LongStream flatMapToLong(Function mapper) 返回一个 LongStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。
void forEach(Consumer action) 对此流的每个元素执行操作。
void forEachOrdered(Consumer action) 如果流具有定义的遇到顺序,则以流的遇到顺序对该流的每个元素执行操作。
static Stream generate(Supplier s) 返回无限顺序无序流,其中每个元素由提供的 Supplier
static Stream iterate(T seed, UnaryOperator f) 返回有序无限连续 Stream由函数的迭代应用产生 f至初始元素 seed ,产生 Stream包括 seedf(seed)f(f(seed)) ,等
Stream limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过 maxSize
Stream map(Function mapper) 返回由给定函数应用于此流的元素的结果组成的流。
DoubleStream mapToDouble(ToDoubleFunction mapper) 返回一个 DoubleStream ,其中包含将给定函数应用于此流的元素的结果。
IntStream mapToInt(ToIntFunction mapper) 返回一个 IntStream ,其中包含将给定函数应用于此流的元素的结果。
LongStream mapToLong(ToLongFunction mapper) 返回一个 LongStream ,其中包含将给定函数应用于此流的元素的结果。
Optional max(Comparator comparator) 根据提供的 Comparator返回此流的最大元素。
Optional min(Comparator comparator) 根据提供的 Comparator返回此流的最小元素。
boolean noneMatch(Predicate predicate) 返回此流的元素是否与提供的谓词匹配。
static Stream of(T... values) 返回其元素是指定值的顺序排序流。
static Stream of(T t) 返回包含单个元素的顺序 Stream
Stream peek(Consumer action) 返回由该流的元素组成的流,另外在从生成的流中消耗元素时对每个元素执行提供的操作。
Optional reduce(BinaryOperator accumulator) 使用 associative累积函数对此流的元素执行 reduction ,并返回描述减小值的 Optional (如果有)。
T reduce(T identity, BinaryOperator accumulator) 使用提供的身份值和 associative累积功能对此流的元素执行 reduction ,并返回减小的值。
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 执行 reduction在此流中的元素,使用所提供的身份,积累和组合功能。
Stream skip(long n) 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
Stream sorted() 返回由此流的元素组成的流,根据自然顺序排序。
Stream sorted(Comparator comparator) 返回由该流的元素组成的流,根据提供的 Comparator进行排序。
Object[] toArray() 返回一个包含此流的元素的数组。
A[] toArray(IntFunction generator) 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。
使用案例
/**
 * @Author: akieay
 * @Description:
 *  现在有 5 个用户,筛选:
 *  1、ID 必须是偶数
 *  2、年龄必须大于 23 岁
 *  3、用户名转为大写字母
 *  4、用户名字母倒着排序
 *  5、只输出一个用户!
 */
public class StreamTest {
     

    public static void main(String[] args) {
     
        User user1 = new User(1, "a", 21);
        User user2 = new User(2, "b", 22);
        User user3 = new User(3, "c", 23);
        User user4 = new User(4, "d", 24);
        User user5 = new User(6, "e", 25);
        // 集合负责存储
        List<User> list = Arrays.asList(user1, user2, user3, user4, user5);

        // 计算交给 stream 流
        list.stream()
                .filter(user -> {
      return user.getId() % 2 == 0;})
                .filter((user -> {
     return user.getAge() > 23;}))
                .map(user -> {
     
                    user.setName(user.getName().toUpperCase());
                    return user;
                })
                .sorted((u1, u2) -> {
     return u2.getName().compareTo(u1.getName());})
                .limit(1)
                .forEach(System.out::println);
    }
}

你可能感兴趣的:(并发编程,java,java)