简介
线程简介
-
程序:是指令和数据的有序集合,是一个静态的概念,程序内有进程;
-
进程(Process):是程序的一次执行过程,是一个动态的概念,是系统资源分配的单位,一个进程可有多个线程,进程内有线程;
-
线程(Thread):线程是 CPU 调度和执行的单位,一个进程中至少有一个线程,真正执行的是线程。
线程相关概念
线程就是独立的执行路径,
在程序运行时,一定会有一个主线程,如:主线程、GC线程,
main() 方法称为主线程,是系统的入口,用于执行整个程序,
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不可人为干预,
对同一份资源操作时,会存在资源抢夺问题,需要引入并发控制,
线程会带来额外的开销,如:CPU 调度时间,并发控制的开销等,
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致,
线程开启不一定立即执行,由 CPU 调度执行,
进程间通信的方式
数据传输、资源共享、通知事件、进程控制
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1 把数据从用户空间拷到内核缓冲区,进程2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。
-
管道pipe(匿名管道):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
-
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
-
高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式
-
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
-
信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
-
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
线程间通信的方式
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺导致的数据不一致问题。
-
共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。volatile共享内存。
-
消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。wait/notify等待通知方式、join方式。
-
管道流:用于以管道为媒介的数据传输管道。输入/输出流的形式。
创建线程的方式
继承(extends) Thread类
Thread类继承了 Object类,实现了 Runnable接口。
但是不推荐使用,为了避免面向对象编程(OOP)中的单继承局限性。
创建步骤
-
自定义线程类继承 Thread类
-
重写 run()方法,编写线程执行体
-
创建自定义线程对象,调用 start()方法启动线程
实现(implements) Runnable接口,无返回值
推荐使用,因为避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用。
创建步骤
-
自定义类实现 Runnable接口
-
重写 run()方法,编写线程执行体
-
创建线程对象,创建线程对象时将自定义类的对象作为线程对象的参数,调用 start()方法启动线程
实现(implements) Callable接口,有返回值
创建步骤
-
自定义类实现Callable接口,Callable需要指定泛型,即call方法的返回值类型,假定为 Boolean
-
重写 call方法,编写执行体,有返回值
线程的生命周期
新建状态(NEW)、就绪(可运行)状态(RUNNABLE)、死亡状态(TERMINATED)、有期限等待状态(TIMED_WAITING)、无期限等待状态(WAITING )、锁阻塞状态(BLOCKED)。
线程方法
方法 | 说明 |
---|---|
setPrioriry(int new Priorirt) | 改变线程的优先级。<br>记住优先级只是提高可能,不是说高优先级的一定比低优先级的线程先获得资源。 |
static void sleep(long millis) | 让正在执行的线程休眠指定毫秒数 |
void join() | 即插队,在 B线程执行时,让 A线程插队,B线程立即停止然后等待 A线程结束才可进入就绪状态 |
static void yield() | 即重新竞争,暂停正在执行的线程,所有线程重新竞争。<br>只是重新竞争,得看CPU的调度程序。 |
void interrupt() | 中断线程,最好别使用此方法 |
boolean isAlive() | 判断线程是否处于活动状态 |
getState() | 获取线程状态 |
getPrioriry() | 获取线程的优先级 |
线程停止
-
最好别使用 JDK 提供的 stop()、destroy()方法,已经废弃
-
推荐让线程自己停止下来,即建议使用标志位,
线程休眠 sleep
-
sleep(毫秒数)可以当当前运行线程休眠指定毫秒数
-
sleep 存在 InterruptedException异常
-
sleep 结束就会从有期限等待状态变成可运行状态
-
sleep 可以模拟网络延时、倒计时等
-
sleep 不会释放锁
-
推荐使用 TimeUnit.SECONDS.sleep(10),JUC包下的。
守护线程
-
线程分为 用户线程 和 守护线程;
-
JVM 必须保证主线程必须执行完毕,但 JVM 不关心守护线程;
-
主线程结束,守护线程也必须结束;但守护线程结束,主线程照旧;
-
守护线程比如:后台记录操作日志、监控内存、垃圾回收等。
线程同步相关问题
-
发生在多个线程操作同一个资源时,即线程并发、多线程时
-
线程同步就相当于一种等待机制,需要 队列 + 锁,保证安全性,即数据一致性,
-
一个线程持有锁会导致其他所有需要该锁的线程挂起
-
多线程竞争时,加锁、释放锁会导致比较多的 上下文切换、调度延时,引起性能问题
synchronized
-
我们对 synchronized 不可见其开始和结束,
-
synchronized 用来修饰在 方法和对象 上,
-
被修饰的代码块称为同步语句块
public synchronized void talk(){} synchronized(锁资源){}
-
synchronized修饰方法,此方法可以被继承,但是子类的方法并不是线程安全的,需要重新加上 synchronized
synchronized 锁的区分
虽然 synchronized 只能放在两个地方,但是 synchronized 可以修饰:类、方法、静态方法、引用对象。
-
锁一个类:锁的是类,即类模板,如果 A 抢到了类锁然后 sleep,B 调用类对象的方法不受影响。因为 B 调用的是类对象的方法,即使对这个类对象加了锁,和类锁也是不同的锁资源,然后这个类的锁资源只有一个;
-
锁一个非静态的方法:锁的是类对象,即调用该方法的对象,被修饰的方法称为同步方法,多个类对象是不同的锁资源;
-
锁一个静态的方法:锁的是类,即类锁,类模板锁,和第一种锁资源是同一个;
-
锁一个引用对象(即任意一个非基本类型的对象):
-
假如你锁的引用对象能使用常量池,即 String、Integer 这种,那不管多少个对象,只有一个锁资源,就算它被放在方法里面,
-
如果你锁的是不能使用常量池的引用对象,只要它没被放在方法区里面,即只要不是 static修饰的,就是不同的锁资源,就算它被 final 修饰;
public void method() { //锁类模板 synchronized(ClassName.class) {} } public void method() { Student s1 = new Student(); //锁这个类对象 synchronized(s1) {} } //锁类对象 public synchronized void talk(){} //锁类模板 public synchronized static void talk(){}
lock(锁)
-
java.util.concurrent.locks,lock接口是控制多个线程对共享资源进行访问的工具,
-
锁提供了对共享资源的独占访问,每次只能有一个线程对 lock对象加锁,线程开始访问共享资源之前得先获得 lock对象,提供了更强大的同步机制,
-
ReentrantLock(可重入锁)类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,ReentrantLock 更常用,因为可以显示地加锁、释放锁。
while (true) { try { lock.lock(); if (ticketNums > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNums--); } else { break; } } finally { lock.unlock(); } }
synchronized 与 Lock 的对比
-
Lock 是显示锁(手动开启和关闭锁);而 synchronized 是隐式锁,自动释放锁;采用 synchronized 不需要用户去手动释放锁,当 synchronized方法或者 synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
-
Lock 只有代码块锁,即只能锁代码体;synchronized 可以锁类对象(即修饰方法),也可以锁代码块(即修饰对象);
-
Lock 不是 Java语言内置的,Lock 是一个类,通过这个类可以实现同步访问;synchronized 是 Java语言的关键字,因此是内置特性。
-
synchronized 是大粒度锁,lock 是细粒度锁,lock 因为是对象,所以还可分为读写锁,更灵活。所以 synchronized 适合锁小代码块使用,lock 适合在大代码块中使用。
生产者消费者问题
-
仓库只能存放一件产品
-
若仓库中没有产品,通知消费者等待,然后生产者生产产品然后放入仓库,并通知消费者来拿走;
-
若仓库中有产品,则消费者拿走产品,并通知生产者继续生产;
分析
-
若仅有 synchronized 是不足的,它只能实现 同步,但不可实现 通信
-
所以需要 wait()、wait(long timeout)、notify()、notifyAll(),
-
解决方法1:管程法(即使用缓冲区)
-
解决方法2:信号灯法(即设置标志位)
简单实现
管程法
/** * 使用管程法解决生产者消费者问题 * * @author 秋白、 * */ public class TestProducerConsumer1 { public static void main(String[] args) { BufferArea bufferArea = new BufferArea(); new Producer(bufferArea).start(); new Consumer(bufferArea).start(); } // 生产者 static class Producer extends Thread { BufferArea bufferArea; public Producer(BufferArea bufferArea) { this.bufferArea = bufferArea; } @Override public void run() { for (int i = 0; i < 100; i++) { bufferArea.push(new Chicken(i)); System.out.println("生产了第" + i + "个产品"); } } } // 消费者 static class Consumer extends Thread { BufferArea bufferArea; public Consumer(BufferArea bufferArea) { this.bufferArea = bufferArea; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了第" + bufferArea.pop().id + "个产品"); } } } // 产品 static class Chicken { int id;// 产品编号 public Chicken(int id) { this.id = id; } } // 缓冲区 static class BufferArea { // 容器大小 Chicken[] chickens = new Chicken[10]; // 容器计数器 int count = 0; // 生产者放入产品 public synchronized void push(Chicken chicken) { // 如果容器满了,就得等待消费者消费 if (count == chickens.length) { // 通知消费者消费,生产者等待 try { this.wait(); } catch (Exception e) { e.printStackTrace(); } } // 如果容器没有满,就放入产品 chickens[count++] = chicken; // 通知消费者消费 this.notifyAll(); } // 消费者拿走产品 public synchronized Chicken pop() { // 判断可否消费 if (count == 0) { // 等待生产者生产,消费者等待 try { this.wait(); } catch (Exception e) { e.printStackTrace(); } } // 如果可以消费,就消费产品 // 整理--count是因为当拿到count是10时,但没有count[10] Chicken chicken = chickens[--count]; // 通知生产者生产 this.notifyAll(); return chicken; } } }
信号灯法
/** * 使用信号灯法解决生产者消费者问题 * * @author 秋白、 * */ public class TestProducerConsumer2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Watcher(tv).start(); } // 生产者 static class Player extends Thread { TV tv; public Player(TV tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i % 2 == 0) { tv.play("快乐大本营播放中"); } else { tv.play("抖音记录美好生活"); } } } } // 消费者 static class Watcher extends Thread { TV tv; public Watcher(TV tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { tv.watch(); } } } // 产品 static class TV { // 演员表演,观众等待 // 观众观看,演员等待 String show; // flag代表是否有产品 boolean flag = false; // 表演 public synchronized void play(String show) { if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员表演了" + show); // 通知观众观看 this.notifyAll(); // 更新节目 this.show = show; this.flag = !this.flag; } // 观看 public synchronized void watch() { if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观众观看了" + show); // 通知演员表演 this.notifyAll(); this.flag = !this.flag; } } }
Java 其实开启不了线程
-
线程的 start() 方法,group.add(this) 会把当前线程加入线程组,
-
然后将 started 标志位设为 true,即调用 start0() 方法去启动这个线程,但是 start0 是本地方法,Java 无法直接操作硬件。
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } // 本地方法,底层的C++ ,Java 无法直接操作硬件 private native void start0();
wait 与 sleep 的区别
-
来自不同的类
-
wait 来自 Object类
-
sleep 来自 Thread类
-
-
是否会释放锁
-
wait 会释放锁
-
sleep 不会释放锁
-
-
使用的范围不同
-
wait 必须在同步块中使用
-
sleep 可以在任何地方使用
-
-
是否需要捕获异常
-
wait 不需要捕获异常,因为没抛出编译时异常
-
sleep 必须捕获异常
-
线程的三大特性
原子性(Atomicity)
原子性是指在一个任务操作中,CPU 不可以在中途暂停然后再调度,即不可被中断操作,要么执行完成,要么就不执行。原子性指的就是一个操作是不可分割,不可中断,即使有多个线程执行,一个操作开始也不会受其他线程影响。
可以理解为线程的最小执行单元,不可被分割。当然这个最小执行单元可以只是一个操作也可以是一段代码。就比如:num++ 就不是原子性操作,它分为3步:1、获取值,2、+1,3、赋值。
在线程中实现原子性的操作可以为 synchronized 修饰或通过 lock 实现。
可见性(Visibility)
可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。
通过:volatile关键字、synchronized 或 lock 实现均可实现变量的可见性。
final 关键字的可见性:被 final 修饰的字段在构造前中一旦初始化完成, 并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final字段的值。
有序性(Ordering)
有序性即指令的顺序有序,未经过重排序。
JUC线程并发(java.util.concurrent)
即java.util.concurrent、java.util.concurrent.atomic(原子性)、java.util.concurrent.locks这三个包。
Thread 只是一个普通的线程类、Runnable 没有返回值,效率也比 Callable 低,所以Callable 用的更多,并且现在用的大多的有返回值的线程,都是基于 Callable 的。
ReentrantLock
创建公平锁或非公平锁
若 new ReentrantLock 时没有参数,就默认创建一个 非公平锁,如果传参 true,就是创建一个公平锁,false 就是创建一个非公平锁。
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
Condition
Condition 取代了对象监视器方法的使用,如同 Lock 代替 synchronized,
Condition 可以精准地通知和唤醒线程,使用同一个 Lock对象,创建多个 Condition对象去监听每一个同步块,A业务中使用 condition2.signal() 唤醒业务B,以此类推,最后业务n 使用 condition1.signal() 唤醒业务A,精准唤醒。
基本使用
创建
使用 Lock对象创建,lock.newCondition();
等待、唤醒
condition.await(); 使线程等待,代替 wait():
condition.signal(); condition.signalAll(); 唤醒等待该锁的线程,代替 notify()、notifyAll();
虚假唤醒(虚假等待)问题
等待代码块应该出现在 while 循环中,才可以防止虚假唤醒。
虚假唤醒详解:两个线程请求同一个同步块,然后同步块中使用的是 if 判断去 wait,而不是 while 判断,假如 A 进去了,然后 wait 等待;然后 B 也进去了,wait 等待。然后当条件发生改变,notifyAll 它们,因为使用的是 if,所以 A,B 都会直接去执行同步块下面的代码,然后此时线程A 已经改变了锁资源,按我们想的 B应该等待的,但是因为使用的是 if,B 已经执行过 if 代码了,所以 B 就会跟 A 一样继续执行,这就导致了多线程中的错误。
关于锁的一些问题
其实就是说,static 同步方法是锁类模板,普通同步方法是锁类对象。
两线程调用同一类对象的同步方法
-
问题1:A、B 线程都调用同步方法,不加睡眠,两线程谁先执行?
-
问题2:A、B 线程都调用同步方法,加上任意睡眠,两线程谁先执行?
分析
因为 synchronized 锁方法锁的是类对象,所以其实哪个线程先拿到 类对象 锁资源,谁就会先执行,不管被休眠了多久。所以就看 main方法中,哪个线程在前面,那就是它先执行。
代码与结果
-
不管怎么睡眠,main方法中都是A线程先被定义,先拿到phone这个锁
public class Test1 { public static void main(String[] args) { Phone phone = new Phone(); //锁的存在 new Thread(()->{phone.sendSms();},"A").start(); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} new Thread(()->{phone.call();},"B").start(); } } class Phone{ // synchronized 锁的对象是方法的调用者 // 两个方法用的是同一个锁,谁先拿到谁执行 public synchronized void sendSms(){ try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } }
同一个类对象,一线程调用同步方法,另一线程调用普通方法
-
问题3:A线程调用同步方法,B线程调用普通方法,两线程谁先执行?
分析
A 需要获取锁资源,B 不需要,那就看代码顺序和睡眠时间长短了,假如都不睡,那一定是A 先执行。
两个类对象,两个线程调用不同类对象的同步方法
-
问题4:A线程调用类对象1 的同步方法,B线程调用类对象2 的同步方法,两线程谁先执行?
分析
两个类对象,那么就是两份不同的锁资源,然后 A、B 使用不同的锁去调用同步方法,两者锁都不一样,所以 A、B 之间不影响,只看它们的先后顺序和睡眠时间。
两个类对象,两个线程调用不同类对象的 static 同步方法
-
问题5:A线程调用类对象1 的 static 同步方法,B线程调用类对象2 的 static 同步方法,两线程谁先执行?
分析
因为是对 static 加锁,那么锁的就是类模板了,只有一个锁资源,那么 A、B 线程不管是通过同一个类对象还是不同的类对象去调用这个 static 同步方法,都需要获取同一个锁资源。那就是 A 先拿到锁先执行。
不安全的集合类
List 不安全
会报 ConCurrentModificationException 异常
List<String> list = Arrays.asList("1", "2", "3"); //也可以创建List public class UnsafeConllection { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 5)); System.out.println(list); }, String.valueOf(i)).start(); } } }
解决方法
COW 用的是 Lock,而 Vector 用的是 synchronized,所以 Vector 效率更低。
-
使用Vector
List<String> list = new Vector<String>();
-
使用 Collections 工具类,将的 new 的集合 synchronized 一下
List<String> list = Collections.synchronizedList(new ArrayList<>());
-
使用 JUC,COW
List<String> list = new CopyOnWriteArrayList<String>();
CopyOnWrite(COW)
CopyOnWrite:要写入时复制一份,即 COW,是计算机程序设计领域的一种优化策略,读写分离思想。
多个线程调用 list 的时候,读取 list 时是固定的,但是写入的时候我们不允许一起写入,因为会产生覆盖;所以使用 COW,可以避免写入时覆盖,即多个线程如果只是读,那么读的都是原文件,如果某个线程要去修改,那么会将原文件复制一份出来给此线程去修改,此线程修改的是专用的复制文件,其他线程继续读取原文件,这个过程对其他调用者是透明的,并且原文件会加锁,即同一时刻只允许一个线程去修改,当修改完成之后,会使用这个复制文件(副本)去更新原文件。
Set 不安全
会报 ConCurrenntModificationException 异常
public class UnsafeConllection2 { public static void main(String[] args) { Set<String> set = new HashSet<>(); 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 工具类,将的 new 的集合 synchronized 一下
Set<String> set = Collections.synchronizedSet(new HashSet<String>());
-
使用 JUC,COW
Set<String> set = new CopyOnWriteArraySet<String>();
HashSet 其实就是 HashMap 的 key
我们知道 ArrayList 的底层是数组,那么 HashSet 的底层其实就是 HashMap。
public HashSet() { map = new HashMap<>(); } // set 本质就是 map 的 key,而key是无法重复的! public boolean add(E e) { return map.put(e, PRESENT)==null; } //这是一个常量,不变的值 private static final Object PRESENT = new Object();
Map不安全
resize 扩容的时候会调用 transfer方法,就会产生环状链表。
然后一定记住,链表中分清栈区和堆区,比如 e.next=newTable[i],newTable[i]=e,e=next。
-
这是头插法,e 和 newTable[i] 都是栈区指针对象,指向堆区的 eTrue 和 nTrue,而 e.next 则是在改变堆区,因为 e 已经在栈区了,e.next 是在堆区的。第一句就是将 nTrue 的地址赋给 eTrue.next,
-
然后第二句是将 eTrue 的地址赋给 newTable[i]。因为 newTable 是指向 nTrue 的,如果是 newTable[i].XXX,那么就是在改变堆区,但是 newTable[i] 的话就是在改变栈区,
-
第三句话则是将 e 这个栈区指针指向下一个,使得 while循环继续进行。 JDK1.7和JDK1.8中HashMap为什么是线程不安全的?-CSDN博客
解决方法
-
使用 Collections工具类,将的 new 的集合 synchronized 一下
Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
-
使用 JUC,ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
ConcurrentHashMap
ConcurrentHashMap 是线程安全的数组,是 Hashtable 的替代品,同为线程安全,其性能要比 Hashtable 更好
ConcurrentHashMap 诞生的原因:兼顾 HashMap 和 Hashtable
-
HashMap 线程不安全:在并发环境下,可能会形成环状链表(扩容时可能造成),导致 get 操作时,cpu 空转,所以,在并发环境中使用 HashMap 是非常危险的,
-
Hashtable 是线程安全的:Hashtable 和 HashMap 的实现原理几乎一样,
-
差别:Hashtable 不允许 key 和 value 为 null;
-
Hashtable 线程安全的策略实现代价却比较大,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞。
ConcurrentHashMap 的构成
-
数据结构:Synchronized + CAS +Node +红黑树
-
Node 的 val 和 next 都用 volatile 保证,保证可见性,查找、替换、赋值操作都使用 CAS
-
读操作无锁。
为什么在有Synchronized 的情况下还要使用CAS
-
因为 CAS 是乐观锁,在一些场景中(并发不激烈的情况下)它比 Synchronized 和 ReentrentLock 的效率要高
-
当 CAS 保障不了线程安全的情况下(扩容或者 hash冲突的情况下)转成 Synchronized 来保证线程安全,大大提高了低并发下的性能。
Callable
可以有返回值、可以抛出异常、call()。
使用 Callable 创建线程
使用 new Thread(new FutureTask<Integer>(myThread)).start(),这个产生的结果会被缓存,效率高,
并且即使你 new 了另一个也以 myThread 为核心的线程去跑,其内核 Callable 的 call() 方法只会跑一次,
public class CallableTest1 { public static void main(String[] args) { MyThread myThread = new MyThread(); FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread); //结果会被缓存,效率高, new Thread(futureTask).start(); try { // 这个 get 方法会阻塞拿结果 System.out.println(futureTask.get()); } catch (Exception e) { e.printStackTrace(); } } static class MyThread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("call"); return 123; } } }
FutureTask
-
是 Runnable 的实现类,所以可以直接 new Thread(new FutureTask(callable对象)) 来创建线程,
-
可以将 Callable 作为构造器的参数,这样就可以让 Callable 去创建一个线程了
-
FutureTask 的 get() 方法可以得到 Callable 的 call() 方法的返回结果,会使当前线程阻塞等待至获得结果。
三大线程辅助类(三个线程计数器)
CountDownLatch(减法计数闭锁)
-
先 new 一个 CountDownLatch 出来,在参数里面设定要等待的线程个数,我们可以把它想象成看门者;
-
然后线程里面的 run() 方法或 call()方法的代码块的随便一行加上 countDownLatch.countDown(); 即每个线程执行了就将计数 -1,可以想象成孩子出教室门,然后看门人在计数,看门人只关心孩子有没有跨出门框,只要孩子一踏出门框,就将人数 -1,
-
然后使用 countDownLatch.await(); 来等待,即这个减法计数器一直阻塞等待,直到计数 =0 的时候,然后执行这个方法下面的代码。
-
但是 CountDownLatch 不可重置计数,如果需要重置计数的版本,请考虑使用 CyclicBarrier,
-
闭锁是让线程每执行完然后将计数 -1,并不会让线程阻塞等待计数归零,即线程就只管执行自己的,
-
countDown():数量-1,该方法不会产生阻塞;写在线程里面,
-
await():等待计数器归零,然后向下执行,会让线程阻塞等待,一般用在 main 线程中,
public class CountDownLatchTest1 { public static void main(String[] args) { // 总数为6,即从6开始计数 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "出门"); // 数量-1 countDownLatch.countDown(); }).start(); } try { // 等待计数器为0,然后再向下执行 关门,否则 main 线程就一直阻塞等待 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("关门"); } }
CyclicBarrier(循环栅栏)(加法计数器)
-
有两个构造器,
-
public CyclicBarrier(int parties, Runnable barrierAction)
-
public CyclicBarrier(int parties)
-
-
栅栏能阻塞 一组线程 直到某个事件的发生,
-
栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。而减法计数器就是计数,然后通过 await()方法来阻塞。
-
所有线程都到达栅栏,就是说当每个线程执行到 CyclicBarrier.await() 后,该线程就会进入阻塞,一直等待至线程个数达到 CyclicBarrier 参数中设置的数量;然后再各自执行 CyclicBarrier.await() 后面剩下的代码,
-
然后假如 CyclicBarrier 的构造器中有 Runnable,那么线程个数到达设置数量后,执行这个 Runnable,它执行完后,那些等待的线程才可以继续执 CyclicBarrier.await() 后面的代码。
Semaphore(信号量)(并发限流器)
-
有两个构造器,permits 就是总的信号量,即资源数,
-
public Semaphore(int permits)
-
public Semaphore(int permits, boolean fair)
-
-
使用 semaphore.acquire() 去请求获取资源,如果有资源,就得到,如果没有资源,就阻塞等待至有资源。就是说 acquire()方法一定会产生等待,因为你想要去获得资源,发送这个请求和获取响应的这段时间一定会产生,
-
acquire() 方法必须在 try块中,所以 release()方法一定要记得放在 finally块中。
读写锁 ReentrantReadWriteLock
简介
-
ReadWriteLock接口,其只有一个实现类:ReentrantReadWriteLock
-
读的时候可以被多个线程一起读,写的时候只能有一个线程去写。写的时候不允许别人读,即保持数据是最新的,但是自己可以读。读锁就是共享锁,写锁就是独占排他锁。
-
读锁可以在没有写锁的时候被多个线程同时持有;写锁是独占的(排他的)
-
读写锁会比 Lock、ReentrantLock 有更加细粒度的控制
实践
实践1
public class ReadWriteLockTest1 { public static void main(String[] args) { MyLockedCache myLockedCache = new MyLockedCache(); // 写 for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { myLockedCache.put(String.valueOf(temp), String.valueOf(temp)); }, String.valueOf(i)).start(); } // 读 for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { myLockedCache.get(String.valueOf(temp)); }, String.valueOf(i)).start(); } } static class MyLockedCache { private volatile Map<String, Object> map = new HashMap<String, Object>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 写 public void put(String key, Object value) { readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "写入"); map.put(key, value); System.out.println(Thread.currentThread().getName() + "写入完毕"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } // 读 public void get(String key) { readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "读取"); System.out.println(Thread.currentThread().getName() + "读取到:" + map.get(key) + ",读取完毕"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } } }
BlockingQueue<E>接口(阻塞队列)
-
一个队列,一端放入,另一端拿走,FIFO
-
如果队列是空的,拿走端就阻塞等待放入;如果队列被放满了,放入端就会阻塞
-
使用场景:多线程并发处理、线程池
-
BlockingQueue<E>阻塞队列接口,其父类是 Collection<E>,其子类有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue(同步队列)。一般使用 ArrayBlockingQueue 实现类,传的参数是队列大小
四组API
-
如果队列满了或空了,以下不同的方法会产生不同的结果
方式 | 抛出异常 | 有返回值且<br>不抛出异常 | 阻塞等待<br>(一直阻塞) | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(E e, long timeout, TimeUnit unit) |
移除 | remove() | poll() | take() | poll() |
获取队首元素 | element() | peek() | - | - |
SynchronousQueue同步队列
-
队列大小只能 =1,即放入一个元素,必须得等拿走了才能继续放,
-
两个线程,一个只管放,另一个只管取,那么当队列为空的时候,取的那个必须得等放的先放一个,然后队列有元素时,放的那个得等取的把元素取走才能继续放,所以全都是成对出现。
线程池
池化技术,比如说:线程池、数据库连接池、内存池、对象池。池化技术:事先准备好指定数量的资源,便于使用、复用和管理,降低资源消耗,提高响应速度。因为不停地 创建、销毁十分浪费资源。
经常创建和销毁线程,使用量大的资源且高并发的情况下的线程,对性能影响很大。使用线程池的话,就可以减少创建线程、销毁线程、调度线程等的开销,便于线程管理、提高响应速度、降低资源消耗;
ExecutorService、Executors 是一些线程工具类,线程池的工厂类,可以用于创建不同类型的线程池。
Executors工具类(不推荐使用)
线程池不推荐使用 Executors 去创建,而是推荐通过 ThreadPoolExecutor 的方式。因为这样的处理方式可以更明确线程池的运行规则,并且规避资源耗尽的风险。比如:
-
FixedThreadPool 和 SingleThreadPool:允许请求队列的长度为 Integer.MAX_VALUE(约为21亿),可能会堆积大量请求,从而导致 JVM 的 OOM,
-
CacheThreadPool 和 ScheduleThreadPool:允许创建线程的数度为 Integer.MAX_VALUE(约为21亿),可能会创建大量的线程,从而导致 JVM 的 OOM。
使用 Executors 的弊端
-
不一定适合当前业务
-
线程名字无法控制
-
七大参数有 6个都无法修改控制
Executors 创建四种线程池
指定的参数都是核心线程数大小。
// 单个线程的线程池 ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 固定线程数量的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(3); // 可伸缩数量的线程池,会根据线程量需求量自动伸缩 ExecutorService threadPool = Executors.newCachedThreadPool(); // 定时任务线程池,延迟执行或周期循环执行 ExecutorService threadPool = Executors.newScheduledThreadPool(2); try { for (int i = 0; i < 100; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " OK"); }); } } catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}
ThreadPoolExecutor(推荐使用的线程池)
简介
Executors 的几个创建线程池的方法的底层其实都是 new ThreadPoolExecutor。
拒绝策略推荐还是自己实现,比如去使用 消息中间件,防止遗漏。
推荐使用有界队列,并且合理长度。无界队列的话有可能全装满了然后出错。
-
线程池的运行规则:
-
核心线程池大小 < 需要的线程数量 <= 核心线程池大小+阻塞队列大小,就让没拿到的先进入阻塞队列;
-
核心线程池大小+阻塞队列大小 < 需要的线程数量 <= 最大核心线程池大小+阻塞队列大小,就开启最大核心线程池大小,还不够就进入阻塞队列;
-
如果 最大核心线程池大小+阻塞队列大小 也不够,就实施拒绝策略
-
public ThreadPoolExecutor( // 核心线程数 int corePoolSize, // 最大线程数 int maximumPoolSize, // 最大保留(空闲)时间。使用最大线程数后若一直无人调用,就释放掉 // 核心线程数的超时时间可另外自己去设置 long keepAliveTime, // 时间单位 TimeUnit unit, // 阻塞队列 BlockingQueue<Runnable> workQueue, // 线程工厂。方便给线程指定名称 ThreadFactory threadFactory, // 拒绝策略 RejectedExecutionHandler handler)
ThreadPoolExecutor 提供的四种拒绝策略
//满了后,抛出异常。即当最大线程池大小+阻塞队列大小 < 线程连接请求时,抛出异常 new ThreadPoolExecutor.AbortPolicy() //满了后,线程池不受理,退回给使用线程池的线程去处理线程请求里面的线程业务。哪来的回哪里 //即通过我写的就是,线程池不受理这个请求,然后main线程去处理线程业务,输出了main OK new ThreadPoolExecutor.CallerRunsPolicy() //满了后,丢弃任务,且不抛出异常。把线程的任务直接不要了,然后正常执行 new ThreadPoolExecutor.DiscardPolicy() //满了后,尝试和最早的线程去竞争,且不会抛出异常。竞争成功就执行,竞争失败就被丢弃 new ThreadPoolExecutor.DiscardOldestPolicy()
线程池状态变化
实践
实践1
public static void main(String[] args) { //自定义线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); try { for (int i = 0; i < 7; i++) { threadPoolExecutor.execute(() -> { System.out.println(Thread.currentThread().getName() + " OK"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPoolExecutor.shutdown(); } }
实践2
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(2), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("abc"); return t; } }, new ThreadPoolExecutor.AbortPolicy()); executor.execute(() -> { for (int i = 0; i < 10; i++) { System.out.println(i); } });
线程池调优
如何设置定义 最大核心线程大小
要学会根据业务去分辨,线程池业务是 CPU 使用得多还 IO 使用得多。分为 CPU密集型和 IO密集型。CPU 的话,一般电脑的 CPU 是几核就定义为几。核心线程数要能满足平时的一般需求量,最大则是满足最高需求量。
// 获取CPU的核数 Runtime.getRuntime().availableProcessors();
Future 及相关异步回调
-
Future 设计的初衷:对将来的某个事件的结果进行建模
-
Future 的间接实现类:CompletableFuture、ForkJoinTask、FutureTask、RecursiveAction、RecursiveTask。
-
异步调用就像服务器与客户端用的 Ajax 一样,有:异步执行、成功回调、异步回调。
ForkJoin(分支合并)
使用 ForkJoin 可以并行执行任务,提高效率,并且维护的队列是双端队列。
ForkJoinPool 与 ThreadPoolExecutor 一样,都是 Executor 的间接实现类,
核心思想就是任务拆解,可以把大任务拆分成小任务,如果子任务还大,就继续拆,然后拆成一颗树的样子,执行完结果后,再进行分支合并,一步步合成最终结果。
ForkJoin的特点:工作窃取。
* 工作窃取:A线程有 4个任务,此时刚执行到第2个,B线程有 5个任务,都执行完了,那么 B线程就会把 A线程剩下的两个没有执行过的任务拿过来一个去执行,提高效率。但是工作窃取会有弊端,可能会发生线程争抢任务的情况。
实践
实践1
通过 ForkJoinPool 的:forkjoinPool.execute(ForkJoinTask task) 来同步执行计算任务,那么我们就需要 ForkJoinTask 子对象。其实 ForkJoinPool 还有一个 submit 方法,这个方法有很多重载方法,可以异步执行并提交计算任务,
注意:execute 是直接执行,submit 是执行再提交;execute 是同步的,submit 是异步的;execute 没有返回值,submit 有返回值,返回值为 ForkJoinTask<T>,再通过返回值类型的 get()方法获取真正的返回值,get()方法会阻塞等待任务执行完,
而 ForkJoinTask 有三个实现类:CountedCompleter(技术完成者)、RecursiveAction(递归事件,没有返回值)、RecursiveTask(递归任务,有返回值),一般使用RecursiveTask:
-
自定义类 extends 继承 RecursiveTask<V>,重写其:V compute()方法,在方法里面进行任务拆分,
-
自定义类的对象创建对象,使其成为一个被拆分的子任务,然后该对象调用 fork()方法:将此任务,在当前任务正在运行的池中异步执行此任务,
-
然后你可以通过 idDone() 来判断是否已完成任务;还有 join()方法:当任务完成了,就会返回这个任务的计算结果
-
写完递归任务后,在方法里面 new ForkJoinPool,然后再 new 自定义的任务类对象,让forkJoinPool 去 execute 或 submit 这个任务,用 submit 的话可以获得自定义类对象的返回值。
public class ForkJoinTest1 extends RecursiveTask<Long> { private static final long serialVersionUID = 1L; private Long start; // 1 private Long end; // 1990900000 // 临界值 private Long temp = 10000L; public ForkJoinTest1(Long start, Long end) { this.start = start; this.end = end; } @Override protected Long compute() { // 如果任务量不大,就不去使用ForkJoin if ((end - start) < temp) { Long sum = 0L; for (Long i = start; i <= end; i++) { sum += i; } System.out.println(sum); return sum; } else { // 中间值 long middle = (start + end) / 2; // 拆分任务,我们这里拆分成了两个子任务 ForkJoinTest1 task1 = new ForkJoinTest1(start, middle); // 把子任务压入线程队列 task1.fork(); ForkJoinTest1 task2 = new ForkJoinTest1(middle + 1, end); task2.fork(); return task1.join() + task2.join(); } } public static void main(String[] args) { // test1();// 15722ms // test2();// 9646ms test3();// 196ms } // 普通程序员,15722 public static void test1() { Long sum = 0L; long start = System.currentTimeMillis(); for (Long i = 1L; i <= 10_0000_0000; i++) { sum += i; } long end = System.currentTimeMillis(); System.out.println("sum=" + sum + " 时间:" + (end - start)); } // 使用ForkJoin public static void test2() { long start = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinTest1(0L, 10_0000_0000L); // forkJoinPool.execute(task);// 直接执行任务,该方法没有返回值 ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务 Long sum = null; try { sum = submit.get(); } catch (Exception e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("sum=" + sum + " 时间:" + (end - start)); } // Stream并行流 public static void test3() { long start = System.currentTimeMillis(); long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum); long end = System.currentTimeMillis(); System.out.println("sum=" + sum + "时间:" + (end - start)); } }
CompletableFuture
简介
supplyAsync 表示创建带返回值的异步任务的,相当于 ExecutorService submit(Callable<T> task) 方法,
runAsync 表示创建无返回值的异步任务,相当于 ExecutorService submit(Runnable task)方法,这两方法的效果跟 submit 是一样的。
-
得到 CompletableFuture 的结果有两个方法,get() 和 join() 可以得到异步执行请求的返回结果,
-
使用 completableFuture.whenComplete 这个方法来定义如果异步执行请求成功执行,会怎么样,方法里面要给的是 BiConsumer 函数式接口,里面的第一个参数t 是异步执行请求正常的返回结果,然后第二个参数u 是错误信息,
-
然后在后面 .exceptionally 这个方法定义如果失败会怎么样,方法里面要给的是 Function函数式接口;
-
最后 .get()类获取真正执行时的返回结果,如果正确执行,就 get 到异步执行请求的 return结果,如果发生错误,就 get 到 exceptionally 里面的 return结果
实践
实践1
public class FutureTest1 { public static void main(String[] args) { // CompletableFuture<Void> completableFuture1 = // CompletableFuture.runAsync(() -> { // try { // TimeUnit.SECONDS.sleep(2); // } catch (Exception e) { // e.printStackTrace(); // } // System.out.println(Thread.currentThread().getName() + "runAsync"); // }); // System.out.println("CompletableFuture1"); // try { // System.out.println(completableFuture1.get()); // } catch (Exception e) { // e.printStackTrace(); // } CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "supplyAsync"); //int i = 10 / 0; return 1024; }); try { System.out.println( completableFuture2.whenComplete((t, u) -> System.out.println("t:" + t + ",u:" + u)) .exceptionally(e -> { System.out.println(e.getMessage()); return 2333; }).get()); } catch (Exception e) { e.printStackTrace(); } } }
实践2
@Override public List<String> getResult() { // 有返回值的异步任务线程集合 List<CompletableFuture<String>> completableFutureList = Lists.newArrayList(); // requestList 就假设是入参集合,然后 100个为一批创建异步线程去处理 Lists.partition(requestList, 100).forEach(it -> { // 创建异步任务线程 CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> { // 其他数据处理 return "ok了"; }); completableFutureList.add(completableFuture); }); // 获取多个异步任务的返回值 List<String> stringList = completableFutureList.stream().map(CompletableFuture::join).collect(Collectors.toList()); return stringList; }
CAS
CAS 基于乐观锁思想。
Unsafe类是很底层的类,里面全是 native方法。
在 Java 中,使用 atomic包下面的各种类,会有 compareAndSet方法,也是比较并交换,到达到期望值时更新值。compareAndSet 方法有两个参数,第一个是 期望值,第二个是 更新值。
compareAndSet方法的下面一层的方法是:compareAndSetInt方法,然后这个方法是native 本地方法,Java 无法调用内存,但 Java 可以通过C++ 的 native方法,操作内存。
-
记住 Integer 有 byte 范围的常量池,而如果使用大于 byte 范围的数,就会 new 堆内存对象,所以比较的时候一定不同,因为引用地址不同。
// 参数为初始值 AtomicInteger atomicInteger = new AtomicInteger(21); // 第一个参数是期望值,第二个参数是期望达到是更新成的值 System.out.println(atomicInteger.compareAndSet(21, 22)); // 获取此时的值 System.out.println(atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(21, 23)); System.out.println(atomicInteger.get());
自旋锁
自旋锁存在以下问题:
-
循环会耗时
-
一次性只能保证一个共享变量的原子性
-
存在 ABA问题
ABA问题
-
CAS 可能会产生 ABA问题
-
原始值1;A线程期望 1,更新成 2;B线程比 A 快,期望 1,更新成 3,然后又期望 3,更新成 1,
-
虽然 A线程最后能正常执行,但其实过程中已经被 B 戏耍了,但是 A 只关心它去执行操作的时候的当前值是否为 1,然后执行更新操作,所以对 A 而言无感知。
解决ABA问题-版本号
用带版本号的原子类可以防止 ABA 问题:AtomicStampedReference、AtomicReference。
因为 AtomicStampedReference 可以携带版本号,即每次被更新,就会迭代版本号,那么就可以通过 期望值+版本号 判断是否被动过手脚,
new AtomicStampedReference<Integer>(10, 0); 这个泛型是第一个参数的类型,第二个参数是版本号,其是固定类型 int 。
通过 atomicStampedReference.getStamp() 可获取当前的版本号。
AtomicStampedReference 的 compareAndSet方法有四个参数:期望的值、更新后的值、期望的版本号、更新后的版本号,只要有一个不达到期望值,就返回 false。
public static void main(String[] args) { AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(10, 0); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "拿到版本号:" + atomicStampedReference.getStamp()); System.out.println(atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1)); System.out.println(Thread.currentThread().getName() + "拿到版本号:" + atomicStampedReference.getStamp()); System.out.println(atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1)); System.out.println(Thread.currentThread().getName() + "拿到版本号:" + atomicStampedReference.getStamp()); }, "A").start(); new Thread(() -> { // 获得版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "拿到版本号:" + stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicStampedReference.compareAndSet(10, 11, stamp, stamp + 1)); }, "B").start(); }
volatile
-
volatile 是 Java虚拟机提供的轻量级的同步机制,
-
volatile 只能修饰变量,
-
synchronized 能保证:可见性、原子性,但无法保证有序性,即无法禁止指令重排序,volatile 能保证:可见性、有序性(禁止指令重排序),但不保证原子性,
volatile原理
指令重排序
指令重排只是一种存在的现象,可能性很小,但是逻辑上存在,只要大量尝试,一定会出现一次指令重排。
什么是指令重排序?
首先要讲一下 as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
为了使指令更加符合 CPU 的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
三种重排序:编译器重排序、指令级并行的重排序、内存系统重排序。
源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 -> 内存系统也会重排 -> 最终执行的指令顺序。
内存屏障
-
volatile 的底层就是 Lock 和内存屏障。
-
内存屏障是CPU指令,可以保证:
-
保证特定操作的执行顺序
-
保证某些变量的内存可见性
-
四类内存屏障
-
LoadLoad屏障:对于这样的语句 Load1,LoadLoad,Load2。在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。
-
StoreStore屏障:对于这样的语句 Store1, StoreStore, Store2,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。
-
LoadStore屏障:对于这样的语句 Load1, LoadStore,Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。
-
StoreLoad屏障:对于这样的语句 Store1, StoreLoad,Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。
内存屏障的生效简介
-
在每个 volatile读操作后插入 LoadLoad屏障,在读操作后插入 LoadStore屏障。
-
在每个 volatile写操作的前面插入一个 StoreStore屏障,后面插入一个 SotreLoad屏障。
各种锁
各种锁
非公平锁、公平锁
-
非公平锁(默认的锁):可以插队,不公平,因为假如 3h 的线程在 3s 的之前,应该让 3s 的先执行,所以一般使用非公平锁,更灵活(默认都是非公平锁)。
-
公平锁:先来后到,必须排队,很公平,得加 true,Lock 锁那块
共享锁、排他锁(读写锁)
-
共享锁:多个线程可以同时占有,比如读锁
-
排他锁:只能被一个线程占有,比如写锁
悲观锁、乐观锁
-
悲观锁:坏事一定会发生,所以先做预防(上锁)。就比如改值,会先加锁,防止别人动。
-
乐观锁:坏事不一定会发生,所以事后补偿。就比如改值,会先不加锁,把值拿到线程栈的工作空间后先做自己的事,到真要改回主存的时候,进行判断,如果真变了,再做进一步的操作
-
自旋锁:是乐观锁的一种实现。把值拿到线程栈的工作空间,做完修改后自旋比较,若被改动过,就一直循环修改,直到成功。
-
存在 ABA问题解决方法:加版本或是否被改动标记:
-
version:AtomicStampedReference
-
boolean:AtomicMarkableReference
-
-
统一锁、分段锁
-
统一锁:大粒度的锁。用于解决死锁问题,比如锁A等B,锁B等A,那么把A+B做成一把统一锁,就能解决死锁。但是粒度太大,并发度小
-
分段锁:小粒度的锁。比如CHM(ConcurrentHashMap)。粒度小了,并发大了,但是容易出现死锁
可重入锁(递归锁)、不可重入锁
可重入锁
可重入锁:可重复可递归调用的锁(即锁资源知道自己被谁拿了,下次这个线程想调用另一个同步块,再来请求锁资源,本来就是它拿了锁资源,所以不会发生死锁,它可以直接入)。请求了两次,都是请求同一个锁资源
所以说,锁是一个虚拟的概念,但锁资源是真实存在的,锁其实就是将线程ID 放在锁资源的 markword 里面,这就算是被占用了,被锁住了,
可重入锁有:synchronized、ReentrantLock。
不同级别的锁的重入策略
-
偏向锁:单线程独占,重入只用检查 threadId 等于该线程;
-
轻量级锁:重入:将栈帧中 lock record 的 header 设置为 null,重入退出:只用弹出栈帧,直到最后一个重入退出 CAS 写回数据释放锁;
-
重量级锁:重入 recursions++,重入退出 _recursions--,recursions=0 时释放锁。
synchronized 的可重入锁实践
public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.send(); }, "A").start(); new Thread(() -> { phone.send(); }, "B").start(); } static class Phone { public synchronized void send() { System.out.println(Thread.currentThread().getName() + "send"); call(); } public synchronized void call() { System.out.println(Thread.currentThread().getName() + "call"); } }
ReentrantLock 的可重入锁实践
public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.send(); }, "A").start(); new Thread(() -> { phone.send(); }, "B").start(); } static class Phone { Lock lock = new ReentrantLock(); public void send() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "send"); call(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void call() { lock.lock(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } try { System.out.println(Thread.currentThread().getName() + "call"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
不可重入锁
使用自旋锁来创建不可重入锁,第二次 lock,就会死锁,
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>(); // 加锁 public void lock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + " lock"); // 自旋锁,再次lock就死锁了 while (!atomicReference.compareAndSet(null, thread)) { } } // 解锁 public void unlock() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + " unlock"); atomicReference.compareAndSet(thread, null); }