JUC浅学
1.JUC概述
1.1 什么是JUC
JAVA中一个包:java.util.concurrent的名称。这是一个处理线程的包,JDK1.5后出现。
1.2 进程和线程的基本概念
进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令,数据及其组织形式的描述,进程是程序的实体。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结:
- 进程就是一个程序,程序一旦运行就开启一个进程。进程是资源分配的最小单位
- 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程是程序执行的最小单位。
1.3 线程的状态
1.3.1 线程状态枚举类
Thread.State
public enum State {
//新建
NEW,
//就绪
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//过时不候---
TIMED_WAITING,
//终结
TERMINATED;
}
1.3.2 wait和sleep的区别
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(代码在synchronized中)
- 他们都可以被interrupted方法中断
- 在哪里睡就会在哪里醒
并发和并行
串行: 一个一个执行
并行:同时执行,之后再汇总
- 例子:泡方便面时候,烧水的时候,可以先准备面
并发 同一时刻多个线程访问一个资源,多个线程对一个点
- 例子:春运抢票,电商秒杀
管程: monitor监视器【java中的锁】
- 是一种同步机制,保证同一时间,只有一个线程访问被保护的数据或者代码
- JVM同步基于进入和退出,使用管程对象实现(随着java对象一起创建和销毁),对临界区加锁
用户线程:自定义的线程
守护线程:后台线程,比如垃圾回收
判断方式:Thread.currentThread().isDaemon()==false
是用户线程,否则是守护线程
注意:
1、主线程结束了,用户线程还在运行,则jvm存活
2、如果用户线程都结束了,只剩下守护线程,则jvm也会结束
3、设置用户线程为守护线程需要在start方法之前执行xx.setDaemon(true)
2.Lock接口
2.1 synchronized
synchronized是java中的关键字,是一种同步锁。修饰的对象有以下几种:
- 修饰一个代码块:被修饰的代码块称为同步语句块,作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
- 修饰一个方法 :被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。
- 修饰一个静态方法:作用范围是整个静态方法,作用对象是这个类的所有对象。
- 修饰一个类:作用范围是synchronized后面的部分,作用的对象是类的所有对象
注意 :
- 虽然可以使用synchronized来定义方法,但是synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。
- 如果在父类中的方法使用了synchronized而在子类中覆盖了这个方法,子类的方法默认是不同步的。必须显示的加上synchronized关键字。当然可以在子类中调用父类的该方法,相当于同步了。
多线程编程步骤(上):
- 创建资源类,在资源类中创建属性和方法
- 创建多个线程,调用资源类的操作方法
案例
3个售票员,一共有30张票。通过三个线程模拟3个售票员
//1.创建资源类,定义属性和方法
class Ticket{
//票数
private int number = 30;
//买票
public synchronized void sale(){
//买票过程
if(number>0){
System.out.println(Thread.currentThread().getName() + " 卖出一张票 ,剩余票数: " + --number);
}
}
}
public class SaleTicket {
//售票员卖票
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
//售票员A卖票
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}
},"售票员A").start();
new Thread(new Runnable() {
@Override
public void run() {
//售票员B卖票
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}
},"售票员B").start();
new Thread(new Runnable() {
@Override
public void run() {
//售票员C卖票
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}
},"售票员C").start();
}
}
遇到问题
刚刚在写上面代码的时候,创建线程后没有写.start()
而是写了.run()
,结果是正常运行了,但是自己创建的线程名字并没有被打印,打印出来的是主线程的名字。引出run()
和start()
的区别:
start()
的作用是开启一个线程,如果不使用start线程不会被开启,只是创建了线程。调用start之后,线程处于就绪状态,等待cpu分配时间片 得到时间片后会自动执行run方法,直到run方法结束,线程终止。run
方法只是一个普通方法,不启动线程而写run的结果是主线程直接调用run方法。并且新的线程不会阻塞主线程的后续执行。
2.2 Lock接口【手动操作锁】
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
实现类:ReentrantLock
| ReentrantReadWriteLock
| ReadLock
| WriteLock
Lock和synchronized的区别:
- Lock不是java语言内置的,synchronized是Java的关键字,是内置特性。Lock是一个接口,通过这个接口可以实现同步访问。
- Lock和Synchronized一个非常大的不同,采用synchronized不需要手动释放锁,当synchronized方法或synchronized代码块执行完成之后,系统会自动释放锁。Lock需要手动释放锁
- Lock可以让等待锁的线程响应中断,而synchronized不行,使用synchronized时,等待的线程会一直等待下去,不能响应中断
- 通过Lock可以知道有没有成功获取锁,synchronized不行
- Lock可以提高多个线程的读操作的效率
- 竞争激烈的时候Lock比较优秀
2.3 创建线程的方式
1) 继承Thread类
直接继承线程类,重写run方法,调用线程start方法后,等待JVM为当前线程分配到CPU的时间片会自动调用run方法进行执行。
class MyThread extends Thread{
@Override
public void run(){
//操作
}
}
2) 实现Runnable接口
实现Runnable接口,重写run方法,下hi行线程需要丢入Runnable接口实现类,调用Thread对象start方法执行。
Thread的本质也是实现了Runnable接口,Runnable里面只有一个run方法。
由于java的单继承,所以实际中这种方法使用的最多。
class Runner implements Runnable{
@Override
public void run(){
//操作
}
}
class Use {
public static void main(String[] args) {
//线程执行
Thread thread = new Thread(new Runner());
thread.start();
}
}
3) 实现Callable接口的实现类
callable
:返回结果并且可能抛出异常的任务。
优点:
- 可以获得任务执行返回值
- 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class ThirdMethod {
class CallableImpl implements Callable {
@Override
public String call() throws Exception {
return "1";
}
}
public static void main(String[] args) {
FutureTask stringFutureTask = new FutureTask<>(new CallableImpl());
stringFutureTask.run();
}
}
使用流创建:
本质是通过lambda表达式创建线程,得到一个Runnable接口的实例
Runnable runnable = () -> {
System.out.println("createThreadForStream");
};
new Thread(runnable).start();
4) 线程池
ThreadPoolExcutor
是创建线程池的核心工具类,创建线程池后,使用excute
或者submint
方法提交线程执行任务,进入线程池中。等待线程池
分配资源。
参考文章:线程池的七种创建方式
3. 线程间通信
多线程编程(下)
1、第一步:创建资源类,在资源类中创建属性和操作方法
2、第二部:在资源类中操作方法 --1)判断 —(2)干活 —(3)通知
3、第三步:创建多个线程,调用资源类的操作方法。
4、第四步:防止虚假唤醒的问题【把wait方法放在循环里】
通信编写步骤:判断等待,业务,通知
通信方式
synchronized实现
class Share {
private int number = 0;
/**
* wait只能在循环里使用
* 因为wait在哪里等待,被唤醒的位置是等待的位置,会忽略条件继续执行,会导致虚假唤醒
* @throws InterruptedException
*/
public synchronized void inc() throws InterruptedException {
//+1的方法
//第二步:判断 干活 通知
while (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知其他线程
this.notifyAll();
}
public synchronized void dec() throws InterruptedException {
//-1的方法
while (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + " :: " + number);
this.notifyAll();
}
}
public class SynchronizedNumber {
public static void main(String[] args) {
//第三步:创建多个线程,调用资源类
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.inc();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "加法线程").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.dec();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "减法线程").start();
}
}
Lock方式实现:
class LNumber{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void inc(){
lock.lock();
try {
//判断
while (number!=0){
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: " + number);
//通知
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void dec(){
lock.lock();
try {
while (number!=1){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+" :: " + number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
4. 线程定制化通信
需求:
启动三个线程,按照如下要求构造:
- AA打印5次,BB打印10次,CC打印15次
- 上面的操作进行10轮,上面的线程按顺序执行
package demo.personalizeCommunica;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1; //1 AA 2BB 3CC
private Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
public void print5(int loop) {
lock.lock();
try {
while (flag != 1) {
c1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + " 轮数 " + loop);
}
flag = 2;
c2.signal();//唤醒BB线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10(int loop) {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + " 轮数 " + loop);
}
flag = 3;
c3.signal();//唤醒BB线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15(int loop) {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + " 轮数 " + loop);
}
flag = 1;
c1.signal();//唤醒BB线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class Personalized {
public static void main(String[] args) {
ShareResource sr = new ShareResource();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
sr.print5(i);
}
},"AA").start();
new Thread(()->{
int loop = 1;
while (loop<=10){
sr.print10(loop);
loop++;
}
},"BB").start();
new Thread(()->{
int loop = 1;
while (loop<=10){
sr.print15(loop);
loop++;
}
},"CC").start();
}
}
5.集合的线程安全
list
list会有并发修改异常
三种解决方法:
- 使用
vector
【古老的方法】 Collections.synchronizedList(list)
- 使用
CopyOnWriteArrayList
hashSet && hashMap
使用CopyOnWriteArraySet
和ConcurrentHashMap
6.多线程锁
- 当在方法上加上
synchronized
关键字,实际上锁的当前的对象 - 在static方法上加上
synchronized
关键字,实际上锁的是当前类的class文件 - 对于同步方法块,锁的是
synchronized
关键字括号里配置的对象 - synchronized关键字底层原理:点击查看
sychronized锁升级:
在jdk1.6之前,synchronized使用重量级锁方式来实现线程之间锁竞争的关系,之所以称它为重量级锁,是因为他的底层依赖操作系统层面的mutex Lock来实现互斥锁的功能。mutex是系统方法,由于权限隔离的关系,应用系统调用系统方法的时候会出现系统调用需要转换到内核态执行,涉及到用户态到内核态的相互转换,这会带来性能上的损耗。所以jdk1.6之后,synchronized增加了锁升级的机制。在轻量级锁状态下,竞争锁的线程会根据自适应自旋次数去尝试自旋占用锁资源。如果在轻量级状态下还是没有竞争到锁,只能升级到重量级锁。在重量级锁状态下,没有竞争到锁的线程会被阻塞。这是线程的状态叫做blocked,处于一个等待的状态。需要获得锁的线程释放锁资源后出发唤醒。锁升级的设计思路本质上是性能与安全性的平衡。
1)偏向锁有锁标志位,如果当前尝试获取锁的线程不是同一个线程,那么锁就直接升级成轻量级锁。如果是同一个线程,判断标志位,看看看是否已经获得过锁,如果已经获得过,则标志位+1,如果没有就获取偏向锁。
2)轻量级锁的过程:首先会判断当前线程锁的状态,如果是无锁状态,则会在线程的栈帧种创建一个Lock Record(锁记录)空间,用于存储对象目前的Mark Word的拷贝。当前线程将LockRecoed种的owner指针指向当前对象(synchronized锁的),并使用CAS操作将对象的Mark Word更新为指向Lock Record的指针。(哪个线程更新成功,哪个线程就成功获取到锁。)更新成功后将Mark Word中的锁标志位设置成00.,然后执行同步方法块。
如果不成功,则先判断当前对象的Mark Word是否指向当前线程的栈帧,是则表示当前线程已经持有锁了,这是一次重入,那就直接执行同步方法(锁消除)。如果不是,那就通过自旋操作尝试获取锁资源,一直到达指定的自旋次数后,就会升级成重量级锁。
轻量级锁这么设计的思想:操作系统的进程执行的时候会有一个阻塞状态,阻塞的时候进程会被挂起(suspend),挂起操作是操作系统原语实现的。既然要执行原语,肯定需要把用户态切换到内核态,这里会有目态到管态切换的开销。为了减小这种开销,就提出阻塞的线程不是直接挂起,而是自旋等待一会儿,这样可以实现。
公平锁与非公平锁
new ReentrantLock()
默认是非公平锁,里面默认传值false
。可能造成一个线程执行时间很长,别的线程拿不到资源。
公平锁:所有线程具有相同的抢占资源的能力。
公平锁特点:
- 阳光普照
- 效率相对于非公平锁来说低一些
非公平锁特点:
- 有线程可能会饿死
- 效率高
可重入锁:
synchronized
【隐式】和Lock
【显式】都是可重入锁,显式是需要手动加锁和解锁。- 可重入锁是获取到这个锁后具有对加锁资源里面的资源有访问权限。例子
[大资源[中等资源[小资源][小资源]][中等资源[小资源]]]
。获得该锁后可以访问中等资源和小资源,而不需要获取其他锁。说白了就是这些资源加的是同一把锁。
死锁:
- 两个或两个以上进行在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力干涉,他们无法继续执行。
- 产生死锁的原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 资源分配不当
- 死锁的必要条件:【操作系统】
- 互斥
- 占有并等待
- 非抢占
- 循环等待
- 验证是否死锁
- jps 类似linux中的ps -ef
- jstack jvm自带的堆栈跟踪工具
7.Callable接口
前面的创建线程的方法有两种,一种是继承Thread类,一种是实现Runnable接口。Thread类由于单继承的问题,很少使用。实现Runnable接口缺少一个功能:线程run完了之后没办法返回线程结果。所以Callable接口新加了这个功能。
特点:
- 实现
Runnable
,需要实现不返回任何内容的run()
方法,而对于Callable
需要实现完成时返回结果的call()
方法。【实现方法不同】 call()
方法会引发异常,而run()
不能。【run没有返回值,call有返回值】- 实现
Callable
必须重写call()
,不写会抛出异常【是否可以抛出异常】
Callable
使用
使用FutureTask(Runnable 的一个实现类,可以传递callable的对象)
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//直接使用new callable对象替换runnable会报错
// new Thread(new MyThread(),"BB").start();
//使用FutureTask
FutureTask<Integer> task = new FutureTask<>(new MyThread());
//使用lambda表达式创建
FutureTask<Integer> task1 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
return 1024;
});
//创建线程
new Thread(task1,"lucy").start();
while (!task1.isDone()){
System.out.println("wait...");
}
//调用get方法
System.out.println(task1.get());
new Thread(task,"jack").start();
System.out.println(task.get());
}
static class MyThread implements Callable{
@Override
public Integer call() throws Exception {
return 200;
}
}
}
/**
* FutureTask原理 ---?未来任务----不影响主线程执行的情况下,单开一个线程做一些加成特效
* 1、老师上课,但是老师上课的时候口渴了。去买水显然不合适。
* 讲课的线程继续进行
* 班上的某个同学去帮老师买水(单开一个线程)
* 水买回来了,有需要的时候直接get喝水
*
* 2、现有4个同学,需要让4个同学做不同操作【同学1计算1+2...+5 同学2计算 10+11+...+50 同学3计算 60+61+62 同学4计算90-89-10】
* 第2个同学计算量比较大
* 这时候可以给第2个同学单独开一个线程,先汇总134同学的结果,最后再汇总第2个同学的答案
*
* 3、考试:先做会做的题目,再做不会做的题目
*
* 最终汇总的时候只需要汇总一次
*/
8.辅助类
CountDownLatch–减少计数
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句
- CountDownLatch主要的两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
例子:
现在教室中有6个同学,当6个同学都离开之后,老师才可以下班
public class CountDownDemo {
/**
* 需求
* 班上现在还剩下六个学生,要求学生全部走完之后,老师才能下班。
*/
public static void main(String[] args) throws InterruptedException {
//创建countDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
//模拟六个同学陆续离开教室
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开教室。");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//计数器为0 才会放开主线程
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 老师下班");
}
}
不使用countDownLatch的执行结果(代码中相关内容去掉):
/**
1 号同学离开教室。
3 号同学离开教室。
2 号同学离开教室。
main 老师下班
5 号同学离开教室。
4 号同学离开教室。
6 号同学离开教室。
*/
使用后结果:
/**
1 号同学离开教室。
2 号同学离开教室。
6 号同学离开教室。
5 号同学离开教室。
3 号同学离开教室。
4 号同学离开教室。
main 老师下班
*/
CyclicBarrier–循环栅栏
是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点。在设计一组固定大小的线程程序中这些线程必须时不时相互等待,这时候CyclicBarrier
非常有用。因为该barrier在释放等待线后可以重新使用,所以称为循环栅栏。
CyclicBarrier
支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。
若在继续所有参与线程之前更新共享状态,此屏障操作有用。
例子: 【七龙珠】
必须集齐七颗龙珠才可以召唤神龙,没集齐之前几个龙珠都在互相等待。
public class CyclicBarrierDemo {
//创建固定值
private static final int NUMBER = 7;
public static void main(String[] args) {
//1. 创建CyclicBarrier对象
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("七颗龙珠都集齐后可以召唤神龙");
});
//集齐龙珠的过程
for (int i = 1; i <= 7 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"星龙珠被收集...");
//等待
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
},String.valueOf(i)).start();
}
}
}
结果如下:
/**
* 1星龙珠被收集...
* 3星龙珠被收集...
* 2星龙珠被收集...
* 4星龙珠被收集...
* 6星龙珠被收集...
* 5星龙珠被收集...
* 7星龙珠被收集...
* 七颗龙珠都集齐后可以召唤神龙
*/
如果线程完成数量少于NUMBER,则会一直等待下去
Semaphore–信号灯
是一个计数信号量。从概念上讲,信号量维护了一个许可集。在有必要的时候,在许可可用前会阻塞每一个acquire()
,然后再获取该许可。每个release()
添加一个许可,从而可能释放一个正在阻塞的获取者。但是不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。
Semaphore通常用于限制可以访问某些资源(物理或逻辑)的线程数目。
例子:
现有6辆汽车,但是只有3个停车位,实现这个等待操作
public class SemaphoreDemo {
//实现6辆汽车停到三个停车位
public static void main(String[] args) {
//创建Semaphore对象,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 号抢到车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + " 号车走了,多了一个空位");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
结果如下:
/**
* 1 号抢到车位
* 3 号抢到车位
* 2 号抢到车位
* 1号车走了,多了一个空位
* 4 号抢到车位
* 2号车走了,多了一个空位
* 5 号抢到车位
* 3号车走了,多了一个空位
* 4号车走了,多了一个空位
* 6 号抢到车位
* 6号车走了,多了一个空位
* 5号车走了,多了一个空位
*/
9. 读写锁
悲观锁:
- 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。 java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
乐观锁:
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复 读-比较-写的操作。
- java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
**读写锁:**一个资源可以被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程,因为读写操作是互斥的,读读是共享的
缺点:
- 造成锁的饥饿问题,一直读,没有写操作
- 读的时候不能进行写操作,只有读完后才能写。但是写的时候可以读
读写锁的演变:
- 无锁—多线程抢夺资源----乱
- 添加锁【synchronized|ReentrantLock】–都独占,每次只能操作一个
- 读写锁【ReentrantReadWriteLock】–读读共享,提升性能。写写独占
锁降级:
把写入锁降级为读锁,目的是提高数据可见性
步骤:
- 获取写锁
- 获取读锁
- 释放写锁
- 释放读锁
例子: 模拟缓存操作
class MyCache{
//创建map集合
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是"+key);
TimeUnit.MILLISECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写入数据完毕,key是" + key);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放
readWriteLock.writeLock().unlock();
}
}
//获取
public Object get(String key){
readWriteLock.readLock().lock();
Object res = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在读取");
TimeUnit.MILLISECONDS.sleep(300);
res = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取数据完毕");
return res;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
readWriteLock.readLock().unlock();
}
}
}
public class Cache {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+" ",num +"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
上面代码使用到了volatile
关键字,是用于保证可见性、有序性的
其底层原理是内存屏障。1)对volatile
变量的写指令后会加入写屏障。2)volatile
读指令后会加入读屏障
10.阻塞队列
阻塞队列首先是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另一端输出。
当队列是空的,从队列中获取元素的操作会阻塞
当队列是满的,往队列中添加元素的操作会阻塞
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或完全清空,使队列变得空闲起来才能继续新增。
在多线程领域中:所谓的阻塞时在某些情况下会挂起线程,一旦满足条件,被挂起的线程又会自动被唤起。
为什么需要阻塞队列?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要就唤醒线程,因为这一起BlockingQueue都给一手包办了。
基本架构
超接口:Collection<E> | Iterable<E> | Queue<E>
子接口:BlockingDeque<E>
实现类:ArrayBlockingQueue
|DelayQueue
|LinkedBlockingDeque
|LinkedBlockingQueue
|PriorityBlockingQueue
|SynchronousQueue
核心方法:
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
操作 | 解释 |
---|---|
抛出异常 | 当阻塞队列满时,再往队列里add插入元素会抛出IllegalStateException:Queue full 当阻塞队列空时,再往队列里remove元素会抛出 NoSuchElementException |
特殊值 | 插入方法,成功为true失败为false 移除方法,成功返回出队元素,队列里面没有返回null |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
11.线程池
三大方法、七个参数、四个拒绝策略
池化技术:程序的运行本质是占用系统资源!优化资源的使用演进出一种策略叫做池化技术。比如线程池、连接池、内存池、对象池。
线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待监督管理者分配可并发执行的任务 。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能保证内核的充分利用,还能防止过分调度。
特点
- 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务达到时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
- java中的线程池通过Executor框架实现,用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
线程池的分类
Executors.newFixedThreadPool(int)
:一池N线程Executors.newSingleThreadExeecutor()
:一池一线程,一个任务一个任务的执行Executors.newCachedThreadPool
:线程池根据需求创建线程,可扩容,遇强则强
线程池的使用
public class ThreadPoolDemo {
public static void main(String[] args) {
ontToN();
ontToOne();
oneToLarger();
}
/**
* 一池n线程
*/
public static void ontToN(){
System.out.println("一池N线程示例---------");
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
try {
//10个顾客
for (int i = 1; i <= 10 ; i++) {
threadPool1.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 办理业务");
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool1.shutdown();
}
}
/**
* 一池一线程
*/
public static void ontToOne(){
System.out.println("一池一线程示例---------");
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
//10个顾客
for (int i = 1; i <= 10 ; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 办理业务");
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();
}
}
/**
* 一池可扩容
*/
public static void oneToLarger(){
System.out.println("一池可扩容示例---------");
ExecutorService pool = Executors.newCachedThreadPool();
try {
//10个顾客
for (int i = 1; i <= 20 ; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 办理业务");
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
pool.shutdown();
}
}
}
底层实现:都是new ThreadThreadPoolExecutor
七个参数:
int corePoolSize
核心线程数量,常驻线程数量int maximumPoolSize
最大线程数量long keepAliveTime
保持存活时间,多久不用就直接结束TimeUnit unit
时间单位BlockingQueue<Runnable> workQueue
阻塞队列ThreadFactory threadFactory
线程工厂RejectedExecutionHandler handler
拒绝策略,线程满了没办法提供服务会拒绝
工作流程
主线程–>execute执行,创建线程–>常驻线程先服务,如果满了则进入阻塞队列,阻塞队列满了之后会创建新的线程,线程数量不会超过最大线程数量
如果又来了一个线程,这时线程池满了,阻塞队列满了 会执行拒绝策略。
拒绝策略
AbortPolicy
(默认):直接抛出RejectExecutionException
异常阻止系统正常运行CallerRunsPolicy
:调用者运行,是一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,让调用者执行,从而降低新任务的流量。DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务DiscardPolicy
:该策略默默丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的策略。
自定义线程池–最常用 (阿里开发规范要求)
public class PersonalizedPool {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,//参数1 常驻线程数量
5,//参数2 最大线程数量
2L,//参数3 保持存活时间
TimeUnit.SECONDS,//参数4 时间单位
new ArrayBlockingQueue<>(3),//参数5 阻塞队列
Executors.defaultThreadFactory(),//参数6 线程工厂
new ThreadPoolExecutor.AbortPolicy()//参数7 拒绝策略
);
System.out.println("一池N线程示例---------");
try {
//10个顾客
for (int i = 1; i <= 10 ; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 办理业务");
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
拓展 最大线程应该如何定义?
1、CPU密集型 几核cpu定义为几,可以保证cpu效率最高
- 使用代码获取
Runtime.getRuntime().avaliableProcessors()
CPU密集指的是系统中硬盘、内存的效率比CPU高,总是在等待CPU的响应。一般来说设置线程数量是CPU数量+1,这是一个经验值,不同硬件配置的参数不一样。2、IO密集型
- 判断程序中十分耗io的线程,设置线程数大于它就行了(最简单)
IO密集指的是系统中CPU的效率比内存和硬盘高,CPU等待IO的执行。所以这时候加大IO,一般来说设置的量可以是CPU数量的两倍,这是一个经验值,不同配置的参数不一样。
12.fork & join
并行执行任务提高效率。
可以将一个大的任务差分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join框架完成两件事:
- fork: 把一个复杂的任务进行拆分,大事化小
- join: 把差分任务的结果进行合并
特点:工作窃取,就是现有两个任务一个A一个B,现在B执行的快,A还没结束B就结束了,B就会继续执行A的工作。
例子:
现有需求:计算1+2+3…+100,将这个计算方式拆分成多个小任务。要求两个数相加,他们的差值不能超过10.
class MyTask extends RecursiveTask<Integer>{
//拆分时差值不能超过10
private static final int VALUE = 10;
private int begin;//拆分开始值
private int end;//拆分结束值
private int res;//结果值
//创建一个有参数的构造
public MyTask(int begin, int end){
this.begin = begin;
this.end = end;
}
//拆分和合并过程
@Override
protected Integer compute() {
//判断相加的两个数是不是大于10
if((end-begin) <= VALUE){
//相加操作
for (int i = begin; i <= end ; i++) {
res = res+i;
}
}else {
//进一步拆分
//获取数据中间值
int mid = (begin+end)/2;
//拆分左边
MyTask task01 = new MyTask(begin,mid);
//拆分右边
MyTask task02 = new MyTask(mid+1,end);
//调用方法拆分
task01.fork();
task02.fork();
//合并结果
res = task01.join()+task02.join();
}
return res;
}
}
public class ForkJoinDemo {
public static void main(String[] args) {
try {
MyTask task = new MyTask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(task);
//获取合并后结果
Integer integer = null;
integer = forkJoinTask.get();
System.out.println(integer);
forkJoinPool.shutdown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
13.四大函数式接口【必须掌握】
jdk8的新特性:
新时代的程序员必须会的操作:lambda表达式,链式编程,函数式接口,Stream流式计算
函数式接口:只有一个方法的接口(Runnable)
@FunctionalInterface
public interface Runnable{
public abstract void run();
}
//很多函数式接口
//简化编程模型,在新版本的框架底层大量使用
//foreach(consumer c)
四大函数式接口:(原生)
Function
、Supplier
、Consumer
、Prediate
函数式接口:有一个输入,有一个输出,都可以使用lambda表达式简化
@FunctionalInterface
public interface Function<T,R> {
/**
* 传入参数T 返回参数R
* @param t
* @return
*/
R apply(T t);
}
断定型接口: 有一个参数,返回值只能是boolean
public class PreDemo{
public static void main(String[] args) {
Predicate<String> predicate = new Predicate<String>() {
//判断字符串是否为空
@Override
public boolean test(String s) {
return s.isEmpty();
}
};
System.out.println(predicate.test("as"));
Predicate<String> predicate1 =(s)->{return s.isEmpty();};
System.out.println(predicate1.test("aaa"));
}
}
消费型接口:只有输入没有返回
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String o) {
System.out.println(o);
}
};
consumer.accept("asda");
Consumer<String> consumer1 = (s)->{System.out.println(s);};
consumer1.accept("saa");
}
}
供给型接口:Supplier,没有入参,只有返回
public class SupplierDemo {
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "aaa";
}
};
System.out.println(supplier.get());
Supplier<String> supplier1 = ()->{return "aaa";};
System.out.println(supplier1.get());
}
}
14.Stream流式计算
什么是Stream流式计算
大数据:存储+计算
存储交给容器(Collections),计算应该交给流计算 java.util.Stream
例子:
/**
* 题目要求:一分钟之内完成此题,只能使用一行代码
* 5个用户进行筛选
* 1、ID必须是偶数
* 2、年龄必须大于23
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只能输出一个用户
*/
public class StreamTest {
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
//集合用于存储,计算交给流Stream
List<User> users = Arrays.asList(u1, u2, u2, u4, u5);
//lambda表达式,链式编程,函数式接口,Stream流式计算
users.stream()
//1、ID必须是偶数
.filter(user-> {return user.getId()%2==0;})
//2、年龄必须大于23
.filter(user -> {return user.getAge()>23;})
//3、用户名转为大写字母
.map(user -> {return user.getName().toUpperCase();})
//4、用户名字母倒着排序
.sorted((o1,o2)->{return o2.compareTo(o1);})
//5、只能输出一个用户
.limit(1)
.forEach(System.out::println);
}
}
15.异步回调
Future对未来的事情进行建模
/**
* 异步调用:Ajax CompletableFuture
* /成功执行
* 失败回调
* 成功回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的 runAsync 异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+" runAsync=>Void");
});
System.out.println("11111111");
completableFuture.get();//获取阻塞执行结果,不get就不会执行
//有返回值的异步回调
//成功和失败, 成功返回值,失败返回错误
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+" supplyAsync=>Integer");
// int i = 10/0; //错误
return 1024;
});
completableFuture1.whenComplete((t,u)->{
System.out.println("t= "+ t + " u=" + u);
//t是返回值 u 是异常
}).exceptionally((e)->{
e.printStackTrace();
return 233;
}).get();
}
}
上述程序的结果:
/**
* 11111111
* ForkJoinPool.commonPool-worker-9 runAsync=>Void
* ForkJoinPool.commonPool-worker-9 supplyAsync=>Integer
* t= 1024 u=null
*/
16.JMM?
请你谈谈对Volatile的理解
是java虚拟机提供的轻量级同步机制
三个特点:
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM?
JVM是java虚拟机,JMM是java内存模型,不是真实存在的,其实是一个约定。
关于JMM的一些同步约定:
- 线程解锁前必须把共享变量立刻刷回
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
内存交互的8种操作:
- 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前,必须把此变量同步回主存
保证可见性
问题:线程A不知道变量在主内存中已经被改变了。怎么解决?
A:加上volatile,可以保证可见性,就是说对变量的修改线程是可见的。
不保证原子性
示例:
public class VolatileTest {
public static volatile int num ;
//最终结果应该输出
public static void main(String[] args) {
num = 0;
for (int i = 0; i <20 ; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
System.out.println(num);
}
public static void add(){
num++;
}
}
上述代码中执行结果预期是2W,但是结果总是达不到2W。说明volatile不能保证原子性。
原因:
add函数实际执行过程的步骤是这样的:(javap xxxclass可以看到)
0: getstatic //获得这个值
3: iconst_1 //复制到寄存器
4: iadd //给这个值加1
5: putstatic //重新写入这个值
可能会有多个线程同时进来,同时修改num这个值,所以不能保证原子性
如果不加lock
和synchronized
怎么保证原子性?
可以使用原子类解决原子性问题
原子类为什么这么牛?
因为这些类的底层都直接和CPU挂钩(native),在内存中修改值,Unsafe是一个特殊的存在
使用演示:
public class VolatileTest {
public volatile static AtomicInteger num2 = new AtomicInteger();
//最终结果应该输出
public static void main(String[] args) {
num = 0;
for (int i = 0; i <20 ; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
System.out.println(num);
}
public static void add(){
num2.getAndIncrement();//底层是CAS
}
}
禁止指令重排?
什么是指令重排?自己写的程序并不是按照顺序执行的。
源代码–>编译器优化的重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在指令重排的时候会考虑数据的依赖性
volatile如何避免指令重排?
内存屏障–>禁止上面执行和下面指令交换顺序
CPU指令的作用:
- 保证特定的操作的执行顺序
- 保证某系变量的内存可见性(利用这些特性 volatile实现了可见性)
加了volatile会产生内存屏障,在volatile上下都会加一个屏障。
volatile内存屏障在哪个地方使用的最多?
单例模式出场了
17.单例模式
饿汉式
public class Hungry {
/**
* 单例最重要的是构造器私有
*/
private Hungry(){
}
/**
* 里面有很多的属性,但是类初始化就会创建这些属性,非常消耗资源
* 于是产生了懒汉式单例,需要用的时候再创建
*/
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
DCL懒汉式单例
public class LazyMan {
private static boolean qinjaing = false;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
synchronized (LazyMan.class) {
// if (lazyMan != null) {
// throw new RuntimeException("不要试图用反射破坏异常");
}
if (qinjaing != false) {
qinjaing = true;
} else {
throw new RuntimeException("不要试图用反射破坏异常");
}
}
//单线程下确实单例ok
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.把这个对象指向这个空间
//真实步骤可能执行132.此时lazyman还没被完成构造
}
}
}
return lazyMan;
}
// //多线程并发
// public static void main(String[] args) {
// for (int i = 0; i < 10; i++) {
// new Thread(()->{
// LazyMan.getInstance();
// }).start();
// }
// }
// //反射破解使其不安全,破坏单例
// public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// LazyMan instance = LazyMan.getInstance();
// //获得无参构造器
// Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
// declaredConstructor.setAccessible(true);
// LazyMan lazyMan = declaredConstructor.newInstance();
// //单例模式.LazyMan@15fbaa4
// //单例模式.LazyMan@1ee12a7
// System.out.println(instance);
// System.out.println(lazyMan);
// }
//两个对象都使用反射再次破坏单例模式
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//通过反射破坏标志位qinjiang
Field qinjaing = LazyMan.class.getDeclaredField("qinjaing");
qinjaing.setAccessible(true);
//获得无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan = declaredConstructor.newInstance();
//
qinjaing.set(lazyMan,false);
LazyMan instance = declaredConstructor.newInstance();
//单例模式.LazyMan@15fbaa4
//单例模式.LazyMan@1ee12a7
System.out.println(instance);
System.out.println(lazyMan);
}
}
静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
单例不安全,枚举登场
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;
//反射不能破坏枚举
// EnumSingle enumSingle2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle2 = declaredConstructor.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
//Cannot reflectively create enum objects
// at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
}
}
18.CAS
CAS是什么?
AtomicInteger源码:
Unsafe
类是java的后门,valueOffset是内存地址偏移值。unsafe里面全是native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
这就是cas
自旋锁
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就操作。如果不是就一直循环。底层是自旋锁。
缺点:
- 底层是自旋锁,循环会耗时
- 一次性只能保证一个共享变量的原子性
- 存在ABA问题
ABA问题是什么:狸猫换太子
同时存在AB两个线程,线程A先读取内存中的值(假设是1),此时线程B进来对内存做了一些操作(先把1改成2,又改成1),这是A再去读取内存时感觉内存中的值
没有发生变化,事实上已经变了(这个1不是原来的1)。
19.原子引用
带版本号的原子操作
可以用来解决ABA问题
class ABA {
public static void abaTest() {
//Integer的坑,默认Integer的范围是-127~128,超过则会new一个,并不会复用已有对象
//如果泛型是包装类,要注意引用问题。
//正常业务中里面比较的都是对象,对象肯定是唯一的
AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference(1, 1);
new Thread(() -> {
int stamp = atomicInteger.getStamp();//获得版本号
System.out.println("a1==>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicInteger.compareAndSet(1, 3, atomicInteger.getStamp(), atomicInteger.getStamp() + 1));
System.out.println("a2==>" + atomicInteger.getStamp());
System.out.println(atomicInteger.compareAndSet(3, 1, atomicInteger.getStamp(), atomicInteger.getStamp() + 1));
System.out.println("a3==>" + atomicInteger.getStamp());
}, "a").start();
//与乐观锁原理相同
new Thread(() -> {
int stamp = atomicInteger.getStamp();//获得版本号
System.out.println("b1==>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicInteger.compareAndSet(1, 6,
stamp,
stamp + 1));
System.out.println("b2==>" + atomicInteger.getStamp());
}, "b").start();
}
}