第二章 并发编程

一、线程的基本知识

 1.1 线程的概念
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一个进程可以有很多线程,每条线程并行执行不同的任务。

    
为什么会有线程?
- 在多核CPU中,利用多线程可以实现真正意义上的并行执行。
- 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务 被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创 建不同的线程去处理,可以提升程序处理的实时性。
- 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快。
   
1.2 应用场景
为什么要用多线程?
- 异步执行(避免阻塞)。
- 利用多CPU资源实现真正意义行的并行执行。
  
1.3 Java中多线程应用
1.3.1 继承Thread类

既然线程启动时会去调用 run 方法,那么我们只要重写 Thread 类的 run 方法也是可以定义出我们的线程类的。
public class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadDemo thread = new ThreadDemo();
        thread.start();
    }
}
   
执行结果:
当前线程:Thread-0
   
1.3.2 实现Runnable接口
public class RunnableDemo implements Runnable {
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();
        Thread thread = new Thread(runnableDemo);
        thread.start();
    }
}
     
执行结果:
当前线程:Thread-0
  
1.3.3 实现Callable接口
import java.util.concurrent.*;

public class CallableDemo implements Callable<String> {
    public String call() throws Exception {
        System.out.println("当前线程:" + Thread.currentThread().getName());
        Thread.sleep(10000);    // 等待sleep执行完后才会返回“hello ly”
        return "hello ly";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> future = executorService.submit(new CallableDemo());
        // future.get() 是一个阻塞方法
        System.out.println(Thread.currentThread().getName() + " - " + future.get());
    }
}
    
执行结果:
当前线程:pool-1-thread-1
main - hello ly
   

1.4 线程的生命周期 
Java线程从创建到销毁,可能会经历一下6个状态
- NEW:初始状态,线程被构建,但是还没有调用start方法。
- RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为”运行中”。
- BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使 用权,阻塞也分为几种情况。
- WAITING: 等待状态。
- TIME_WAITING:超时等待状态,超时以后自动返回。
- TERMINATED:终止状态,表示当前线程执行完毕。

  

     
import java.util.concurrent.TimeUnit;

public class ThreadStatusDemo {

