多线程基础笔记

2020/1/1

# 程序、进程、线程

# 程序

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

# 进程

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期

如:运行中的QQ,运行中的MP3播放器

  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域

# 线程

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间。它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患

一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

狂神说:Java默认有几个线程?2个线程! main线程、GC线程

# 图解单线程与多线程

image-20200608185019405

# 单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。

    例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱, 那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费) 。 但是因为CPU时间单元特别短,因此感觉不出来。

  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)

# 并行与并发

并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。

CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。

并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

CPU多核,多个线程可以同时执行。 我们可以使用线程池!

# 多线程程序的优点

以单核CPU为例, 只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

# 何时需要使用多线程?

  1. 程序需要同时执行两个或多个任务。
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 需要一些后台运行的程序时。

# Thread类

# Thread类的特性

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现

  1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
  2. 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

# 构造器

构造器 备注
Thread() 创建新的Thread对象
Thread(String threadname) 创建线程并指定线程实例名
Thread(Runnable target) 指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name) 创建新的Thread对象

# 方法

方法 备注
void start() 启动线程,并执行对象的run()方法
run() 线程在被调度时执行的操作
String getName() 返回线程的名称
void setName(String name) 设置该线程名称
static Thread currentThread() 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield() 线程让步
1. 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
2. 若队列中没有同优先级的线程,忽略此方法
join() 当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止(低优先级的线程也可以获得执行)
static void sleep(long millis) (指定时间:毫秒) 1.令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。2.抛出InterruptedException异常
stop() 强制线程生命期结束,不推荐使用
boolean isAlive() 返回boolean,判断线程是否还活着

# 多线程的创建

JDK1.5之前创建新执行线程有两种方法:

  1. 继承Thread类的方式
  2. 实现Runnable接口的方式
  3. 实现Callable接口(JDK 5.0新增)
  4. 使用线程池(JDK 5.0新增)

# 方式一:继承于Thread类

# 创建流程

  1. 创建一个继承于Thread类的子类

  2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中

  3. 创建Thread类的子类的对象

  4. 通过此对象调用start()

# 实例演示

//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();

        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        t1.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
//        t1.run();

        //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//        t1.start();
        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 注意事项

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException” 。

# 方式二:实现Runnable接口

# 创建流程

  1. 创建一个实现了Runnable接口的类

  2. 实现类去实现Runnable中的抽象方法:run()

  3. 创建实现类的对象

  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

  5. 通过Thread类的对象调用start()

# 实例演示

//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{

    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 继承Thread类VS实现Runnable接口

相同点:

两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

不同点:

  1. 继承Thread:线程代码存放Thread子类run方法中。
  2. 实现Runnable:线程代码存在接口的子类的run方法。

联系:

public class Thread implements Runnable

优先选择:实现Runnable接口的方式

原因:

  1. 实现的方式没有类的单继承性的局限性
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

# 方式三:实现Callable接口(JDK 5.0新增)

详情参看下文: Callable

# 方式四:使用线程池(JDK 5.0新增)

详情参看下文: 线程池

# 线程的开启

# 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 */
            }
        }
    }
	//这是一个C++底层,Java是没有权限操作底层硬件的
    private native void start0();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Java是没有权限去开启线程、操作硬件的,start0()这是一个native的一个本地方法,它调用的底层的C++代码。

# 线程的调度策略

时间片

同优先级线程组成先进先出队列(先到先服务),使用时间片策略

image-20200608204627166

抢占式

对高优先级,使用优先调度的抢占式策略,高优先级的线程抢占CPU

# 线程的优先级

# 线程的优先级等级

MAX_PRIORITY: 10 MIN _PRIORITY: 1 NORM_PRIORITY: 5

# 涉及的方法

getPriority(): 获取线程的优先级 setPriority(int newPriority): 设置线程的优先级

# 说明

  1. 线程创建时继承父线程的优先级
  2. 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

# 线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程

  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
  • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
  • Java垃圾回收就是一个典型的守护线程。
  • JVM会在所有的非守护线程(用户线程)执行完毕后退出;
  • main线程是用户线程;
    • 仅有main线程一个用户线程执行完毕,不能决定JVM是否退出,也即是说main线程并不一定是最后一个退出的线程
    • @Test 修饰的方法 多线程都是守护线程所以 当测试方法完成后就会退出 而main方法是用户线程,多线程也都是用户线程

# 线程的生命周期(状态)

源码分析

JDK中用Thread.State类定义了线程的几种状态

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
    	//初始
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
    	//运行
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
    	//阻塞
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
    	//等待
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
    	//超时等待
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
    	//终止
        TERMINATED;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

另一种说法

要想实现多线程, 必须在主线程中创建新的线程对象。 Java语言使用Thread类及其子类的对象来表示线程, 在它的一个完整的生命周期中通常要经历如下的五种状态:

  1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  2. 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  3. 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  4. 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  5. 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

image-20200608211039799

# ==线程同步及线程安全==

# 线程安全问题

# 产生的原因?

  1. 多个线程执行的不确定性引起执行结果的不稳定
  2. 多个线程对数据的共享,会造成操作的不完整性,会破坏数据。

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

# 实例(买票的超卖问题)

/**
 * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
 *
 * 存在线程的安全问题,待解决。
 */
class Window extends Thread{

    private static int ticket = 100;
    @Override
    public void run() {

        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }

    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

结果

出现了共享数据错误

窗口2:卖票,票号为:100
窗口1:卖票,票号为:100
窗口3:卖票,票号为:98
窗口3:卖票,票号为:97
窗口2:卖票,票号为:97
窗口1:卖票,票号为:95
窗口1:卖票,票号为:94
窗口2:卖票,票号为:93
窗口3:卖票,票号为:92
窗口3:卖票,票号为:91
......
窗口1:卖票,票号为:10
窗口3:卖票,票号为:10
窗口2:卖票,票号为:10
窗口2:卖票,票号为:7
窗口3:卖票,票号为:7
窗口1:卖票,票号为:5
窗口3:卖票,票号为:4
窗口2:卖票,票号为:4
窗口1:卖票,票号为:2
窗口2:卖票,票号为:1
窗口3:卖票,票号为:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

分析

  1. 问题:三条线程同时共享ticket的票,卖票过程中,出现了重票、错票 -->出现了线程的安全问题,导致数据错误。
  2. 原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
  3. 解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

# 如何确定是否存在线程安全问题?

  • 明确哪些代码是多线程运行的代码

