JUC并发编程
脑图
1. 什么是JUC
源码 + 官方文档 面试高频问!
JUC就是java.util.concurrent
java.util 工具包、包、分类
业务:普通的线程代码 Thread
- Runnable 没有返回值、效率相比入 Callable 相对较低!
2、线程和进程
2.1 进程 / 线程是什么?
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
白话:
进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Q:Java默认有几个线程?
A: 2 个 mian、GC
线程:开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// native 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
2.2 并发 / 并行是什么?
并发不一定是同时的
并行一定是同时的
做并发编程之前,必须首先理解什么是并发,什么是并行。
并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不
同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生
(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于”同时执行”。
严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运
行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是
串行并发的,也会造成是多个任务并行执行的错觉。
实际上,如果系统内只有一个CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不
可能真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发
的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个CPU的系统中(多
核CPU)。
并发的动机:在计算能力恒定的情况下处理更多的任务, 就像我们的大脑, 计算能力相对恒定, 要在一天中
处理更多的问题, 我们就必须具备多任务的能力. 现实工作中有很多事情可能会中断你的当前任务, 处理这
种多任务的能力就是你的并发能力。
并行的动机:用更多的CPU核心更快的完成任务. 就像一个团队, 一个脑袋不够用了, 一个团队来一起处理
一个任务。
例子:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是
同时的)
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
所以并发编程的目标是
充分的利用处理器的每一个核,以达到最高的处理性能
。
2.3 线程的状态
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
阻塞和等待的区别
1、一句话概括
在java中,线程阻塞状态是线程本身不可计划的,而线程等待状态是线程本身计划之内的。
2、相同点与不同点
相同点:
(1)都会暂停线程的执行。
区别点:
(1)线程进入阻塞状态是被动的, 而线程进入等待状态是主动的。
阻塞状态的被动:线程在同步代码外,获取对象锁失败时,线程进入阻塞状态;何时获取对象锁失败不可知,即线程阻塞状态是线程本身不可计划的。
等待状态的主动:线程在同步代码内,等待其他线程操作时,线程接入等待状态;何时等待其他线程操作可知,即线程等待状态是线程本身计划之内的。
2.4 wait和sleep的区别
1、来自不同的类
这两个方法来自不同的类分别是,sleep来自Thread类,wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2、关于锁的释放
最主要是sleep方法没有释放锁,而wait方法释放了锁
,使得其他线程可以使用同步控制块或者方法。sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。
sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify。
3、使用范围不同
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
//或者wait()
x.notify()
}
4、是否需要捕获异常
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
3、Lock锁(重点)
3.1 传统的 synchronized
package org.example.juc.demo01;
/**
* 真正的多线程开发,公司中的开发,降低耦合性
* 线程就是一个单独的资源类,没有任何附属操作
* 1. 属性 方法
*/
public class SaleTicketDemo01 {
public static void main(String[] args) {
// 并发编程:多线程操作同一个资源类,把资源类扔进线程
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket {
//属性方法
private int number = 20;
//卖票
// synchronized 本质:队列,锁
public synchronized void sale(){
if (number > 0){
System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张票,剩余:"+number);
}
}
}
3.2 Lock接口
文档中建议使用try catch finally , unlock在finally中
ReentrantLock类
可重入锁
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (ReentrantLock和synchronized默认)
package org.example.juc.demo01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 真正的多线程开发,公司中的开发,降低耦合性
* 线程就是一个单独的资源类,没有任何附属操作
* 1. 属性 方法
*/
public class SaleTicketDemo02 {
public static void main(String[] args) {
// 并发编程:多线程操作同一个资源类,把资源类扔进线程
Ticket2 ticket = new Ticket2();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket2 {
//属性方法
private int number = 20;
private Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); //加锁
try {
if (number > 0){
System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张票,剩余:"+number);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();//解锁
}
}
}
3.3 Synchronized 和 Lock 区别
- Synchronized 内置的Java关键字, Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去(tryLock尝试获取锁);
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
锁是什么,如何判断锁的是谁!
4、生产者和消费者问题
面试的:单例模式、排序算法、生产者和消费者、死锁
线程间的通信 , 线程之间要协调和调度
4.1生产者和消费者 Synchronized 版
package org.example.juc.demo02;
/**
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* 线程交替执行 A B操作同一个资源
* A num += 1
* B num -= 1
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
}
}
//数字 资源类
//判断是否等待-->业务-->通知
class Data{
private int number= 10;
//+1
public synchronized void increment() throws InterruptedException {
if( number != 0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来消费,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if( number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来生产,我-1完毕了
this.notifyAll();
}
}
4.2 虚假唤醒问题
如果这时候创建四个线程,两个生产两个消费,就会发生虚假唤醒
问题
if 改为 while 判断
Q:为什么用if会发生虚假唤醒问题?
A:拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。
(重点) wait方法应该永远放在while循环中
package org.example.juc.demo02;
/**
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* 线程交替执行 A B操作同一个资源
* A num += 1
* B num -= 1
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"D").start();
}
}
//数字 资源类
//判断是否等待-->业务-->通知
class Data{
private int number= 0;
//+1
public synchronized void increment() throws InterruptedException {
while ( number != 0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来消费,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while ( number == 0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来生产,我-1完毕了
this.notifyAll();
}
}
4.3 JUC版生产者和消费者写法
Lock中使用await 和 signal替换 wait 和notify
package org.example.juc.demo02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* 线程交替执行 A B操作同一个资源
* A num += 1
* B num -= 1
*/
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"D").start();
}
}
//数字 资源类
//判断是否等待-->业务-->通知
class Data2 {
private int number= 0;
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock(); //加锁
try {
//业务
while ( number != 0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来消费,我+1完毕了
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock(); //解锁
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock(); //加锁
try {
//业务
while ( number == 0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他进程来生产,我-1完毕了
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock(); //解锁
}
}
}
此时线程是随机的状态,如何让他有序执行呢?
4.4 精确通知顺序访问
通过多个Condition来控制
package org.example.juc.demo02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程之间顺序执行
* A执行完调用B,B执行完调用C
*/
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
//资源类
class Data3{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; //1-A执行 2-B执行 3-C执行
public void printA(){
lock.lock();
try {
while (number!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "执行");
number = 2;
//通知 唤醒指定的人 B
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (number!=2){
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "执行");
number = 3;
//通知 唤醒指定的人 C
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (number!=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "执行");
number = 1;
//通知A 进行新一轮
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5、8锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
1、标准访问,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized修饰的方法,锁的对象是方法的调用者
。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。
2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4); //add
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized修饰的方法,锁的对象是方法的调用者
。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行
,第二个方法只有在第一个方法执行完释放锁之后才能执行。
3、新增一个普通方法hello()没有同步,请问先打印邮件还是hello?
/**
* 多线程的8锁
* 1、标准访问,请问先打印邮件还是短信?
* 2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
* 3、新增一个普通方法hello()没有同步,请问先打印邮件还是hello?
*/
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.hello();
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
//不受锁的影响
public void hello(){
System.out.println("Hello");
}
}
//Hello
//sendEmail
//sendSMS
结论:新增的方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。其他线程共用了一把锁,所以还需要等待。
4、两部手机、请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
public void hello(){
System.out.println("Hello");
}
}
//sendSMS
//sendEmail
结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public static synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:static锁的不是对象,锁的是Class
. 被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。
6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
package org.example.juc.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public static synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,即便用了两个不同的对象调用方法,两个方法用的还是同一个锁,后调用的方法需要等待先调用的方法。
7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendSMS
//sendEmail
结论:被synchronized和static修饰的方法,锁的对象是类的class对象
。仅仅被synchronized修饰的方法,锁的对象是方法的调用者
。因为两个方法锁的对象不是同一个
,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
注意:class被锁了不会影响这个类的对象
8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
package org.example.juc.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendSMS
//sendEmail
结论:被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
9. ReentrantLock 锁的对象
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
private Lock lock = new ReentrantLock();
private Lock lock2 = new ReentrantLock();
public void sendEmail() throws Exception{
lock.lock();
try {
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void sendSMS() throws Exception{
lock2.lock();
try {
System.out.println("sendSMS");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock2.unlock();
}
}
}
//SMS
//EMAIL
ReentrantLock 锁的不是当前的Phone类或者Phone对象
而是ReentrantLock对象
比如例子中用两把锁分别锁EMAIL和SMS,就没法锁住
小结
new this 具体的一个手机
static class 唯一的一个模板
一个对象里面如果有多个synchronized方法,某个时刻内,只要一个线程去调用其中一个synchronized方法了,其他的线程都要等待,换句话说,在某个时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法
加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁,情况立刻变化
都换成静态同步方法后,情况又变化了。所有的非静态的同步方法用的都是同一把锁----实例对象本身synchronized实现同步的基础:java中的每一个对象都可以作为锁
具体的表现为以下三种形式:
对于普通同步方法,锁的是当前实例对象
对于静态同步方法,锁的是当前的Class对象。
对于同步方法块,锁是synchronized括号里面的配置对象
当一个线程试图访问同步代码块时,他首先必须得到锁,退出或者是抛出异常时必须释放锁,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可以是别的实例对象非非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以必须等待该实例对象已经获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁----类对象本身这两把锁的是两个不同的对象,所以静态的同步方法与非静态的同步方法之间是不会有竞争条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们用一个的是同一个类的实例对象。
6、集合类不安全
6.1 list 不安全
public class ListTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}).start();
}
}
}
java.util.ConcurrentModificationException 并发修改异常
6.2 写入时复制(CopyOnWrite)思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
CopyOnWriteArrayList为什么并发安全且性能比Vector好
CopyOnWriteArrayList 读没加锁 Vector 增删改查都加锁了
我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降
,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。
public class ListTest {
public static void main(String[] args) {
//并发下ArrayList不安全的解决方案
/**
* 解决方案:
* 1. List<String> list = new Vector<>();
* 2. List<String> list = Collections.synchronizedList(new ArrayList<>()); Collections工具类下
* 3. List<String> list = new CopyOnWriteArrayList<>();
*/
//CopyOnWrite 写入时复制 COW
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}).start();
}
}
}
6.3 set 不安全
public class SetTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
Exception in thread “Thread-4” java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
/**
* 解决方法
* 1. Collections 工具类
* Set<String> set = Collections.synchronizedSet(new HashSet<String>());
* 2.Set<String> set = new CopyOnWriteArraySet<>();
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
hashset底层是什么
hashset底层就是hashMap
,只用了HashMap的Key
add方法 就是map的put方法
6.4 map 不安全
hashMap底层是数组+链表+红黑树
Map<String,String> map = new HashMap<>();
// 等价于 容量 加载因子
Map<String,String> map = new HashMap<>(16,0.75);
// 工作中,常常会自己根据业务来写参数,提高效率
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName().toString(),
UUID.randomUUID().toString().substring(0,5).toString());
System.out.println(map);
}).start();
}
}
}
java.util.ConcurrentModificationException
public class MapTest {
public static void main(String[] args) {
/**
* 解决方法
* 1.Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* 2.Map<String, String> map = new ConcurrentHashMap<>(); 名字不再是CopyOnWrite开头
*/
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName().toString(),
UUID.randomUUID().toString().substring(0,5).toString());
System.out.println(map);
}).start();
}
}
}
7、Callable ( 简单 )
多线程中,第3种获得多线程的方式,Callable。它与Runnable有什么区别呢?
- 有返回值
- 可以抛异常
- 方法不一样,一个是call,一个是run
泛型的参数类型等于方法的返回值类型
FutureTask是 Runnable接口的实现类,所以我们可以拿到FutureTask传入new Thread()中
在FutureTask类中,有两个构造方法,分别能与Callable和Runnable连接
所以通过Futuretask传入一个Callable就能创建实现Runnable接口的Futuretask对象
package org.example.juc.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new Thread(new Runnable()).start();
//new Thread(new FutureTask<V>()).start();
//new Thread(new FutureTask<V>( Callable )).start();
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); //结果会被缓存,只执行一次
//获得返回值 get方法可能会产生堵塞
Integer integer = futureTask.get();
System.out.println(integer);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("callable");
return 666;
}
}
8、常用辅助类(必会)
8.1、CountDownLatch
减法计数器,能保证所有线程执行完再继续执行
原理:
- CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
- 其他线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会阻塞)
- 当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行
//计数器
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
//里面的执行顺序我不关心,等里面全都跑完了,我就关门
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "走了");
countDownLatch.countDown(); //-1
}, String.valueOf(i)).start();
}
countDownLatch.await();// 等待计数器归零然后向下执行
System.out.println("关门");
}
}
8.2、CyclicBarrier
加法计数器 调用等待的个数到达目标数字后开启一个线程
翻译:CyclicBarrier 篱栅
作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会!
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
//召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
//lambda拿不到i,但是能拿到final的类型
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集了第" + temp + "颗龙珠");
try {
cyclicBarrier.await(); //等待,到达指定数量后运行cyclicBarrier
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
8.3、Semaphore
翻译:Semaphore 信号量;信号灯;信号
作用:抢车位
原理:
在信号量上我们定义两种操作:
- acquire(获取)
- 当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1)
- 要么一直等下去,直到有线程释放信号量,或超时
- release (释放)
- 实际上会将信号量的值 + 1,然后唤醒等待的线程。
信号量主要用于两个目的:一个是用于多个共享资源的互斥使用
,另一个用于并发线程数
的控制。
package org.example.juc.demo03;
import com.sun.org.apache.bcel.internal.generic.ATHROW;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @author QiuYuSY
* @create 2022-12-19 23:29
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位 限流的时候用 acquire的线程同时只能存在3个
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
// acquire 获得
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
semaphore.release();
}
// release 释放
},String.valueOf(i)).start();
}
}
}
1抢到车位
2抢到车位
3抢到车位
2离开车位
3离开车位
4抢到车位
5抢到车位
1离开车位
6抢到车位
4离开车位
6离开车位
5离开车位
9、读写锁
读的时候可以多个线程同时读
写的时候只能有一个线程写
package org.example.juc.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 完成多线程可以同时读,只能一个线程写
* ReadWriteLock 中有读锁和写锁
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多线程可以同时占有
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
//写入
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <=5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
*/
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存,写 希望只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock(); //加写锁
try {
System.out.println(Thread.currentThread().getName() + "写入"+ key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入完毕");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
readWriteLock.writeLock().unlock();
}
}
//取,读 读锁共享锁可保证并发读是非常高效的
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取完毕");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
readWriteLock.readLock().unlock();
}
}
}
注意: 读锁和写锁是互斥的,就是说读的时候不能写,写的时候不能读
独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独
占锁。
共享锁(读锁):该锁可被多个线程所持有。
对于ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效
的。
- 写—写 互斥不共存 写的时候不能写
- 写—读
互斥不共存
读的时候不能写,写的时候不能读- 读–读 不互斥共享 读的时候可以读
10、阻塞队列
阻塞队列的用处:
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自
动被唤起。
为什么需要 BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都
给你一手包办了。
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要
兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
接口架构图
ArrayBlockingQueue
四组API
检查方法返回的是处于队列首部的元素
抛出异常
public static void main(String[] args) {
ArrayBlockingQueue blockQueue = new ArrayBlockingQueue<>(3);
blockQueue.add("a");
blockQueue.add("b");
blockQueue.add("c");
//add java.lang.IllegalStateException: Queue full 异常
//blockQueue.add(4);
System.out.println(blockQueue.element()); //a
System.out.println(blockQueue.remove());
System.out.println(blockQueue.remove());
System.out.println(blockQueue.remove());
//remove java.util.NoSuchElementException异常
//System.out.println(blockQueue.remove());
}
返回true/false
不会不异常
public static void main(String[] args) {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a")); // true
System.out.println(blockingQueue.offer("b")); // true
System.out.println(blockingQueue.offer("c")); // true
System.out.println(blockingQueue.offer("d")); // false
System.out.println(blockingQueue.peek()); // 检测队列队首元素!
// public E poll()
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println(blockingQueue.poll()); // null
}
一直阻塞
public static void main(String[] args) throws InterruptedException {
// 队列大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
// 一直阻塞
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("d"); //卡住
System.out.println(blockingQueue.take()); // a
System.out.println(blockingQueue.take()); // b
System.out.println(blockingQueue.take()); // c
System.out.println(blockingQueue.take()); //
}
超时阻塞
public static void main(String[] args) throws InterruptedException {
// 队列大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
blockingQueue.offer("d",3L, TimeUnit.SECONDS); // 等待3秒超时退出
System.out.println("====3秒后===");
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println(blockingQueue.poll(3L,TimeUnit.SECONDS)); // 阻塞不停止等待
}
SynchronousQueue 同步队列
SynchronousQueue 容量为1。
与其他的 BlockingQueue 不同,SynchronousQueue是一个不存储元素的 BlockingQueue 。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put
1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+" put
2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+" put
3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
}
}
11、线程池(重点)
3大方法 7大参数 4中拒绝策略
11.1 池化技术
程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源
是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段。
通过学习线程池原理,明白所有池化技术的基本设计思路。遇到其他相似问题可以解决。
池化技术
前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?
池化技术简单点来说,就是提前保存大量的资源
,以备不时之需。在机器资源有限的情况下,使用池化
技术可以大大的提高资源的利用率,提升性能等。
在编程领域,比较典型的池化技术有:
线程池、连接池、内存池、对象池等。
主要来介绍一下其中比较简单的线程池的实现原理,希望读者们可以举一反三,通过对线程池的理解,
学习并掌握所有编程中池化技术的底层原理。
我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核
CPU。当一个任务结束,当前线程就接收。
但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成 很大的性能开销
。(普通线程结束后就销毁了没法再次start)
那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。
这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接
获取,避免多次重复创建、销毁
带来的开销。
11.2 为什么使用线程池
10 年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球 ,CPU 需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:
线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这
些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中
取出任务来执行。
它的主要特点为:线程复用
,控制最大并发数
,管理线程
。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一分配,调优和监控。
11.3 线程池的三大方法
Java中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors,
ExecutorService,ThreadPoolExecutor 这几个类。
三大方法说明:
Executors.newFixedThreadPool(int)
- 执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程。
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "结束");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
//最多同时5个线程在运行
pool-1-thread-1执行
pool-1-thread-5执行
pool-1-thread-4执行
pool-1-thread-3执行
pool-1-thread-2执行
pool-1-thread-4结束
pool-1-thread-5结束
pool-1-thread-1结束
pool-1-thread-3结束
pool-1-thread-1执行
pool-1-thread-4执行
pool-1-thread-2结束
pool-1-thread-3执行
pool-1-thread-5执行
pool-1-thread-2执行
pool-1-thread-5结束
pool-1-thread-4结束
pool-1-thread-3结束
pool-1-thread-2结束
pool-1-thread-1结束
Executors.newSingleThreadExecutor()
- 最多只能有一个线程运行
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
}
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
Executors.newCachedThreadPool();
- 执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。
可扩容,遇强则强
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
11.4 ThreadPoolExecutor 七大参数
操作:查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor
( 7 个参数 )
//本质都是调用了 `new ThreadPoolExecutor`
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(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小(最多线程数)
long keepAliveTime, //超时没人调用就释放
TimeUnit unit, //超时的单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工程 创建线程一半不用动
RejectedExecutionHandler handler //拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数理解:
corePollSize
:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。maximumPoolSize
:最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。keepAliveTime
:空闲的线程保留的时间。TimeUnit
:空闲线程的保留时间单位。BlockingQueue< Runnable>
:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。ThreadFactory
:线程工厂,用来创建线程,一般默认即可RejectedExecutionHandler
:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
线程池用哪个?生产中如何设置合理参数
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多? 坑
答案是一个都不用,我们工作中只能使用自定义的;
Executors 中
ThreadPoolExecutor 底层工作原理
业务例子
-
银行分前台和候客区,前台平时只开两个窗口1和2,如果1和2窗口已经有人了,后进来的客人需要到候客区
进行排队. -
有一天,来的客人特别多,导致候客区也满了,这时候银行就要打开3 4 5窗口来处理客人
-
这时候又有客人进来了,如果12345窗口都满了,候客区也满了,新的客人要么走要么等这就是拒绝策略
-
平时开的1和2窗口就是
核心线程池大小
-
候客区的人满了开的3 4 5就是
最大核心线程池大小
-
如果客人处理完业务都走了,一段时间没有人来处理业务,那么就关闭这个线程 这就是
超时等待
-
候客区就是
阻塞队列
也可以这样理解
举例:8个人进银行办理业务
1、1~2人被受理(核心大小core)
2、3~5人进入队列(Queue)
3、6~8人到最大线程池(扩容大小max)
4、再有人进来就要被拒绝策略接受了
-
在创建了线程池后,开始等待请求。
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务:
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列:
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非
核心线程立刻运行这个任务; - 如果队列满了且正在运行的线程数量大于或等于1Size,那么线程池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepA1iveTime)时,线程会判断:
- 如果当前运行的线程数大于coreP佣1Size,那么这个线程就被停掉。
- 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
手动创建线程池
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //默认核心数
5, //最大核心数
3L, //多久关闭窗口
TimeUnit.SECONDS, //时间单位
new LinkedBlockingQueue<>(3), //阻塞队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() //有四种拒绝策略
);
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
11.5 四种拒绝策略
可以看到这个handler有四个实现类
-
AbortPolicy
(默认): 银行满了,还有人进来,不处理这个人的,并抛出异常
-
DiscardPolicy
: 队列满了丢任务,不抛出异常 -
CallerRunsPolicy
: 哪来的回哪去,这里因为是main调用的,所有会执行main线程
-
DiscardOldestPolicy
: 队列满了,将最早进入队列的任务删,之后再尝试加入队列
11.6小结和拓展
池的最大的大小如何去设置!
了解:IO密集型,CPU密集型:(调优)
CPU密集型 : 几核几个线程
IO密集型 : 线程数设置为大于你程序中十分耗IO的线程
一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu
核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间
的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在
CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的
话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比
方说多线程网络传输,多线程往不同的目录写文件,等等。
此时 线程数等于IO任务数是最佳的。
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 > 判断你程序中十分耗IO的线程,
// 程序 15个大型任务 io十分占用资源!
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),//获取电脑几个核心
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和
最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
12、四大函数式接口(重点必需掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口: 只有一个方法的接口
//我们熟悉的Runnable就是函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 泛型、枚举、反射
// lambda表达式、链式编程、函数式接口、Stream流式计算
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用!
// foreach(消费者类的函数式接口)
函数型接口 Function
传入一个参数,返回一个参数,泛型中规定参数的类型
public static void main(String[] args) {
// 函数式接口,可以改为 lambda 表达式
//Function<String,Integer> function = new Function<String, Integer>() {
// @Override
// public Integer apply(String s) {
// return 1024;
// }
//};
// 简写
Function<String,Integer> function = s->{return s.length();};
System.out.println(function.apply("abc"));
}
断定型接口 Predicate
有一个输入参数,返回只有布尔值
。
public static void main(String[] args) {
Predicate<Integer> function = integer -> {
if(integer > 10){
return true;
}else{
return false;
}
};
System.out.println(function.test(11)); //true
System.out.println(function.test(5)); //false
}
消费者接口 Comsumer
有一个输入参数,没有返回值
public static void main(String[] args) {
Consumer<String> function = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Consumer<String> function2 = (s)->{
System.out.println(s);
};
function.accept("qiuyu");
function2.accept("qiuyu2");
}
供给者接口 Supplier
没有输入参数,只有返回参数
public static void main(String[] args) {
Supplier<String> function = new Supplier<String>() {
@Override
public String get() {
return "qiuyu";
}
};
Supplier<String> function2 = ()->{
return "qiuyu2";
};
System.out.println(function.get());
System.out.println(function2.get());
}
13、Stream流式计算
链式编程、流式计算、lambda表达式,现在的 Java程序员必会!
流(Stream)到底是什么呢?
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算!”
特点:
- Stream 自己不会存储元素。
- Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
package org.example.juc.bq;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class Demo02 {
public static void main(String[] args) {
/*
* 题目:请按照给出数据,找出同时满足以下条件的用户
* 也即以下条件:
* 1、全部满足偶数ID
* 2、年龄大于24
* 3、用户名转为大写
* 4、用户名字母倒排序
* 5、只输出一个用户名字 limit
**/
User2 u1 = new User2(11, "a", 23);
User2 u2 = new User2(12, "b", 24);
User2 u3 = new User2(13, "c", 22);
User2 u4 = new User2(14, "d", 28);
User2 u5 = new User2(16, "e", 26);
List<User2> list = Arrays.asList(u1, u2, u3, u4, u5);
/*
* 1. 首先我们需要将 list 转化为stream流
* 2. 然后将用户过滤出来,这里用到一个函数式接口Predicate<? super T>,我们可
以使用lambda表达式简化
* 3. 这里面传递的参数,就是Stream流的泛型类型,也就是User,所以,这里可以直接
返回用户id为偶数的用户信息;
* 4. 通过forEach进行遍历,直接简化输出 System.out::println ,等价于
System.out.println(u);
*/
//链式编程
list.stream()
.filter(u->{return u.getId() % 2 == 0;}) //Predicate
.filter(u->{return u.getAge() > 24;})
.map(u->{return u.getName().toUpperCase();}) //Function接口
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//Comparator
.limit(1)
.forEach(System.out::println); //Consumer
}
}
class User2{
private int id;
private String name;
private int age;
public User2() {
}
public User2(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User2{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
14、分支合并
什么是ForkJoin
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任
务,最终汇总每个小任务的结果得到这个大任务的结果。
这种思想和MapReduce很像(input --> split --> map --> reduce --> output)
主要有两步:
第一、任务切分
第二、结果合并
它的模型大致是这样的:线程池中的每个线程都有自己的工作队列
(PS:这一点和ThreadPoolExecutor
不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当
自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
工作窃取
另外,forkjoin有一个工作窃取的概念。简单理解,就是一个工作线程下会维护一个包含多个子任务的双
端队列。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度
较快,很快就把所有任务消耗完了。那这个时候怎么办呢,总不能空等着吧,多浪费资源啊。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如
下
那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程
间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里
的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的
任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干
活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减
少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列
,被窃取任务线程永远从双端队列
的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还
是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个
双端队列。
于是,先做完任务的工作线程会从其他未完成任务的线程尾部
依次获取任务去执行。这样就可以充分利
用CPU的资源。这个非常好理解,就比如有个妹子程序员做任务比较慢,那么其他猿就可以帮她分担一
些任务,这简直是双赢的局面啊,妹子开心了,你也开心了。
核心类
ForkJoinPool
WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃
取。
什么叫线程的任务窃取呢?就是说你和你的一个伙伴一起吃水果,你的那份吃完了,他那份没吃完,那
你就偷偷的拿了他的一些水果吃了。存在执行2个任务的子线程,这里要讲成存在A,B两个个
WorkQueue在执行任务,A的任务执行完了,B的任务没执行完,那么A的WorkQueue就从B的
WorkQueue的ForkJoinTask数组中拿走了一部分尾部的任务来执行,可以合理的提高运行和计算效率。
每个线程都有一个WorkQueue,而WorkQueue中有执行任务的线程(ForkJoinWorkerThread
owner),还有这个线程需要处理的任务(ForkJoinTask<?>[] array)。那么这个新提交的任务就是加
到array中。
ForkJoinTask
ForkJoinTask代表运行在ForkJoinPool中的任务。
主要方法:
- fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
- join() 当任务完成的时候返回计算结果。
- invoke() 开始执行任务,如果必要,等待计算完成。
子类: Recursive :递归
- RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
- RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
代码验证
package org.example.juc.forkjoin;
import java.util.concurrent.RecursiveTask;
//计算1-10000000000的和
public class ForkJoinTest extends RecursiveTask<Long> {
private Long start;//起始值
private Long end;//结束值
public static final Long critical = 10000L;//临界值
public ForkJoinTest(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
//判断是否是拆分完毕
Long lenth = end - start;
//小于临界值就不拆分
if(lenth<=critical){
//如果拆分完毕就相加
Long sum = 0L;
for (Long i = start;i<=end;i++){
sum += i;
}
return sum;
}else {
//没有拆分完毕就开始拆分
Long middle = (end + start)/2;//计算的两个值的中间值
ForkJoinTest right = new ForkJoinTest(start,middle);
right.fork();//拆分,并压入线程队列
ForkJoinTest left = new ForkJoinTest(middle+1,end);
left.fork();//拆分,并压入线程队列
//合并
return right.join() + left.join();
}
}
}
三种测试
package org.example.juc.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
/**
* @author QiuYuSY
* @create 2022-12-20 22:44
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
//普通
public static void test1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 0; i < 10_0000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
//forkjoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
//生成任务
ForkJoinTask<Long> task = new ForkJoinTest(1L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task); //提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
//stream 并行流 比上面快几十倍
public static void test3(){
long start = System.currentTimeMillis();
//parallel() 并行
long sum = LongStream.rangeClosed(0L, 10_0000_1000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
}
打个比方,假设一个酒店有400个房间,一共有4名清洁工,每个工人每天可以打扫100个房间,这样,4
个工人满负荷工作时,400个房间全部打扫完正好需要1天。
Fork/Join的工作模式就像这样:首先,工人甲被分配了400个房间的任务,他一看任务太多了自己一个
人不行,所以先把400个房间拆成两个200,然后叫来乙,把其中一个200分给乙。
紧接着,甲和乙再发现200也是个大任务,于是甲继续把200分成两个100,并把其中一个100分给丙,
类似的,乙会把其中一个100分给丁,这样,最终4个人每人分到100个房间,并发执行正好是1天。
15、异步回调 Future
15.1概述
Future设计的初衷:对将来某个时刻会发生的结果进行建模。
当我们需要调用一个函数方法时。如果这个函数执行很慢,那么我们就要进行等待。但有时候,我们可能并
不急着要结果。
因此,我们可以让被调用者立即返回,让他在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些
其他任务,在真正需要数据的场合再去尝试获取需要的数据。
它建模了一种异步
计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在
Future中出发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要
等待耗时的操作完成。
Future的优点:比更底层的Thread更易用。要使用Future,通常只需要将耗时的操作封装在一个
Callable对象中,再将它提交给ExecutorService。
为了让程序更加高效,让CPU最大效率的工作,我们会采用异步编程。首先想到的是开启一个新的线程
去做某项工作。再进一步,为了让新线程可以返回一个值,告诉主线程事情做完了,于是乎Future粉墨
登场。然而Future提供的方式是主线程主动问询新线程,要是有个回调函数就爽了。所以,为了满足
Future的某些遗憾,强大的CompletableFuture随着Java8一起来了。
类似于AJAX
15.2 实例
package org.example.juc.demo04;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的 runAsync 异步调用
CompletableFuture<Void> completableFuture =
CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "没有返回, update mysql ok");
});
System.out.println("111111"); // 先执行
completableFuture.get();
System.out.println();
//有返回值的 供给型参数接口
CompletableFuture<Integer> completableFuture2 =
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() +
"completableFuture2");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture2.whenComplete((t, u) -> {
//编译完成,正常结束输出
System.out.println("编译正常t:" + t); //正常结果
System.out.println("编译异常u:" + u); //报错的信息
}).exceptionally(e -> {
//结果异常,执行下面的
System.out.println("结果异常exception:" + e.getMessage());
return 555;
}).get());
}
}
16、JMM
问题:请你谈谈你对volatile的理解
volatile
是 Java 虚拟机提供的轻量级的同步机制,三大特性:
保证可见性
不保证原子性
禁止指令重排
16.1 什么是JMM
JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范~
JMM 关于同步的规定:
1、线程解锁前
,必须立刻
把共享变量的值刷新回主内存
2、线程加锁前
,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
JMM即为JAVA 内存模型
(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝
,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
16.2 JMM的内存模型
线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用
Volatile
关键字即可!volatile 保证线程间变量的可见性
,
简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,
更详细地说是要符合以下两个规则 :
- 线程对变量进行修改之后,要立刻回写到主内存。
- 线程对变量读取的时候,要从主内存中读,而不是缓存。
各线程的工作内存间彼此独立,互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,
不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的
副本,即,为了提高执行效率。
16.3 内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的
(对于double和long类
型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不
安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分
析。更多的时候,使用java的happen-before规则
来进行分析。
happens-before字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?
不准确!在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。 讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
我们再来看看为什么需要这几条规则?
因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。详情可以看我之前的文章
面试官:你知道并发Bug的源头是什么吗?
所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器
还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了
//这个程序不会停下来,因为线程A不知道num已经被改成1了
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
},"A").start();
//保证线程开启了
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//想要让线程A知道我们主线程修改了num的值从而停下来
num = 1;
}
17. Volatile
保证可见性
volatile
是 Java 虚拟机提供的轻量级的同步机制,三大特性:
保证可见性
不保证原子性
禁止指令重排
//添加volitile后A线程能够看到主线程对num进行了修改
public class JMMDemo {
//不加volatile就会死循环
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
},"A").start();
//保证线程开启了
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//想要让线程A知道我们主线程修改了num的值从而停下来
num = 1;
}
}
不保证原子性
原子性理解:
不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需
要整体完整,要么同时成功,要么同时失败。
package org.example.juc.jmm;
public class VDemo {
//volatile 保证不了原子性 synchronized和lock能保证
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
// 理论上结果为20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
//add在汇编中并不是一条语句,不具备原子性
add();
}
}).start();
}
while (Thread.activeCount() > 2){ // main gc
Thread.yield();
}
System.out.println(num); //17114
}
}
因为我们的 add 方法没有加锁,但是加了 volatile ,说明 volatile 不能保证原子性;画图解释,数值被
覆盖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BLEbNSyC-1671632512787)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/202212210057744.png)]
不用synchronized和lock如何能保证原子性
使用java.util.concurrent.atomic下的类
package org.example.juc.jmm;
import java.util.concurrent.atomic.AtomicInteger;
public class VDemo {
//volatile 保证不了原子性 synchronized和lock能保证
//使用AtomicInteger保证原子性
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();//+1
}
public static void main(String[] args) {
// 理论上结果为20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2){ // main gc
Thread.yield();
}
System.out.println(num); //17114
}
}
AtomicInteger这些类内部使用了unsafe类,这个在cas中讲
禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排理解测试1:
public class TestHappensBefore {
public static void main(String[] args) {
int x = 11; // 语句1
int y = 12; // 语句2
x = x + 5; // 语句3
y = x * x; // 语句4
}
// 指令顺序预测: 1234 2134 1324
// 问题:请问语句4可以重排后变成第一条吗? 答案:不可以
}
重排理解测试2:
// 多线程环境中线程交替执行,由于编译器优化重排的存在
// 两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
public class TestHappensBefore {
int a = 0;
boolean flag = false;
public void m1(){
a = 1; // 语句1
flag = true; // 语句2
}
public void m2(){
if (flag){
a = a + 5; // 语句3
System.out.println("m2=>"+a);
}
}
}
指令重排小结:
volatile 实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障
(Memory Barrier)又称内存栅栏,是一个CPU 指令,它的作用有两个:
1、保证特定操作的执行顺序。
2、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器
和CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说,通过插入内存屏障禁止
在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此
任何CPU上的线程都能读取到这些数据的最新版本。
保证了可见性
经过,可见性,原子性,指令重排的话,线程安全性获得保证:
- 工作内存与主内存同步延迟现象导致的可见性问题,可以使用 synchronized 或 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
- 对于指令重排导致的可见性问题 和 有序性问题,可以利用 volatile 关键字解决,因为 volatile 的另外一个作用就是禁止重排序优化。
volatile在懒汉式的单例模式中使用,所以接下来聊聊单例模式
18、深入单例模式
单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东西,比如 多线程是否安全,是否懒加载,性能等等。还有你知道几种单例模式的写法呢?如何防止反射破坏单例模式?今天,我们来探究单例模式。
关于单例模式的概念,在这里就不在阐述了,相信每个小伙伴都了如指掌。我们直接进入正题:
18.1 饿汉式
public class Hungry {
public Hungry() {
}
//一开始就把所有资源都加载完,可能会造成浪费资源
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉式是最简单的单例模式的写法,保证了线程的安全
,在很长的时间里,我都是饿汉模式来完成单例的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:
public class Hungry {
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() {
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模式:懒汉式。
18.2 DCL懒汉式
正常的懒汉 存在多线程问腿
public class LazyMan {
public LazyMan() {
System.out.println(Thread.currentThread().getName());
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
多加一层检测可以避免问题,也就是DCL懒汉式
!
public class LazyMan {
public LazyMan() {
System.out.println(Thread.currentThread().getName());
}
//volatile防止指令重排
private volatile static LazyMan lazyMan;
//双重检测锁模式 懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan();
/**
* 1. 分配内存空间
* 2. 执行构造方法,初始化对象
* 3. 把这个对象指向这个空间
*
* 可能发生指令重排 我们需要给lazyMan加上volatile
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
这是因为编译器在编译时会进行指令重排,而 volatile 可以禁止指令重排。
对象的创建大概有这么三个步骤(从指令层面来看啊,从JVM来讲具体还有很多步骤):
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
正常是按 123 的顺序执行,但由于指令重排的存在,可能会存在 A 线程按照132 执行,当执行到 3 时,线程 B 来取对象,就会得到空值。因此必须给单例对象加上volatile
关键字。
18.3 静态内部类
还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.holder;
}
private static class InnerClass {
private static final Holder holder = new Holder();
}
}
18.4 万恶的反射
万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面
newInstance,破坏我们辛辛苦苦写的单例模式。
try {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor =
LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
System.out.println(lazyMan1 == lazyMan2); //false
} catch (Exception e) {
e.printStackTrace();
}
我们可以在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常!
public LazyMan() {
synchronized (LazyMan.class){
if(lazyMan != null){
throw new RuntimeException("别想用反射破环");
}
}
System.out.println(Thread.currentThread().getName());
}
但是这种写法还是有问题:
上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有
构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方
法,一上来就直接用反射创建对象,我们的判断就不生效了:
public static void main(String[] args) {
try {
Constructor<LazyMan> declaredConstructor =
LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//两个都用反射
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
System.out.println(lazyMan1 == lazyMan2); //false
} catch (Exception e) {
e.printStackTrace();
}
}
那么如何防止这种反射破坏呢?因为两次都会走构造器.我们可以利用标志位,在构造器中进行判断
public LazyMan() {
synchronized (LazyMan.class){
if(qiuyu == false){
qiuyu = true;
}else{
throw new RuntimeException("别想用反射破坏");
}
}
System.out.println(Thread.currentThread().getName());
}
但是还是能够通过反汇编得到qiuyu的变量名然后反射修改这个遍历的值
并没有一个很好的方案去避免反射破坏单例模式,所以轮到我们的枚举登场了。
18.5 枚举(推荐)
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类
(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性
以及便捷性。
package org.example.juc.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE; //枚举类对象
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle enumSingle1 = EnumSingle.INSTANCE;
//报错,IDEA显示有空参构造,但是实际上没有空参构造,需要用jad看,发现有个string,int的构造
// Constructor<EnumSingle> declaredConstructor =
// EnumSingle.class.getDeclaredConstructor(null);
//报错,不能使用反射破环,这种才是我们想要的
Constructor<EnumSingle> declaredConstructor2 =
EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor2.setAccessible(true);
EnumSingle enumSingle2 = declaredConstructor2.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
}
}
枚举是目前最推荐的单例模式的写法
,因为足够简单,不需要开发自己保证线程的安全,同时又可以有
效的防止反射破坏我们的单例模式.
19、深入理解CAS
CAS : 比较并交换
前言:互联网缩招之下,初级程序员大量过剩,高级程序员重金难求,除非你不吃这碗饭,否则就要逼
自己提升!
用代码理解下什么是CAS CompareAndSet
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!``
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
System.out.println(atomicInteger.get());//2021
atomicInteger.getAndIncrement(); //2022
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //false
System.out.println(atomicInteger.get());
}
看下源码
getAndIncrement使用的是unsafe类
- this–当前对象
- valueOffset-- 表示内存地址的偏移值
- 1 —增加的值
进入unsafe看下unsafa由Unsafe类创建
点进GetAndAddInt看下
var5 获取 当前的值var2
然后循环比较当前的值和var5是否相等,如果相等就加上var4
这就是一个自旋锁,不断的循环进行判断
问题:这个UnSafe类到底是什么? 可以看到AtomicInteger源码中也是它!
- UnSafe
UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,
UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据
,Unsafe类存在于 sun.misc包中,其
内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是Native修饰的
,也就是说Unsafe类中的方法都直接调用操作系统底层
资源执行相应任务 - 变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。 - 变量 value用volatile修饰,保证了多线程之间的内存可见性
最后解释CAS 是什么
CAS 的全称为 Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
。
CAS并发原语体现在JAVA语言中就是 sun.misc.Unsafe 类中的各个方法。调用UnSafe类中的CAS方法,
JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强
调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功
能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU
的原子指令,不会造成所谓的数据不一致问题
。
汇编层面理解
总结
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作
内存中的值一致为止。
CAS 应用
CAS 有3个操作数,内存值V,旧的预期值A,要修改的更新值B。且仅当预期值A 和 内存值 V 相同时,
将内存值 V 修改为B,否则什么都不做。
CAS 的缺点
- 循环时间长开销很大。
可以看到源码中存在 一个 do…while 操作,如果CAS失败就会一直进行尝试。 - 只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。 - 引出来
ABA 问题
(狸猫换太子)???
ABA问题
- 两个线程同时操作A变量
- 左边线程期待A=1,但是右边线程把A先变成了3然后又变成了1
- 左边线程对A被修改过了完全不知情
public class CASDemo {
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
20、原子引用
原子类 AtomicInteger 的ABA问题谈谈?原子更新引用知道吗?
解决ABA 问题,引入原子引用 AtomicReference! 也就是加入版本号, 对应的思想:乐观锁!
演示ABA问题: AtomicReference
/**
* ABA 问题的解决 AtomicStampedReference
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"T1").start();
new Thread(()->{
// 暂停一秒钟,保证上面线程先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//已经被修改了,却不知道
System.out.println(atomicReference.compareAndSet(100, 2019)); //修改成功!
System.out.println(atomicReference.get());
},"T2").start();
}
}
解决方案:AtomicStampedReference 和乐观锁的原理相同
package org.example.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo01 {
//默认值为1 ,默认版本号为1
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);//注意这里的Integer默认值得在-128~127之间,不然会重新创建对象
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(()->{
//获取当前版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("a1=>开始时的版本号"+stamp);
//把1修改为2
System.out.println(atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>版本号为"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>版本号为"+atomicStampedReference.getStamp());
}).start();
new Thread(()->{
//获取当前版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("b1=>开始时的版本号"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把1修改为2 修改失败,因为这里期待的stamp为1 但是实际上已经被另一个线程修改了
System.out.println(atomicStampedReference.compareAndSet(1, 2,
stamp, stamp + 1));
System.out.println("b1=>版本号为"+atomicStampedReference.getStamp());
}).start();
}
}
21. Java锁
21.1 公平锁 非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁
,类似排队打饭,先来后到。
非公平锁(默认):是指多个线程获取锁的顺序并不是按照申请锁的顺序
,有可能后申请的线程比现申请的线程
优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
//设置公平锁 非公平锁
// 无参
public ReentrantLock() {
sync = new NonfairSync();
}
// 有参
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
区别
并发包中的 ReentrantLock 的创建可以指定构造函数 的 boolean类型来得到公平锁或者非公平锁,默认
是非公平锁!
公平锁:就是很公平,在并发环境中,每个线程在获取到锁时会先查看此锁维护的等待队列,如果为
空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规
则从队列中取到自己。
非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就会采用类似公平锁那种方
式。
Java ReentrantLock 而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在
于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。
21.2 可重入锁
可重入锁(也叫递归锁
)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法 获取锁的时候,在进入内层方法会自动获取锁
。
也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可
以进入里面的房间了;
ReentrantLock、Synchronized 就是一个典型的可重入锁;
可重入锁最大的作用就是避免死锁
Synchronized版
package org.example.juc.relock;
import java.util.concurrent.TimeUnit;
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sms();
},"A").start();
new Thread(() -> {
phone.sms();
},"B").start();
}
/**
* A sms
* A call
* B sms
* B call
*/
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName() + " sms");
call();//这里也有锁
}
private synchronized void call() {
System.out.println(Thread.currentThread().getName() + " call");
}
}
Lock版
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sms();
},"A").start();
new Thread(() -> {
phone.sms();
},"B").start();
}
}
class Phone{
private Lock lock = new ReentrantLock();
public void sms(){
//细节:
// 1.sum方法和call方法中都有lock锁,sms在这里其实就拿到了call中的锁了
// 2.lock 和 unlock必须配对,否则会死锁
// 3.Lock的对象是可以复用的,及可以多次lock()
lock.lock();
lock.lock(); //lock几次下面必须unlock几次
try {
System.out.println(Thread.currentThread().getName() + " sms");
call();//这里也有锁
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
lock.unlock();
}
}
private void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " call");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
21.3 自旋锁
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下
文切换的消耗,缺点是循环会消耗CPU。
前面在CAS中遇到的其实就是自旋锁
package org.example.juc.spinlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author QiuYuSY
* @create 2022-12-21 19:26
*/
public class SpinLockDemo {
//int 0
//Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程进入到mylock");
//自旋锁
while(!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程离开到mylock");
atomicReference.compareAndSet(thread,null);
}
}
class SpinLockTest{
public static void main(String[] args) throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
//底层使用自旋锁CAS
SpinLockDemo lock = new SpinLockDemo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"B").start();
/**
* A线程进入到mylock
* B线程进入到mylock //进入的时候还没拿锁
* A解锁后B才能加锁,因为A解锁后线程才会变为null,B才有机会拿到锁
* A线程离开到mylock
* B线程离开到mylock
*
*/
}
}
21.4 死锁
死锁是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干
涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性
就很低,否者就会因为争夺有限的资源而陷入死锁。
产生死锁主要原因:
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
package org.example.juc.deadlock;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"T2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockA + "想要lock:" + lockB);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockB + "想要lock:"+lockA);
}
}
}
}
解决
拓展java自带工具操作:
- 查看JDK目录的bin目录
- 使用
jps -l
命令定位进程号
- 使用
jstack 进程号
找到死锁查看
可以看到这两个线程互相拿着一个锁,然后要对方的锁
问10个人,9个说看日志,还有一个分析堆栈信息,这一步,他就已经赢了!