多线程进阶=》JUC并发编程
1、什么是JUC
? JUC是java.util.concurrent的简写。
? 用中文概括一下,JUC的意思就是java并发编程工具包。
? 并发编程的本质就是充分利用CPU资源。
2、线程和进程
? 进程:是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
? 线程:是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
? 例如 开了一个进程Typora记笔记,等待几分钟就会自动保存(自动保存这个操作就是由线程完成的。)
?
? 一个进程可以包含多个线程,至少包含一个线程
? Java默认有两个线程:main线程和GC线程(垃圾回收线程)
? 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) {
}
}
}
private native void start0();
? Java是没有权限去开启线程,操作硬件的,这是一个native的本地方法,它调用的是底层的C++代码。
并发、并发
? 并发:是指多个线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程 任务是在同时进行的,但其实并发只是一种逻辑上的同时进行;
? 并行:是指多个线程任务在不同CPU上同时进行,是真正意义上的同时执行。
? 并发编程的本质:充分利用CPU的资源。
线程的6个状态
? 新建、运行、阻塞、等待、超时等待、终止
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
wait和sleep的区别
| wait | sleep |
---|
同步 | 只能在同步上下文中调用,否则抛出IllegalMonitorStateExcepton异常 | 不需要在同步方法块或同步块中调用 | 作用对象 | wait方法定义在Object类对象中,作用于对象本身 | sleep方法定义在java,lang.Thread中,作用于当前线程 | 释放锁资源 | 是 | 否 | 唤醒条件 | 其他线程调用notify()或者notifyAll()方法 | 超时或调用interrupt()方法体 | 方法属性 | wait是实例方法 | sleep是静态方法 |
一般企业中使用休眠的是
TimeUnit.DAYS.sleep(1);
TimeUnit.SECONDS.sleep(1);
3、Lock锁(重点)
3.1、可重入锁(递归锁)
3.1.1、synchronized同步锁
public class SaleTicketDemo01 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for(int i=0;i<40;i++){
ticket.sale();
}
},"A").start();
new Thread(()->{
for(int i=0;i<40;i++){
ticket.sale();
}
},"B").start();
new Thread(()->{
for(int i=0;i<40;i++){
ticket.sale();
}
},"C").start();
}
}
class Ticket{
private int number=50;
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");
number--;
}
}
}
3.1.2、Lock接口
? 使用ReentrantLock可重入锁(实现了Lock接口)实现同步:
? 可重入锁也叫作递归锁,指的是同一个线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但 不受影响。
? 就像在饭堂打饭,你在窗口排队。排到你的时候,突然你舍友A让你顺路带个饭,然后你就打了两份饭;这时 你还没离开窗口,舍友B又叫你打一份汤,于是你又额外打了一份汤。
? lock()方法:上锁
? unlock()方法:释放锁
public class Restaurant {
private Lock windows = new ReentrantLock();
public void getMeals() throws Exception {
try {
windows.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打饭");
} finally {
windows.unlock();
}
}
public void getSoup() throws Exception {
try {
windows.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打汤");
} finally {
windows.unlock();
}
}
public void today() throws Exception {
try {
windows.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打饭");
getMeals();
getSoup();
} finally {
windows.unlock();
}
}
public static void main(String[] args) {
Restaurant test = new Restaurant();
new Thread(() -> {
try {
test.today();
} catch (Exception e) {
e.printStackTrace();
}
}, "我").start();
new Thread(() -> {
try {
test.getSoup();
} catch (Exception e) {
e.printStackTrace();
}
}, "某人").start();
new Thread(() -> {
try {
test.getMeals();
} catch (Exception e) {
e.printStackTrace();
}
}, "另一个人").start();
}
}
输出:
我打饭
我打饭
我打汤
某人打汤
另一个人打饭
3.2、不可重入锁(自旋锁)
在另一个菜式比较好吃且热门的窗口,可不是这样的,在这里你在窗口,只能点一个菜(进入一次临界区),点完后,你想要再点别的菜,只能重新排一次队(虽然可以插队,当然我们可以引入服务员队伍管理机制:private Lock windows = new ReentrantLock(true);,指定该锁是公平的。) 即:自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。
public class Restaurant {
boolean isLock = false;
public synchronized void getMeals() throws Exception {
while (isLock) {
wait();
}
isLock = true;
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打饭");
} finally {
isLock = false;
}
}
public synchronized void getSoup() throws Exception {
while (isLock) {
wait();
}
isLock = true;
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打汤");
} finally {
isLock = false;
}
}
public void today() throws Exception {
while (isLock) {
wait();
}
isLock = true;
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "打饭");
getSoup();
} finally {
isLock = false;
}
}
public static void main(String[] args) {
Restaurant test = new Restaurant();
new Thread(() -> {
try {
test.today();
} catch (Exception e) {
e.printStackTrace();
}
}, "我").start();
new Thread(() -> {
try {
test.getSoup();
} catch (Exception e) {
e.printStackTrace();
}
}, "某人").start();
new Thread(() -> {
try {
test.getMeals();
} catch (Exception e) {
e.printStackTrace();
}
}, "另一个人").start();
}
}
输出:
我打饭
然后死锁了……
3.3、读写锁
ReentrantReadLock和ReentrantReadLock
? 然而餐次的人流量一大,老板发现经常排起很长的队伍,厨师却都闲着没事干。老板拍脑子一想,这样不行 啊,所以稍微改进了一下点餐方式。所有人都可以扫二维码用网页进行点餐,只要这个菜不是正在做(写 锁),那么就可以随便点。
? 即:假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的 时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有 一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。
public class Restaurant {
private ReentrantReadWriteLock food = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock getFoodLock = food.readLock();
private ReentrantReadWriteLock.WriteLock cookingLock = food.writeLock();
public void getFood() throws Exception {
try {
getFoodLock.lock();
System.out.println(Thread.currentThread().getName() + "点饭");
} finally {
getFoodLock.unlock();
}
}
public void cooking() throws Exception {
try {
cookingLock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "做菜");
} finally {
cookingLock.unlock();
}
}
public static void main(String[] args) {
Restaurant test = new Restaurant();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
test.getFood();
} catch (Exception e) {
e.printStackTrace();
}
}, "某人").start();
if (i == 2) {
new Thread(() -> {
try {
test.cooking();
} catch (Exception e) {
e.printStackTrace();
}
}, "厨师").start();
}
}
}
}
输出:
某人点饭
某人点饭
某人点饭
厨师做菜
==等待1秒==
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭
某人点饭
3.4、公平锁和非公平锁
公平锁:十分公平,必须遵守先来后到的原则;
非公平锁:十分不公平,可以插队。
在创建锁的时候如果没有添加参数,那么默认就是非公平锁
private Lock windows = new ReentrantLock(true);
3.5、synchronized和Lock的区别
| synchronized | Lock |
---|
存在层次 | java的关键字,在JVM层面上 | 是一个类 | 锁的释放 | 以获取锁的线程执行完同步代码或者线程执行发生异常,就立即释放锁 | 必须在finally中释放锁,不然有可能造成死锁 | 锁的获取 | 假设线程A获得锁,线程B等待。如果线程A阻塞,那么线程B就会一直等待 | Lock有四种获取锁的方法,但是线程可以通过类方法尝试获取锁,就不用一直等待。 | 锁状态 | 无法判断 | 可以判断 | 锁类型 | 可重入锁、不可中断、非公平的 | 可重入、可中断、可公平(也可非公平,自己设置) | 性能 | 少量同步 | 大量同步 |
关于synchronized和Lock性能区别的详细描述
**synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。**独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
3.6、锁的到底是什么?
在静态方法中,实际上是对调用该方法的对象加锁,俗称“对象锁”。
在非静态方法中,实际上是对该类对象加锁,俗称“类锁”。
类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法区别的。
4、生产者和消费者问题
4.1、synchronized的传统版本
我们这里使用synchronized来解决生产者消费者问题,比如当num为0时,使B线程等待,唤醒A线程让其加1,当num为1时使A线程等待,并唤醒B线程让其减1。
public class PCSyn{
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i ++){
new Thread(()->{data.increment();},"A").start();
new Thread(()->{data.decrement();},"B").start();
}
}
}
class Data{
private int num = 0;
public synchronized void increment(){
if(num != 0)
this.wait();
num ++;
System.out.println(Thread.currentThread().getName() + "->" + num);
notifyAll();
}
public synchronized void decrement(){
if(num == 0)
this.wait();
num --;
System.out.println(Thread.currentThread().getName() + "->" + num);
notifyAll();
}
}
至此,在AB两个线程执行时,结果确实是按照我们的预想交替出现01,但是,同样的情况如果我们在上述代码中再加入两个线程CD执行同样的操作,即线程AC执行+1操作,线程BD执行-1操作。
此时,出现的结果就不如我们的意了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-21OYQLD2-1632053112970)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210913102336420.png)]
我们可以看到,当num因上一次的操作数值发生变化之后,我们进行了if判断,if判断只判断了一次,当这个条件被满足时,程序就继续往下执行。比如num此时为0,那么线程A和线程C都被唤醒了,但是其中只有一个线程是有用的,那么另一个就是虚假唤醒。
虚假唤醒:在线程的 等待/唤醒 的过程中,等待的线程被唤醒后,在条件不满足的情况依然继续向下运行了。
我们只需要将代码中的if换成while就能解决虚假唤醒的问题。因为唤醒后还是需要再次判断条件,而 if 就 直接运行下去了。
public class PCSyn{
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i ++){
new Thread(()->{data.increment();},"A").start();
new Thread(()->{data.decrement();},"B").start();
}
}
}
class Data{
private int num = 0;
public synchronized void increment(){
while(num != 0) {
this.wait();
}
num ++;
System.out.println(Thread.currentThread().getName() + "->" + num);
notifyAll();
}
public synchronized void decrement(){
while(num == 0) {
this.wait();
}
num --;
System.out.println(Thread.currentThread().getName() + "->" + num);
notifyAll();
}
}
4.2、Lock
同样的问题我们可以通过Lock接口来实现。
public class PCLock{
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i ++){
new Thread(()->{data.increment();},"A").start();
new Thread(()->{data.decrement();},"B").start();
new Thread(()->{data.increment();},"C").start();
new Thread(()->{data.decrement();},"D").start();
}
}
}
class Data{
private int num = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try{
while(num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "->" + num);
condition.signalAll();
} catch(Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void decrement(){
lock.lock();
try{
while(num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "->" + num);
condition.signalAll();
} catch(Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
但是,明明能用synchronized解决的问题,为什么要引入一个新的技术呢?这个技术肯定有他自己的过人之处。
那么在Lock的condition中,我们是可以精准的控制是哪个线程被唤醒,哪个线程等待,比如:实现A线程打印A,B线程打印B,C线程打印C,并且让其每一次出现的顺序是A->B->C。
condition的优势:精准的通知和唤醒线程。
public class PrintLock{
public static void main(String[] args){
Data data = new Data();
for(int i = 0; i < 10; i ++){
new Thread(()->{data.printA();},"A").start();
new Thread(()->{data.printB();},"B").start();
new Thread(()->{data.printC();},"C").start();
}
}
}
class Data{
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void printA(){
lock.lock();
try{
while(num != 1)
conditino1.await();
System.out.println(Thread.currentThread().getName() + "->AAA");
condition2.signal();
} catch(Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void printB(){
lock.lock();
try{
while(num != 2)
conditino1.await();
System.out.println(Thread.currentThread().getName() + "->BBB");
condition3.signal();
} catch(Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void printA(){
lock.lock();
try{
while(num != 3)
conditino1.await();
System.out.println(Thread.currentThread().getName() + "->CCC");
condition1.signal();
} catch(Exception e){
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
5.、8锁现象
以下名词解释:
顺序执行:先调用的先执行;
随机执行:没有规律,与计算机硬件资源有关,哪个线程先得到资源就先执行,各个线程之间互不干扰
5.1、多个线程使用同一把锁——顺序执行
示例1、 标准访问,请问是先打印邮件还是先发短信
public class MultiThreadUseOneLock01 {
public static void main(String[] args){
Mobile mobile = new Mobile();
new Thread(()->mobile.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
}
}
class Mobile {
public synchronized void sendEmail() {
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
答案:先发邮件,再发短信。
5.2、多个线程使用同一把锁,其中某个线程里面还有阻塞——顺序执行
示例2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
public class MultiThreadUseOneLock02 {
public static void main(String[] args){
Mobile2 mobile = new Mobile2();
new Thread(()->mobile.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
}
}
class Mobile2 {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
答案:还是先发邮件,后发短信。
5.3、多个线程,有的线程有锁,有的线程没锁——随机执行
示例3、新增一个普通方法接收微信()没有同步,请问先打印邮件还是接收微信?
public class MultiThreadHaveLockAndNot03 {
public static void main(String[] args){
Mobile3 mobile = new Mobile3();
new Thread(()->mobile.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
new Thread(()->mobile.getWeixinMs(),"C").start();
}
}
class Mobile3 {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
public void getWeixinMs() {
System.out.println("getWeixinMs");
}
}
答案:先执行getWeixinMs()方法,然后执行发邮件,最后是发短信。原因是getWeixinMs()方法是一个普通方法,不受Synchronized锁的影响。
5.4、多个线程使用多耙锁——随机执行
被Synchronized修饰的方法,锁的对象是方法的调用者;
调用者不同,它们之间用的不是同一个锁,相互之间没有关系。
示例4、两部手机、请问先打印邮件还是短信?
public class MultiThreadUseMultiLock04 {
public static void main(String[] args){
Mobile4 mobile1 = new Mobile4();
Mobile4 mobile2 = new Mobile4();
new Thread(()->mobile1.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile2.sendMS(),"B").start();
}
}
class Mobile4 {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
答案:先发短信,再发邮件。
5.5、Class锁:多个线程使用一个对象——顺序执行
被Synchronized和static同时修饰的方法,锁的对象是类的class对象,是唯一的一把锁,线程之间是顺序执行。
锁Class和锁对象的区别:
? 1. Class锁,类模板只有一个;
-
对象锁,通过类模板可以new多个对象。 如果全部都锁了Class,那么这个类下的所有对象都具有同一把锁。
示例5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class MultiThreadUseOneObjectOneClassLock05 {
public static void main(String[] args){
Mobile5 mobile = new Mobile5();
new Thread(()->mobile.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
}
}
class Mobile5 {
public synchronized static void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized static void sendMS() {
System.out.println("sendMS");
}
}
答案: 先发邮件,后发短信。
5.6、Class锁:多个线程使用多个对象——顺序执行
被 synchronized 修饰 和 static 修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。
Class锁是唯一的,所以多个对象使用的也是同一个Class锁。
示例6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
public class MultiThreadUseMultiObjectOneClassLock06 {
public static void main(String[] args){
Mobile6 mobile1 = new Mobile6();
Mobile6 mobile2 = new Mobile6();
new Thread(()->mobile1.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile2.sendMS(),"B").start();
}
}
class Mobile6 {
public synchronized static void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized static void sendMS() {
System.out.println("sendMS");
}
}
答案:先发邮件,后发短信。先创建的对象先执行。
5.7、Class锁与对象锁:多个线程使用一个对象-随机执行
被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;
只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。
示例7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class MultiThreadUseOneObjectClassLockAndObjectLock07 {
public static void main(String[] args){
Mobile7 mobile = new Mobile7();
new Thread(()->mobile.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile.sendMS(),"B").start();
}
}
class Mobile7 {
public synchronized static void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
答案:先发短信,后发邮件。
5.8、Class锁与对象锁:多个线程使用多个对象-随机执行
被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;
只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。
示例8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
public class MultiThreadUseMultiObjectClassLockAndObjectLock08 {
public static void main(String[] args){
Mobile8 mobile1 = new Mobile8();
Mobile8 mobile2 = new Mobile8();
new Thread(()->mobile1.sendEmail(),"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->mobile2.sendMS(),"B").start();
}
}
class Mobile8 {
public synchronized static void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendMS() {
System.out.println("sendMS");
}
}
答案:先发短信,后发邮件。
6、集合类不安全
6.1、List不安全
有如下List的集合类:
public class ListTest {
public static void main(String[] args) {
List<Object> arrayList = new ArrayList<>();
for(int i=1;i<=10;i++){
new Thread(()->{
arrayList.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(arrayList);
},String.valueOf(i)).start();
}
}
}
这就会造成:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1oDKhdb-1632053112972)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210914212930786.png)]
这是由于确实线程锁导致的。
解决方案一:使用Vector(但是不建议这种使用,因为其发布比ArrayList还早)
List<String> list = new Vector<>();
解决方案二:使用工具类Collections,使ArrayList变的安全
List<String> list = Collections.synchronizedList(new ArrayList<>());
解决方案三:使用JUC包下的CopyOnWriteArrayList<>();
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWrite:写入时复制,简称COW。 计算机程序设计领域的一种优化策略.
原因:多个线程调用的时候,List,读取的时候是固定的,写入的时候存在覆盖问题,所以写入的时候复制一份容器再写入(且写入操作加锁),避免写入覆盖。
CopyOnWriteArrayList比Vector厉害在哪里?
Vector底层是使用synchronized关键字来实现的:效率特别低下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z2enYQ1H-1632053112973)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210914214309523.png)]
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqqhL3lZ-1632053112975)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210914214838168.png)]
6.2、Set不安全
有如下Set的集合类:
public class SetTest {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
解决方案一:使用工具类Collections,使HashSet变的安全
Set<String> set = Collections.synchronizedSet(new HashSet<>());
解决方案二:使用JUC包下的CopyOnWriteArraySet<>();
List<String> list = new CopyOnWriteArraySet<>();
HashSet底层是什么?
HashSet底层就是一个HashMap;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object();
add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储。
hashSet就是使用了HashMap key不能重复的原理。
PRESENT是什么? 是一个常量 不会改变的常量 无用的占位。
6.3、Map不安全
回顾Map的基本操作;
默认等价什么? new HashMap<>(16, 0.75);
Map<String, String> map = new HashMap<>(); //加载因子、初始化容量
默认加载因子是0.75,默认的初始容量是16
有如下Map的集合类:
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
解决方案一:使用工具类Collections,使HashMap<>()变的安全
Map<String, String> map = Collections.synchronizedMap(new HashMap());
解决方案二:使用JUC包下的ConcurrentHashMap<>();
Map<String, String> map = new ConcurrentHashMap<>();
7、Callable(简单)
与Runnable的区别:
? 1、可以有返回值
? 2、可以抛出异常
? 3、方法名不同, run()/call()
想象这一种情况,有很多线程。我们想挑出有问题的线程,如果按照之前的方式是很麻烦的,但如果使用Callable接口的方式,我们让没问题的线程返回true,有问题的返回false,就能很轻松的解决这个问题。
下面来看看如何使用Callable创建出线程:
但是怎么将Callable放入到Thread里面呢?
源码分析:
接下来我们查看Runnable接口及其子接口和实现类,发现有个叫RunnableFuture的子接口
接着查看RunnableFuture,发现它有一个实现类叫FutureTask
而这个FutureTask的构造函数中有Callable接口:
所以我们给FutureTask的构造方法中传入Callable接口,而FutureTask本身又实现了Runnable接口,所以Callable和Runnable就搭上线了,即我们在Thread的构造方法中传入FutureTask就行了。
所以使用Callable接口创建多线程的正确写法是:
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 1; i < 10; i++) {
MyThread thread= new MyThread();
FutureTask<String> futureTask = new FutureTask<>(thread);
new Thread(futureTask,String.valueOf(i)).start();
String s = futureTask.get();
System.out.println("返回值:"+ s);
}
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Call:"+Thread.currentThread().getName());
return "String"+Thread.currentThread().getName();
}
}
再来看Callable的返回值
使用get()方法获取返回值即可。
futureTask.get();
问题思考:Callable接口给线程加了返回值,为什么非要专门弄个get方法来获取呢?直接在线程运行完成后获取不行吗?
分析:想象这样一种情况,主线程中有四个线程,分别是A、B、C和D。假设B的执行时间是20s,其他线程执行时间都为5s,如果我们以下图方式获取返回值,那就会造成时间上的浪费:
上图是在每个线程启动后紧接着就用get方法等待返回值,那么在得到get方法的返回值之前主线程是不会向下运行的,所以对于每个线程来说,只有自己运行结束后才轮得到其他线程,这就和顺序执行一样了。
那如果将所有的get方法都放在程序的最后,如下图所示,则能缩短执行时间:
看上图,因为我们先让所有线程都运行起来并没有立刻获取返回值,因此获取到A.get()的值意味着已经过了5s,且在这5s内是ABCD4个线程同时执行的,而B由于执行时间为20s,所以拖慢了总执行时间。
同一个FutureTask对象只能被不同的线程调用一次
? 只有一个futureTask对象,所以即使A和B两个线程调用,也只会执行一次。
FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
8、常用辅助类(必会!!!)
8.1、 CountDownLatch
其实就是一个减法计数器,对于计数器归零之后再进行后面的操作,这是一个计数器!
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("close door");
}
}
主要方法:
-
countDown 减一操作; -
await 等待计数器归零。
await等待计数器为0,就唤醒,再继续向下运行。
8.2、CyclickBarrier
其实就是一个加法计数器;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙~");
});
for (int i = 1; i <= 7; i++) {
int finalI = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 收集了第 {"+ finalI+"} 颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3、Semaphore
Semaphore:信号量
抢车位:
3个车位 6辆车:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
int finalI = i;
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 抢到了车位{"+ finalI +"}");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
9、阻塞队列(BlockingQueue)
9.1、什么是阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue 是Collection的一个子类,以下是BlockingQueue常见的子类:
-
ArrayBlockingQueue -
LinkedBlockingQueue -
SynchronousQueue
阻塞队列提供了四种处理方法:
方法/处理方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞,等待 | 超时等待 |
---|
添加 | add | offer | put | offer(timeNum, timeUnit) | 移除 | remove | poll | take | poll(timeNum, timeUnit) | 返回队首元素 | element | peek | | |
public static void test1(){
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
=======================================================================================
public static void test2(){
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
=======================================================================================
public static void test3() throws InterruptedException {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
=======================================================================================
public static void test4() throws InterruptedException {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
System.out.println("开始等待");
blockingQueue.offer("d",2, TimeUnit.SECONDS);
System.out.println("结束等待");
System.out.println("===========取值==================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println("开始等待");
blockingQueue.poll(2,TimeUnit.SECONDS);
System.out.println("结束等待");
}
9.2、ArrayBlockingQueue
ArrayBlockingQueue 是最典型的有界队列,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程
我们在创建它的时候就需要指定它的容量,之后也不可以再扩容了,在构造函数中我们同样可以指定是否是公平的,代码如下:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
9.3、LinkedBlockingQueue
从命名可以看出,这是一个内部用链表实现的 BlockingQueue。如果我们不指定它的初始容量,那么它容量默认就为整型的最大值 Integer.MAX_VALUE,由于这个数非常大,约为 2 的 31 次方,我们通常不可能放入这么多的数据,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限。
其他特点:
- 同样也利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程
- 无法设置 ReentrantLock 的公平非公平,默认是非公平
- 也可以设置固定大小
默认无参构造函数如下,默认最大值 Integer.MAX_VALUE:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
9.4、SynchronousQueue同步队列
SynchronousQueue 最大的不同之处在于,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
需要注意的是,SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候,SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。
为什么说它的容量是 0 ,我们可以看其中的几个方法:
peek 方法永远返回 null,代码如下:
public E peek() {
return null;
}
因为 peek 方法的含义是取出头结点,但是 SynchronousQueue 的容量是 0,所以连头结点都没有,peek 方法也就没有意义,所以始终返回 null。
同理,element 方法始终会抛出 NoSuchElementException 异常,但是这个方法的实现在它的父类AbstractQueue 中
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
SynchronousQueue 的 size 方法始终返回 0,因为它内部并没有容量,代码如下:
public int size() {
return 0;
}
SynchronousQueue 的take() 是使用了lock锁保证线程安全的。
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" Put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+" Put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+" Put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
10、线程池(重点)
10.1、什么是线程池、
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池,多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 机器完成一项任务所需时间为:创建线程时间t1,执行任务的时间t2, 销毁线程时间t3;如果t1 + t3 远大于 t2,则可以采用线程池,以提高服务器性能。线程池技术主要关注于线程的创建和销毁时间,把创建时间和销毁时间分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有创建时间和销毁时间的开销了。
10.2、线程池三大方法
ExecutorService threadPool1 = Executors.newSingleThreadExecutor(); //一次只能执行一个线程的线程池
ExecutorService threadPool2 = Executors.newFixedThreadPool(3); //一次可以执行固定参数的线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可以自动变化的线程池
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 1; i <=100 ; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 1; i <=100 ; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 1; i <=100 ; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
10.3、线程池七大参数
对于三大方法的源码分析:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
本质:三种方法都是开启的ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
比如我们定义一个线程池corePoolSize = 3,maximumPoolSize = 5,阻塞队列大小为3,我们用银行柜台的案例来解释整个过程,可以分成四个阶段;
- 当来的人不超过三个人时,系统正常运行,业务都能顺利办理;
- 当来的人超过3而不超过6时,那么整个系统也可以正常运行,只不过这个时候银行的休息区(BlockingQueue<>(3))里等待着要办理业务的人;
- 当来的人超过6而不超过8(8 = maximumPoolSize + 阻塞队列大小)时,这个时候系统就会有些变化,银行会重新新开一些还未使用的柜台共来的人使用,如果开启之后在keepAliveTime时间段内柜台还没有一个人,柜台又会重新关闭;
- 当银行来的人超过8之后,系统已经承受不住人流量了,系统就会崩溃,这个时候就需要一种拒绝策略来拒绝爆满之后的人;
10.4、线程的四大拒绝策略
-
第一种拒绝策略:new ThreadPoolExecutor.AbortPolicy(),如果线程满了,则不处理新的进程,抛出异常; static void demo1(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
int totalThread = 8;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
-
第二种处理策略:new ThreadPoolExecutor.CallerRunsPolicy(),线程池满了,如果有新的哪里来的去哪里,不会抛出异常; static void demo2(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
int totalThread = 12;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
-
第三种处理策略:new ThreadPoolExecutor.DiscardPolicy(),线程池满了,丢掉线程,不会抛出异常 static void demo3(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
int totalThread = 15;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
-
第四种处理策略:new ThreadPoolExecutor.DiscardOldestPolicy(), 线程池满了,会尝试和最早的线程去竞争,如果成功则加入,否则丢弃,不会抛出异常 static void demo4(){
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
3,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
int totalThread = 14;
try {
for (int i = 0; i < totalThread; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了!!!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
11、四大函数式接口(必须掌握)
函数式接口:有且仅有一个抽象方法的接口
11.1、Function函数型接口
@FunctionalInterface
public interface Function<T, R> {
R apply(T t)
传入参数T,返回类型R
举例:
public class Demo01 {
public static void main(String[] args) {
Function<String,String> function = (str) ->{return str;};
System.out.println(function.apply("starasdas"));
}
}
11.2、Predicate断定型接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
有一个输入参数,返回值只能是布尔值。
举例:
public class Demo2 {
public static void main(String[] args) {
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test("11"));
System.out.println(predicate.test(""));
}
}
11.3、Consumer消费性接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
有一个输入参数,没有返回值。(消费者消费不需要返回值)
举例:
public class Demo3 {
public static void main(String[] args) {
Consumer<String> consumer = (str)->{
System.out.println(str);
};
consumer.accept("abc");
}
}
11.4、Supplier供给型接口
@FunctionalInterface
public interface Supplier<T> {
T get();
}
没有参数,只有返回值。
举例:
public class Demo4 {
public static void main(String[] args) {
Supplier<String> supplier = ()->{return "1024";};
System.out.println(supplier.get());
}
}
12、Stream流式计算
什么是Stream流式计算?
存储 + 计算
集合、Mysql本质是存储东西的。
计算都应该交给流来操作。
示例:
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
list.stream()
.filter(u -> {return u.getId()%2 == 0;})
.filter(u -> {return u.getAge() > 23})
.map(u -> {return u.getName().toUpperCase();})
.sorted((uu1, uu2) -> {return uu1.compareTo(uu2);})
.limit(1)
.forEach(System.out::println);
}
}
13、ForkJoin
13.1、什么是ForkJoin
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,就是将一个大任务分割成若干小任务,最终汇总每个个小任务的结果得到这个大任务的结果。
过程主要有两步:
-
任务切割 -
结果合并 13.2、ForkJoin特点 工作窃取(work-stealing): 是指某个线程从其他队列里窃取任务来执行。 就是一个工作线程下会维护一个包含多个子任务的==双端队列==。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度较快,很快就把所有任务消耗完了。 线程的任务窃取: 比如你和你的小伙伴在一起吃水果,你的那份吃完了,他那份没吃完,这个时候你偷偷的拿了他的一些水果吃了。存在执行2个任务的子线程,存在A,B两个WorkQueue在执行任务,A的任务执行完了,B的任务没执行完,那么A的WorkQueue就从B的WorkQueue的ForkJoinTask数组中拿走了一部分尾部的任务来执行,可以合理的提高运行和计算效率。
13.2、如何使用ForkJoin
- 通过ForkJoinPool来执行
- 计算任务execute(ForkJoinTask<?> task)
- 计算类要继承ForkJoinTask(ForkJoinTask代表运行在ForkJoinPool中的任务)。
主要方法:
子类: Recursive :递归
- RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
- RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
代码示例:
核心代码
package cn.guardwhy.forkJoin;
import java.util.concurrent.RecursiveTask;
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if((end - start) < temp){
Long sum = 0L;
for (Long i = start; i <= end ; i++) {
sum += i;
}
return sum;
}else {
long middle = (start + end) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
task2.fork();
return task1.join() + task2.join();
}
}
}
测试代码:
package cn.guardwhy.forkJoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class ForkJoinTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test2();
}
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));
}
private static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum=" +sum+" 时间: " + (end-start));
}
private static void test03() {
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));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3MSsN95O-1632053112977)(C:\Users\爸爸\AppData\Roaming\Typora\typora-user-images\image-20210917234608412.png)]
14、异步回调
14.1、Future接口
Future的设计初衷:对将来某个时刻会发生的结果进行建模。
Future接口建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future 中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不需要等待耗时的操作完成。
其实就是前端 --> 发送ajax异步请求给后端。
示例:使用Future 以异步的方式执行一个耗时的操作:
ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> future = executor.submit(new Callable<Double>() {
public Double call() {
return doSomeLongComputation();
}
});
doSomethingElse();
try {
Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
} catch (InterruptedException ie) {
} catch (TimeoutException te) {
}
这种编程方式让你的线程可以在ExecutorService以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他任务。如果已经运行到没有异步操作的结果就无法继续进行时,可以调用它的get方法去获取操作结果。如果操作已经完成,该方法会立刻返回操作结果,否则它会阻塞线程,直到操作完成,返回相应的结果。
为了处理长时间运行的操作永远不返回的可能性,虽然Future提供了一个无需任何参数的get方法,但还是推荐使用重载版本的get方法,它接受一个超时的参数,可以定义线程等待Future结果的时间,而不是永无止境地等待下去。
14.2、CompletableFuture
**CompletableFuture**类提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,不用担心实现细节。
-
无返回值的runAsync异步回调 public class CompletableFutureTest {
public static void main(String[] args) throws Exception {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsync=>Void");
});
System.out.println("1111");
completableFuture.get();
}
}
-
有返回值的supplyAsync异步回调 public class CompletableFutureTest {
public static void main(String[] args) throws Exception {
CompletableFuture<Integer> completableFuture =
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
int i = 10 / 0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t);
System.out.println("u=>" + u);
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 404;
}).get());
}
}
如果发生了异常,get可以获取到exceptionally返回的值;
15、JMM
15.1、什么是JMM
? JMMJMM即为JAVA 内存模型(java memory model)。它并不是一种实际存在的东西,而是一种人为形成的约定,是一种概念。JAVA 内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。
关于JMM的一些同步的约定:
- 线程在解锁前,必须将线程中的工作内存中存储的值即时刷新到主内存中的共享变量!
- 线程在加锁前,必须读取主存中的最新值到工作内存中!
- 加锁和解锁是同一把锁!
线程中操作的数据要从主内存中读取,并备份到线程自己的工作内存中,作为副本,主存并不会主动向线程更新数据。
15.2、线程的8种内存交互操作
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的共享变量释放
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
- load(加载):作用于工作内存的变量,把通过read操作获取的变量值放入工作内存中
- use(使用):作用于工作内存的变量,把工作内存中的变量传输给执行引擎,每当虚拟机遇到需要使用到变量的值,就会使用到这个指令
- assign(赋值):作用于工作内存的变量,把执行引擎传输过来的值放入工作内存
- store(存储):作用于主内存的变量,把一个从线程中的工作内存的变量值传送到主内存中,以便后续的write操作
- write(写入):作用于主内存的变量,将store操作从工作内存获取的变量值放入主内存中
15.3、JMM对以上八种内存操作指令做出了如下约束
- read和load、user和assign、store和write、lock和unlock必须成对出现,不允许单独操作其中一条指令
- 不允许线程丢弃离它最近的assign操作,即 工作内存中的变量值改变之后,必须告知主内存
- 不允许一个线程将没有assign过的数据从工作内存同步会主内存
- 一个新的变量必须在主内存中产生,不允许工作内存私自初始化一个变量来作为共享变量,即 实施use 和 store操作之前 , 必须经过 load 和 assign操作
- 同一变量同一时间只允许一个线程对其进行lock操作;多次lock之后,必须执行相同次数的unlock对其解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值, 即 每次获得锁的线程,加锁前必须要重新读取主内存中的变量值,才能提交给执行引擎进行use操作
- 如果一个变量没有被lock,就不能对其进行unlock操作,也不能对一个被其他线程锁住的变量进行unlock
- 对一个变量加锁之前,必须把工作内存中的变量值同步回主内存
但是存在一个问题:
假设现在有一个main线程和一个普通线程,普通线程执行的操作是:当num为 0 时 ,一直循环下去;此时main线程给num赋值为 1 ,普通线程并不知道num已经被修改,程序就会一直执行,不会停止!
public class VolatileDemo {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0) {
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
解决办法:volatile关键字,请看下一节。
16、Volatile
16.1、什么是Volatile
? Volatile 是 Java 虚拟机提供 轻量级的同步机制(相对于synchronized来说)
- 保证可见性 => JMM 主内存中的共享变量修改之后,会通知所有线程备份到各自的工作内存中
- 不保证原子性
- 禁止指令重排
16.2、保证可变性
public class JMMDemo01 {
private volatile static Integer number = 0;
public static void main(String[] args) {
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
while (number==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number=1;
System.out.println(number);
}
}
只要是加了volatile的变量,及时通知main线程number变量改变了,main线程中主内存拷贝到工作内存。
16.3、不保证原子性
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。同生共死
先上代码:
class MyData{
volatile int number = 0;
public void changeData(){
number ++;
}
}
public class Demo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);
}
}
由此可见volatile的使用线程不安全 。
那么为什么volatile不保证原子性?
举例:当线程1,线程2,线程3同时拿到主内存中的number=0,并且在工作内存中进行加一时,这个时候各个线程的工作内存里的变量要写回主内存,线程1在写回主内存这一个过程中因为cpu线程资源被抢占,挂起了,没有来的及通知其他线程number的值已经改为1了,线程2就将自己之前从主内存拷贝的number=0的变量(还没有更新number=1的变量)进行加一(这个时候理想性是number=2再写回主内存),而实际情况是线程2的工作内存中的变量number=1写回主内存,将线程1中已经写回主内存的number=1的覆盖了,导致数值丢失。
先解释一下number++这个命令:
MyData.java->MyData.class->JVM字节码
number++这个命令在JVM字节码被拆分成三个指令:
执行getfield拿到原始值 执行iadd进行加一的操作 执行putfield把累加的值写回主内存 javap -c里的是java字节码,这个是汇编底层原始命令
volatile不保证原子性,怎么解决原子性?
- 加synchronized,但是影响并发
class MyData{
volatile int number = 0;
public synchronized void changeData(){
number++;
}
}
public class Demo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.number);
}
}
- 使用AtomicInteger原子整型
class MyData{
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void changeData(){
atomicInteger.getAndIncrement();
}
}
public class Demo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.changeData();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t 打印number的数量:" + myData.atomicInteger);
}
}
16.4、禁止指令重排
这里先引入一个概念:
什么是JMM的有序性?
数据依赖性怎么理解?就是先有你爸再有你。
16.1、什么是指令重排
假设我写的第20行代码,执行的时候不一定会从第一行执行到第20行,打个比方:参加高考做卷子,出题人给的题目,你不一定会从第一题做到最后一题,你可能会先把会的写了,其他有难度的题目最后写。
代码案例1:
再多线程环境下语句执行有1234和2134以及1324三种顺序,语句4不能重排后变成第一个,原因是什么说的数据依赖性,变量要先声明再使用。
代码案例2:
正常的结果是x=0, y=0;但是如果发生指令重排,
可能在线程A中会出现,先执行b=1,然后再执行x=a;
在B线程中可能会出现,先执行a=2,然后执行y=b;
那么就有可能结果如下:x=2; y=1.
代码案例3:
在多线程环境下指令重排,会导致二种结果:一个是0+5=5,一个是1+5=6
正常单线程环境下会执行语句1再执行语句2最后执行语句3,结果打印为5
多线程环境下指令重排了先执行语句2再执行语句3最后执行语句1,结果打印为6
很恐怖的好吧,数据的一致性不能保证,所以volatile需要禁止指令重排。
volatile禁止指令重排小总结
那么在实际应用中,什么地方在应用这个内存屏障应用得最多呢? 单例模式
17、单例模式
单例模式在多线程环境下可能存在安全问题
单线程下的单例模式:
class MyData{
private static MyData myData = null;
private MyData(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static MyData getInstance(){
if(myData == null){
myData = new MyData();
}
return myData;
}
}
public class Demo {
public static void main(String[] args) {
System.out.println(MyData.getInstance() == MyData.getInstance());
System.out.println(MyData.getInstance() == MyData.getInstance());
}
}
控制台打印正确:
多线程下的单例模式:
public class Demo {
private static Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
demo = new Demo();
}
return demo;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
此时控制台打印错误:
本来应该打印一次的,结果10个线程打印了5次,这就有问题了。
有人会说加一个synchronized:
public class Demo {
private static Demo demo = null;
private void Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static synchronized Demo getInstance(){
if(demo == null){
demo = new Demo();
}
return demo;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
控制台打印正确:
但是这个不好,synchronized是重量型的,数据一致型得到保证了,但是影响并发
那怎么解决这个问题呢?
这个时候先介绍DCL双关检测锁机制,后面再一点点讲解。
17.1、DCL双关检锁机制
加锁前后都进行一次判断。
public class Demo {
private static Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
synchronized (Demo.class){
if(demo == null){
demo = new Demo();
}
}
}
return demo;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
为什么要加二层判断呢?更加牢固一些
打个比方:你上厕所,你确认没有人了,然后再进去,进去了把门插上,再推一下门,推不动确认安全后该干啥干啥。
在多线程环境下使用DCL(双关检锁机制)是否就百分百OK呢?
不是,DCL(双关检锁机制)不一定线程安全,在多线程环境下,JMM中的有序性会让指令出现重排,让执行顺序发送变化,不能保证百分百。加入volatile可以禁止指令重排。
单例模式volatile分析
在多线程环境下,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,造成线程安全问题。
所以加上volatile才可以保证百分百ok。
public class Demo {
private static volatile Demo demo = null;
private Demo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static Demo getInstance(){
if(demo == null){
synchronized (Demo.class){
if(demo == null){
demo = new Demo();
}
}
}
return demo;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Demo.getInstance();
},String.valueOf(i)).start();
}
}
}
17.2、饿汉式
public class Hungry {
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
17.3、懒汉式
public class Lazy {
private static Lazy lazy = null;
private Lazy(){}
public static synchronized Lazy getInstance(){
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
17.4、静态内部类
public class Holder {
private Holder(){}
private static class InnerSingleton{
private static final Holder holder = new Holder();
}
public static final Holder getInstance(){
return InnerSingleton.holder;
}
}
17.5、枚举类型
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
小结:
- 懒汉式:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
- 饿汉式:空间换时间,无论会不会使用到,都会创建这个实例。
- 双检锁模式:充分的结合了懒汉式和饿汉式的优点,既节省了空间,又提高了效率.
- 静态内部类:静态内部类的特性保证了,只有在调用getInstance()方法的时候,我们的JVM才会初始化singleton实例,所以这种创建单例的方法同时解决了线程安全、饿汉式引起的性能问题,而且也无需加锁,是相对比较推荐的创建单例的方式。
- 枚举方式:枚举的方式是比较少见的一种实现方式,但是看代码实现,却更简洁清晰。并且她还自动支持序列化机制。
18、深入理解CAS
18.1什么是CAS
CAS是CompareAndSwap的缩写,作用是比较与交换。
线程对变量的读取赋值等操作,要先将变量从主内存拷贝自己线程的工作内存空间,在工作内存中进行操作,操作完成后再将变量写回主内存。
public class Demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t : "+ atomicInteger.get());
new Thread(()->{
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t : "+ atomicInteger.get());
},"a").start();
}
}
控制台:
就是为什么说使用AtomicInteger可以解决原子性,因为他在写回主内存时会有一个比较并交互。
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();
查看调用的getAndIncrement() 方法的源代码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到方法体内又通过调用Unsafe类中的getAndAddInt()方法。
源码分析:
var5 = this.getIntVolatile(var1, var2)
this.compareAndSwapInt(var1, var2, var5, var5 + var4)
其实使用的是自旋锁。
接下来可以进一步了解CompareAndSwapInt方法:
而Unsafe类是jdk中rt.jar包下的
小总结:
18.2、CAS的缺点
第一个缺点:
第二个缺点:
第三个缺点:
19、AtomicReference原子引用
讲解了volatile不保证原子性,为解决原子性使用了AtomicInteger原子整型,解决了基本类型运算操作的原子性的问题,那我们自定义的实体类或者基本数据类型都要保证原子性呢?那就是使用AtomicReference原子引用。
AtomicReference原子引用直接上代码:
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Demo {
public static void main(String[] args) {
User user1 = new User("java_wxid",25);
User user2 = new User("javaliao",22);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1, user2)+"\t"+atomicReference.get().toString());
new Thread(()->{
System.out.println(atomicReference.compareAndSet(user1, user1)+"\t"+atomicReference.get().toString());
},"a").start();
}
}
控制台:
但是这不能解决CAS的ABA问题
ABA问题代码:
public class Demo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019));
System.out.println(atomicReference.get());
},"t2").start();
}
}
这中间肯定是有猫腻的,所以提供解决方案:
使用AtomicStampedReference版本号原子引用。
只要T1的版本号弱于T2的线程版本号就需要更新,假设线程T1的第二个版本号的值为2019,而线程T2已经修改了二次了,版本号为3,那此时就不能那线程T2的版本号为2的进行比较并交换,需要重新将线程T3的版本号的值拷贝更新再进行操作。
public class Demo {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("===============解决ABA问题方案===============");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp+"\t 当前实际最新值:"+atomicStampedReference.getReference());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t 最新版本号:"+atomicStampedReference.getStamp()+"\t 当前t4的版本号是:"+stamp);
System.out.println(Thread.currentThread().getName()+"\t 只有最新的版本号和t4的版本号一致时,才可以写回主内存,是否写回成功:"+
atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1));
System.out.println(Thread.currentThread().getName()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
},"t4").start();
}
}
控制台:
这个时候就可以让t4线程去更新版本号为3的值100,解决了CAS只管结果不管过程的问题。
w Thread(()->{ System.out.println(atomicReference.compareAndSet(user1, user1)+"\t"+atomicReference.get().toString()); },“a”).start(); } }
控制台:
[外链图片转存中...(img-FpNAJbMH-1632053112992)]
但是这不能解决CAS的ABA问题
ABA问题代码:
```java
public class Demo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
//执行ABA操作
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
//暂停一秒,保证t1线程完成了一次ABA操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019));
System.out.println(atomicReference.get());
},"t2").start();
}
}
[外链图片转存中…(img-XLZermfS-1632053112993)]
这中间肯定是有猫腻的,所以提供解决方案:
使用AtomicStampedReference版本号原子引用。
[外链图片转存中…(img-FzSITG4o-1632053112993)]
只要T1的版本号弱于T2的线程版本号就需要更新,假设线程T1的第二个版本号的值为2019,而线程T2已经修改了二次了,版本号为3,那此时就不能那线程T2的版本号为2的进行比较并交换,需要重新将线程T3的版本号的值拷贝更新再进行操作。
public class Demo {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("===============解决ABA问题方案===============");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp+"\t 当前实际最新值:"+atomicStampedReference.getReference());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t 最新版本号:"+atomicStampedReference.getStamp()+"\t 当前t4的版本号是:"+stamp);
System.out.println(Thread.currentThread().getName()+"\t 只有最新的版本号和t4的版本号一致时,才可以写回主内存,是否写回成功:"+
atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1));
System.out.println(Thread.currentThread().getName()+"\t 当前实际最新值:"+atomicStampedReference.getReference());
},"t4").start();
}
}
控制台:
[外链图片转存中…(img-mUbB2BUA-1632053112993)]
这个时候就可以让t4线程去更新版本号为3的值100,解决了CAS只管结果不管过程的问题。
|