  • 明确多个线程是否有共享数据

  • 明确多线程运行代码中是否有多条语句操作共享数据

# 如何解决线程安全问题?

对多条操作共享数据的语句, 只能让一个线程都执行完, 在执行过程中, 其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

切记:

  • 范围太小:没锁住所有有安全问题的代码
  • 范围太大:没发挥多线程的功能。

# 线程同步

Java对于多线程的安全问题提供了专业的解决方式: 同步机制

# 方式一:同步代码块

synchronized (对象){
	// 需要被同步的代码;
}
1
2
3

说明

  1. 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。

  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。

  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

要求:多个线程必须要共用同一把锁。

补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

# 示例:实现Runnable
class Window1 implements Runnable{

    private int ticket = 100;
//    Object obj = new Object();
    
    @Override
    public void run() {
        while(true){
            //此时的this:唯一的Window1的对象 ,方式二:synchronized (obj)
            synchronized (this){ 
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 示例:继承Thread

使用同步代码块解决继承Thread类的方式的线程安全问题。在继承Thread类创建多线程的方式中,慎用this充当同步监视器(this可能代表不同的对象),考虑使用当前类充当同步监视器。

class Window2 extends Thread{

    private static int ticket = 100;

    private static Object obj = new Object();

    @Override
    public void run() {
        while(true){
            //正确的
//            synchronized (obj){
            synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
                //错误的方式:this代表着t1,t2,t3三个对象
//              synchronized (this){
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

public synchronized void show (String name){

}
1
2
3
# 示例:使用同步方法解决实现Runnable接口的线程安全问题
class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private synchronized void show(){//同步监视器:this
        //synchronized (this){
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }
        //}
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 说明
  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  2. 非静态的同步方法,同步监视器是:this静态的同步方法,同步监视器是:当前类本身
# 示例:使用同步方法处理继承Thread类的方式中的线程安全问题

继承方式实现多线程,同步方法最好声明成静态的

class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){//同步监视器:Window4.class
        //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

线程就是一个单独的资源类,没有任何的附属操作!

/**
 * 真正的多线程开发
 * 线程就是一个单独的资源类,没有任何的附属操作!
 */
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        //多线程操作
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Ticket ticket = new Ticket();

        //@FunctionalInterface 函数式接口 jdk1.8之后 lambda表达式
        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();
    }
}
//资源类
//属性+方法
//oop
class Ticket{
    private int number=50;

    //卖票的方式
    // synchronized 本质:队列,锁
    public synchronized void sale(){
        if(number>0){
            System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");
            number--;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 方式三:Lock(锁)(JDK5.0新增)

详情见下文JUC中对Lock(锁)的介绍: Lock锁(重点)

# 同步机制

# 同步锁机制

在《Thinking in Java》 中, 是这么说的:对于并发工作, 你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争) 。 防止这种冲突的方法就是当资源被一个任务使用时, 在其上加锁。 第一个访问某项资源的任务必须锁定这项资源, 使其他任务在其被解锁之前, 就无法访问它了, 而在其被解锁之时, 另一个任务就可以锁定并使用它了。

# 注意事项:

  1. 必须确保使用同一个资源的多个线程共用一把锁, 这个非常重要, 否则就无法保证共享资源的安全
  2. 一个线程类中的所有静态方法共用同一把锁(类名.class) , 所有非静态方法共用同一把锁(this) , 同步代码块(指定需谨慎)

# 同步方式的优缺点

优点

同步的方式,解决了线程的安全问题。

缺点

操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

# ==synchronized==

# 释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception, 导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

# 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
    • 应尽量避免使用suspend()resume()来控制线程

# synchronized锁的对象

Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。

  • 对于静态同步方法,锁是当前类的Class对象。

  • 对于同步方法块,锁是Synchonized括号里配置的对象。

# 原理

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

# ==死锁(synchronized)==

这里只简单阐述synchronized引发的死锁问题,更详细的会在下文的JUC的死锁问题进行研究。

# 问题
  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
# 示例
//死锁的演示
class A {
	public synchronized void foo(B b) { //同步监视器:A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器:A类的对象:a
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器:b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器:b
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	@Override
	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();
		
		dl.init();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 结果

线程死锁,程序不会正常停止

image-20200608222839529

# 解决办法
  1. 专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

# 线程通信

线程通信,即线程交替执行

# wait() 与 notify() 和 notifyAll()

  • wait():令当前线程挂起并放弃CPU、 同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。(一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。)
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。(一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。)
  • notifyAll ():唤醒正在排队等待资源的所有线程结束等待。(一旦执行此方法,就会唤醒所有被wait的线程。)

wait()

  • 在当前线程中调用方法: 对象名.wait()
  • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或notifyAll) 为止。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()

  • 在当前线程中调用方法: 对象名.notify()
  • 功能:唤醒等待该对象监控权的一个/所有线程。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

注意事项

  1. 这三个方法只有在synchronized方法synchronized代码块中才能使用,调用者必须是同步代码块或同步方法中的同步监视器,否则会报java.lang.IllegalMonitorStateException异常。
  2. 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明(定义在java.lang.Object类中)。

# 示例1——交替打印

使用两个线程打印 1-100。线程1, 线程2 交替打印

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                obj.notify();
                if(number <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

结果

线程1:1
线程2:2
线程1:3
线程2:4
线程1:5
线程2:6
线程1:7
线程2:8
线程1:9
线程2:10
线程1:11
线程2:12
线程1:13
......
线程1:95
线程2:96
线程1:97
线程2:98
线程1:99
线程2:100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 示例2——生产者/消费者问题

/**
 * 线程通信的应用:经典例题:生产者/消费者问题
 *
 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
 * 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
 * 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
 * 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
 *
 * 分析:
 * 1. 是否是多线程问题?是,生产者线程,消费者线程
 * 2. 是否有共享数据?是,店员(或产品)
 * 3. 如何解决线程的安全问题?同步机制,有三种方法
 * 4. 是否涉及线程的通信?是
 */
class Clerk{

    private int productCount = 0;
    //生产产品
    public synchronized void produceProduct() {
        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //消费产品
    public synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;
            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread{//生产者

    private Clerk clerk;
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品.....");
        while(true){
           try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }

    }
}

class Consumer extends Thread{//消费者
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品.....");
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class ProductTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

可能出现的问题

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  2. 消费者比生产者快时,消费者会取相同的数据。.

# 面试题1:sleep() 和 wait()的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:

  1. 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
  2. 调用的要求不同(使用范围不同):sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
  3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

# Sleep的使用技巧

TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
1
2

# ==线程池==

# 为什么要使用线程池?

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

优点

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

# 线程池相关API

# 创建线程池

  • JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 (可根据需要创建新线程的线程池)
      • 创建方式: Executors.newCachedThreadPool()
    • Executors.newFixedThreadPool(n): 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置,如Runtime.getRuntime().availableProcessors()
      • 创建方式: Executors.newFixedThreadPool()
    • Executors.newSingleThreadExecutor() :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
      • 创建方式: Executors.newSingleThreadExecutor ()
    • Executors.newScheduledThreadPool(n):创建一个定长线程池,支持定时及周期性任务执行。
      • 创建方式: Executors.newScheduledThreadPool ()
# 源码分析
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
1
2
3
4
5
6
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
1
2
3
4
5

本质:三种方法都是开启的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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Executors说明

Executors 类提供了使用了 ThreadPoolExecutor 的简单的 ExecutorService 实现,也就是上面所说的四种Executors线程池,但是 ThreadPoolExecutor 提供的功能远不止于此。

# 线程池的一些问题

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 问题

但是在使用Executors创建线程池的时候,阿里巴巴开发规范并不推荐这样去使用

image-20200609114828157

# 具体警告如下
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下:
1FixedThreadPoolSingleThreadPool:
  允许的请求队列长度为Integer.MAX_VALUE(约为21亿),可能会堆积大量的请求,从而导致OOM。
2CachedThreadPool:
  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
            
Positive example 1//org.apache.commons.lang3.concurrent.BasicThreadFactory
    ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
                         
Positive example 2ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    //Common Thread Pool
    ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    pool.execute(()-> System.out.println(Thread.currentThread().getName()));
    pool.shutdown();//gracefully shutdown
                  
Positive example 3<bean id="userThreadPool"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    //in code
    userThreadPool.execute(thread);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 分析

在创建线程池的时候,大部分人还是会选择使用Executors去创建。

下面是创建定长线程池(FixedThreadPool)的一个例子,严格来说,当使用如下代码创建线程池时,是不符合编程规范的。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
1
# 原因:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下: 1)FixedThreadPool和SingleThreadPool:   允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 2)CachedThreadPool:   允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

# 弊端
  1. SingleThreadPool只有一个线程在处理任务,如果达到了FixedThreadPool设置的线程最大数量,线程就会等待。所以如果任务比较多的时候,就可能造成任务(请求)堆积,造成内存溢出(OOM)。

  2. CachedThreadPool创建可缓存的线程池,线程没有限制,如果任务比较多,就会造成创建大量的线程而造成内存溢出。

# OOM

Out Of Memory,一般是由于程序编写者对内存使用不当,如对该释放的内存资源没有释放,导致其一直不能被再次使用而使计算机内存被耗尽的现象。

所以可以使用ThreadPoolExecutor创建线程池。

# 通过ThreadPoolExecutor创建线程池

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。

# 构造方法
public class ThreadPoolExecutor extends AbstractExecutorService {

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

}
1
2
3
4
5
6
7
8
9
10
11

ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

# 构造器各个参数含义
  • corePoolSize:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位,有7种取值。TimeUnit.DAYS、TimeUnit.HOURS、TimeUnit.MINUTES、TimeUnit.SECONDS、TimeUnit.MILLISECONDS、TimeUnit.MICROSECONDS、TimeUnit.NANOSECONDS
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。 ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:
    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行此任务(重复此过程)
    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,可以保证任务都被执行,但是可能会有一定的延迟。

线程池原理

# ThreadPoolExecutor类中几个重要方法
  1. execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

  2. submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

  3. shutdown()和shutdownNow()是用来关闭线程池的。

  4. 还有一大波get的方法, 可以获取与线程池相关属性的方法。

# 示例1
/**
 * 测试ThreadPoolExecutor对线程的执行顺序
 **/
public class ThreadPoolSerialTest {
    public static void main(String[] args) {
        //核心线程数
        int corePoolSize = 3;
        //最大线程数
        int maximumPoolSize = 6;
        //超过 corePoolSize 线程数量的线程最大空闲时间
        long keepAliveTime = 2;
        //以秒为时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        //创建工作队列,用于存放提交的等待执行任务
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
        ThreadPoolExecutor threadPoolExecutor = null;
        try {
            //创建线程池
            threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
                    maximumPoolSize,
                    keepAliveTime,
                    unit,
                    workQueue,
                    new ThreadPoolExecutor.AbortPolicy());

            //循环提交任务
            for (int i = 0; i < 8; i++) {
                //提交任务的索引
                final int index = (i + 1);
                threadPoolExecutor.submit(() -> {
                    //线程打印输出
                    System.out.println("大家好,我是线程:" + index);
                    try {
                        //模拟线程执行时间,10s
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                //每个任务提交后休眠500ms再提交下一个任务,用于保证提交顺序
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

image-20200609161521764

# 示例2——银行排队

理解四种拒绝策略

/**
 * new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
 * new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
 * new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
 * new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
 */
public class Demo01 {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor

        // 最大线程到底该如何定义
        // 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
        // 2、IO  密集型   > 判断你程序中十分耗IO的线程,
        // 程序   15个大型任务  io十分占用资源!

        // 获取CPU的核数
        System.out.println(Runtime.getRuntime().availableProcessors());

        List  list = new ArrayList();

        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                Runtime.getRuntime().availableProcessors(),
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());  //队列满了,尝试去和最早的竞争,也不会抛出异常!
        try {
            // 最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 如何去设置线程池的最大大小如何去设置?

1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小

2、I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

# 线程池小结

  1. Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
  2. 然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
  3. 抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
  4. 然后ThreadPoolExecutor继承了类AbstractExecutorService。

# JUC(多线程与高并发进阶)

# 什么是JUC?

JUC是 java util concurrent

image-20200812203120902

# Java获取CPU核数

Runtime.getRuntime().availableProcessors()
1

# ==Lock锁(重点)==

# 基本介绍

  • 从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁。
    • ReentrantLock默认的构造方法是非公平锁,如下源码分析
	/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 公平锁和非公平锁

公平锁: 十分公平,必须先来后到;

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

public class SaleTicketDemo02 {
    public static void main(String[] args) {
        //多线程操作
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Ticket2 ticket = new Ticket2();
        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();
    }
}

//lock三部曲
//1、    Lock lock=new ReentrantLock();
//2、    lock.lock() 加锁
//3、    finally=> 解锁:lock.unlock();
class Ticket2{
    private int number=50;

    Lock lock=new ReentrantLock();

    //卖票的方式
    // 使用Lock 锁
    public void sale(){
        //加锁
        lock.lock();
        try {
            //业务代码
            if(number>=0){
                System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");
                number--;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            //解锁
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 分类

在这里插入图片描述

# 如何使用?

class A{
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        lock.lock();
        try{
        	//保证线程安全的代码;
        }
        finally{
        	lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

注意:如果同步代码有异常,要将unlock()写入finally语句块

# 面试题:synchronized 与 Lock 的对比

相同:

二者都可以解决线程安全问题

不同:

  1. Synchronized 内置的Java关键字,Lock是一个Java类
  2. Synchronized 无法判断获取锁的状态,Lock可以判断
  3. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁,可能会遇到死锁), synchronized是隐式锁,出了作用域自动释放
  4. Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
  5. Lock只有代码块锁, synchronized有代码块锁和方法锁
  6. Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
  7. 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock ——> 同步代码块(已经进入了方法体,分配了相应资源) ——> 同步方法(在方法体之外)

# 读写锁(ReadWriteLock)

先对于不加锁的情况:

如果我们做一个我们自己的cache缓存。分别有写入操作、读取操作;

我们采用五个线程去写入,使用十个线程去读取。

我们来看一下这个的效果,如果我们不加锁的情况!

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache_ReadWriteLock mycache = new MyCache_ReadWriteLock();
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

class MyCache_ReadWriteLock{
    private volatile Map<String,String> map=new HashMap<>();

    //使用读写锁
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    //普通锁
    private Lock lock=new ReentrantLock();

    public void put(String key,String value){
        //写入
        System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName()+" 线程 写入OK");
    }

    public String get(String key){
        //得到
        System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
        String o = map.get(key);
        System.out.println(Thread.currentThread().getName()+" 线程 读取OK");
        return o;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

结果:

Thread-1 线程 开始写入
Thread-1 线程 写入OK
Thread-0 线程 开始写入
Thread-2 线程 开始写入
Thread-2 线程 写入OK
Thread-0 线程 写入OK
Thread-4 线程 开始写入
Thread-4 线程 写入OK
Thread-5 线程 开始读取
Thread-5 线程 读取OK
Thread-6 线程 开始读取
Thread-6 线程 读取OK
......
1
2
3
4
5
6
7
8
9
10
11
12
13

所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是我们可以采用更细粒度的锁:ReadWriteLock 读写锁来保证

image-20200813100700283

/**
 * 独占锁(写锁) 一次只能被一个线程占有
 * 共享锁(读锁) 多个线程可以同时占有
 * ReadWriteLock
 * 读-读  可以共存!
 * 读-写  不能共存!
 * 写-写  不能共存!
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

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

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

    }
}

// 加锁的
class MyCacheLock{

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

    // 存,写入的时候,只希望同时只有一个线程写
    public void put(String key,Object value){
        readWriteLock.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 {
            readWriteLock.writeLock().unlock();
        }
    }

    // 取,读,所有人都可以读!
    public void get(String key){
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

}

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

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

    // 存,写
    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");
    }

    // 取,读
    public void get(String key){
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取OK");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

结果:

1写入1
1写入OK
2写入2
2写入OK
3写入3
3写入OK
4写入4
4写入OK
1读取1
1读取OK
2读取2
2读取OK
5写入5
5写入OK
4读取4
4读取OK
3读取3
3读取OK
5读取5
5读取OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 生产者和消费者问题

# Synchronized wait notify实现(传统方式)

public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        },"A").start();
        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }},"B").start();
    }
}
class Data{
    //数字  资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if(number!=0){
            //等待操作
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if(number==0){
            //等待操作
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程  我-1完毕了
        this.notifyAll();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 虚假唤醒

20200200107238

==解决方案==if 改为while即可,防止虚假唤醒。这样就不存在问题了:

public class A {
    public static void main(String[] args) {
        Data data = new Data();

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

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

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


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

// 判断等待,业务,通知
class Data{ // 数字 资源类

    private int number = 0;

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

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){ // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

# JUC版本的生产者和消费者问题

await、signal 替换 wait、notify

image-20200813084524835

通过Lock找到Condition

image-20200813084635161

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

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

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

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

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

    }
}

// 判断等待,业务,通知
class Data2{ // 数字 资源类

    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    //condition.await(); // 等待
    //condition.signalAll(); // 唤醒全部
    //+1
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            // 业务代码
            while (number!=0){  //0
                // 等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            // 通知其他线程,我+1完毕了
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number==0){ // 1
                // 等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            // 通知其他线程,我-1完毕了
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

执行结果:

现在是随机执行

A=>1
B=>0
C=>1
B=>0
C=>1
B=>0
C=>1
B=>0
C=>1
D=>0
1
2
3
4
5
6
7
8
9
10

Condition的优势:精准的通知和唤醒的线程!

如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~,比如A->B->C->A

public class C {

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

        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printA();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printB();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                data.printC();
            }
        },"C").start();
    }

}

class Data3{ // 资源类 Lock

    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1; // 1A  2B  3C

    public void printA(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=1){
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
            // 唤醒,唤醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
            // 唤醒,唤醒指定的人,c
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            // 业务,判断-> 执行-> 通知
            while (number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
            // 唤醒,唤醒指定的人,c
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

执行结果:

A=>AAAAAAA
B=>BBBBBBBBB
C=>BBBBBBBBB
A=>AAAAAAA
B=>BBBBBBBBB
C=>BBBBBBBBB
A=>AAAAAAA
B=>BBBBBBBBB
C=>BBBBBBBBB
A=>AAAAAAA
1
2
3
4
5
6
7
8
9
10

# 8锁现象

情况1、2说明:

  • synchronized 锁的对象是方法的调用者!

  • 谁先拿到谁执行!另外一个则等待!与执行时间的先后无关!

    如果你认为是顺序在前? 这个答案是错误的!

/**
 * 8锁,就是关于锁的8个问题
 * 1、标准情况下,两个线程先打印 发短信还是 打电话? 1/发短信  2/打电话
 	结果是:先发短信,如何再打电话!
 	为什么? 如果你认为是顺序在前? 这个答案是错误的!
 * 2、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信  2/打电话
 	结果:还是先发短信,然后再打电话!
 	原因:并不是顺序执行!是因为synchronized 锁的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行!另外一个则等待!
 */
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        //锁的存在
        new Thread(()->{
            phone.sendSms();
        },"A").start();

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

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

class Phone{

    // synchronized 锁的对象是方法的调用者!、
    // 两个方法用的是同一个锁,谁先拿到谁执行!
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

情况3、4说明:

  • synchronized 锁的对象是方法的调用者!
  • 没有锁,不是同步方法,不受锁的影响。
  • 不同对象的调用加锁的同步方法不受影响。
/**
 * 3、 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
 	如果我们添加一个普通方法,那么先执行哪一个呢?
 	答案是:先执行hello,然后再执行发短信!原因是hello是一个普通方法,不受synchronized锁的影响,但是我发现,如果我把发短信里面的延迟4秒去掉,那么就会顺序执行,先执行发短信然后再执行hello,原因应该是顺序执行的原因吧,不是太理解。
 * 4、 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
 	如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?
 	答案是:先打电话,后发短信。原因:在发短信方法中延迟了4s,又因为synchronized锁的是对象,但是我们这使用的是两个对象,所以每个对象都有一把锁,所以不会造成锁的等待。正常执行
 */
public class Test2  {
    public static void main(String[] args) {
        // 两个对象,两个调用者,两把锁!
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

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

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

class Phone2{

    // synchronized 锁的对象是方法的调用者!
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

    // 这里没有锁!不是同步方法,不受锁的影响
    public void hello(){
        System.out.println("hello");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

情况5、6说明:

  • 静态同步方法锁的对象是类本身(Class)
  • 两个对象的Class类模板只有一个,static,锁的是Class
  • 静态方法用该用类去调用,即使用对象调用,也还是该类的Class对象,是同一个,所以互相之间会受到影响
/**
 * 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?   发短信
 * 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?     发短信
 	如果我们把synchronized的方法加上static变成静态方法!那么顺序又是怎么样的呢?
    (1)我们先来使用一个对象调用两个方法!
    答案是:先发短信,后打电话
    (2)如果我们使用两个对象调用两个方法!
    答案是:还是先发短信,后打电话
    原因是什么呢? 为什么加了static就始终前面一个对象先执行呢!为什么后面会等待呢?
    原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
 */
public class Test3  {
    public static void main(String[] args) {
        // 两个对象的Class类模板只有一个,static,锁的是Class
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

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

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

// Phone3唯一的一个 Class 对象
class Phone3{

    // synchronized 锁的对象是方法的调用者!
    // static 静态方法
    // 类一加载就有了!锁的是Class
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call(){
        System.out.println("打电话");
    }


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

情况7、8说明:

  • 锁的对象不同,即不受锁的影响(Class与对象不同)
/**
 * 7、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?  打电话
 	答案是:先打电话,后发短信了。
	因为一个锁的是Class类模板,一个锁的是对象调用者。后面那个打电话不需要等待发短信,直接运行就可以了。
 * 8、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?  打电话
 	答案是:先打电话、后发短信!
	因为两个对象,一样的原因:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。
 */
public class Test4  {
    public static void main(String[] args) {
        // 两个对象的Class类模板只有一个,static,锁的是Class
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

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

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

// Phone3唯一的一个 Class 对象
class Phone4{

    // 静态的同步方法 锁的是 Class 类模板
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // 普通的同步方法  锁的调用者
    public synchronized void call(){
        System.out.println("打电话");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# ==线程不安全集合==

# List不安全

# 测试list不安全

//java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
    public static void main(String[] args) {

        List<Object> arrayList = new ArrayList<>();

        for(int i=1;i<=10;i++){
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

会造成==ConcurrentModificationException 并发修改异常==!

image-20200813091719960

ArrayList 在并发情况下是不安全的!

# 解决方案1:使用Vector

源码分析Vector的add方法:

/**
  * Vector add()
  */
public synchronized void insertElementAt(E obj, int index) {
    modCount++;
    if (index > elementCount) {
        throw new ArrayIndexOutOfBoundsException(index
                                                 + " > " + elementCount);
    }
    ensureCapacityHelper(elementCount + 1);
    System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    elementData[index] = obj;
    elementCount++;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Vector底层是使用==synchronized==关键字来实现的:效率特别低下。

Vector(jdk1.0)比ArrayList(jdk1.2)出现的早,由此可看Vector的性能确实存在一些问题。

# 解决方案2:使用synchronizedList

synchronizedList底层也是是使用==synchronized==关键字来实现的:效率特别低下。

image-20200813092320872

# 解决方案3:使用CopyOnWriteArrayList

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

/**
  * CopyOnWriteArrayList add()
  */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!

# 总结

public class ListTest {
    public static void main(String[] args) {
        // 并发下 ArrayList 不安全的吗,Synchronized;
        /**
         * 解决方案;
         * 1、List<String> list = new Vector<>();
         * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3、List<String> list = new CopyOnWriteArrayList<>();
         */
        List<String> list = new CopyOnWriteArrayList<>();

        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();
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Set不安全

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;

# 解决方案

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案
public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        // hashmap
        // Set<String> set = Collections.synchronizedSet(new HashSet<>());
        // Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 1; i <=30 ; i++) {
           new Thread(()->{
               set.add(UUID.randomUUID().toString().substring(0,5));
               System.out.println(set);
           },String.valueOf(i)).start();
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# HashSet底层是什么?

hashSet底层就是一个HashMap

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

//add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储
//hashSet就是使用了hashmap key不能重复的原理
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
//PRESENT是什么? 是一个常量  不会改变的常量  无用的占位
private static final Object PRESENT = new Object();
1
2
3
4
5
6
7
8
9
10
11

# Map不安全

# Map的基本操作

//map 是这样用的吗?  不是,工作中不使用这个
//默认等价什么? new HashMap<>(16,0.75);
Map<String, String> map = new HashMap<>();
//加载因子、初始化容量
1
2
3
4

默认加载因子是0.75,默认的初始容量是16

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
1
2
3
4
5
6
7
8
9

同样的HashMap基础类也存在并发修改异常

public static void main(String[] args) {
        //map 是这样用的吗?  不是,工作中不使用这个
        //默认等价什么? new HashMap<>(16,0.75);
        Map<String, String> map = new HashMap<>();
        //加载因子、初始化容量
        for (int i = 1; i < 100; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12

结果同样的出现了:异常java.util.ConcurrentModificationException 并发修改异常

# 解決方案

  • 使用Collections.synchronizedMap(new HashMap<>());处理
  • 使用ConcurrentHashMap进行并发处理

# 研究ConcurrentHashMap底层原理:

# Callable

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
1
2
3
4
5
6
7
8
9
10

jdk介绍:

@FunctionalInterface
public interface Callable<V>返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为call 。 
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。 

该Executors类包含的实用方法,从其他普通形式转换为Callable类。 

从以下版本开始: 
1.5 
1
2
3
4
5
6
7
8

# 使用分析

Calleable 泛型T就是call运行方法的返回值类型;但是如何使用呢?Callable怎么放入到Thread里面呢?

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
1
2
3

对于Thread运行,只能传入Runnable类型的参数;我们这是Callable 怎么办呢?看JDK api文档:在Runnable里面有一个叫做FutureTask的实现类

image-20200813095238672

这样我们就可以先把Callable 放入到FutureTask中, 然后再把FutureTask 放入到Thread就可以了。

# 使用流程

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  5. FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callablecall方法的返回值,get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。

# 对比实现Runnable方式

  • 相比run()方法,可以有返回值

  • 方法可以抛出异常

  • 支持泛型的返回值

  • 需要借助FutureTask类,比如获取返回结果

# Future接口

  • 可以对具体RunnableCallable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutrueTaskFutrue接口的唯一的实现类
  • FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • get()可以得到执行的泛型返回值

# 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

  1. call()可以有返回值的。

  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息

  3. Callable是支持泛型的

# 示例

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# ==常用的辅助类==

# CountDownLatch

image-20200813100032673

其实就是一个减法计数器,对于计数器归零之后再进行后面的操作,这是一个计数器!

// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); // 数量-1
            },String.valueOf(i)).start();
        }

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

        System.out.println("Close Door");

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

主要方法:

  • countDown 减一操作;
  • await 等待计数器归零。

await等待计数器为0,就唤醒,再继续向下运行。

# CyclickBarrier

image-20200813100337278

其实就是一个加法计数器

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */
        // 召唤龙珠的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{
            System.out.println("召唤神龙成功!");
        });

        for (int i = 1; i <=7 ; i++) {
            final int temp = i;
            // lambda能操作到 i 吗
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Semaphore(信号量)

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

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

            },String.valueOf(i)).start();
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

原理:

semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

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

作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!

# 阻塞队列

image-20200813105500085

image-20200813105535341

image-20200813105631762

image-20200813105656214

什么情况下我们会使用 阻塞队列:多线程并发处理,线程池!

# BlockingQueue

四组API

方式 抛出异常 不会抛出异常,有返回值 阻塞 等待 超时 等待
添加 add offer put offer(timenum,timeUnit)
移除 remove poll take poll(timenum,timeUnit)
判断队列首 element peek - -
	/**
     * 抛出异常
     */
    public static void test1(){
        //需要初始化队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        //抛出异常:java.lang.IllegalStateException: Queue full
//        System.out.println(blockingQueue.add("d"));
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //如果多移除一个
        //这也会造成 java.util.NoSuchElementException 抛出异常
        System.out.println(blockingQueue.remove());
    }
=======================================================================================
	/**
     * 不抛出异常,有返回值
     */
    public static void test2(){
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        //添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常
        System.out.println(blockingQueue.offer("d"));

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //弹出 如果没有元素 只会返回null 不会抛出异常
        System.out.println(blockingQueue.poll());
    }
=======================================================================================
	/**
     * 等待 一直阻塞
     */
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        //一直阻塞 不会返回
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");

        //如果队列已经满了, 再进去一个元素  这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
//        blockingQueue.put("d");

        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        //如果我们再来一个  这种情况也会等待,程序会一直运行 阻塞
        System.out.println(blockingQueue.take());
    }
=======================================================================================
	/**
     * 等待 超时阻塞
     *  这种情况也会等待队列有位置 或者有产品 但是会超时结束
     */
    public static void test4() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        System.out.println("开始等待");
        blockingQueue.offer("d",2, TimeUnit.SECONDS);  //超时时间2s 等待如果超过2s就结束等待
        System.out.println("结束等待");
        System.out.println("===========取值==================");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println("开始等待");
        blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了
        System.out.println("结束等待");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

# SynchronousQueue同步队列

同步队列 没有容量,也可以视为容量为1的队列

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

/**
 * 同步队列
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
        //研究一下 如果判断这是一个同步队列

        //使用两个进程
        // 一个进程 放进去
        // 一个进程 拿出来
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" Put 1");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName()+" Put 2");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName()+" Put 3");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
//                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
//                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# ForkJoin

# 什么是ForkJoin?

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

image-20200814092319548

# ForkJoin的原理特点

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

image-20200814092422487

# 如何使用ForkJoin?

  1. 通过ForkJoinPool来执行

  2. 计算任务 execute(ForkJoinTask<?> task)

image-20200814092608886

  1. 计算类要去继承ForkJoinTask;

    /**
     * 求和计算的任务!
     * 3000   6000(ForkJoin)  9000(Stream并行流)
     * // 如何使用 forkjoin
     * // 1、forkjoinPool 通过它来执行
     * // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
     * // 3. 计算类要继承 ForkJoinTask
     */
    public class ForkJoinDemo extends RecursiveTask<Long> {
    
        private Long start;  // 1
        private Long end;    // 1990900000
    
        // 临界值
        private Long temp = 10000L;
    
        public ForkJoinDemo(Long start, Long end) {
            this.start = start;
            this.end = end;
        }
    
        // 计算方法
        @Override
        protected Long compute() {
            if ((end-start)<temp){
                Long sum = 0L;
                for (Long i = start; i <= end; i++) {
                    sum += i;
                }
                return sum;
            }else { // forkjoin 递归
                long middle = (start + end) / 2; // 中间值
                ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
                task1.fork(); // 拆分任务,把任务压入线程队列
                ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
                task2.fork(); // 拆分任务,把任务压入线程队列
    
                return task1.join() + task2.join();
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

    测试类

    public class Test {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            test1(); // 8483
            test2(); // 4824
            test3(); // 294
        }
    
        // 普通程序员
        public static void test1(){
            Long sum = 0L;
            long start = System.currentTimeMillis();
            for (Long i = 1L; i <= 10_0000_0000; i++) {
                sum += i;
            }
            long end = System.currentTimeMillis();
            System.out.println("sum="+sum+" 时间:"+(end-start));
        }
    
        // 会使用ForkJoin
        public static void test2() throws ExecutionException, InterruptedException {
            long start = System.currentTimeMillis();
    
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
            ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
            Long sum = submit.get();
    
            long end = System.currentTimeMillis();
    
            System.out.println("sum="+sum+" 时间:"+(end-start));
        }
    
        public static void test3(){
            long start = System.currentTimeMillis();
            // Stream并行流 ()  (]
            long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
            long end = System.currentTimeMillis();
            System.out.println("sum="+"时间:"+(end-start));
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

三种方式执行结果:

sum=500000000500000000 时间:8483
sum=500000000500000000 时间:4824
sum=时间:294
1
2
3

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

image-20200814093004767

reduce方法的优点:

image-20200814093043025

# 异步回调

Future 设计的初衷:对将来的某个事件结果进行建模!

其实就是前端 --> 发送ajax异步请求给后端

image-20200814093243806

我们平时都使用CompletableFuture

(1)没有返回值的runAsync异步回调

public static void main(String[] args) throws ExecutionException, InterruptedException 
{
        // 发起 一个 请求

        System.out.println(System.currentTimeMillis());
        System.out.println("---------------------");
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            //发起一个异步任务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+".....");
        });
        System.out.println(System.currentTimeMillis());
        System.out.println("------------------------------");
        //输出执行结果
        System.out.println(future.get());  //获取执行结果
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

(2)有返回值的异步回调supplyAsync

//有返回值的异步回调
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
    System.out.println(Thread.currentThread().getName());
    try {
        TimeUnit.SECONDS.sleep(2);
        int i=1/0;
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
    //success 回调
    System.out.println("t=>" + t); //正常的返回结果
    System.out.println("u=>" + u); //抛出异常的 错误信息
}).exceptionally((e) -> {
    //error回调
    System.out.println(e.getMessage());
    return 404;
}).get());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

whenComplete: 有两个参数,一个是t 一个是u

T:是代表的 正常返回的结果

U:是代表的 抛出异常的错误信息

如果发生了异常,get可以获取到exceptionally返回的值;

# JMM

在并发编程中,需要处理两个关键问题:==线程之间如何通信==及==线程之间如何同步==

请你谈谈你对Volatile 的理解

Volatile 是 Java 虚拟机提供 轻量级的同步机制

1、保证可见性 2、不保证原子性 3、禁止指令重排

什么是JMM?

JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回主存;

2、线程加锁前,必须读取主存中的最新值到工作内存中;

3、加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

8种操作:

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

image-20200814093746851

image-20200814093811178

JMM对这8种操作给了相应的规定

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

image-20200814093845687

# Volatile

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

# 定义与实现原理

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为==它不会引起线程上下文的切换和调度==。

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀的指令在多核处理器下会引发了两件事情。

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

# 实现原则

  1. Lock前缀指令会引起处理器缓存回写到内存

  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

# Volatile的特点

# 1、保证可见性

public class JMMDemo01 {

    // 如果不加volatile 程序会死循环
    // 加了volatile是可以保证可见性的
    private volatile static Integer number = 0;

    public static void main(String[] args) {
        //main线程
        //子线程1
        new Thread(()->{
            while (number==0){
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //子线程2
        new Thread(()->{
            while (number==0){
            }

        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number=1;
        System.out.println(number);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2、不保证原子性

原子性:不可分割;

线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。

/**
 * 不保证原子性
 * number <=2w
 * 
 */
public class VDemo02 {

    private static volatile int number = 0;

    public static void add(){
        number++; 
        //++ 不是一个原子性操作,是两个~3个操作
        //
    }

    public static void main(String[] args) {
        //理论上number  === 20000

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            //main  gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",num="+number);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

执行五次main方法结果:

main,num=19052
main,num=20000
main,num=20000
main,num=19722
main,num=20000
1
2
3
4
5

有一定的概率不能保证数据的正确性

分析:

number++不是一个操作,而是三个操作,所以不能保证原子性:

  1. get取到number的值
  2. 进行+1操作
  3. put写number的值

即理论值应该是20000,但可能达不到

如果不加lock和synchronized ,怎么样保证原子性?

解决方法:使用JUC下的java.util.concurrent.atomic包下的class;

image-20200814095936365

public class VDemo02 {

    private static volatile AtomicInteger number = new AtomicInteger();

    public static void add(){
//        number++;
        number.incrementAndGet();  //底层是CAS保证的原子性
    }

    public static void main(String[] args) {
        //理论上number  === 20000

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            //main  gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",num="+number);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这些类的底层都直接和操作系统挂钩!是在内存中修改值。

Unsafe类是一个很特殊的存在;

image-20200814100351230

原子类为什么这么高级?

# 3、禁止指令重排

我们写的程序,计算机并不是按照我们自己写的那样去执行的

源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我们期望的执行顺序是 1_2_3_4  可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
1234567
1
2
3
4
5
6
7
8

可能造成的影响结果:前提:a b x y这四个值 默认都是0

线程A 线程B
x=a y=b
b=1 a=2

正常的结果: x = 0; y =0;

线程A 线程B
x=a y=b
b=1 a=2

可能在线程A中会出现,先执行b=1,然后再执行x=a;

在B线程中可能会出现,先执行a=2,然后执行y=b;

那么就有可能结果如下:x=2; y=1.

volatile可以避免指令重排:

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令。作用:

1、保证特定的操作的执行顺序;

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

image-20200814100613459

小结:

  • volatile可以保证可见性;
  • 不能保证原子性
  • 由于内存屏障,可以保证避免指令重排的现象产生

面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

# Volatile优化

JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。LinkedTransferQueue的代码如下:

/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
     // 使用很多4个字节的引用追加到64个字节
     Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
     PaddedAtomicReference(T r) {
        super(r);
     }
}
public class AtomicReference <V> implements java.io.Serializable {
     private volatile V value;
     // 省略其他代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

追加字节如何能优化性能?

LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头节点(head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类AtomicReference只做了一件事情,就是将共享变量追加到64字节。我们可以来计算下,一个对象的引用占4个字节,它追加了15个变量(共占60个字节),再加上父类的value变量,一共64个字节。

对于英特尔酷睿i7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,==在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率==。

==使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定==。

在两种场景下不应该使用volatile变量时追加到64字节

==缓存行非64字节宽的处理器==。如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。

==共享变量不会被频繁地写==。因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。

# 原子操作

# 处理器如何实现原子操作

(1)使用总线锁保证原子性

(2)使用缓存锁保证原子性

# Java如何实现原子操作

在Java中可以通过==锁==和==循环CAS==的方式来实现原子操作

# 循环CAS

从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count:

private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;
    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<Thread>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
    // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicI.get());
        System.out.println(System.currentTimeMillis() - start);
    }
    /**        * 使用CAS实现线程安全计数器        */
    private void safeCount() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }
    /**
     * 非线程安全计数器


     */
    private void count() {
        i++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

# 深入理解CAS

# 什么是CAS?

大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //boolean compareAndSet(int expect, int update)
        //期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        //因为期望值是2020  实际值却变成了2021  所以会修改失败
        //CAS 是CPU的并发原语
        atomicInteger.getAndIncrement(); //++操作
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

源码分析:

AtomicInteger的getAndIncrement()

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
	return unsafe.getAndAddInt(this, valueOffset, 1);
}
1
2
3
4
5
6
7
8

Unsafe的getAndAddInt

使用了compareAndSwapInt==自旋锁==

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
1
2
3
4
5
6
7
8

compareAndSwapInt:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
1

image-20200814101650846

image-20200814101719291

总结:

CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。

缺点:

  • 循环会耗时;
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

# CAS问题

# 1.循环时间长开销大。

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

# 2.只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

# 3.ABA问题?(狸猫换太子)

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。

Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

image-20200814101850855

线程1:期望值是1,要变成2;

线程2:两个操作:

  • 1、期望值是1,变成3
  • 2、期望是3,变成1

所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;

示例:

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        //boolean compareAndSet(int expect, int update)
        //期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        //因为期望值是2020  实际值却变成了2021  所以会修改失败
        //CAS 是CPU的并发原语
//        atomicInteger.getAndIncrement(); //++操作
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 原子引用

# 解决ABA问题,对应的思想:就是使用了==乐观锁==

带版本号的 原子操作!

Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间。

image-20200814102253204

所以如果遇到,使用大于128的时候,使用原子引用的时候,如果超过了这个值,那么就不会进行版本上升!

即:static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(2020,1);,则使用会出现问题。

正常业务操作中,我们一般使用的是一个个对象,一般情况不会遇到这种情况。

public class CASDemo {

    //AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题

    // 正常在业务操作,这里面比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);

    // CAS  compareAndSet : 比较并交换!
    public static void main(String[] args) {
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>"+stamp);

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

            Lock lock = new ReentrantLock(true);

            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);

            System.out.println("a2=>"+atomicStampedReference.getStamp());


            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a3=>"+atomicStampedReference.getStamp());

        },"a").start();


        // 乐观锁的原理相同!
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>"+stamp);

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

            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));

            System.out.println("b2=>"+atomicStampedReference.getStamp());

        },"b").start();

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

执行结果

a1=>1
b1=>1
a2=>2
true
a3=>3
false
b2=>3
1
2
3
4
5
6
7

b线程的邮票值不对应,则不能进行修改。

# 各种锁的理解

# 1、公平锁、非公平锁

公平锁:非常公平;不能插队的,必须先来后到;

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}
1
2
3
4
5
6
7

非公平锁:非常不公平,允许插队的,可以改变顺序。

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
1
2
3
4
5
6
7
8
9

# 2、可重入锁

可重入锁(递归锁)

image-20200814111337962

Synchronized锁

public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }

}

class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"=> sms");
        call();//这里也有一把锁
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"=> call");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

lock锁

//lock
public class Demo02 {

    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }

}
class Phone2{

    Lock lock=new ReentrantLock();

    public void sms(){
        lock.lock(); //细节:这个是两把锁,两个钥匙
        //lock锁必须配对,否则就会死锁在里面
        try {
            System.out.println(Thread.currentThread().getName()+"=> sms");
            call();//这里也有一把锁
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void call(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "=> call");
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  • lock锁必须配对,相当于lock和 unlock 必须数量相同;
  • 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;

# 3、自旋锁

spinlock

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}
1
2
3
4
5
6
7

自我设计自旋锁:

public class SpinlockDemo {

    //int 0
    //thread null
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

    //加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> mylock");

        //自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
            System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
        }
    }


    //解锁
    public void myunlock(){
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName()+"===> myUnlock");
        atomicReference.compareAndSet(thread,null);
    }

}

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        reentrantLock.unlock();


        //使用CAS实现自旋锁
        SpinlockDemo spinlockDemo=new SpinlockDemo();
        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myunlock();
            }
        },"t1").start();

        TimeUnit.SECONDS.sleep(1);


        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myunlock();
            }
        },"t2").start();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

运行结果:

t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。。

# 4、死锁

死锁是什么?

image-20200814111731992

死锁测试,怎么排除死锁:

public class DeadLock {
    public static void main(String[] args) {
        String lockA= "lockA";
        String lockB= "lockB";

        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").start();
    }
}

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

解决问题

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:jps -l

image-20200814112257872

2、使用jstack 进程进程号 找到死锁信息

image-20200814112401945

一般在最后: image-20200814112512148

面试,工作中!排查问题!

1、日志

2、堆栈信息

如何避免死锁?

  1. 避免一个线程同时获取多个锁。
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

# 5.无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

image-20200819210526187

# 并发编程的艺术

# 并发编程的挑战

并发编程的目的是==为了让程序运行得更快==。

并发编程的挑战,即在并发编程中遇到的那些问题,会影响程序运行的更快。

# 1. 上下文切换问题

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过==时间片分配算法==来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

==上下文切换也会影响多线程的执行速度==。就像单词不认识,必须放下技术书去查单词,还必须记住书读到了哪一页,以便查完单词,重新读这本书。

多线程并发执行,并不一定比串行执行的快!!!

当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。那么,为什么并发执行的速度会比串行慢呢?这是因为线程有创建和上下文切换的开销。

如何减少上下文切换?

  1. ==无锁并发编程==。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

  2. ==CAS算法==。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

  3. ==使用最少线程==。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

  4. ==协程==:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

# 2.死锁问题

详情参看上文: 死锁

# 3. 资源限制的问题

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源

例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为==增加了上下文切换和资源调度的时间==

如何解决资源限制的问题?

对于硬件资源限制,可以考虑使用集群并行执行程序

对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

如何在资源限制的情况下,让程序执行得更快呢?

方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作 时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

多使用JDK并发包提供的并发容器和工具类来解决并发问题,因为这些类都已经通过了充分的测试和优化,均可解决了提到的几个挑战。