多线程进阶=》JUC并发编程

1、什么是JUC

​ JUC是java.util.concurrent的简写。

​ 用中文概括一下,JUC的意思就是java并发编程工具包

​ 并发编程的本质就是充分利用CPU资源

2、线程和进程

​ 进程:是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

​ 线程:是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

​ 例如 开了一个进程Typora记笔记,等待几分钟就会自动保存(自动保存这个操作就是由线程完成的。)

​ 一个进程可以包含多个线程,至少包含一个线程

​ Java默认有两个线程:main线程和GC线程(垃圾回收线程)

​ Java可以开启线程吗?

public synchronized void start() {/*** This method is not invoked for the main method thread or "system"* group threads created/set up by the VM. Any new functionality added* to this method in the future may have to also be added to the VM.** A zero status value corresponds to state "NEW".*/if (threadStatus != 0)throw new IllegalThreadStateException();/* Notify the group that this thread is about to be started* so that it can be added to the group's list of threads* and the group's unstarted count can be decremented. */group.add(this);boolean started = false;try {start0();started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {/* do nothing. If start0 threw a Throwable thenit will be passed up the call stack */}}}//这是一个C++底层,Java是没有权限操作底层硬件的private native void start0();

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

并发、并发

​ 并发:是指多个线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程 任务是在同时进行的,但其实并发只是一种逻辑上的同时进行;

​ 并行:是指多个线程任务在不同CPU上同时进行,是真正意义上的同时执行。

​ 并发编程的本质:充分利用CPU的资源。

线程的6个状态

​ 新建、运行、阻塞、等待、超时等待、终止

public enum State {/*** Thread state for a thread which has not yet started.*///新建NEW,/*** Thread state for a runnable thread.  A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*///运行RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*///阻塞BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>*   <li>{@link Object#wait() Object.wait} with no timeout</li>*   <li>{@link #join() Thread.join} with no timeout</li>*   <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called <tt>Object.wait()</tt>* on an object is waiting for another thread to call* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on* that object. A thread that has called <tt>Thread.join()</tt>* is waiting for a specified thread to terminate.*///等待WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>*   <li>{@link #sleep Thread.sleep}</li>*   <li>{@link Object#wait(long) Object.wait} with timeout</li>*   <li>{@link #join(long) Thread.join} with timeout</li>*   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>*   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*///超时等待TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*///终止TERMINATED;}

wait和sleep的区别

wait sleep
同步 只能在同步上下文中调用,否则抛出IllegalMonitorStateExcepton异常 不需要在同步方法块或同步块中调用
作用对象 wait方法定义在Object类对象中,作用于对象本身 sleep方法定义在java,lang.Thread中,作用于当前线程
释放锁资源
唤醒条件 其他线程调用notify()或者notifyAll()方法 超时或调用interrupt()方法体
方法属性 wait是实例方法 sleep是静态方法

一般企业中使用休眠的是

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

3、Lock锁(重点)

3.1、可重入锁(递归锁)

3.1.1、synchronized同步锁

/*** 真正的多线程开发* 线程就是一个单独的资源类,没有任何的附属操作!*/
public class SaleTicketDemo01 {public static void main(String[] args) {//多线程操作//并发:多线程操作同一个资源类,把资源类丢入线程Ticket ticket = new Ticket();//@FunctionalInterface 函数式接口 jdk1.8之后 lambda表达式new Thread(()->{for(int i=0;i<40;i++){ticket.sale();}},"A").start();new Thread(()->{for(int i=0;i<40;i++){ticket.sale();}},"B").start();new Thread(()->{for(int i=0;i<40;i++){ticket.sale();}},"C").start();}
}
//资源类
//属性+方法
//oop
class Ticket{private int number=50;//卖票的方式// synchronized 本质:队列,锁public synchronized void sale(){if(number>0){System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");number--;}}
}

3.1.2、Lock接口

​ 使用ReentrantLock可重入锁(实现了Lock接口)实现同步:

可重入锁也叫作递归锁,指的是同一个线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但 不受影响。

​ 就像在饭堂打饭,你在窗口排队。排到你的时候,突然你舍友A让你顺路带个饭,然后你就打了两份饭;这时 你还没离开窗口,舍友B又叫你打一份汤,于是你又额外打了一份汤。

​ lock()方法:上锁

​ unlock()方法:释放锁

public class Restaurant {private Lock windows = new ReentrantLock();public void getMeals() throws Exception {try {windows.lock();Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打饭");} finally {windows.unlock();}}public void getSoup() throws Exception {try {windows.lock();Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打汤");} finally {windows.unlock();}}public void today() throws Exception {try {windows.lock();Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打饭");getMeals();getSoup();} finally {windows.unlock();}}public static void main(String[] args) {Restaurant test = new Restaurant();new Thread(() -> {try {test.today();} catch (Exception e) {e.printStackTrace();}}, "我").start();new Thread(() -> {try {test.getSoup();} catch (Exception e) {e.printStackTrace();}}, "某人").start();new Thread(() -> {try {test.getMeals();} catch (Exception e) {e.printStackTrace();}}, "另一个人").start();}
}输出:
我打饭
我打饭
我打汤
某人打汤
另一个人打饭

3.2、不可重入锁(自旋锁)

在另一个菜式比较好吃且热门的窗口,可不是这样的,在这里你在窗口,只能点一个菜(进入一次临界区),点完后,你想要再点别的菜,只能重新排一次队(虽然可以插队,当然我们可以引入服务员队伍管理机制:private Lock windows = new ReentrantLock(true);,指定该锁是公平的。)
即:自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。

public class Restaurant {boolean isLock = false;public synchronized void getMeals() throws Exception {while (isLock) {wait();}isLock = true;try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打饭");} finally {isLock = false;}}public synchronized void getSoup() throws Exception {while (isLock) {wait();}isLock = true;try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打汤");} finally {isLock = false;}}public void today() throws Exception {while (isLock) {wait();}isLock = true;try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "打饭");getSoup();} finally {isLock = false;}}public static void main(String[] args) {Restaurant test = new Restaurant();new Thread(() -> {try {test.today();} catch (Exception e) {e.printStackTrace();}}, "我").start();new Thread(() -> {try {test.getSoup();} catch (Exception e) {e.printStackTrace();}}, "某人").start();new Thread(() -> {try {test.getMeals();} catch (Exception e) {e.printStackTrace();}}, "另一个人").start();}
}输出:
我打饭然后死锁了……

3.3、读写锁

ReentrantReadLock和ReentrantReadLock

​ 然而餐次的人流量一大,老板发现经常排起很长的队伍,厨师却都闲着没事干。老板拍脑子一想,这样不行 啊,所以稍微改进了一下点餐方式。所有人都可以扫二维码用网页进行点餐,只要这个菜不是正在做(写 锁),那么就可以随便点。

​ 即:假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的 时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有 一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。

public class Restaurant {private ReentrantReadWriteLock food = new ReentrantReadWriteLock();private ReentrantReadWriteLock.ReadLock getFoodLock = food.readLock();private ReentrantReadWriteLock.WriteLock cookingLock = food.writeLock();public void getFood() throws Exception {try {getFoodLock.lock();System.out.println(Thread.currentThread().getName() + "点饭");} finally {getFoodLock.unlock();}}public void cooking() throws Exception {try {cookingLock.lock();Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "做菜");} finally {cookingLock.unlock();}}public static void main(String[] args) {Restaurant test = new Restaurant();for (int i = 0; i < 10; i++) {new Thread(() -> {try {test.getFood();} catch (Exception e) {e.printStackTrace();}}, "某人").start();if (i == 2) {new Thread(() -> {try {test.cooking();} catch (Exception e) {e.printStackTrace();}}, "厨师").start();}}}
}输出:
某人点饭
某人点饭
某人点饭
厨师做菜
==等待1秒==
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭

3.4、公平锁和非公平锁

公平锁:十分公平,必须遵守先来后到的原则;

非公平锁:十分不公平,可以插队。

在创建锁的时候如果没有添加参数,那么默认就是非公平锁

//参数为true,则为公平锁,反之则是非公平锁。
private Lock windows = new ReentrantLock(true);

3.5、synchronized和Lock的区别

synchronized Lock
存在层次 java的关键字,在JVM层面上 是一个类
锁的释放 以获取锁的线程执行完同步代码或者线程执行发生异常,就立即释放锁 必须在finally中释放锁,不然有可能造成死锁
锁的获取 假设线程A获得锁,线程B等待。如果线程A阻塞,那么线程B就会一直等待 Lock有四种获取锁的方法,但是线程可以通过类方法尝试获取锁,就不用一直等待。
锁状态 无法判断 可以判断
锁类型 可重入锁、不可中断、非公平的 可重入、可中断、可公平(也可非公平,自己设置)
性能 少量同步 大量同步

关于synchronized和Lock性能区别的详细描述

**synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。**独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

