Java并发编程
本文内容摘自:Java并发编程之美、深入理解Java虚拟机、Java并发编程的艺术
1.并发编程线程基础
-
什么是线程?
操作系统在运行一个程序时会为其创建一个进程,在一个进程里可以创建多个线程,而操作系统调度的最小单元是线程,也叫轻量级进程(是内核线程的接口,一条Java线程就映射到一条轻量级进程之中); -
操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以说线程是CPU分配的基本单元
-
在Java中我们启动main函数时其实就是启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程;
-
Java线程的调度:协同式线程调度(一个线程执行完了通知另一个线程来执行)和抢占式线程调度(每个线程由系统来分配执行时间,线程的切换不由线程本身决定,Java采用这种方式)
-
什么是线程安全?
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 -
线程安全的实现方法?
1.互斥同步:多线程并发访问共享数据时,保证数据在同一时刻只被一个线程使用,(也叫阻塞同步,悲观的)常见例Sychronized关键字
2.非阻塞同步:基于冲突检测的乐观并发策略,即先进行操作,如果没有其他线程争用共享数据则成功,否则就产生冲突,再采取其他补偿措施(常见的就是冲突后不断重试);不需要挂起线程,但是其不断尝试会占用CPU资源常见例CAS操作补充:CAS操作(由于硬件的支持,CAS操作是原子性的)CAS指令需要三个操作数,分别是内存位置V,旧的预期值A和新值B;CAS操作执行时,当且仅当V符合旧预期值A时,处理器用新值B更行V的值,否则它就不执行更新,但无论是否更新了V的值,都会返回V的旧值。
3.无同步方案:如果一个方法不涉及共享数据,他就不会产生线程安全问题;举两个例子:可重入代码,随时可以中断再运行不涉及公共资源;线程本地存储,把共享数据的可见范围限制在同一个线程之内
1.1线程创建与运行
- Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式;
- 方式一:(继承Thread)在调用start方法后并没有马上执行而是出于就绪状态,即该线程已经获取了CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态,一旦run方法执行完毕该线程就处于终止状态;
- 方式二:(实现Runnable接口)
- 方式三:(FutureTask方式)实现Callable接口的call方法,并且该方式创建的线程执行完毕后有返回值;
public class MyTest {
public static void main(String[] args) throws InterruptedException {
//方式一
new MyThread1("good ").start();//启动线程
new MyThread1("bad ").start();
//方式二
new Thread(new MyThread2("nice ")).start();
new Thread(new MyThread2("haha ")).start();
//方式三
FutureTask<String> task = new FutureTask<>(new MyThread3());
new Thread(task).start();
try {
System.out.println(task.get());//等待任务执行完毕并返回结果
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//继承Thread类重写run方法创建线程
static class MyThread1 extends Thread {
private String message;
public MyThread1(String message) {
this.message = message;
}
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(message);
}
}
}
//实现Runnable接口创建线程
static class MyThread2 implements Runnable {
private String message;
public MyThread2(String message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(message);
}
}
}
static class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
return "hello !";
}
}
}
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
1.2线程通知与等待
注意:wait和notify方法都是线程调用的共享变量的方法,而不是线程的方法
线程等待与线程阻塞的区别?
两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外,而进入 waiting 状态是在同步代码之内(然后马上退出同步)。
- wait()函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,同时会释放对象的锁,直到发生下面几件事才返回:
1.其它线程调用了该共享变量(对象)的notify()方法或notifyAll()方法;
2.其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
注:若调用wait()方法的线程未事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒),即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒;以下代码是经典的调用共享变量 wait()方法的实例:
//模拟生产者/消费者场景
private static volatile LinkedList<Integer> queue = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
Thread product = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == 10) {
//挂起当前线程,并释放通过同步块获取的queue上的锁
// 消费者线程可以获取该锁来获取队列里面的元素
queue.wait();
}
//空闲则生成元素,并通知消费者线程
queue.push(2);
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (queue) {
//消费队列空
while (queue.size() == 0) {
//挂起当前线程,并释放通过同步块获取的queue上的锁
// 生产者线程可以获取该锁来往队列中增加元素
queue.wait();
}
//消费元素并通知唤醒生产者线程
queue.pop();
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
- wait(long timeout)函数
如果一个线程调用了共享对象的该方法挂起后,没有在指定的timeout ms时间内被其它线程调用该共享变量的notify()方法唤醒,那么该函数还是会因为超时而返回;如果将timeout设置为0则和wait方法一样,因为在wait方法内部就是调用了wait(0),再如果传入负的timeout则会抛出IllegalArgumentException异常。
- notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程;一个共享对象上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
被唤醒的线程不能马上从wait()方法返回并继续执行,他必须在获取了共享对象的监视器锁之后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程不一定会获取到共享变量的监视器锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (resourceA) {
System.out.println("A get resourceA lock");
System.out.println("A wait...");
resourceA.wait();
System.out.println("A end wait");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (resourceA) {
System.out.println("B get resourceA lock");
System.out.println("B wait...");
resourceA.wait();
System.out.println("B end wait");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread C = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("C get resourceA lock");
resourceA.notify();
}
}
});
B.start();
A.start();
Thread.sleep(1000);//main线程休眠1s
C.start();
/*运行结果
B get resourceA lock
B wait...
A get resourceA lock
A wait...
C get resourceA lock
B end wait
并且JVM会继续运行,因为线程A还在阻塞...
*/
- notifyAll()函数
不同于在共享变量上调用 notify()函数会唤醒被阻塞到该共享变量上的一个线程, notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
1.3等待线程执行终止的join方法
前面介绍的等待通知方法是object类中的方法,而join()方法则是Thread类直接提供的,是无参数且返回值为void的方法。
- join()方法解决以下场景:就是需要等待某几件事完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("child threadone over");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("child threadTwo over");
}
});
threadOne.start();
threadTwo.start();
// threadOne.join();
// threadTwo.join();
//加入join方法,会输出child threadone/Two over之后再输出main thread over;否则会先输出main thread over
System.out.println("main thread over");
- 注:线程A调用线程B的join方法后会被阻塞,当其它线程调用了线程A的interrupt方法中断了线程A时,线程A会抛出InterruptedException异常而返回(即不再被阻塞,继续执行之后的语句)
1.4让线程睡眠的sleep方法
- Thread类中有一个静态的sleep()方法,当一个执行中的线程调用了Thread的sleep()方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源(比如:锁)还是持有不让出的!【对比wait()方法,当共享变量调用wait方法时,正在执行的线程会让出锁并阻塞】
指定睡眠时间到了之后该函数会正常返回,线程处于就绪状态然后参与CPU的调度,获取到CPU资源就可继续运行了;
如果在睡眠期间其他线程调用该线程的interrupt方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("child thread is in sleep");
Thread.sleep(10000);
System.out.println("child threadOne is awake!");
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
threadOne.start();
Thread.sleep(2000);
threadOne.interrupt();
Thread.sleep(2000);
System.out.println("main thread is over");
//此例可知在一个线程处于睡眠状态时,另一个线程中断了它,会在调用sleep方法处抛出异常并返回;然后另一个线程继续执行
注:另外需要注意的是,如果在调用 Thread.sleep(long millis)时为 millis 参数传递了一个 负数,则抛出IllegalArgumentException 异常
1.5让出CPU执行权的yield方法
- 正常情况下当一个线程把分配给自己的时间片使用完之后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
- 总结: sleep 与 yield 方法的区别在于,当线程调用sleep方法时调用线程会被挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被挂起,而是处于就绪状态,线程调度器下一次调度 时就有可能调度到当前线程执行!
- 以上的join()、sleep()、yield()方法都是Thread类的静态方法,而不是共享变量的方法,相当于是线程的主动操作(主动想让自己干一些事)
1.6线程中断
Java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void interrupt()方法:中断线程, 例如,当线程A运行时,线程 B可以调用钱程A的 interrupt()方法来设置线程A的中断标志为 true 并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 它会继续往下执行。 如果线程A因为调用了wait系列函数、 join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返;
- boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false;
- boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false;并且该方法如果发现当前线程被中断,则会清除中断标志,该方法为static方法通过Thread类直接调用(只作用于当前调用线程)
//根据中断标志判断线程是否终止的例子
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread()+" hello");
}
}
});
threadOne.start();
Thread.sleep(1000);
System.out.println("main thread interrupt thread");
threadOne.interrupt();
threadOne.join();
System.out.println("main thread is over");
interrupt()方法也属于Thread类的方法,主要是一个线程调用别的线程来提醒那个线程,(跟真正的中断后保存断电处理从而处理中断不太一样)
1.7理解上下文切换
CPU同一时刻只能被一个线程使用,为了给用户感觉多个线程在同时执行,CPU资源的分配采用了时间片轮转的策略,即给每个线程分配一个时间片,线程在时间片内占用CPU资源执行任务;但是,在切换之前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文的切换!
1.8线程死锁
- 什么是线程死锁?指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
- 死锁的产生具备一下四个条件:
1)互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用,其他线程只能阻塞等待;
2)请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会阻塞,但阻塞的同时并不释放自己已经获得的资源;
3)不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源;
4)环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…Tn中 的T0正在等待一个 T1占用的资源,T1正在等待T2占用的资源,…Tn正在等待己被 T0占用的资源。
//形成死锁
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread()+" get resourceA");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("waiting get resourceB");
//持有资源A了还要继续请求持有资源B
synchronized (resourceB){
System.out.println(Thread.currentThread()+" get resourceB");
}
}
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread()+" get resourceB");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("waiting get resourceA");
synchronized (resourceA){
System.out.println(Thread.currentThread()+" get resourceA");
}
}
}
});
threadOne.start();
threadTwo.start();
}
- 如何避免死锁?想要避免死锁,只需要破环掉至少一个构造死锁的条件即可,目前只有请求并持有和环路等待条件是可以被破坏的!
- 使用资源申请的有序性原则就可以避免死锁!!!意思是线程A和线程B都需要资源1,2,3,4… 时,对资源进行排序,线程A申请资源顺序是1,2,3,4时线程B申请顺序必须也是1,2,3,4而不能是3,2,1,4这种
1.9守护线程与用户线程
- 守护线程和用户线程的区别?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程;也就是只要有一个用户线程还没结束正常情况下JVM就不会退出!【mian线程是一个用户线程,在JVM内部同时还启动了很多守护线程,如垃圾回收线程】
- 如何设置守护线程?语句:thread1.setDaemon(true);//设置为守护线程
- main线程运行结束后,JVM会自动启动一个叫作DestroyJava VM的线程, 该线程会等待所有用户线程结束后终止JVM进程。
- 总结:如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程;如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程!
1.10ThreadLocal
同步的措施一般是加锁,那么有没有一种方式可以做到,在创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?
-
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本;即创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存!
-
本质:在每个线程内部都有一个名为threadLocals的成员变量, 该变量的类型为HashMap, 其中key为我们定义的ThreadLocal变量的this引用 , value 则为我 们使用set方法设置的值。 每个线程的本地变量存放在线程自己的内存变量threadLocals中, 如果当前线程一直不消亡, 那么这些本地变量会一直存在, 所以可能会造成内存溢出 , 因此使用完毕后要记得调用ThreadLocal的remove 方法删除对应线程的threadLocals中的本地变量。
//同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。
public static ThreadLocal<String> threadlocal = new ThreadLocal<>();//创建线程变量
public static void main(String[] args) throws InterruptedException {
threadlocal.set("hello !");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread: " + threadlocal.get());//子线程输出线程变量的值
}
});
thread.start();
System.out.println("main thread:"+threadlocal.get());//主线程(也是子线程的父线程)输出线程变量的值
}
/*运行结果
main thread:hello !
thread: null
*/
1.11线程状态
2.并发编程的其他基础知识
2.1多线程并发编程
- 并行和并发:并发是指同一时间段内多个任务同时都在执行,且都没有执行结束;并行是说在单位时间内多个任务同时在执行!
- 在一个时间段内,宏观来看有多个程序都在活动(每一瞬间只有一个在执行,只是在一段时间有多个程序都执行过)->并发;在每一瞬间,都有多个程序在同时执行,必须有多个CPU才可以 ->并行。
- 为什么要进行多线程并发编程?因为CPU数量变多了,每个线程可以使用自己的CPU运行,减少了线程上下文切换的开销,所以速度更快。
2.2Java中的线程安全问题
- 共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源
- 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题!!!
2.3Java中共享变量的内存可见性问题
Java内存模型规定,将所有的变量都放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存中的变量。
- 当一个线程操作共享变量时,它首先从主内存复制变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完成后将变量值更新到主内存。
- 对于共享变量X,线程B将X的值修改成了2,但是线程A获取的X的值还是1;这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。【主要是缓存的原因,线程会首先读取自己缓存中的值而不是主内存】
- 如何解决共享变量内存不可见问题?使用Java中的volatile关键字就可以解决;
2.4 synchronized关键字
- synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用这看不到的锁被称为内部锁,也叫监视器锁;内置锁是排它锁,也就是一个线程获取这个锁之后,其他线程必须等待该线程释放锁之后才能获取该锁!
- 由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
- synchronized的内存语义:进入synchronized语句块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取;
退出synchronized语句块的内存语义就是把在synchronized块内对共享变量的修改刷新到主内存中。
【其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存】
2.5 volatile关键字
上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。 对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用 volatile关键字;
- 该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。 volatile的内存语义和 synchronized 有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取volatile 变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。
- 注:volatile虽然提供了可见性保证,但不能保证操作的原子性。
2.6Java中的原子性操作
所谓原子性 操作,是指执行一系列操作时,这些操作要么全部执行完,要么不执行,不存在只执行其中一部分的情况;
2.7Java中CAS操作
在Java中 , 锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就 是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。 Java提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题, 这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读改写等的原子性问题。 CAS 即Compare and Swap,其是JDK提供的非阻塞原子性操作, 它通过硬件保证了比较更新操作的原子性
2.8Unsafe类
JDK的rt.jar包中的Unasfe类提供了硬件级别的原子性操作!!!
2.9Java指令重排序
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序;这里先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下 一定会输出 4 ? 答案是不一定,由于代码 (1 ) ( 2 ) ( 3 ) (4)之间不存在依赖关系,所以写线程的代码 (3) (4可能被重排序为先执行(4)再执行(3) ;那么执行(4)后, 读线程可能已经执行了(1)操作, 并且在(3)执行前开始执行(2)操作, 这时候输出结果为0而不是 4。
private static int num = 0;
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Read read = new Read();
read.start();
Write write = new Write();
write.start();
Thread.sleep(10);
read.interrupt();
System.out.println("main exit");
}
public static class Read extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (ready) //(1)
System.out.println(num+num);//(2)
System.out.println("read thread......");
}
}
}
public static class Write extends Thread {
@Override
public void run() {
num=2;//(3)
ready=true;//(4)
System.out.println("write thread set over......");
}
}
2.10伪共享
当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个 Cache 行大 小的内存复制到 Cache中 。 由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中 。 当多个线程同时修改一个缓存行里面的多个变量时, 由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享!!!
- 如何避免伪共享?在 JDK 8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中;
2.11锁的概述
-
乐观锁和悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。实例sychronized关键字
乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测!实例CAS 操作 -
公平锁和非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得。【在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销】
-
独占锁和共享锁
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁!!!
独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读 操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线 程必须等待当前线程释放锁才能进行读取;
共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。 -
可重入锁
当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线 程再次获取它自己己经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可 重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严格来说是有限次数)地进入被该锁锁住的代码; -
自旋锁
当前线程在获取锁时,如果发现锁已经被其他线程占有, 它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可 以使用 -XX:PreB lockS pinsh 参数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁。 如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。 由此看来自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了
以下主要讲述Java中锁的实现:
- LockSupport工具
首先,锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
当需要阻塞和唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作,而LockSupport也成为构建同步组件的基础工具;
LockSupport定义了一组以park开头的方法用来阻塞当前线程 ,以及unpark方法来唤醒一个被阻塞的线程!
- Condition接口
任意一个Java对象都拥有一组监视器方法,主要包括wait、notify等方法,这些方法与sychronized同步关键字配合,可以实现等待/通知模式;condition接口也提供了类似Object的监视器方法,与lock配合可以实现等待/通知模式!
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前 获取到Condition对象关联的锁;Condition对象是由Lock对象(调用了Lock对象的newCondition方法)创建出来的,即Condition对象是依赖Lock对象的。以下代码实现一个有解队列(类似于生产者消费者模式):
public class BoundedQueue<T> {
private Object[] items;
//添加的下标,删除的下标和数组当前数量
private int addindex, removeIndex, count;
private Lock lock = new ReentrantLock();//独占锁
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
//添加一个元素,如果数组满,则添加线程进入等待状态,直到有“空位”
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length)//如果数组满,则添加线程进入等待状态
notFull.await();
items[addindex] = t;
if (++addindex == items.length)
addindex = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
//由头部删除一个元素,如果数组空,则线程进入等待状态,直到有添加元素
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
3 Java并发容器和框架
3.1并发容器
- ConcurrentHashMap容器
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,从而使得Entry的next结点永不为空产生死循环获取Entry;故HashMap是线程不安全的!
HashTable容器使用sychronized来保证线程安全,无论读写都需要竞争同一个锁,效率低下!
假如容器里有多把锁,每一把锁用于锁容器中一部分数据,那么当多线程访问容器里不同数据时,线程间不会存在锁竞争,从而有效提升高并发访问效率这就是ConcurrentHashMap使用的锁分段技术。
-
ConcurrentLinkedQueue容器
是一个基于链接结点的无界线程安全队列,它采用先进先出的规则对结点进行排序,并且采用CAS来实现。 -
Java中的阻塞队列
阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
阻塞队列(blockingQueue)是一个支持两个附加操作的队列:
- 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满;
- 支持阻塞的移除方法:意思是当队列为空时,获取元素的线程会等待队列变为非空。
3.2 Fork/Join框架
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每一个小任务结果后得到大任务结果的框架。
工作窃取算法:一个比较大的任务分割为若干不相关的子任务,为减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建单独的线程来执行;线程负责A队列里的任务 线程B负责B队列里的任务,当A队列里的任务被线程A执行完而B队列里还有任务,于是线程A去B队列的队尾窃取一个任务来执行!(实现窃取算法一般是要用双端队列的)
框架使用:
我们要使用ForkJoin框架
第一步,创建一个ForkJoin任务,在提供在任务中Fork()和Join()操作的机制;通常情况下我们无需直接继承ForkJoinTask类,只需继承他的子类;
RecursiveAction:用于没有返回结果的任务
RecursiveTask:用于有返回结果的任务
第二步,ForkJoinTask需要通过ForkJoinPool来执行;
使用Fork/Join框架的代码示例:
import java.util.concurrent.*;
public class ForkJoin extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;//阈值
private int start;
private int end;
public ForkJoin(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end - start) <= THRESHOLD;//如果任务足够小就计算任务
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
int middle = (start + end) / 2;//如果任务大于阈值,就分裂成两个任务计算
ForkJoin leftTask = new ForkJoin(start, middle);
ForkJoin rightTask = new ForkJoin(middle + 1, end);
leftTask.fork();
rightTask.fork();//执行子任务,又会进入compute方法
Integer leftResult = leftTask.join();
Integer rightResult = rightTask.join();//等待子任务执行完成并得到其结果
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoin task = new ForkJoin(1, 4);//生成一个计算任务,负责计算1+2+3+4
Future<Integer> result = forkJoinPool.submit(task);//执行一个任务
try {
System.out.println(result.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
/*运行结果为:10*/
4. Java中13个原子操作类
java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更行一个变量的方式。
Atomic包一共提供了13个类,属于4中类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段);其基本都是使用Unsafe类实现的包装类。
- 原子更新基本类型
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
介绍AtomicInteger的常用方法,其他两种的常用方法类似:
int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果;
boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值;
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值;
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
//AtomicInteger的使用
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(1);
System.out.println(a.getAndIncrement());
System.out.println(a.get());
}
/*运行结果:
1
2*/
public final int getAndincrement(){//这是getAndincrement方法的源码
for(;;){
int current=get();
int next=current+1;
if(compareAndSet(current,next))
return current;
}
}
- 其他原子类详见《Java并发编程的艺术》P184
5. Java中的线程池
在开发过程中,合理使用线程池会带来3个好处:
第一,降低资源消耗;通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
第二,提高响应速度;当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三,提高线程的可管理性;线程池可以进行统一分配、调优和监控。
5.1线程池的使用
/**
* 用给定的初始参数创建一个新的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;
}
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略。
5.2线程池的原理分析
为了搞懂线程池的原理,我们需要首先分析一下 execute方法:
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
原理分析如图:
6. Executor框架
- 简介
Executor框架是Java5之后引进的,在Java5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理效率更高外,还有关键一点:有助于避免this逃逸问题;
Executor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,让 并发编程变得更加简单。
- Executor框架两级调度模型
在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架) 将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。总之,应用程序通过Executor框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制!
6.1 Executor框架结构
- 任务:包括被执行任务需要实现的接口:Runnable或Callable接口,或者这两个接口的实现类;
- 任务的执行:包括任务执行机制的核心接口Executor,及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor);
- 异步计算的结果:包括接口Future和实现Future接口的FutureTask类。
6.2 Executor框架的使用
- 第一步,创建实现Runnable或Callable接口的任务对象;
- 第二步,把第一步创建完成的任务对象交给ExecutorService执行;(因为ExecutorService是一个接口,一般是交给它的两个实现类来执行)
ExecutorService.execute(Runnable command)其中execute()方法用于提交不需要返回值的任务;
ExecutorService.submit(Runnable task)其中submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象。
- 第三步,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
- 第四步,可以通过调用线程池的shutdown和shutdownNow方法来关闭线程池。
通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
ThreadPoolExecutor使用示例
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start .time= " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + "end .time= " + new Date());
}
@Override
public String toString() {
return this.command;
}
}
/*************分割线*****************/
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkThread类实现了Runnable接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
运行结果:
pool-1-thread-3start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-4start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-1start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-5start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-2start .time= Wed Mar 04 16:02:09 CST 2020
pool-1-thread-2end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-3end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-4end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-5end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-5start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-1end .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-4start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-3start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-2start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-1start .time= Wed Mar 04 16:02:14 CST 2020
pool-1-thread-2end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-3end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-1end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-4end .time= Wed Mar 04 16:02:19 CST 2020
pool-1-thread-5end .time= Wed Mar 04 16:02:19 CST 2020
Finished all threads
分析:我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
7. J.U.C -AQS
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段,Exchanger工具类则提供了在线程间交换数据的一种手段。
7.1 CountDownLatch
用来控制一个或者多个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
/*运行结果
run..run..run..run..run..run..run..run..run..run..end
*/
7.2 CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
运行结果:before…before…before…before…before…before…before…before…before…before…after…after…after…after…after…after…after…after…after…after…
7.3 Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
/*运行结果:
2 1 2 2 2 2 2 1 2 2
*/