    public static void main(String[] args) {
        // TIME_WAITING
        new Thread(()->{
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Time_Wating_Demo").start();
        // WAITING
        new Thread(()->{
            while (true) {
                synchronized (ThreadStatusDemo.class) {
                    try {
                        ThreadStatusDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Wating").start();
        new Thread(new BlockedDemo(), "Blocked-Demo-01").start();
        new Thread(new BlockedDemo(), "Blocked-Demo-02").start();
    }

    static class BlockedDemo extends Thread {
        @Override
        public void run() {
            synchronized (BlockedDemo.class) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
    
运行ThreadStatusDemo.main后,如果在Terminal下执行jps命令无显示,如下图所示:

  
产生原因:是因为 jps 命令 没有权限 读取 hsperfdata_用户名的文件夹, 这个文件夹运行 java 程序时产生的。
  
解决办法:
1. 找到 `hsperfdata_用户名的文件夹`添加读写权限,例如本机路径`C:\Users\admin\AppData\Local\Temp\hsperfdata_admin`;
2. 右击-->属性-->安全-->高级;

  
3. 选中当前登录用户,点击【更改权限】;

   
4. 选中当前登录用户,点击【编辑】;

  
5. 勾选【完全控制】,点击【确定】。

   

二、线程的基本操作及原理  

2.1 Thread.join的使用及原理

public class ThreadJoinDemo {
    private static int x = 0;
    private static int i = 0;

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            i = 1;
            x = 2;
        });
        Thread t2 = new Thread(()->{
            i = x + 2;
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("result : " + i);
    }
}
  
执行结果可能是:4

  
多执行几次结果有可能是:1

   
由于线程的执行顺序是不确定的,如何保证线程的执行顺序呢?
我们在`t1.start();`和`t2.start();`之间加入`t1.join();`就能保证了。
......
        t1.start();
		t1.join();	// t1线程的执行结果对于t2可见(t1线程一定会比t2线程优先执行)
        t2.start();
......
   
2.2 Thread.sleep的作用
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断。
import java.text.SimpleDateFormat;

public class ThreadSleepDemo extends Thread {
    public static void main(String[] args) {
        new ThreadSleepDemo().start();
    }

    @Override
    public void run() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        long dt1 = System.currentTimeMillis();
        System.out.println("begin: " + dt1 + " | " + sdf.format(dt1));
        try {
            Thread.sleep(3000);
            long dt2 = System.currentTimeMillis();
            System.out.println("end: " + dt2 + " | " + sdf.format(dt2));
            long dt = dt2 - dt1;
            System.out.println("dt2 - dt1 = " + dt);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
    
执行结果:
begin: 1650361034229 | 2022-04-19 17:37:14
end: 1650361037243 | 2022-04-19 17:37:17
dt2 - dt1 = 3014
  
2.2.2 思考问题
- 假设现在是2022-04-19 12:00:00.000,如果调用Thread.Sleep(1000),那么在2022-04-19 12:00:01.000的时候,这个线程会不会被唤醒?
- Thread.Sleep(0)的意义
Thread.sleep(0)并非是真的让线程挂起0毫秒,意义在于调用Thread.sleep(0)的当前线程确实被冻结了一下,让其他线程有机会优先执行,Thread.sleep(0)是使你的线程暂时放弃cpu,也是释放一些未使用的时间片给其他线程或者进程使用,就相当于一个让位动作。
   
2.2.1 Thread.sleep的工作流程
- 挂起线程并修改其运行状态。 
- 用sleep()提供的参数来设置一个定时器。 
- 当时间结束,定时器会触发,内核收到中断后修改线程的运行状态。 
   
例如线程会被标志为就绪而进入就绪队列等待调度。
  
2.3 wait和notify的使用
2.3.1 wait

- wait(),当前线程进入 无限等待状态,必须被唤醒才能继续执行,调用后会释放锁对象。
- wait(long timeout),wait(long timeout,int nanos),当前线程进入等待状态,可以被提前唤醒,但在指定时间后会自动唤醒。
   
2.3.2 notify
- notify(), 随机唤醒一个在锁对象上调用wait的线程。
- notifyAll(),唤醒 全部在锁对象上调用wait的线程。
     
2.3.3 例子
public class TestDemo {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个顾客线程(消费者)
        new Thread() {
            @Override
            public void run() {
                // 保证正等待和唤醒的线程只能有一个执行,需要使用同步技术
                synchronized (obj) {
                    System.out.println("告诉老板要的包子的种类和数量");
                    // 调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 唤醒之后执行的代码
                    System.out.println("包子已经准备好了,开吃!");
                }
            }
        }.start();

        // 创建一个老板线程(生产者)
        new Thread() {
            @Override
            public void run() {
                // 花了5秒钟准备包子
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (obj) {
                    System.out.println("老板5秒钟之后准备好包子,告知顾客,可以吃包子了");
                    // 准备好包子之后,调用notify方法,唤醒顾客吃包子
                    obj.notify();
                }
            }
        }.start();
    }
}
  
执行结果:
告诉老板要的包子的种类和数量
老板5秒钟之后准备好包子,告知顾客,可以吃包子了
包子已经准备好了,开吃!
   
注意:wait 和 notify、notifyAll 都只能在同步代码块或者同步方法中调用,且要通过锁对象进行调用,否则会抛出 IllegalMonitorStateException。
  
2.3.4 生产者消费者模式
- 生产者Producer.java源码。
import java.util.Queue;

public class Producer implements Runnable {
    private Queue<String> bags;
    private int size;

    public Producer(Queue<String> bags, int size) {
        this.bags = bags;
        this.size = size;
    }

    public void run() {
        int i = 0;
        while (true) {
            i++;
            synchronized (bags) {
                while (bags.size() == size) {
                    System.out.println("bags 满了");
                    try {
                        bags.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("生产者 - 生产:bag " + i);
                bags.add("bag " + i);
                // 唤醒处于阻塞状态下的消费者
                bags.notifyAll();
            }
        }
    }
}
  
- 消费者`Consumer.java`源码
import java.util.Queue;

public class Consumer implements Runnable {
    private Queue<String> bags;
    private int size;

    public Consumer(Queue<String> bags, int size) {
        this.bags = bags;
        this.size = size;
    }

    public void run() {
        while (true) {
            synchronized (bags) {
                while (bags.isEmpty()) {
                    System.out.println("bags 为空。");
                    try {
                        bags.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String bag = bags.remove();
                System.out.println("消费者消费:" + bag);
                // 唤醒处于阻塞状态下的生产者
                bags.notifyAll();
            }
        }
    }
}
  
测试`WaitNotifyDemo.java`源码
import java.util.LinkedList;
import java.util.Queue;

public class WaitNotifyDemo {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<String>();
        int size = 10;
        Producer producer = new Producer(queue, size);
        Consumer consumer = new Consumer(queue, size);
        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(consumer);
        t1.start();
        t2.start();
    }
}
  
2.4 Thread.interrupted和Thread.interrupt
2.4.1 为什么Thread.stop不推荐使用?

因为它本质上是不安全的。停止线程会导致它解锁所有已锁定的监视器。(当ThreadDeath异常在堆栈中传播时,监视器被解锁。)如果之前由这些监视器保护的对象中的任何一个处于不一致状态,则其他线程现在可以以不一致的状态查看这些对象。据称这些物体被 损坏。当线程操作受损对象时,可能导致任意行为。这种行为可能微妙且难以检测,或者可能会发音。与其他未经检查的异常不同,可以 ThreadDeath静默地杀死线程;因此,用户没有警告他的程序可能被损坏。腐败现象可能会在实际损害发生后随时出现,甚至可能在未来数小时甚至数天。
  
2.4.2 通过共享变量来终止线程
import java.util.concurrent.TimeUnit;

public class StopDemo {
    static volatile boolean bStop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        TimeUnit.SECONDS.sleep(2000);
        bStop = true;
    }

    static class StopThread implements Runnable {
        public void run() {
            while(!bStop) {
                System.out.println("持续运行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
   
2.4.3 interrupt 方法
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
import java.util.concurrent.TimeUnit;

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()-> {
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("持续执行......");
            }
        });
        thread.start();
        Thread.sleep(1);
        thread.interrupt();
        System.out.println("线程中断了...");
    }
}
  
什么情况下会抛出InterruptedException异常?
当一个线程处于阻塞状态下(例如休眠)的情况下,调用了该线程的interrupt()方法,则会出现InterruptedException。
  
如何处理InterruptedException?
1. 不要生吞此异常;
2. 如果可以处理此异常:完成清理工作之后退出;
3. 不处理此异常,不继续执行任务:重新抛出;
4. 不处理此异常,继续执行任务:捕捉到异常之后恢复中断标记(交由后续程序检查中断)。
     
2.4.4 Interrupted方法
public class MyThreadDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("i = " + (i + 1));
        }
    }

    public static void main(String[] args) {
        MyThreadDemo thread = new MyThreadDemo();
        thread.start();
        thread.interrupt();
        System.out.println("第一次调用thread.isInterrupted() : " + thread.isInterrupted());
        System.out.println("第二次调用thread.isInterrupted() : " + thread.isInterrupted());
        System.out.println("thread是否存活 : " + thread.isAlive());
    }
}
  
运行结果:
第一次调用thread.isInterrupted() : true
第二次调用thread.isInterrupted() : true
thread是否存活 : true
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
  
从结果可以看出调用interrupt()方法后,线程仍在继续运行,并未停止,但已经给线程设置了中断标志,两个isInterrupted()方法都会输出true,也说明isInterrupted()方法并不会清除中断状态。
  
下面我们把代码修改一下,多加两行调用interrupted()方法:
public class MyThreadDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("i = " + (i + 1));
        }
    }

    public static void main(String[] args) {
        MyThreadDemo thread = new MyThreadDemo();
        thread.start();
        thread.interrupt();
        System.out.println("第一次调用thread.isInterrupted() : " + thread.isInterrupted());
        System.out.println("第二次调用thread.isInterrupted() : " + thread.isInterrupted());
        System.out.println("第一次调用threadinterrupted() : " + thread.interrupted());
        System.out.println("第二次调用thread.interrupted() : " + thread.interrupted());
        System.out.println("thread是否存活 : " + thread.isAlive());
    }
}
   
运行结果:
第一次调用thread.isInterrupted() : true
i = 1
第二次调用thread.isInterrupted() : true
i = 2
第一次调用threadinterrupted() : false
第二次调用thread.interrupted() : false
i = 3
thread是否存活 : true
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
    
从输出结果看,可能会有疑惑,为什么后面两个interrupted方法输出的都是false,而不是预料中的一个true一个false?
注意!!!这是一个坑!!!上面说到,interrupted()方法测试的是当前线程是否被中断。
这里当前线程是main线程,而thread.interrupt()中断的是thread线程。
所以当前线程main从未被中断过,尽管interrupted()方法是以thread.interrupted()的形式被调用,但它检测的仍然是main线程而不是检测thread线程,所以thread.interrupted()在这里相当于main.interrupted()。对于这点,下面我们再修改进行测试。
Thread.currentThread()函数可以获取当前线,下面代码中获取的是main线程。
public static void main(String[] args) {
    Thread.currentThread().interrupt();
    System.out.println("第一次调用Thread.currentThread().interrupt():"
                       +Thread.currentThread().isInterrupted());
    System.out.println("第一次调用thread.interrupted():"
                       +Thread.currentThread().interrupted());
    System.out.println("第二次调用thread.interrupted():"
                       +Thread.currentThread().interrupted());
}
    
运行结果:
第一次调用Thread.currentThread().interrupt():true
第一次调用thread.interrupted():true
第二次调用thread.interrupted():false
  
结果证明猜想是正确的。
若果想要是实现调用interrupt()方法真正的终止线程,则可以在线程的run方法中做处理即可,比如直接跳出run()方法使线程结束,视具体情况而定,下面是一个例子。
public class MyThreadDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("i = " + (i + 1));
            if(this.isInterrupted()) {
                System.out.println("通过this.isInterrupted()检测到中断");
                System.out.println("第一个interrupted()" + this.interrupted());;
                System.out.println("第二个interrupted()" + this.interrupted());
                break;
            }
        }
        System.out.println("因为检测到中断,所以跳出循环,线程到这里结束,因为后面没有内容了。");
    }

    public static void main(String[] args) throws InterruptedException {
        MyThreadDemo myThread = new MyThreadDemo();
        myThread.start();
        myThread.interrupt();
        // sleep等待一秒,等myThread运行完
        Thread.currentThread().sleep(1000);
        System.out.println("myThread线程是否存活:" + myThread.isAlive());
    }
}
     
运行结果:
i = 1
通过this.isInterrupted()检测到中断
第一个interrupted()true
第二个interrupted()false
因为检测到中断,所以跳出循环,线程到这里结束,因为后面没有内容了。
myThread线程是否存活:false
       
2.4.5 总结
- interrupt() 是给线程设置中断标志。
- interrupted() 是检测中断并清除中断状态。
- isInterrupted() 只检测中断。
- 还有一点就是interrupted()作用于当前线程,interrupt() isInterrupted() 作用于此线程,即代码中调用此方法的实例所代表的线程
   

三、线程的安全性分析

3.1 可见性、原子性、有序性
3.1.1 线程安全

多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

  
3.1.2 原子性
原子性是指一个事物的操作是不可分割的,要么都发生,要么都不发生。
   
举个例子:
张三到银行给李四转账1000元,张三卡里原来有2000元,李四卡里原来也有两千元,那么转账的步骤应该如下:

     
如果张三的钱扣完,银行系统瘫痪了,怎么办呢?张三的1000块钱会被会没呢,当然不会。这时候你的钱会退回来。也就是说银行的转账业务要么成功张三(1000元)李四(3000元),要么不发生张三(2000元)李四(2000元)。
同样地反映到并发编程中会出现什么结果呢?
举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
  
假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。
  
3.1.3 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;
  
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
  
3.1.4 有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
举个简单的例子,看下面这段代码:
int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2
    
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10;    //语句1
int r = 2;     //语句2
a = a + 3;     //语句3
r = a * a;     //语句4
   
这段代码有4个语句,那么可能的一个执行顺序是:

   
​那么可不可能是这个执行顺序呢: 语句2  语句1  语句4  语句3

   
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
   
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
   
3.2 Java内存模型
在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在Java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在Java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
   
举个简单的例子:在Java中,执行下面这个语句:
i = 10;
  
执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。
那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
  
3.2.1 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
    
上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:
请分析以下哪些操作是原子性操作:
x = 10;        //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4
  
咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
      
3.2.2 可见性
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
   
3.3.3 有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
   
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个`unLock`操作先行发生于后面对同一个锁额lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
   
这8条原则摘自《深入理解Java虚拟机》。
这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
  
下面我们来解释一下前4条规则:
对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
  
3.3 synchronized
3.2.1 synchronized简单介绍
synchronized中文意思是同步,也称之为同步锁
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
  
​synchronized的作用主要有三个:
- 原子性:确保线程互斥地访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
  
3.3.2 synchronized的使用
synchronized的3种使用方式:
- 修饰实例方法:作用于当前实例加锁。
- 修饰静态方法:作用于当前类对象加锁。
- 修饰代码块:指定加锁对象,对给定对象加锁。

   
3.3.3 修饰方法
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
  
方法一:
public synchronized void method() {
   // todo
}
  
方法二:
public void method() {
   synchronized(this) {
      // todo
   }
}
     
写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的,都是锁定了整个方法时的内容。
synchronized关键字不能继承。 虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
   
在子类方法中加上synchronized关键字
class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}
      
在子类方法中调用父类的同步方法
class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public void method() { super.method(); }
} 
   
注意:
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。 
  
3.3.4 修饰一个代码块
1. 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
注意下面两个程序的区别:
class SyncThread implements Runnable {
       private static int count;
 
       public SyncThread() {
          count = 0;
       }
 
       public  void run() {
          synchronized(this) {
             for (int i = 0; i < 5; i++) {
                try {
                   System.out.println(Thread.currentThread().getName() + ":" + (count++));
                   Thread.sleep(100);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
             }
          }
       }
 
       public int getCount() {
          return count;
       }
}
public class Demo00 {
    public static void main(String args[]){
    //调用方式一:test01
    //SyncThread s1 = new SyncThread();
    //SyncThread s2 = new SyncThread();
    //Thread t1 = new Thread(s1);
    //Thread t2 = new Thread(s2);
    //调用方式二:test02        
        SyncThread s = new SyncThread();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        
        t1.start();
        t2.start();
    }
}
   
调用方式二种,当两个并发线程(thread1和thread2)访问同一个对象(`syncThread`)中的 synchronized 代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。thread1 和 thread2 是互斥的,因为在执行 synchronized 代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象
调用方式一中,thread1 thread2 同时在执行。这是因为 synchronized 只锁定对象,每个对象只有一个锁(lock)与之相关联。
   
2.
class Counter implements Runnable{
   private int count;
 
   public Counter() {
      count = 0;
   }
 
   public void countAdd() {
      synchronized(this) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
   public void printCount() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + " count:" + count);
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public void run() {
      String threadName = Thread.currentThread().getName();
      if (threadName.equals("A")) {
         countAdd();
      } else if (threadName.equals("B")) {
         printCount();
      }
   }
}
 
public class Demo00{
    public static void main(String args[]){
        Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();
    }
}
    
可以看见B线程的调用是非 synchronized ,并不影响A线程对 synchronized 部分的调用。从上面的结果中可以看出一个线程访问一个对象的 synchronized 代码块时,别的线程可以访问该对象的非 synchronized 代码块而不受阻塞。
  
3. 指定要给某个对象加锁
/**
 * 银行账户类
 */
class Account {
   String name;
   float amount;
 
   public Account(String name, float amount) {
      this.name = name;
      this.amount = amount;
   }
   //存钱
   public  void deposit(float amt) {
      amount += amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   //取钱
   public  void withdraw(float amt) {
      amount -= amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
 
   public float getBalance() {
      return amount;
   }
}
 
/**
 * 账户操作类
 */
class AccountOperator implements Runnable{
   private Account account;
   public AccountOperator(Account account) {
      this.account = account;
   }
 
   public void run() {
      synchronized (account) {
         account.deposit(500);
         account.withdraw(500);
         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
      }
   }
}
 
public class Demo00{
    
    //public static final Object signal = new Object(); // 线程间通信变量
    //将account改为Demo00.signal也能实现线程同步
    public static void main(String args[]){
        Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);
 
        final int THREAD_NUM = 5;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i ++) {
           threads[i] = new Thread(accountOperator, "Thread" + i);
           threads[i].start();
        }
    }
}
   
在 AccountOperator 类中的 run 方法里,我们用 synchronized 给 account 对象加了锁。这时,当一个线程访问 account 对象时,其他试图访问 account 对象的线程将会阻塞,直到该线程访问 account 对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。 
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序:
public void method3(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj)
   {
      // todo
   }
}
  
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance变量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代码块
      }
   }
 
   public void run() {
 
   }
}
   