3.6、锁的到底是什么?

在静态方法中,实际上是对调用该方法的对象加锁,俗称“对象锁”。

在非静态方法中,实际上是对该类对象加锁,俗称“类锁”。

类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

但是类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法区别的。

4、生产者和消费者问题

4.1、synchronized的传统版本

我们这里使用synchronized来解决生产者消费者问题,比如当num为0时,使B线程等待,唤醒A线程让其加1,当num为1时使A线程等待,并唤醒B线程让其减1。

/**
* 有问题的代码
**/
public class PCSyn{public static void main(String[] args){// 获取资源对象Data data = new Data();// 让其执行十次for(int i = 0; i < 10; i ++){// A线程执行增加操作new Thread(()->{data.increment();},"A").start();// B线程执行减操作new Thread(()->{data.decrement();},"B").start();}}
}class Data{// 信号量private int num = 0;// 增1public synchronized void increment(){// 当num不为0时让该线程等待if(num != 0)this.wait();// 执行业务操作num ++;System.out.println(Thread.currentThread().getName() + "->" + num);// 执行完成,唤醒notifyAll();}public synchronized void decrement(){// 当num为0时让该线程等待if(num == 0)this.wait();// 执行业务操作num --;System.out.println(Thread.currentThread().getName() + "->" + num);// 执行完成,唤醒notifyAll();}}

至此,在AB两个线程执行时,结果确实是按照我们的预想交替出现01,但是,同样的情况如果我们在上述代码中再加入两个线程CD执行同样的操作,即线程AC执行+1操作,线程BD执行-1操作。

此时,出现的结果就不如我们的意了:

我们可以看到,当num因上一次的操作数值发生变化之后,我们进行了if判断,if判断只判断了一次,当这个条件被满足时,程序就继续往下执行。比如num此时为0,那么线程A和线程C都被唤醒了,但是其中只有一个线程是有用的,那么另一个就是虚假唤醒。

虚假唤醒:在线程的 等待/唤醒 的过程中,等待的线程被唤醒后,在条件不满足的情况依然继续向下运行了。

我们只需要将代码中的if换成while就能解决虚假唤醒的问题。因为唤醒后还是需要再次判断条件,而 if 就 直接运行下去了。

public class PCSyn{public static void main(String[] args){// 获取资源对象Data data = new Data();// 让其执行十次for(int i = 0; i < 10; i ++){// A线程执行增加操作new Thread(()->{data.increment();},"A").start();// B线程执行减操作new Thread(()->{data.decrement();},"B").start();}}
}class Data{// 信号量private int num = 0;// 增1public synchronized void increment(){// 当num不为0时让该线程等待while(num != 0)  {this.wait();}// 执行业务操作num ++;System.out.println(Thread.currentThread().getName() + "->" + num);// 执行完成,唤醒notifyAll();}public synchronized void decrement(){// 当num为0时让该线程等待while(num == 0) {this.wait();}// 执行业务操作num --;System.out.println(Thread.currentThread().getName() + "->" + num);// 执行完成,唤醒notifyAll();}}

4.2、Lock

同样的问题我们可以通过Lock接口来实现。

public class PCLock{public static void main(String[] args){Data data = new Data();for(int i = 0; i < 10; i ++){new Thread(()->{data.increment();},"A").start();new Thread(()->{data.decrement();},"B").start();new Thread(()->{data.increment();},"C").start();new Thread(()->{data.decrement();},"D").start();}}
}class Data{private int num = 0;private Lock lock = new ReentrantLock();// 和synchronized不同的是,lock的等待和唤醒在condition对象中,分别对应await()和signalAll()private Condition condition = lock.newCondition();public void increment(){lock.lock();try{while(num != 0) {condition.await();}num++;System.out.println(Thread.currentThread().getName() + "->" + num);condition.signalAll();} catch(Exception e){e.printStackTrace();} finally{lock.unlock();}}public void decrement(){lock.lock();try{while(num == 0) {condition.await();}num--;System.out.println(Thread.currentThread().getName() + "->" + num);condition.signalAll();} catch(Exception e){e.printStackTrace();} finally{lock.unlock();}}}

但是,明明能用synchronized解决的问题,为什么要引入一个新的技术呢?这个技术肯定有他自己的过人之处。

那么在Lock的condition中,我们是可以精准的控制是哪个线程被唤醒,哪个线程等待,比如:实现A线程打印A,B线程打印B,C线程打印C,并且让其每一次出现的顺序是A->B->C。

condition的优势:精准的通知和唤醒线程。

public class PrintLock{public static void main(String[] args){Data data = new Data();for(int i = 0; i < 10; i ++){new Thread(()->{data.printA();},"A").start();new Thread(()->{data.printB();},"B").start();new Thread(()->{data.printC();},"C").start();}}
}class Data{private int num = 1;   // 1.A线程 2.B线程 3.C线程private Lock lock = new ReentrantLock();// 通过多个同步监视器精准唤醒或等待某个线程private Condition condition1 = lock.newCondition();private Condition condition2 = lock.newCondition();private Condition condition3 = lock.newCondition();public void printA(){lock.lock();try{while(num != 1) {condition1.await();}System.out.println(Thread.currentThread().getName() + "->AAA");// 唤醒B线程num = 2;condition2.signal();} catch(Exception e){e.printStackTrace();} finally{lock.unlock();}}public void printB(){lock.lock();try{while(num != 2) {condition2.await();}System.out.println(Thread.currentThread().getName() + "->BBB");// 唤醒C线程num = 3;condition3.signal();} catch(Exception e){e.printStackTrace();} finally{lock.unlock();}}public void printC(){lock.lock();try{while(num != 3) {condition3.await();}System.out.println(Thread.currentThread().getName() + "->CCC");// 唤醒A线程num = 1;condition1.signal();} catch(Exception e){e.printStackTrace();} finally{lock.unlock();}}
}

5.、8锁现象

以下名词解释:

顺序执行:先调用的先执行;

随机执行:没有规律,与计算机硬件资源有关,哪个线程先得到资源就先执行,各个线程之间互不干扰

5.1、多个线程使用同一把锁——顺序执行

示例1、 标准访问,请问是先打印邮件还是先发短信

public class MultiThreadUseOneLock01 {public static void main(String[] args){Mobile mobile = new Mobile();// 两个线程使用的是同一个对象。两个线程是一把锁!先调用的先执行!new Thread(()->mobile.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile {// 被 synchronized 修饰的方法、锁的对象是方法的调用者、public synchronized void sendEmail() {System.out.println("sendEmail");}public synchronized void sendMS() {System.out.println("sendMS");}
}

答案:先发邮件,再发短信。

5.2、多个线程使用同一把锁,其中某个线程里面还有阻塞——顺序执行

示例2、邮件方法暂停4秒钟,请问先打印邮件还是短信?

public class MultiThreadUseOneLock02 {public static void main(String[] args){Mobile2 mobile = new Mobile2();// 两个线程使用的是同一个对象。两个线程是一把锁!先调用的先执行!new Thread(()->mobile.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile2 {// 被 synchronized 修饰的方法、锁的对象是方法的调用者、public synchronized void sendEmail() {//多个线程使用一把锁,这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}public synchronized void sendMS() {System.out.println("sendMS");}
}

答案:还是先发邮件,后发短信。

5.3、多个线程,有的线程有锁,有的线程没锁——随机执行

示例3、新增一个普通方法接收微信()没有同步,请问先打印邮件还是接收微信?

public class MultiThreadHaveLockAndNot03 {public static void main(String[] args){Mobile3 mobile = new Mobile3();// 两个线程使用的是同一个对象。两个线程是一把锁!先调用的先执行!new Thread(()->mobile.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile.sendMS(),"B").start();new Thread(()->mobile.getWeixinMs(),"C").start();}
}// 手机,发短信,发邮件
class Mobile3 {// 被 synchronized 修饰的方法、锁的对象是方法的调用者、public synchronized void sendEmail() {//多个线程使用一把锁,这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}public synchronized void sendMS() {System.out.println("sendMS");}//接收微信,没有锁public void getWeixinMs() {System.out.println("getWeixinMs");}
}

答案:先执行getWeixinMs()方法,然后执行发邮件,最后是发短信。原因是getWeixinMs()方法是一个普通方法,不受Synchronized锁的影响。

5.4、多个线程使用多耙锁——随机执行

被Synchronized修饰的方法,锁的对象是方法的调用者;

调用者不同,它们之间用的不是同一个锁,相互之间没有关系。

示例4、两部手机、请问先打印邮件还是短信?

public class MultiThreadUseMultiLock04 {public static void main(String[] args){// 两个对象,互不干预Mobile4 mobile1 = new Mobile4();Mobile4 mobile2 = new Mobile4();new Thread(()->mobile1.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile2.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile4 {/***  @description:*  被 synchronized 修饰的方法,锁的对象是方法的调用者;*  调用者不同,它们之间用的不是同一个锁,相互之间没有关系。*/public synchronized void sendEmail() {//这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}public synchronized void sendMS() {System.out.println("sendMS");}
}

答案:先发短信,再发邮件。

5.5、Class锁:多个线程使用一个对象——顺序执行

被Synchronized和static同时修饰的方法,锁的对象是类的class对象,是唯一的一把锁,线程之间是顺序执行。

锁Class和锁对象的区别:

​ 1. Class锁,类模板只有一个;

  1. 对象锁,通过类模板可以new多个对象。

    如果全部都锁了Class,那么这个类下的所有对象都具有同一把锁。

示例5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?

public class MultiThreadUseOneObjectOneClassLock05 {public static void main(String[] args){Mobile5 mobile = new Mobile5();new Thread(()->mobile.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile5 {public synchronized static void sendEmail() {//这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}public synchronized static void sendMS() {System.out.println("sendMS");}
}

答案: 先发邮件,后发短信。

5.6、Class锁:多个线程使用多个对象——顺序执行

被 synchronized 修饰 和 static 修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。

Class锁是唯一的,所以多个对象使用的也是同一个Class锁。

示例6、两个静态同步方法,2部手机,请问先打印邮件还是短信?

public class MultiThreadUseMultiObjectOneClassLock06 {public static void main(String[] args){Mobile6 mobile1 = new Mobile6();Mobile6 mobile2 = new Mobile6();new Thread(()->mobile1.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile2.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile6 {public synchronized static void sendEmail() {//这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}public synchronized static void sendMS() {System.out.println("sendMS");}
}

答案:先发邮件,后发短信。先创建的对象先执行。

5.7、Class锁与对象锁:多个线程使用一个对象-随机执行

被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;

只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。

示例7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?

public class MultiThreadUseOneObjectClassLockAndObjectLock07 {public static void main(String[] args){Mobile7 mobile = new Mobile7();new Thread(()->mobile.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile7 {public synchronized static void sendEmail() {//这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}/***  @description:*  普通同步锁:对象锁*/public synchronized void sendMS() {System.out.println("sendMS");}
}

答案:先发短信,后发邮件。

5.8、Class锁与对象锁:多个线程使用多个对象-随机执行

被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;

只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。

示例8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?

public class MultiThreadUseMultiObjectClassLockAndObjectLock08 {public static void main(String[] args){Mobile8 mobile1 = new Mobile8();Mobile8 mobile2 = new Mobile8();new Thread(()->mobile1.sendEmail(),"A").start();// 干扰try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->mobile2.sendMS(),"B").start();}
}// 手机,发短信,发邮件
class Mobile8 {public synchronized static void sendEmail() {//这里设置一个干扰try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("sendEmail");}/***  @description:*  普通同步锁:对象锁*/public synchronized void sendMS() {System.out.println("sendMS");}
}

答案:先发短信,后发邮件。

6、集合类不安全

6.1、List不安全

有如下List的集合类:

public class ListTest {public static void main(String[] args) {List<Object> arrayList = new ArrayList<>();for(int i=1;i<=10;i++){new Thread(()->{arrayList.add(UUID.randomUUID().toString().substring(0,5));System.out.println(arrayList);},String.valueOf(i)).start();}}
}

这就会造成:

这是由于确实线程锁导致的。

解决方案一:使用Vector(但是不建议这种使用,因为其发布比ArrayList还早)

List<String> list = new Vector<>();

解决方案二:使用工具类Collections,使ArrayList变的安全

List<String> list = Collections.synchronizedList(new ArrayList<>());

解决方案三:使用JUC包下的CopyOnWriteArrayList<>();

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWrite:写入时复制,简称COW。 计算机程序设计领域的一种优化策略.

原因:多个线程调用的时候,List,读取的时候是固定的,写入的时候存在覆盖问题,所以写入的时候复制一份容器再写入(且写入操作加锁),避免写入覆盖。

CopyOnWriteArrayListVector厉害在哪里?

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z2enYQ1H-1632053112973)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210914214309523.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqqhL3lZ-1632053112975)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210914214838168.png)]

6.2、Set不安全

有如下Set的集合类:

public class SetTest {public static void main(String[] args) {//Set<String> set = new HashSet<>();//Set<String> set = Collections.synchronizedSet(new HashSet<>());Set<String> set = new CopyOnWriteArraySet<>();for (int i = 0; i < 10; i++) {new Thread(()->{set.add(UUID.randomUUID().toString().substring(0,5));System.out.println(set);},String.valueOf(i)).start();}}
}

解决方案一:使用工具类Collections,使HashSet变的安全

Set<String> set = Collections.synchronizedSet(new HashSet<>());

解决方案二:使用JUC包下的CopyOnWriteArraySet<>();

List<String> list = new CopyOnWriteArraySet<>();

HashSet底层是什么?

HashSet底层就是一个HashMap

public HashSet() {map = new HashMap<>();
}public boolean add(E e) {return map.put(e, PRESENT)==null;
}private static final Object PRESENT = new Object();

add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储。

hashSet就是使用了HashMap key不能重复的原理。

PRESENT是什么? 是一个常量 不会改变的常量 无用的占位。

6.3、Map不安全

回顾Map的基本操作;

默认等价什么? new HashMap<>(16, 0.75);

Map<String, String> map = new HashMap<>(); //加载因子、初始化容量

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

有如下Map的集合类:

public class MapTest {public static void main(String[] args) {//map是这样用的嘛?默认等价于什么?//工作不用map,//Map<String, String> map = new HashMap<>();//默认:HashMap<String, String> map = new HashMap<>(16,0.75)//加载因子、初始容量//Map<String, String> map = Collections.synchronizedMap(new HashMap());Map<String, String> map = new ConcurrentHashMap<>();for (int i = 0; i < 30; i++) {new Thread(()->{map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));System.out.println(map);},String.valueOf(i)).start();}}
}

解决方案一:使用工具类Collections,使HashMap<>()变的安全

Map<String, String> map = Collections.synchronizedMap(new HashMap());

解决方案二:使用JUC包下的ConcurrentHashMap<>();

Map<String, String> map = new ConcurrentHashMap<>();

7、Callable(简单)

与Runnable的区别:

​ 1、可以有返回值

​ 2、可以抛出异常

​ 3、方法名不同, run()/call()

想象这一种情况,有很多线程。我们想挑出有问题的线程,如果按照之前的方式是很麻烦的,但如果使用Callable接口的方式,我们让没问题的线程返回true,有问题的返回false,就能很轻松的解决这个问题。

下面来看看如何使用Callable创建出线程:

但是怎么将Callable放入到Thread里面呢?

源码分析:

接下来我们查看Runnable接口及其子接口和实现类,发现有个叫RunnableFuture的子接口

接着查看RunnableFuture,发现它有一个实现类叫FutureTask

而这个FutureTask的构造函数中有Callable接口

所以我们给FutureTask的构造方法中传入Callable接口,而FutureTask本身又实现了Runnable接口,所以Callable和Runnable就搭上线了,即我们在Thread的构造方法中传入FutureTask就行了。

所以使用Callable接口创建多线程的正确写法是:

public class CallableTest {public static void main(String[] args) throws ExecutionException, InterruptedException {for (int i = 1; i < 10; i++) {//            new Thread(new Runnable()).start();
//            new Thread(new FutureTask<>( Callable)).start();MyThread thread= new MyThread();//适配类:FutureTaskFutureTask<String> futureTask = new FutureTask<>(thread);//放入Thread使用new Thread(futureTask,String.valueOf(i)).start();//获取返回值String s = futureTask.get();System.out.println("返回值:"+ s);}}
}class MyThread implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("Call:"+Thread.currentThread().getName());return "String"+Thread.currentThread().getName();}
}

再来看Callable的返回值

使用get()方法获取返回值即可。

futureTask.get();

问题思考:Callable接口给线程加了返回值,为什么非要专门弄个get方法来获取呢?直接在线程运行完成后获取不行吗?

分析:想象这样一种情况,主线程中有四个线程,分别是A、B、C和D。假设B的执行时间是20s,其他线程执行时间都为5s,如果我们以下图方式获取返回值,那就会造成时间上的浪费:

上图是在每个线程启动后紧接着就用get方法等待返回值,那么在得到get方法的返回值之前主线程是不会向下运行的,所以对于每个线程来说,只有自己运行结束后才轮得到其他线程,这就和顺序执行一样了。

那如果将所有的get方法都放在程序的最后,如下图所示,则能缩短执行时间:

看上图,因为我们先让所有线程都运行起来并没有立刻获取返回值,因此获取到A.get()的值意味着已经过了5s,且在这5s内是ABCD4个线程同时执行的,而B由于执行时间为20s,所以拖慢了总执行时间。

同一个FutureTask对象只能被不同的线程调用一次

​ 只有一个futureTask对象,所以即使A和B两个线程调用,也只会执行一次。

FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();

8、常用辅助类(必会!!!)

8.1、 CountDownLatch

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

//这是一个计数器  减法
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {//总数是6CountDownLatch countDownLatch = new CountDownLatch(6);for (int i = 1; i <= 6 ; i++) {new Thread(()->{System.out.println(Thread.currentThread().getName()+" Go out");countDownLatch.countDown(); //每个线程都数量-1},String.valueOf(i)).start();}countDownLatch.await();  //等待计数器归零  然后向下执行System.out.println("close door");}}

主要方法:

  • countDown 减一操作;

  • await 等待计数器归零。

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

8.2、CyclickBarrier

其实就是一个加法计数器;

public class CyclicBarrierDemo {public static void main(String[] args) {//主线程CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{System.out.println("召唤神龙~");});for (int i = 1; i <= 7; i++) {//子线程int finalI = i;new Thread(()->{System.out.println(Thread.currentThread().getName()+" 收集了第 {"+ finalI+"} 颗龙珠");try {cyclicBarrier.await(); //加法计数 等待} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}).start();}}
}

8.3、Semaphore

Semaphore:信号量

抢车位:

3个车位 6辆车:

public class SemaphoreDemo {public static void main(String[] args) {//停车位为3个Semaphore semaphore = new Semaphore(3);for (int i = 1; i <= 6; i++) {int finalI = i;new Thread(()->{try {semaphore.acquire(); //得到//抢到车位System.out.println(Thread.currentThread().getName()+" 抢到了车位{"+ finalI +"}");TimeUnit.SECONDS.sleep(2); //停车2sSystem.out.println(Thread.currentThread().getName()+" 离开车位");} catch (InterruptedException e) {e.printStackTrace();}finally {semaphore.release();//释放}},String.valueOf(i)).start();}}
}

原理:

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

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

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

9、阻塞队列(BlockingQueue)

9.1、什么是阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

BlockingQueue 是Collection的一个子类,以下是BlockingQueue常见的子类:

  • ArrayBlockingQueue

  • LinkedBlockingQueue

  • SynchronousQueue

阻塞队列提供了四种处理方法:

方法/处理方式 抛出异常 不会抛出异常,有返回值 阻塞,等待 超时等待
添加 add offer put offer(timeNum, timeUnit)
移除 remove poll take poll(timeNum, timeUnit)
返回队首元素 element peek
/*** 抛出异常*/public static void test1(){//需要初始化队列的大小ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);System.out.println(blockingQueue.add("a"));System.out.println(blockingQueue.add("b"));System.out.println(blockingQueue.add("c"));//抛出异常:java.lang.IllegalStateException: Queue full
//        System.out.println(blockingQueue.add("d"));System.out.println(blockingQueue.remove());System.out.println(blockingQueue.remove());System.out.println(blockingQueue.remove());//如果多移除一个//这也会造成 java.util.NoSuchElementException 抛出异常System.out.println(blockingQueue.remove());}
=======================================================================================
/*** 不抛出异常,有返回值*/public static void test2(){ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);System.out.println(blockingQueue.offer("a"));System.out.println(blockingQueue.offer("b"));System.out.println(blockingQueue.offer("c"));//添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常System.out.println(blockingQueue.offer("d"));System.out.println(blockingQueue.poll());System.out.println(blockingQueue.poll());System.out.println(blockingQueue.poll());//弹出 如果没有元素 只会返回null 不会抛出异常System.out.println(blockingQueue.poll());}
=======================================================================================
/*** 等待 一直阻塞*/public static void test3() throws InterruptedException {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);//一直阻塞 不会返回blockingQueue.put("a");blockingQueue.put("b");blockingQueue.put("c");//如果队列已经满了, 再进去一个元素  这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
//        blockingQueue.put("d");System.out.println(blockingQueue.take());System.out.println(blockingQueue.take());System.out.println(blockingQueue.take());//如果我们再来一个  这种情况也会等待,程序会一直运行 阻塞System.out.println(blockingQueue.take());}
=======================================================================================
/*** 等待 超时阻塞*  这种情况也会等待队列有位置 或者有产品 但是会超时结束*/public static void test4() throws InterruptedException {ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);blockingQueue.offer("a");blockingQueue.offer("b");blockingQueue.offer("c");System.out.println("开始等待");blockingQueue.offer("d",2, TimeUnit.SECONDS);  //超时时间2s 等待如果超过2s就结束等待System.out.println("结束等待");System.out.println("===========取值==================");System.out.println(blockingQueue.poll());System.out.println(blockingQueue.poll());System.out.println(blockingQueue.poll());System.out.println("开始等待");blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了System.out.println("结束等待");}

9.2、ArrayBlockingQueue

ArrayBlockingQueue 是最典型的有界队列,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程

我们在创建它的时候就需要指定它的容量,之后也不可以再扩容了,在构造函数中我们同样可以指定是否是公平的,代码如下:

 public ArrayBlockingQueue(int capacity, boolean fair) {if (capacity <= 0)throw new IllegalArgumentException();this.items = new Object[capacity];lock = new ReentrantLock(fair);notEmpty = lock.newCondition();notFull =  lock.newCondition();}

9.3、LinkedBlockingQueue

从命名可以看出,这是一个内部用链表实现的 BlockingQueue。如果我们不指定它的初始容量,那么它容量默认就为整型的最大值 Integer.MAX_VALUE,由于这个数非常大,约为 2 的 31 次方,我们通常不可能放入这么多的数据,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限。

其他特点:

  • 同样也利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程
  • 无法设置 ReentrantLock 的公平非公平,默认是非公平
  • 也可以设置固定大小

默认无参构造函数如下,默认最大值 Integer.MAX_VALUE:

public LinkedBlockingQueue() {this(Integer.MAX_VALUE);}

9.4、SynchronousQueue同步队列

SynchronousQueue 最大的不同之处在于,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。

需要注意的是,SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候,SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。

为什么说它的容量是 0 ,我们可以看其中的几个方法:

peek 方法永远返回 null,代码如下:

 public E peek() {return null;}

因为 peek 方法的含义是取出头结点,但是 SynchronousQueue 的容量是 0,所以连头结点都没有,peek 方法也就没有意义,所以始终返回 null。

同理,element 方法始终会抛出 NoSuchElementException 异常,但是这个方法的实现在它的父类AbstractQueue 中

  public E element() {E x = peek();if (x != null)return x;elsethrow new NoSuchElementException();}

SynchronousQueue 的 size 方法始终返回 0,因为它内部并没有容量,代码如下:

public int size() {return 0;
}

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

public class SynchronousQueueDemo {public static void main(String[] args) {BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();//研究一下 如果判断这是一个同步队列//使用两个进程// 一个进程 放进去// 一个进程 拿出来new Thread(()->{try {System.out.println(Thread.currentThread().getName()+" Put 1");synchronousQueue.put("1");System.out.println(Thread.currentThread().getName()+" Put 2");synchronousQueue.put("2");System.out.println(Thread.currentThread().getName()+" Put 3");synchronousQueue.put("3");} catch (InterruptedException e) {e.printStackTrace();}},"T1").start();new Thread(()->{try {System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
//                TimeUnit.SECONDS.sleep(3);System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
//                TimeUnit.SECONDS.sleep(3);System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());} catch (InterruptedException e) {e.printStackTrace();}},"T2").start();}
}

10、线程池(重点)

10.1、什么是线程池、

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池,多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
机器完成一项任务所需时间为:创建线程时间t1,执行任务的时间t2, 销毁线程时间t3;如果t1 + t3 远大于 t2,则可以采用线程池,以提高服务器性能。线程池技术主要关注于线程的创建和销毁时间,把创建时间和销毁时间分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有创建时间和销毁时间的开销了。

10.2、线程池三大方法

ExecutorService threadPool1 = Executors.newSingleThreadExecutor(); //一次只能执行一个线程的线程池

ExecutorService threadPool2 = Executors.newFixedThreadPool(3); //一次可以执行固定参数的线程池

ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可以自动变化的线程池

  • 一次只能执行一个线程的线程池
public class Demo01 {public static void main(String[] args) {ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程//线程池用完必须要关闭线程池try {for (int i = 1; i <=100 ; i++) {//通过线程池创建线程threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+ " ok");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}/*pool-1-thread-1执行了!!!pool-1-thread-1执行了!!!pool-1-thread-1执行了!!!pool-1-thread-1执行了!!!pool-1-thread-1执行了!!!*/
}
  • 一次可以执行固定参数的线程池
public class Demo01 {public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(3);//固定3个线程//线程池用完必须要关闭线程池try {for (int i = 1; i <=100 ; i++) {//通过线程池创建线程threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+ " ok");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}/*pool-1-thread-1执行了!!!pool-1-thread-2执行了!!!pool-1-thread-3执行了!!!pool-1-thread-2执行了!!!pool-1-thread-1执行了!!!pool-1-thread-3执行了!!!*/
}
  • 可以自动变化的线程池
public class Demo01 {public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(3);//固定3个线程//线程池用完必须要关闭线程池try {for (int i = 1; i <=100 ; i++) {//通过线程池创建线程threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+ " ok");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}/*pool-1-thread-1执行了!!!pool-1-thread-4执行了!!!pool-1-thread-3执行了!!!pool-1-thread-2执行了!!!pool-1-thread-5执行了!!!pool-1-thread-6执行了!!!*/
}

10.3、线程池七大参数

对于三大方法的源码分析:

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

本质:三种方法都是开启的ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小int maximumPoolSize, //最大的线程池大小//CPU密集型和I/O密集型//可以用CPU核数( Runtime.getRuntime().availableProcessors())//也可以根据程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。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;
}

比如我们定义一个线程池corePoolSize = 3,maximumPoolSize = 5,阻塞队列大小为3,我们用银行柜台的案例来解释整个过程,可以分成四个阶段;

  1. 当来的人不超过三个人时,系统正常运行,业务都能顺利办理;
  2. 当来的人超过3而不超过6时,那么整个系统也可以正常运行,只不过这个时候银行的休息区(BlockingQueue<>(3))里等待着要办理业务的人;
  3. 当来的人超过6而不超过8(8 = maximumPoolSize + 阻塞队列大小)时,这个时候系统就会有些变化,银行会重新新开一些还未使用的柜台共来的人使用,如果开启之后在keepAliveTime时间段内柜台还没有一个人,柜台又会重新关闭;
  4. 当银行来的人超过8之后,系统已经承受不住人流量了,系统就会崩溃,这个时候就需要一种拒绝策略来拒绝爆满之后的人;

10.4、线程的四大拒绝策略

  1. 第一种拒绝策略:new ThreadPoolExecutor.AbortPolicy(),如果线程满了,则不处理新的进程,抛出异常;

    static void demo1(){ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3,5,2,TimeUnit.SECONDS,new LinkedBlockingDeque<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());/*当totalThread达到3时,初始柜台刚好够处理;当totalThread达到6时,休息区(阻塞队列)被占满,当totalThread达到8时,额外的柜台也全部开启,当totalThread超过8时,若新进去的线程无法处理,则抛出异常java.util.concurrent.RejectedExecutionException*/int totalThread = 8;try {for (int i = 0; i < totalThread; i++) {threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"执行了!!!");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}
    }
    
  2. 第二种处理策略:new ThreadPoolExecutor.CallerRunsPolicy(),线程池满了,如果有新的哪里来的去哪里,不会抛出异常;

    static void demo2(){ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3,5,2,TimeUnit.SECONDS,new LinkedBlockingDeque<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());//如果超额,这里会交给main线程int totalThread = 12;try {for (int i = 0; i < totalThread; i++) {threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"执行了!!!");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}/*pool-1-thread-2执行了!!!pool-1-thread-4执行了!!!main执行了!!!pool-1-thread-1执行了!!!pool-1-thread-3执行了!!!pool-1-thread-1执行了!!!pool-1-thread-4执行了!!!main执行了!!!pool-1-thread-5执行了!!!pool-1-thread-2执行了!!!pool-1-thread-1执行了!!!pool-1-thread-3执行了!!!*/
    }
    
  3. 第三种处理策略:new ThreadPoolExecutor.DiscardPolicy(),线程池满了,丢掉线程,不会抛出异常

    static void demo3(){ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3,5,2,TimeUnit.SECONDS,new LinkedBlockingDeque<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());//如果超额,这里会交给main线程int totalThread = 15;try {for (int i = 0; i < totalThread; i++) {threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"执行了!!!");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}
    }
    
  4. 第四种处理策略:new ThreadPoolExecutor.DiscardOldestPolicy(), 线程池满了,会尝试和最早的线程去竞争,如果成功则加入,否则丢弃,不会抛出异常

    static void demo4(){ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3,5,2,TimeUnit.SECONDS,new LinkedBlockingDeque<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());//如果超额,这里会交给main线程int totalThread = 14;try {for (int i = 0; i < totalThread; i++) {threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"执行了!!!");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}
    }
    

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

函数式接口:有且仅有一个抽象方法的接口

11.1、Function函数型接口

@FunctionalInterface
public interface Function<T, R> {/*** Applies this function to the given argument.** @param t the function argument* @return the function result*/R apply(T t)

传入参数T,返回类型R

举例:

/*** Function函数型接口*/
public class Demo01 {public static void main(String[] args) {Function<String,String> function = (str) ->{return str;};System.out.println(function.apply("starasdas"));}//输入什么,就打印出什么。
}

11.2、Predicate断定型接口

@FunctionalInterface
public interface Predicate<T> {/*** Evaluates this predicate on the given argument.** @param t the input argument* @return {@code true} if the input argument matches the predicate,* otherwise {@code false}*/boolean test(T t);

有一个输入参数,返回值只能是布尔值。

举例:

public class Demo2 {public static void main(String[] args) {//判断字符串是否为空Predicate<String> predicate = (str)->{return str.isEmpty();};System.out.println(predicate.test("11"));System.out.println(predicate.test(""));}
}

11.3、Consumer消费性接口

@FunctionalInterface
public interface Consumer<T> {/*** Performs this operation on the given argument.** @param t the input argument*/void accept(T t);

有一个输入参数,没有返回值。(消费者消费不需要返回值)

举例:

public class Demo3 {public static void main(String[] args) {Consumer<String> consumer = (str)->{System.out.println(str);};consumer.accept("abc");}
}

11.4、Supplier供给型接口

@FunctionalInterface
public interface Supplier<T> {/*** Gets a result.** @return a result*/T get();
}

没有参数,只有返回值。

举例:

public class Demo4 {public static void main(String[] args) {Supplier<String> supplier = ()->{return "1024";};System.out.println(supplier.get());}
}

12、Stream流式计算

什么是Stream流式计算?

存储 + 计算

集合、Mysql本质是存储东西的。

计算都应该交给流来操作。

示例:

/*** 现有5个用户,筛选:* 1. ID偶数的* 2. 年纪大于23岁* 3. 用户名转为大写字母* 4. 用户名字母倒排序* 5. 只输出一个用户*/
public class Test {public static void main(String[] args) {User u1 = new User(1, "a", 21);User u2 = new User(2, "b", 22);User u3 = new User(3, "c", 23);User u4 = new User(4, "d", 24);User u5 = new User(5, "e", 25);// 集合用于存储List<User> list = Arrays.asList(u1, u2, u3, u4, u5);//计算交给Stream流//链式编程!!!!// filter:筛选// map:转化// sorted:排序// limit:分页list.stream().filter(u -> {return  u.getId()%2 == 0;}).filter(u -> {return u.getAge() > 23}).map(u -> {return u.getName().toUpperCase();}).sorted((uu1, uu2) -> {return uu1.compareTo(uu2);}).limit(1).forEach(System.out::println);}
}

13、ForkJoin

13.1、什么是ForkJoin

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,就是将一个大任务分割成若干小任务,最终汇总每个个小任务的结果得到这个大任务的结果。

过程主要有两步:

  1. 任务切割

  2. 结果合并

    13.2、ForkJoin特点

    工作窃取(work-stealing): 是指某个线程从其他队列里窃取任务来执行。

    就是一个工作线程下会维护一个包含多个子任务的==双端队列==。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度较快,很快就把所有任务消耗完了。

    线程的任务窃取: 比如你和你的小伙伴在一起吃水果,你的那份吃完了,他那份没吃完,这个时候你偷偷的拿了他的一些水果吃了。存在执行2个任务的子线程,存在A,B两个WorkQueue在执行任务,A的任务执行完了,B的任务没执行完,那么A的WorkQueue就从B的WorkQueue的ForkJoinTask数组中拿走了一部分尾部的任务来执行,可以合理的提高运行和计算效率。

13.2、如何使用ForkJoin

  1. 通过ForkJoinPool来执行
  2. 计算任务execute(ForkJoinTask<?> task)
  3. 计算类要继承ForkJoinTask(ForkJoinTask代表运行在ForkJoinPool中的任务)。

主要方法

  • fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。

  • join() 当任务完成的时候返回计算结果。

  • invoke() 开始执行任务,如果必要,等待计算完成。

子类: Recursive :递归

  • RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
  • RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)

代码示例:

核心代码

package cn.guardwhy.forkJoin;import java.util.concurrent.RecursiveTask;
/*
* 求和计算的任务!!
* 1.使用forkjoin
* 1.1forkjoinpool 通过它来执行。
* 1.2 计算任务ForkJoinPool.excute(ForkJoinTask task)
* 1.3 计算类要继承ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {private Long start; // 起始值private Long end; // 结束值// 临界值private Long temp = 10000L;public ForkJoinDemo(Long start, Long end) {this.start = start;this.end = end;}// 计算方法@Overrideprotected Long compute() {// 常规方式if((end - start) < temp){// 定义sum值Long sum = 0L;for (Long i = start; i <= end ; i++) {sum += i;}return sum;}else {// ForkJoin递归long middle = (start + end) / 2; // 中间值ForkJoinDemo task1 = new ForkJoinDemo(start, middle);// 拆分任务,把任务压入线程队列task1.fork();ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);// 拆分任务,把任务压入线程队列task2.fork();return task1.join() + task2.join();}}
}

测试代码:

package cn.guardwhy.forkJoin;import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;public class ForkJoinTest {public static void main(String[] args) throws ExecutionException, InterruptedException {// test1();    // sum=500000000500000000 时间: 6235test2();    // sum=500000000500000000 时间: 6017// test03();   // sum=500000000500000000 时间: 102}// 1.普通方式public static void test1() {Long sum = 0L;long start = System.currentTimeMillis();for (Long i = 1L; i <=10_0000_0000 ; i++) {sum += i;}Long end = System.currentTimeMillis();System.out.println("sum=" +sum+" 时间: " + (end-start));}// 2.使用ForkJoin方法private static void test2() throws ExecutionException, InterruptedException {long start = System.currentTimeMillis();ForkJoinPool forkJoinPool = new ForkJoinPool();ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);// 提交任务ForkJoinTask<Long> submit = forkJoinPool.submit(task);Long sum = submit.get();long end = System.currentTimeMillis();System.out.println("sum=" +sum+" 时间: " + (end-start));}// 3.链式编程private static void test03() {long start = System.currentTimeMillis();// Stream并行流long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);long end = System.currentTimeMillis();System.out.println("sum=" +sum+" 时间: " + (end-start));}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3MSsN95O-1632053112977)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210917234608412.png)]

14、异步回调

14.1、Future接口

Future的设计初衷:对将来某个时刻会发生的结果进行建模。

Future接口建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不需要等待耗时的操作完成。

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

示例:使用Future以异步的方式执行一个耗时的操作:

ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> future = executor.submit(new Callable<Double>() { //向ExecutorService提交一个Callable对象 public Double call() {return doSomeLongComputation();//以异步方式在新线程中执行耗时的操作}
});
doSomethingElse();
try {Double result = future.get(1, TimeUnit.SECONDS);//获取异步操作结果,如果被阻塞,无法得到结果,在等待1秒钟后退出
} catch (ExecutionException ee) {// 计算抛出一个异常
} catch (InterruptedException ie) {// 当前线程在等待过程中被中断
} catch (TimeoutException te) {// 在Future对象完成之前超时
}

这种编程方式让你的线程可以在ExecutorService以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他任务。如果已经运行到没有异步操作的结果就无法继续进行时,可以调用它的get方法去获取操作结果。如果操作已经完成,该方法会立刻返回操作结果,否则它会阻塞线程,直到操作完成,返回相应的结果。

为了处理长时间运行的操作永远不返回的可能性,虽然Future提供了一个无需任何参数的get方法,但还是推荐使用重载版本的get方法,它接受一个超时的参数,可以定义线程等待Future结果的时间,而不是永无止境地等待下去。

14.2、CompletableFuture

**CompletableFuture**类提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,不用担心实现细节。

  1. 无返回值的runAsync异步回调

    public class CompletableFutureTest {public static void main(String[] args) throws Exception {CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "runAsync=>Void");});System.out.println("1111");completableFuture.get();}
    }
    
  2. 有返回值的supplyAsync异步回调

    public class CompletableFutureTest {public static void main(String[] args) throws Exception {CompletableFuture<Integer> completableFuture =CompletableFuture.supplyAsync(() -> {System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");int i = 10 / 0;return 1024;});System.out.println(completableFuture.whenComplete((t, u) -> {System.out.println("t=>" + t); // 正常的返回结果System.out.println("u=>" + u); // 错误信息:java.util.concurrent.CompletionException:java.lang.ArithmeticException: /byzero}).exceptionally((e) -> {//error回调System.out.println(e.getMessage());return 404; // 可以获取到错误的返回结果}).get());}
    }
    

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

15、JMM

15.1、什么是JMM

​ JMMJMM即为JAVA 内存模型(java memory model)。它并不是一种实际存在的东西,而是一种人为形成的约定,是一种概念。JAVA 内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。

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

  1. 线程在解锁前,必须将线程中的工作内存中存储的值即时刷新到主内存中的共享变量!
  2. 线程在加锁前,必须读取主存中的最新值到工作内存中!
  3. 加锁和解锁是同一把锁!

线程中操作的数据要从主内存中读取,并备份到线程自己的工作内存中,作为副本,主存并不会主动向线程更新数据。

15.2、线程的8种内存交互操作

  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的共享变量释放
  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
  • load(加载):作用于工作内存的变量,把通过read操作获取的变量值放入工作内存中
  • use(使用):作用于工作内存的变量,把工作内存中的变量传输给执行引擎,每当虚拟机遇到需要使用到变量的值,就会使用到这个指令
  • assign(赋值):作用于工作内存的变量,把执行引擎传输过来的值放入工作内存
  • store(存储):作用于主内存的变量,把一个从线程中的工作内存的变量值传送到主内存中,以便后续的write操作
  • write(写入):作用于主内存的变量,将store操作从工作内存获取的变量值放入主内存中

15.3、JMM对以上八种内存操作指令做出了如下约束

  • read和load、user和assign、store和write、lock和unlock必须成对出现,不允许单独操作其中一条指令
  • 不允许线程丢弃离它最近的assign操作,即 工作内存中的变量值改变之后,必须告知主内存
  • 不允许一个线程将没有assign过的数据从工作内存同步会主内存
  • 一个新的变量必须在主内存中产生,不允许工作内存私自初始化一个变量来作为共享变量,即 实施use 和 store操作之前 , 必须经过 load 和 assign操作
  • 同一变量同一时间只允许一个线程对其进行lock操作;多次lock之后,必须执行相同次数的unlock对其解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值, 即 每次获得锁的线程,加锁前必须要重新读取主内存中的变量值,才能提交给执行引擎进行use操作
  • 如果一个变量没有被lock,就不能对其进行unlock操作,也不能对一个被其他线程锁住的变量进行unlock
  • 对一个变量加锁之前,必须把工作内存中的变量值同步回主内存

但是存在一个问题:

假设现在有一个main线程和一个普通线程,普通线程执行的操作是:当num为 0 时 ,一直循环下去;此时main线程给num赋值为 1 ,普通线程并不知道num已经被修改,程序就会一直执行,不会停止!

public class VolatileDemo {private static int num = 0;public static void main(String[] args) {new Thread(()->{ // 线程1while (num == 0) {}}).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}num = 1;System.out.println(num);}
}

解决办法:volatile关键字,请看下一节。

16、Volatile

16.1、什么是Volatile

Volatile 是 Java 虚拟机提供 轻量级的同步机制(相对于synchronized来说)

  • 保证可见性 => JMM 主内存中的共享变量修改之后,会通知所有线程备份到各自的工作内存中
  • 不保证原子性
  • 禁止指令重排

16.2、保证可变性

public class JMMDemo01 {// 如果不加volatile 程序会死循环// 加了volatile是可以保证可见性的private volatile static Integer number = 0;public static void main(String[] args) {//main线程//子线程1new Thread(()->{while (number==0){}}).start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}//子线程2new Thread(()->{while (number==0){}}).start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}number=1;System.out.println(number);}
}

只要是加了volatile的变量,及时通知main线程number变量改变了,main线程中主内存拷贝到工作内存。

16.3、不保证原子性

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。同生共死

先上代码:

class MyData{volatile int number = 0;public void changeData(){number ++;}
}/*** 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存*/
public class Demo {//主线程main,程序入口public static void main(String[] args) {//创建对象,number在主内存为0MyData myData = new MyData();for (int i = 1; i <= 20; i++) {//创建20个线程new Thread(()->{//一个线程执行1000次加一的操作for (int j = 1; j <= 1000; j++) {myData.changeData();}},String.valueOf(i)).start();}//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二,说明还有线程没有执行完任务,继续执行上面的代码while (Thread.activeCount() > 2){Thread.yield();}//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的值不是20000System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);}}

由此可见volatile的使用线程不安全 。

那么为什么volatile不保证原子性?

举例:当线程1,线程2,线程3同时拿到主内存中的number=0,并且在工作内存中进行加一时,这个时候各个线程的工作内存里的变量要写回主内存,线程1在写回主内存这一个过程中因为cpu线程资源被抢占,挂起了,没有来的及通知其他线程number的值已经改为1了,线程2就将自己之前从主内存拷贝的number=0的变量(还没有更新number=1的变量)进行加一(这个时候理想性是number=2再写回主内存),而实际情况是线程2的工作内存中的变量number=1写回主内存,将线程1中已经写回主内存的number=1的覆盖了,导致数值丢失。

先解释一下number++这个命令:

MyData.java->MyData.class->JVM字节码

number++这个命令在JVM字节码被拆分成三个指令:

执行getfield拿到原始值
执行iadd进行加一的操作
执行putfield把累加的值写回主内存
javap -c里的是java字节码,这个是汇编底层原始命令

volatile不保证原子性,怎么解决原子性?

  1. 加synchronized,但是影响并发

class MyData{volatile int number = 0;public synchronized void changeData(){number++;//加一}
}/*** 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存*/
public class Demo {//主线程main,程序入口public static void main(String[] args) {//创建对象,number在主内存为0MyData myData = new MyData();for (int i = 1; i <= 20; i++) {//创建20个线程new Thread(()->{//一个线程执行1000次加一的操作for (int j = 1; j <= 1000; j++) {myData.changeData();}},String.valueOf(i)).start();}//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二继续执行上面的代码,while (Thread.activeCount() > 2){Thread.yield();}//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的数量不是20000System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);}}
  1. 使用AtomicInteger原子整型
class MyData{volatile int number = 0;AtomicInteger atomicInteger = new AtomicInteger();public void changeData(){atomicInteger.getAndIncrement();//加一}
}/*** 线程对变量的读取赋值要先将变量从主内存拷贝自己的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存*/
public class Demo {//主线程main,程序入口public static void main(String[] args) {//创建对象,number在主内存为0MyData myData = new MyData();for (int i = 1; i <= 20; i++) {//创建20个线程new Thread(()->{//一个线程执行1000次加一的操作for (int j = 1; j <= 1000; j++) {myData.changeData();}},String.valueOf(i)).start();}//程序不关闭会继续执行main线程和GC线程,判断线程数量大于二继续执行上面的代码,while (Thread.activeCount() > 2){Thread.yield();}//理想中number的数量为20*1000=20000,而volatile不保证原子性,实际情况一般打印number的数量不是20000System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.atomicInteger);}}

16.4、禁止指令重排

这里先引入一个概念:

什么是JMM的有序性?

数据依赖性怎么理解?就是先有你爸再有你。

16.1、什么是指令重排

假设我写的第20行代码,执行的时候不一定会从第一行执行到第20行,打个比方:参加高考做卷子,出题人给的题目,你不一定会从第一题做到最后一题,你可能会先把会的写了,其他有难度的题目最后写。

代码案例1:

再多线程环境下语句执行有1234和2134以及1324三种顺序,语句4不能重排后变成第一个,原因是什么说的数据依赖性,变量要先声明再使用。

代码案例2:

正常的结果是x=0, y=0;但是如果发生指令重排,

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

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

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

代码案例3:

在多线程环境下指令重排,会导致二种结果:一个是0+5=5,一个是1+5=6

正常单线程环境下会执行语句1再执行语句2最后执行语句3,结果打印为5

多线程环境下指令重排了先执行语句2再执行语句3最后执行语句1,结果打印为6

很恐怖的好吧,数据的一致性不能保证,所以volatile需要禁止指令重排。

volatile禁止指令重排小总结

那么在实际应用中,什么地方在应用这个内存屏障应用得最多呢? 单例模式

17、单例模式

单例模式在多线程环境下可能存在安全问题

单线程下的单例模式:

class MyData{private static MyData myData = null;private MyData(){System.out.println(Thread.currentThread().getName()+"\t 构造方法");}public static MyData getInstance(){if(myData == null){myData = new MyData();}return myData;}
}public class Demo {//主线程main,程序入口public static void main(String[] args) {System.out.println(MyData.getInstance() == MyData.getInstance());System.out.println(MyData.getInstance() == MyData.getInstance());}}

控制台打印正确:

多线程下的单例模式:


public class Demo {private static Demo demo = null;private Demo(){System.out.println(Thread.currentThread().getName()+"\t 构造方法");}public static Demo getInstance(){if(demo == null){demo = new Demo();}return demo;}//主线程main,程序入口public static void main(String[] args) {for (int i = 1; i <= 10; i++) {new Thread(()->{Demo.getInstance();},String.valueOf(i)).start();}}}

此时控制台打印错误:

本来应该打印一次的,结果10个线程打印了5次,这就有问题了。

有人会说加一个synchronized:


public class Demo {private static Demo demo = null;private void Demo(){System.out.println(Thread.currentThread().getName()+"\t 构造方法");}public static synchronized Demo getInstance(){if(demo == null){demo = new Demo();}return demo;}//主线程main,程序入口public static void main(String[] args) {for (int i = 1; i <= 10; i++) {new Thread(()->{Demo.getInstance();},String.valueOf(i)).start();}}}

控制台打印正确:

但是这个不好,synchronized是重量型的,数据一致型得到保证了,但是影响并发

那怎么解决这个问题呢?

这个时候先介绍DCL双关检测锁机制,后面再一点点讲解。

17.1、DCL双关检锁机制

加锁前后都进行一次判断。

public class Demo {private static Demo demo = null;private Demo(){System.out.println(Thread.currentThread().getName()+"\t 构造方法");}public static  Demo getInstance(){if(demo == null){synchronized (Demo.class){if(demo == null){demo = new Demo();}}}return demo;}//主线程main,程序入口public static void main(String[] args) {for (int i = 1; i <= 10; i++) {new Thread(()->{Demo.getInstance();},String.valueOf(i)).start();}}}

为什么要加二层判断呢?更加牢固一些

打个比方:你上厕所,你确认没有人了,然后再进去,进去了把门插上,再推一下门,推不动确认安全后该干啥干啥。

在多线程环境下使用DCL(双关检锁机制)是否就百分百OK呢?

不是,DCL(双关检锁机制)不一定线程安全,在多线程环境下,JMM中的有序性会让指令出现重排,让执行顺序发送变化,不能保证百分百。加入volatile可以禁止指令重排。

单例模式volatile分析

在多线程环境下,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,造成线程安全问题。

所以加上volatile才可以保证百分百ok。

public class Demo {private static volatile Demo demo = null;private Demo(){System.out.println(Thread.currentThread().getName()+"\t 构造方法");}public static  Demo getInstance(){if(demo == null){synchronized (Demo.class){if(demo == null){demo = new Demo();}}}return demo;}//主线程main,程序入口public static void main(String[] args) {for (int i = 1; i <= 10; i++) {new Thread(()->{Demo.getInstance();},String.valueOf(i)).start();}}}

17.2、饿汉式

public class Hungry {/*** 第一次进来就会创建实例* 可能会浪费空间*/private byte[] data1=new byte[1024*1024];private byte[] data2=new byte[1024*1024];private byte[] data3=new byte[1024*1024];private byte[] data4=new byte[1024*1024];//构造方法私有化private Hungry(){}private final static Hungry hungry = new Hungry();public static Hungry getInstance(){return hungry;}}

17.3、懒汉式

//懒汉式
public class Lazy {//懒汉是名称的来源private static Lazy lazy = null;//构造方法私有化private Lazy(){}//提供获取实例的方法public static synchronized Lazy getInstance(){ //使用synchronized保证线程的安全if(lazy == null){lazy = new Lazy();}return lazy;}}

17.4、静态内部类

public class Holder {//构造方法私有化private Holder(){}//其实就是通过一个静态的私有内部类,返回外部类的实例对象private static class  InnerSingleton{private static final Holder holder = new Holder();}//提供外部获取实例公有方法public static final Holder getInstance(){return InnerSingleton.holder;}
}

17.5、枚举类型

//enum 是什么? enum本身就是一个Class 类
public enum EnumSingle {INSTANCE;public EnumSingle getInstance(){return INSTANCE;}
}class Test{public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumSingle instance1 = EnumSingle.INSTANCE;Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);//java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>()EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}
}

小结:

  • 懒汉式:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
  • 饿汉式:空间换时间,无论会不会使用到,都会创建这个实例。
  • 双检锁模式:充分的结合了懒汉式和饿汉式的优点,既节省了空间,又提高了效率.
  • 静态内部类:静态内部类的特性保证了,只有在调用getInstance()方法的时候,我们的JVM才会初始化singleton实例,所以这种创建单例的方法同时解决了线程安全、饿汉式引起的性能问题,而且也无需加锁,是相对比较推荐的创建单例的方式。
  • 枚举方式:枚举的方式是比较少见的一种实现方式,但是看代码实现,却更简洁清晰。并且她还自动支持序列化机制。

18、深入理解CAS

18.1什么是CAS

CAS是CompareAndSwap的缩写,作用是比较与交换。

线程对变量的读取赋值等操作,要先将变量从主内存拷贝自己线程的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存。

public class Demo {//主线程main,程序入口public static void main(String[] args) {//原子类整型,在主内存中创建这个对象给初始值为5,默认初始值为0AtomicInteger atomicInteger = new AtomicInteger(5);//当线程中的工作内存要写回主内存时,拿第一个参数作为期望值和主内存中的值进行比较,如果期望值和主内存中的值相同都为5,更新值主内存中的值为2019System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t : "+ atomicInteger.get());//线程anew Thread(()->{//现在主内存中对象的值已经为2019了,期望值为5,和主内存中对象的值不一样,无法将1024写回主内存System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t : "+ atomicInteger.get());},"a").start();}
}

控制台:

就是为什么说使用AtomicInteger可以解决原子性,因为他在写回主内存时会有一个比较并交互。

AtomicInteger atomicInteger = new AtomicInteger();//默认初始值为0
atomicInteger.getAndIncrement();//加一的操作

查看调用的getAndIncrement() 方法的源代码:

public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {//通过objectFieldOffset方法获取内存地址或者说是内存偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;public final int getAndIncrement() {//this是当前对象,valueOffset是内存地址,1是写死的value值return unsafe.getAndAddInt(this, valueOffset, 1);}

可以看到方法体内又通过调用Unsafe类中的getAndAddInt()方法。

源码分析:

//获取当前对象的地址值,var1是这个对象,var2的内存地址,相当于当前线程先从主内存中拷贝变量的值到自己的工作内存中,var5就是工作内存中的变量值
var5 = this.getIntVolatile(var1, var2)
//调用CAS方法类型是Int类型的,当前对象(var1)的地址的值(var2的值)和期望值(var5)相同,就将更新值(var5+var4)写回主内存
this.compareAndSwapInt(var1, var2, var5, var5 + var4)
//只有比较成功才可以写回

其实使用的是自旋锁。

接下来可以进一步了解CompareAndSwapInt方法:

而Unsafe类是jdk中rt.jar包下的

小总结:

18.2、CAS的缺点

第一个缺点:

第二个缺点:

第三个缺点:

19、AtomicReference原子引用

讲解了volatile不保证原子性,为解决原子性使用了AtomicInteger原子整型,解决了基本类型运算操作的原子性的问题,那我们自定义的实体类或者基本数据类型都要保证原子性呢?那就是使用AtomicReference原子引用。

AtomicReference原子引用直接上代码:

class User{String userName;int age;public User(String userName, int age) {this.userName = userName;this.age = age;}@Overridepublic String toString() {return "User{" +"userName='" + userName + '\'' +", age=" + age +'}';}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
public class Demo {//主线程main,程序入口public static void main(String[] args) {User user1 = new User("java_wxid",25);User user2 = new User("javaliao",22);AtomicReference<User> atomicReference = new AtomicReference<>();atomicReference.set(user1);System.out.println(atomicReference.compareAndSet(user1, user2)+"\t"+atomicReference.get().toString());new Thread(()->{System.out.println(atomicReference.compareAndSet(user1, user1)+"\t"+atomicReference.get().toString());},"a").start();}
}

控制台:

但是这不能解决CAS的ABA问题

ABA问题代码:

public class Demo {static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);public static void main(String[] args) {new Thread(()->{//执行ABA操作atomicReference.compareAndSet(100,101);atomicReference.compareAndSet(101,100);},"t1").start();new Thread(()->{try {//暂停一秒,保证t1线程完成了一次ABA操作Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(atomicReference.compareAndSet(100, 2019));System.out.println(atomicReference.get());},"t2").start();}
}

这中间肯定是有猫腻的,所以提供解决方案:

使用AtomicStampedReference版本号原子引用

只要T1的版本号弱于T2的线程版本号就需要更新,假设线程T1的第二个版本号的值为2019,而线程T2已经修改了二次了,版本号为3,那此时就不能那线程T2的版本号为2的进行比较并交换,需要重新将线程T3的版本号的值拷贝更新再进行操作。

public class Demo {static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);public static void main(String[] args) {System.out.println("===============解决ABA问题方案===============");new Thread(()->{//获取版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp+"\t 当前实际最新值:"+atomicStampedReference.getReference());try {//暂停一秒Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());},"t3").start();new Thread(()->{//获取版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);try {//暂停一秒,保证t3线程完成了一次ABA操作Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t 最新版本号:"+atomicStampedReference.getStamp()+"\t 当前t4的版本号是:"+stamp);System.out.println(Thread.currentThread().getName()+"\t 只有最新的版本号和t4的版本号一致时,才可以写回主内存,是否写回成功:"+atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1));System.out.println(Thread.currentThread().getName()+"\t 当前实际最新值:"+atomicStampedReference.getReference());},"t4").start();}
}

控制台:

这个时候就可以让t4线程去更新版本号为3的值100,解决了CAS只管结果不管过程的问题。

多线程进阶=》JUC并发编程相关推荐

  1. 多线程进阶=》JUC并发编程02

    在JUC并发编程01中说到了,什么是JUC.线程和进程.Lock锁.生产者和消费者问题.8锁现象.集合类不安全.Callable(简单).常用辅助类.读写锁 https://blog.csdn.net ...

  2. 基于《狂神说Java》JUC并发编程--学习笔记

    前言: 本笔记仅做学习与复习使用,不存在刻意抄袭. -------------------------------------------------------------------------- ...

  3. JUC并发编程(java util concurrent)(哔站 狂神说java juc并发编程 摘录笔记)

    JUC并发编程(java util concurrent) 1.什么是JUC JUC并不是一个很神秘的东西(就是 java.util 工具包.包.分类) 业务:普通的线程代码 Thread Runna ...

  4. 爬梯:JUC并发编程(三)

    学习资源整理自:B站<狂神说> 书接上回 JUC并发编程 12.CompletableFuture 异步回调 理解 父类:Future,对将来的某个事件的结果进行建模 可以用ajax进行理 ...

  5. 【尚硅谷】大厂必备技术之JUC并发编程——笔记总结

    [JUC并发编程01]JUC概述 关键字:进程和线程.进程和线程.wait和sleep.并发与并行.管程.用户线程和守护线程 [JUC并发编程02]Lock接口 关键字:synchronized.Lo ...

  6. ❤️《JUC并发编程从入门到高级》(建议收藏)❤️

    JUC并发编程 1.什么是JUC JUC的意思就是java并发编程工具包,与JUC相关的有三个包:java.util.concurrent.java.util.concurrent.atomic.ja ...

  7. Java JUC并发编程详解

    Java JUC并发编程详解 1. JUC概述 1.1 JUC简介 1.2 进程与线程 1.2 并发与并行 1.3 用户线程和守护线程 2. Lock接口 2.1 Synchronized 2.2 什 ...

  8. JUC并发编程小总结

    JUC是Java编发编程中使用的工具类,全称为java.util.concurrent.近期在大厂面试中屡屡被问到关于JUC的相关知识点问题,其重要性不言而喻,学好用好JUC可以说是每一个Java程序 ...

  9. 爬梯:JUC并发编程(一)

    学习资源整理自:B站<狂神说> JUC并发编程 1.基础概念 JUC 就是 java.util.concurrent java到底能否自己开启线程? 答案是否定的,在创建线程的底层,使用的 ...

最新文章

  1. Discovery studio画蛋白质构象叠合图
  2. 使用dplyr进行数据操作(30个实例)
  3. Python中fnmatch模块的使用
  4. VC 中字符串比较和查找
  5. 计算机视觉算法与应用清华大学,计算机视觉-清华大学.ppt
  6. XShell中浏览文件时上拉下拉
  7. 职场人必备的几个PPT进阶小技巧
  8. HTML 字符实体 lt; gt: amp;等
  9. mysql数据类型范围导致失败
  10. 覆盖原有div或者Input的鼠标移上去描述
  11. 检查版本是否需要更新的Demo
  12. 编译」(compile),与「反编译」(decompile)..哪些语言容易被反编译.
  13. mysql如何创建视图语句_创建视图的语句
  14. IE 浏览器重置方法
  15. 金字塔简单代码(java)
  16. beego 最新版本_iTunes 企业版停止更新,教你如何使用企业版与最新版
  17. !impotent的标准支持
  18. RAKsmart高防服务器防御形式解析
  19. re匹配中文格式的字符
  20. 小米Pro 安装Ubuntu,以及安装成功之后,各种关机重启的卡死问题

热门文章

  1. 万字长文之JDK1.8的LinkedList源码解析
  2. 摘录:Linux打Patch的方法
  3. 计算2个时间之间的间隔多长时间
  4. 老子云:近乎100%的还原,感觉自己玩到了端游
  5. android版本怎么升级6,android系统怎样从4.2升级到6.0
  6. e-mark认证标志是怎样的?
  7. vant indexbar 做城市列表
  8. 区块链开发公司解析区块链怎样与大数据完美结合
  9. 【TypeScript入门】TypeScript入门篇——模块
  10. Houdini----Python