3.3.5 修饰一个静态方法
synchronized 也可修饰一个静态方法,用法如下:
public synchronized static void method() {
   // todo
}
  
静态方法是属于类的而不属于对象的。同样的,`synchronized` 修饰的静态方法锁定的是这个类的所有对象。
/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Demo00{
    
    public static void main(String args[]){
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}
     
syncThread1 和 syncThread2 是 SyncThread 的两个对象,但在 thread1 和 thread2 并发执行时却保持了线程同步。这是因为 run 中调用了静态方法 method ,而静态方法是属于类的,所以 syncThread1 和 syncThread2 相当于用了同一把锁。
   
3.3.6 修饰一个类
Synchronized还可作用于一个类,用法如下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public static void method() {
      synchronized(SyncThread.class) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
  
本例的的给 class 加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁。
  
使用总结
- 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。 
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
  
3.3.7 synchronized的底层实现
谈 synchronized 的底层实现,就不得不谈数据在 JVM 内存的存储:Java 对象头,以及 Monitor 对象监视器。
  
对象头
在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

      
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
- 对象头:Java 对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是 64bit),但是如果对象是数组类型,则需要3个机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  
synchronized 用的锁就是存在 Java 对象头里的,那么什么是Java对象头呢?Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下:
| 长度         | 内容                       | 说明                                                    |
| ---------   | ----------------------      | ----------------------------------           |
| 32/64 bit   | Mark Word                   | 存储对象的hashCode或锁信息等。     |
| 32/64 bit   | Class Metadata Address      | 存储到对象类型数据的指针。          |
| 32/64 bit   | Array length                | 数组的长度(如果当前对象是数组)。  |
  
Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。下图是Java对象头 无锁状态下Mark Word部分的存储结构(32位虚拟机):
|         | 25 bit          | 4 bit          | 1 bit是否是偏向锁   | 2 bit锁标志位     |
| ------- | -------------   | ------------   | ----------------   | -------------    |
| 无锁状态  | 对象的hashCode | 对象分代年龄     | 0                 | 01                |
  
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,可能变化为存储以下4种数据:

     
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

   
对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。从这里我们可以看到,“锁”这个东西,可能是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也可能是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。

     
对象头中Mark Word与线程中Lock Record
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。
Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:
| Lock Record     | 描述                                                                                                                                                                                                        |
| -----------     | ------------------------------------------------------------                                                                                                                          |
| Owner           | 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL; |
| EntryQ          | 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;                                                                            |
| RcThis          | 表示blocked或waiting在该monitor record上的所有线程的个数;                                                                                                            |
| Nest            | 用来实现 重入锁的计数;                                                                                                                                                                          |
| HashCode        | 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。                                                                                                                |
| Candidate       | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。 |
  
监视器(Monitor)
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。
synchronized 在 JVM 里的实现都是 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
MonitorEnter 指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁;
MonitorExit 指令:插入在方法结束处和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit;
  
那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}
  
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
  
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
   
什么时候需要协作? 比如:
一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。

  
如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器;还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。
  
注意:当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。
  
3.3.8 synchronized 锁的升级顺序
锁解决了数据的安全性,但是同样带来了性能的下降。`hotspot` 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率。
synchronized 在 JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁、自旋锁、重量级锁,锁的状态根据竞争激烈的程度从低到高不断升级。
   
锁主要存在四种状态,依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。而且这个过程就是开销逐渐加大的过程。
| 锁       | 优点                                                                     | 缺点             | 适用场景                                   |
| -------- | ------------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ ------------- |
| 偏向锁   | 加锁和解锁不需要额外<br>的消耗,和执行非同步<br>方法比仅存在纳秒级的<br/>差距。| 如果线程间存在锁竞争,会<br/>带来额外的锁撤销的消耗。 | 适用于只有一个线程访<br/>问同步块场景。      |
| 轻量级锁 | 竞争的线程不会阻塞,<br/>提高了程序的响应速度。                              | 如果始终得不到锁竞争的线<br/>程使用自旋会消耗CPU。    | 追求响应时间。<br/>同步块执行速度非常快。    |
| 重量级锁 | 线程竞争不使用自旋,<br/>不会消耗CPU。                                      | 线程阻塞,响应时间缓慢。                             | 追求吞吐量。<br/>同步块执行速度较长。       |
   
3.4 volatile关键字
volatile 的主要作用有两点: 
- 保证变量的内存可见性 。
- 禁止指令重排序。
  
​可见性问题
先来看看这个比较常见的多线程访问共享变量的例子。
/**
 * 变量的内存可见性例子
 *
 * @author star
 */
public class VolatileExample {

    /**
     * main 方法作为一个主线程
     */
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 开启线程
        myThread.start();

        // 主线程执行
        for (; ; ) {
            if (myThread.isFlag()) {
                System.out.println("主线程访问到 flag 变量");
            }
        }
    }

}

/**
 * 子线程类
 */
class MyThread extends Thread {

    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 修改变量值
        flag = true;
        System.out.println("flag = " + flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
    
执行上面的程序,你会发现,控制台永远都不会输出 “主线程访问到 flag 变量”这句话。我们可以看到,子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子。
  
那么,我们思考一下为什么会出现这种情况呢?这里我们就要了解一下 Java 内存模型(简称 JMM)。
   
3.4.1 Java 内存模型
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
   
JMM的规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
  
JMM的抽象示意图:

   
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。
  
正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题。
  
那我们要如何解决可见性问题呢?接下来我们就聊聊内存可见性以及可见性问题的解决方案。
   
内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
   
可见性问题的解决方案
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
  
这里有两种方案:加锁和使用 volatile 关键字。
   
下面我们使用这两个方案对上面的例子进行改造。
  
加锁
使用 synchronizer 进行加锁。
/**
  * main 方法作为一个主线程
  */
public static void main(String[] args) {
    MyThread myThread = new MyThread();
    // 开启线程
    myThread.start();

    // 主线程执行
    for (; ; ) {
        synchronized (myThread) {
            if (myThread.isFlag()) {
                System.out.println("主线程访问到 flag 变量");
            }
        }
    }
}
  
这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了?因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。
这里除了 synchronizer 外,其它锁也能保证变量的内存可见性。
   
使用 volatile 关键字
使用 volatile 关键字修饰共享变量。
/**
 * 子线程类
 */
class MyThread extends Thread {

    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 修改变量值
        flag = true;
        System.out.println("flag = " + flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
   
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
   
接下来我们就聊聊一个比较底层的知识点:总线嗅探机制。
总线嗅探机制
在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。
由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。

   
> 注意,缓存的一致性问题,不是多处理器导致,而是多缓存导致的。
   
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
  
> 注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。
  
可见性问题小结
上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
  
volatile 的原子性问题
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
  
3.4.2 禁止指令重排序
什么是重排序?
为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。
  
一般重排序可以分为如下三种类型:
- 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
   
> 数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
  
从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:

   
为了更好地理解重排序,请看下面的部分示例代码:
int a = 0;
// 线程 A
a = 1;           // 1
flag = true;     // 2

// 线程 B
if (flag) { // 3
  int i = a; // 4
}
  
单看上面的程序好像没有问题,最后 i 的值是 1。但是为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序。假设线程 A 在执行时被重排序成先执行代码 2,再执行代码 1;而线程 B 在线程 A 执行完代码 2 后,读取了 flag 变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。那么如何程序执行结果正确呢?这里仍然可以使用 volatile 关键字。
   
​这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。
  
那么,让我们继续往下探索, volatile 是如何禁止指令重排序的呢?这里我们将引出一个概念:内存屏障指令。
   
3.4.3 内存屏障指令
为了实现 volatile 内存语义(即内存可见性),JMM 会限制特定类型的编译器和处理器重排序。为此,JMM 针对编译器制定了 volatile 重排序规则表,如下所示:
| 是否重排序      |                 | 第二次操作    |             |
| -------------  | ----------      | ------------ | ----------- |
| 第一次操作      | 普通读/写        | volatile读   | volatile写  |
| 普通读/写       | YES             | YES          | NO          |
| volatile读     | NO              | NO           | NO          |
| volatile写     | YES             | NO           | NO          |
  
使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。
内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
  
JMM把内存屏障指令分为下列四类:
| 屏障类型                | 指令示例                    | 说明                                                                                                                                          |
| -------------- ------- | --------------------------- | ----------------------------------------------------------------------------------------------------  |
| LoadLoad Barriers      | Load1;LoadLoad;Load2      | 确保Load1数据的装载先于Load2及所有后续装载指令的装载                                                     |
| StoreStore Barriers    | Store1;StoreStore;Stored  | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2<br>及所有后续存储指令的存储  |
| LoadStore Barriers     | Load1;LoadStore;Store2    | 确保Load1数据装载咸鱼Store2及所有后续的存储指令刷新到内存                                                  |
| StoreLoad Barriers     | Store1;Store Load;Load2   | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2<br>及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的<br>所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后<br>的内存访问指令 |
  
3.4.4 总结
本质上来说:
volatile实际上是通过内存屏障来防止指令重排序以及禁止CPU高速缓存来解决可见性问题。 
而#Lock指令,它本意上是禁止高速缓存解决可见性问题,但实际上在 这里,它表示的是一种内存屏障的功能。也就是说针对当前的硬件环境,`JMM`层面采用Lock指令作为内存屏障来解决可见性问题。
  
3.5 final域
3.5.1 概述

final关键字代表最终、不可改变的。
  
常见四种用法:
1. 可以用来修饰一个类。
2. 可以用来修饰一个方法。
3. 还可以用来修饰一个局部变量。
4. 还可以用来修饰一个成员变量。
  
final域和线程安全有什么关系?
对于final域,编译器和处理器要遵守两个重排序规则。
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一 个引用变量,这两个操作之间不能重排序。 
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操 作之间不能重排序。
   
3.5.2 final修饰类:太监类,没有儿子
当final关键字用来修饰一个类的时候,格式:
public final class 类名称 { // ... }
  
含义:当前这个类不能有任何的子类。(太监类)
注意:一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写(因为没儿子。)
  
3.5.3 final修饰方法
1. 含义:
当final关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。
  
2. 格式:
修饰符 final 返回值类型 方法名称(参数列表) {
    // 方法体
}
    
3. 注意事项:对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾。有抽象方法的abstract类被继承时,其中的方法必须被子类Override,而final不能被Override。
public abstract class Fu {
 
    public final void method() {
        System.out.println("父类方法执行!");
    }
 
    public abstract /*final*/ void methodAbs() ; //有final会报红
 
}

public class Zi extends Fu {
    @Override
    public void methodAbs() {
    }
 
    // 错误写法!不能覆盖重写父类当中final的方法
//    @Override
//    public void method() {
//        System.out.println("子类覆盖重写父类的方法!");
//    }
}
  
3.5.4 final修饰局部变量

   
// 正确写法!只要保证有唯一一次赋值即可
        final int num3;
        num3 = 30;
   
对于基本类型来说,不可变说的是变量当中的数据不可改变。
对于引用类型来说,不可变说的是变量当中的地址值不可改变。
     
一个标准类:
public class Student {
 
    private String name;
 
    public Student() {
    }
 
    public Student(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
   
Main函数里:
1. 若无final修饰
public class TestDemo {

    public static void main(String[] args) {
        final Student stu1 = new Student("张三");
        System.out.println(stu1);  // 打印地址
        System.out.println(stu1.getName()); // 张三
        stu1 = new Student("李四");
        System.out.println(stu1);
        System.out.println(stu1.getName()); // 李四
    }
}
   
结果输出:<span style="color:red">**地址不同**</span> 
  
com.gupaoedu.Student@7006c658
张三
com.gupaoedu.Student@34033bd0
李四
  
2. 若用final修饰,地址值不可改变,否则报红

  
<span style="color:red">但是,地址值虽然不能变,内容可以变(地址所指向的对象可以变)。</span>
  
public class TestDemo {

    public static void main(String[] args) {
        final Student stu1 = new Student("张三");
        System.out.println(stu1);
        System.out.println(stu1.getName());
        stu1.setName("李四");
        System.out.println(stu1);
        System.out.println(stu1.getName()); // 李四
    }
}
   
输出结果:
com.gupaoedu.Student@7006c658
张三
com.gupaoedu.Student@7006c658
李四
   
3.5.5 final修饰成员变量
对于成员变量来说,如果使用final关键字修饰,那么这个变量也照样是不可变。
1. 由于成员变量具有默认值,所以用了final之后必须手动赋值,不会再给默认值。

   
2. 对于final的成员变量,要么使用直接赋值,要么通过构造方法赋值。二者选其一。

  
3. 必须保证类当中所有重载的构造方法,都最终会对final的成员变量进行赋值。
如果选择在构造方法中赋值,则要把setname()函数取消掉。

      
3.6 Happens-Before原则
3.6.1 happens-before 定义

happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:
1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面的 1 是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
2 是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
  
具体规则 :
具体的一共有六项规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
     
3.7 原子类Atomic
3.7.1 什么是原子类?用处是什么?如何使用?

在java.util.concurrent.atomic包下,有一系列“Atomic”开头的类,统称为原子类。
     
考虑以下情景:
private static final ExecutorService exec = Executors.newCachedThreadPool();
private static int i = 0;

public static void main(String[] args) throws InterruptedException {
    Runnable r = () -> {
        for (int k = 0; k < 1000; k++) {
            i++;
            sleep(1); // 交出CPU控制权,增大竞争
        }
    };
    for (int j = 0; j < 10; j++) {
        Future<?> f = exec.submit(r);
    }
    exec.shutdown();
    exec.awaitTermination(60, TimeUnit.SECONDS); // 等待所有任务执行完毕
    System.out.println(i);
}
  
在多个线程竞争同一个资源的情形下,可能会造成意想不到的错误,如上述代码i本应输出10000,实际情况下是小于10000的。
这是由于i++这一步在字节码中分为了四次操作:getstatic将静态变量i的值入栈,iconst_1将1入栈,iadd将栈顶两个数出栈、并将它们的和入栈,putstatic将栈顶的值赋给变量i。
  
这就导致了i++不能保证单步代码的原子性,所以线程不安全。如果想要使其变得线程安全,可以使用synchronized同步代码块进行加锁:
private static final ExecutorService exec = Executors.newCachedThreadPool();
private static int i = 0;
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
    Runnable r = () -> {
        for (int k = 0; k < 1000; k++) {
            synchronized (lock) { // 使用同步代码块
                i++;
            }
            sleep(1);
        }
    };
    for (int j = 0; j < 10; j++) {
        Future<?> f = exec.submit(r);
    }
    exec.shutdown();
    exec.awaitTermination(60, TimeUnit.SECONDS);
    System.out.println(i);
}
   
另外一种方式就是使用Atomic类:
private static final ExecutorService exec = Executors.newCachedThreadPool();
//private static int i = 0;
private static AtomicInteger i = new AtomicInteger(0); // 使用AtomicInteger类保证原子性

public static void main(String[] args) throws InterruptedException {
    Runnable r = () -> {
        for (int k = 0; k < 1000; k++) {
            i.getAndIncrement(); // 使用AtomicInteger类保证原子性
            sleep(1); 
        }
    };
    for (int j = 0; j < 10; j++) {
        Future<?> f = exec.submit(r);
    }
    exec.shutdown();
    exec.awaitTermination(60, TimeUnit.SECONDS); 
    System.out.println(i);
}
  
使用AtomicInteger替代int,同时使用i.getAndIncrement()替代i++,也可以保证这一步的原子性。
   
3.7.2 为什么Atomic类可以保证原子性?
以AtomicInteger为例。在AtomicInteger中有一个volatile修饰的value变量,也就是这个整型的值。在调用getAndIncrement()时,AtomicInteger会通过Unsafe类的getAndAddInt方法对变量value进行一次CAS操作。由于CAS是具有原子性的,所以AtomicInteger就保证了操作的线程安全。
  
3.8 ThreadLocal的实现原理
3.8.1 什么是 ThreadLocal?

ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。
可以通过 ThreadLocal<T> value = new ThreadLocal<T>(); 来使用。
会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。
   
3.8.2 ThreadLocal 内存泄漏
ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
  
那么如何避免内存泄漏呢?
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。
  
3.8.3 ThreadLocal 应用场景
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
- 线程间数据隔离,各线程的 ThreadLocal 互不影响。
- 方便同一个线程使用某一对象,避免不必要的参数传递。
- 全链路追踪中的 `traceId` 或者流程引擎中上下文的传递一般采用 ThreadLocal。
- Spring 事务管理器采用了 ThreadLocal。
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal。
  

四、如何安全发布对象  

4.1 发布与逃逸
发布的意思是是一个对象能够被当前范围之外的代码所使用。

  
4.1.1 对象溢出
一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。
  
4.1.2 安全发布对象的四种方法
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者 AtomicReference 对象中(利用volatile happen-before规则)。
- 将对象的引用保存到某个正确构造对象的final类型域中(初始化安全性)。
- 将对象的引用保存到一个由锁保护的域中(读写都上锁)。
  
第一种:静态初始化构造单利
public class StaticDemo {
    private StaticDemo(){}
    private static StaticDemo instance = new StaticDemo();
    private static StaticDemo getInstance(){
        return instance;//当静态构造方法初始化加载的时候
        //一定是安全发布的,但是不保证后续操作这个instance的安全
    }
}
  
第二种:final域
public class FinalDemo {
   private final Map<String,String> states;
   public FinalDemo(){
       states = new HashMap<>();
       states.put("yojofly","yojofly");
   }
}
  
第三种:volatile
- 第一种单利模式,此时当多线程访问时候,可能存在instance == null都判断成功,返回多个实例的情况。
public class VolatileDemo { 
    public VolatileDemo(){}
    private static VolatileDemo instance;
    public static VolatileDemo getInstance(){
        if (instance == null){
                    instance = new VolatileDemo();
                    return instance;
                }
        return instance;
    }
}
  
- 第二种单利模式,为了方式上述问题,我们可以给`getInstance`方法加锁,或者代码块加锁。
public class VolatileDemo {
    public VolatileDemo(){}
    private static VolatileDemo instance;
    public static VolatileDemo getInstance(){
        if (instance == null){
            synchronized (VolatileDemo.class){
               if (instance == null){
                    instance = new VolatileDemo();
                    return instance;
                }
            }
        }
        return instance;
    }
}
  
上述代码加if (instance == null)的原因是,在第一次获取到为null的基础上,再做一次加锁,做判断,消除掉多线程获取if (instance == null)产生的结果相同的问题,但是在Java层面。
instance = new VolatileDemo();
  
是一句话,但是在 jvm 层面,这句话会被编译成多个指令。
例如:指令 m,n,p,那么m,n,p指令的执行顺序一定是顺序执行的吗,此时依然存在指令重排序问题。 
  
- 第三种方法:volatile关键字
public class VolatileDemo {
    public VolatileDemo(){}
    private volatile static VolatileDemo instance;  //给发布对象加上volatile关键字,防止指令重排序
    public static VolatileDemo getInstance(){
        if (instance == null){
            synchronized (VolatileDemo.class){
                if (instance == null){
                    instance = new VolatileDemo();
                    return instance;
                }
            }
        }
        return instance;
    }
}
  

五、J.U.C核心指AQS

5.1 重入锁ReentrantLock
重入锁的特点:
- 实现重进入功能

重进入是指任意线程获取锁之后能够再次获取该锁而不会被锁阻塞。
  
锁的获取和释放过程如下:
1. 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次获取成功。
2. 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数器等于0时表示锁已经成功释放了。
   
- 分为公平锁和非公平锁
- 公平锁:
对先发起请求的线程即等待最久的线程优先满足,获取锁是顺序的,符合FIFO原则,不会产生线程饥饿;
获取锁调用tryAcquire方法,与非公平锁不一样的地方在于判断条件多了hasQueuedPredecessors()方法,这个方法判断队列中是否有其他节点,如果队列中还有其他节点,但是head后面还没关联节点 / 或者队列中head节点的后继节点关联的线程不是当前线程,如果是返回true,则表示有线程比当前线程更早地请求获取锁,因为要等待前驱节点获取并释放锁后才嫩继续获取到锁。
- 非公平锁(默认的):
获取是使用nonfairTryAcquire方法,只要CAS设置同步状态成功,则当前线程获取了锁。
非公平锁比公平锁效率更高,因为公平锁为了保证公平性会去切换线程导致上下文切换,存在额外的开销,所以非公平锁性能更好(所以作为默认的实现方式),保证了更大的吞吐量,但是可能会产生线程饥饿。
   
5.2 AQS是什么
什么是AQS?
ReentrantLock、ReentrantReadWriteLock 底层都是基于 AQS 来实现的
AQS的全称是 AbstractQueuedSynchronizer,是抽象队列同步器,其实他就是一个用来构建锁和同步器的框架,内部实现的关键是:先进先出的队列、state状态,在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建。
  
AQS的核心思想是什么?它是怎么实现的?
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用。
AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。`AQS`使用`CAS`对该同步状态进行原子操作实现对其值的修改。
AQS定义了两种资源获取方式:独占(只有一个线程能访问执行)和共享(多个线程可同时访问执行)
  
5.4 CountDownLatch
CountDownLatch允许一个或者多个线程去等待其他线程完成操作。
CountDownLatch接收一个int型参数,表示要等待的工作线程的个数。
当然也不一定是多线程,在单线程中可以用这个`int`型参数表示多个操作步骤。
  
5.4.1 Sync 内部类
CountDownLatch通过内部类Sync来实现同步语义。
Sync继承AQS,源码如下:
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        // 设置同步状态的值
        Sync(int count) {
            setState(count);
        }

        // 获取同步状态的值
        int getCount() {
            return getState();
        }

        // 尝试获取同步状态,只有同步状态的值为0的时候才成功
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        // 尝试释放同步状态,每次释放通过CAS将同步状态的值减1
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                // 如果同步状态的值已经是0了,不要再释放同步状态了,也不要减1了
                if (c == 0)
                    return false;
                // 减1
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
  
5.4.2 await() 源码
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
   
调用的是AQS的acquireSharedInterruptibly(int arg)方法:
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 如果被中断,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取同步状态
        if (tryAcquireShared(arg) < 0)
            // 获取同步状态失败,自旋
            doAcquireSharedInterruptibly(arg);
    }
  
首先,通过tryAcquireShared(arg)尝试获取同步状态,具体的实现被Sync重写了,查看源码:
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }
  
如果同步状态的值为0,获取成功。这就是CountDownLatch的机制,尝试获取latch的线程只有当latch的值减到0的时候,才能获取成功。
如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 将当前线程加入同步队列的尾部
        final Node node = addWaiter(Node.SHARED);
        try {
            // 自旋
            for (;;) {
                // 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是头结点,则尝试获取同步状态
                if (p == head) {
                    // 当前节点尝试获取同步状态
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 如果获取成功,则设置当前节点为头结点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                // 如果当前节点的前驱不是头结点,尝试挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
   
这里,调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 挂起当前线程。
  
5.4.3 countDown() 源码解析
countDown()源码如下:
    public void countDown() {
        sync.releaseShared(1);
    }
    
调用的是AQS的releaseShared(int arg)方法:
    public final boolean releaseShared(int arg) {
        // 尝试释放同步状态
        if (tryReleaseShared(arg)) {
            // 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点
            doReleaseShared();
            return true;
        }
        return false;
    }
  
首先,通过tryReleaseShared(arg)尝试释放同步状态,具体的实现被Sync重写了,源码:
    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            // 同步状态值减1
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
  
如果同步状态值减到0,则释放成功,进入自旋,尝试唤醒同步队列中头结点的后继节点,调用的是AQS的doReleaseShared()函数:
    private void doReleaseShared() {
        for (;;) {
            // 获取头结点
            Node h = head;
            if (h != null && h != tail) {
                // 获取头结点的状态
                int ws = h.waitStatus;
                // 如果是SIGNAL,尝试唤醒后继节点
                if (ws == Node.SIGNAL) {
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒头结点的后继节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
   
这里调用了unparkSuccessor(h)去唤醒头结点的后继节点。
  
5.4.4 如何唤醒所有调用 await() 等待的线程呢?
此时这个后继节点被唤醒,那么又是如何实现唤醒所有调用await()等待的线程呢?
回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)方法中:
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 将当前线程加入同步队列的尾部
        final Node node = addWaiter(Node.SHARED);
        try {
            // 自旋
            for (;;) {
                // 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是头结点,则尝试获取同步状态
                if (p == head) {
                    // 当前节点尝试获取同步状态
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 如果获取成功,则设置当前节点为头结点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                // 如果当前节点的前驱不是头结点,尝试挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
   
该方法里面,通过调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()将线程挂起。
当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。
此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。
  
查看源码:
    // 如果执行这个函数,那么propagate一定等于1
    private void setHeadAndPropagate(Node node, int propagate) {
        // 获取头结点
        Node h = head;
        // 因为当前节点被唤醒,设置当前节点为头结点
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 获取当前节点的下一个节点
            Node s = node.next;
            // 如果下一个节点为null或者节点为shared节点
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
   
这里,当前节点被唤醒,首先设置当前节点为头结点。
如果当前节点的下一个节点是shared节点,调用doReleaseShared(),源码:
    private void doReleaseShared() {
        // 自旋
        for (;;) {
            // 获取头结点,也就是当前节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果head没有改变,则调用break退出循环
            if (h == head)
                break;
        }
    }
  
首先,注意if (h == head) break; 这里每次循环的时候判断head头结点有没有改变,如果没有改变则退出循环。因为只有当新的节点被唤醒之后,新节点才会调用setHead(node)设置自己为头结点,头结点才会改变。
其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)。
  
如果队列中有两个或以上个节点,那么检查局部变量h的状态:
- 如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒。
- 如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
- 如果状态为PROPAGATE,直接判断head是否变化。
  
5.5 Semaphore
5.5.1 什么是Semaphore
Semaphore(信号量),是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果。
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。
  
5.5.2 基本使用
Semaphore的使用也是比较简单的,我们创建一个Runnable的子类,如下:
private static class MyRunnable implements Runnable {
    // 成员属性 Semaphore对象
    private final Semaphore semaphore;

    public MyRunnable(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    public void run() {
        String threadName = Thread.currentThread().getName();
        // 获取许可
        boolean acquire = semaphore.tryAcquire();
        // 未获取到许可 结束
        if (!acquire) {
            System.out.println("线程【" + threadName + "】未获取到许可,结束");
            return;
        }
        // 获取到许可
        try {
            System.out.println("线程【" + threadName + "】获取到许可");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放许可
            semaphore.release();
            System.out.println("线程【" + threadName + "】释放许可");
        }
    }
}
   
测试方法如下:
public static void main(String[] args) {
    Semaphore semaphore = new Semaphore(2);
    for (int i = 0; i <= 10; i ++) {
        MyRunnable runnable = new MyRunnable(semaphore);
        Thread thread = new Thread(runnable, "Thread-" + i);
        thread.start();
    }
}
   
5.6 CyclicBarrier
现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
首先,CyclicBarrier 的源码实现和 CountDownLatch 大同小异,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。
在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。
  

六、线程池

6.1 线程池的基本认识
什么是线程池?
提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行,任务处理完以后这个线程不会被销毁, 而是等待后续分配任务。
线程池的优点:
- 避免线程因为不限制创建数量导致的资源耗尽风险。
- 任务队列缓冲任务,支持忙线不均的作用。
- 节省了大量频繁创建/销毁线程的时间成本。

 
6.2 Java中提供的线程池
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool创建一个定长线程池,支持定时和周期性任务执行。
- newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
   
6.3 线程池的基本原理
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好的利用CPU资源。Java线程池的工作原理为:JVM先根据用户给定的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大[并发](https://so.csdn.net/so/search?q=并发&spm=1001.2101.3001.7020)数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
    
线程复用
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。在Thread类的run方法中其实调用了Runnable对象的run方法,吟慈可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。可以将再循环方法中不断获取的Runnable对象放在Queue中,当线程在获取下一个Runnable对象之前是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。
   
线程池的核心组件和核心类
Java线程池主要由以下4个核心组件组成
- 线程池管理器:用户创建并管理线程池。
- 工作线程:线程池中执行具体任务的线程。
- 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
- 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。
  
Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类。其中tahreadPoolExecutor是构建线程的核心方法,方法定义如下
  
ThreadPoolExecutor构造函数的具体参数如下:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        ...
    }
   
- corePoolSize:线程池中核心线程的数量。
- maximumPoolSize:线程池中最大线程的数量。
- keepAliveTime:当前线程数量超过corePoolSize时,空闲线程的存活时间。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,被提交但尚未被执行的任务存放的地方。
- threadFactory:线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂。
- handler:由于任务过多或者其他原因导致线程池无法处理时的任务拒绝策略。
  
线程池的工作流程
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
- 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
- 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
- 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
- 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectException异常。
- 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
- 在非核心线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该非核心线程将会被认定为空闲线程并停止。因此,在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
  

线程池的拒绝策略
若线程池中的核心线程数被用完且阻塞队列已满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。jdk内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。
1、AbortPolicy 直接抛出异常,组织线程正常运行。
2、CallerRunsPolicy  如果被丢弃的线程任务未关闭,则执行该线程任务。
3、DiscardOldestPolicy 移除线程队列中最早的一个线程任务,并尝试提交当前任务。
4、DiscardPolicy 丢弃当前的线程任务而不做任何处理。
  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方寸之间不太闲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值