1.JUC并发编程的核心类
虽然java中的多线程有效的提升了程序的效率,但是也引发了一系列可能发生的问题,比如死锁,公平性、资源管理以及如何面对线程安全性带来的诸多危害。为此,java就提供了一个专门的并发编程包java.util.concurrent(简称JUC)。此包能够有效的减少了竞争条件和死锁问题。
以下介绍JUC包中核心的类
类名 |
描述 |
Executor | Executor 是一个接口,定义了一种执行任务的方式,其目的是将任务的提交与任务的执行解耦。 |
ExecutorService | ExecutorService 是 Executor 的子接口,提供了更丰富的功能,例如线程池管理和任务提交等。 |
ScheduledExecutorService | ScheduledExecutorService 是 ExecutorService 的子接口,可以按照计划(时间或延迟)来执行任务。 |
CompletionService | CompletionService 是一个用于异步执行任务并获取已完成任务结果的框架。 |
Callable | Callable 是一个代表可以返回结果或抛出异常的任务的接口。它类似于 Runnable 接口,但具有返回值。 |
Future | Future 是一个可用于获取异步计算结果的接口。 |
ReentrantLock | ReentrantLock 是一个可重入锁,它提供了更灵活的同步控制和更高级别的功能。 |
BlockingQueue | BlockingQueue 是一个支持阻塞操作的队列,提供了线程安全的生产者-消费者模式的实现。 |
CountDownLatch | CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作后再继续执行。 |
CyclicBarrier | CyclicBarrier 是一个同步辅助类,使得一组线程能够互相等待,直到所有线程都达到某个公共屏障点。 |
2.TimeUnit(时间单元)
这个类能够非常好的让我们实现各种时间之间的转换。TimeUnit类的是枚举类,里面有DAYS(天),HOURS(小时),MINUTES(分钟),SECONDS(秒),MILLISECONDS(毫秒),NANNOSECONDS(纳秒)
TimeUnit类中常用的方法:
方法签名 | 描述 |
public long convert(long sourceDuration, long srcDuration) |
该方法用于将给定的时间源持续时间转换为目标持续时间。 |
public void sleep(long timeout) throws InterruptedException |
该方法使当前线程进入休眠状态,暂停执行一段指定的时间(以毫秒为单位)。如果在休眠期间中断了线程,则会抛出 InterruptedException 异常。 |
具体应用案例:
1.时间转换与输出一个月后的日期
packageExample2101; importjava.util.Date; importjava.util.concurrent.TimeUnit; publicclassjavaDemo { publicstaticvoidmain(String[] args) { // 五个小时时间longhours=5; // 通过SECONDS类将5个小时转为秒longseconds=TimeUnit.SECONDS.convert(hours,TimeUnit.HOURS); System.out.println(seconds); // 获取当前时间longnow=System.currentTimeMillis(); longfurture=now+TimeUnit.MILLISECONDS.convert(30,TimeUnit.DAYS); System.out.println("Now Time is"+newDate(now)); DatefutureDay=newDate(furture); System.out.println("after mounth time is"+futureDay); } }
案例2:定义一个闹钟,这个闹钟在5天后会自动发送消息
这种闹钟形式可以通过线程的睡眠机制进行完成,但是一般情况下如果使用线程的睡眠Thread.sleep()里面放的是毫秒,如果要睡眠五天,那么需要设置的数值会非常非常大的,所以可以使用TimeUnit类的睡眠方法实现自定义睡眠。
package Example2102; import java.util.concurrent.TimeUnit; public class javaDemo { public static void main(String[] args) { new Thread(()->{ try { // 通过TimeUnit下的Days类的sleep函数定义五天时间 TimeUnit.DAYS.sleep(5); System.out.println("闹钟响了!!!!!!"); }catch (InterruptedException e){ e.printStackTrace(); } },"闹钟").start(); } }
3.原子操作类
问题引出:一般情况下如果多线程进行竞争一个变量时候会引发数据错乱的问题。比如多线程下售票员售票案例,由于多个线程竞争,一张票可能已经被卖出去了,但是其他的售票员并不知道,继续售卖同一张票。在之前的时候我们通过了Sychronized()同步位解决了这个问题。但是用这个方法也有不小的弊端,那就是程序效率会大大下降。为此JUC提供了一个新的方式解决这个问题,那就是原子操作类。
首先理解原子性,原子是不可分割的最小物体,在编程中是指一种操作要么做了,要么不做。不可以中断的一种操作。原子操作类具有更高效率,更安全,更简单用法
原子操作类分为很多类,大致分为4类:
基本类型:AtomicInteger 、AtomicLong、AtomicBoolean
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
对象属性修改类型:
AtomicIntegerFieldUpdater;AtomicLongFiledUpdater;AtomicReferenceFieldUpdater;
1.基本类型的原子操作类
基本类型:AtomicInteger 、AtomicLong、AtomicBoolean
基本类型之间的操作是差不多的,这里用AtomicLong举例
AtomicLong的常用方法
方法 | 描述 |
AtomicLong(long initValue) | 创建一个新的AtomicLong实例,并设置初始值为initValue。 |
get() | 获取当前存储在AtomicLong中的值。 |
set(long newValue) | 将AtomicLong的值设置为newValue。 |
getAndIncrement() | 先获取当前存储在AtomicLong中的值,然后将AtomicLong的值增加1。返回先前的值。 |
setAndIncrement() | 将AtomicLong的值增加1。返回增加前的值。 |
decrementAndGet() | 将AtomicLong的值减少1,并返回减少后的值。 |
使用类方法的关键就在于熟悉add(增加) decrement(自减)increment(自增) set(设置值) get(获取类内部的数据) 方法就是这几个操作之间的组合
案例代码:多个售票员售卖100张票
packageExample2103; importjava.util.concurrent.TimeUnit; importjava.util.concurrent.atomic.AtomicInteger; publicclassjavaDemo { publicstaticvoidmain(String[] args) { // 创建原子操作类AtomicIntegerticket=newAtomicInteger(6); AtomicIntegerflag=newAtomicInteger(1);//标志还有票// 创建三个线程进行售票for (inti=0;i<3;i++){ newThread(()->{ while (ticket.get()>0){ System.out.println("售票员"+Thread.currentThread().getName()+"售卖第"+ticket.decrementAndGet()+"张票"); try { // 设置两秒睡眠TimeUnit.SECONDS.sleep(2); }catch (Exceptione){ e.printStackTrace(); } // 如果没有票了就将标志位的值设置为0,表示没有票了if (ticket.get() ==0){ flag.set(0); System.out.println("卖完了"); } } }).start(); } } }
可以看到即使没有使用同步机制也实现了同步的效果。
2.数组原子操作类
数组原子操作类有:AtomicArrayInteger AtomicLongArray AtomicReferenceArray(对象数组)
由于三者这件的使用区别不大,所以这里展示AtomicReferenceArray
AtomicReferenceArray常用方法:
方法 | 描述 |
AtomicReferenceArray(int length) |
构造一个指定长度的AtomicReferenceArray对象。 |
AtomicReferenceArray(E[] array) |
使用给定数组初始化AtomicReferenceArray对象。 |
int length() |
返回AtomicReferenceArray的长度(即元素个数)。 |
boolean compareAndSet(int index, E expect, E update) |
将指定索引位置的元素与期望值进行比较,如果相等,则将其更新为新的值。该操作是原子性的,返回是否更新成功。 |
E get(int index) |
获取指定索引位置的元素的值。 |
void set(int index, E newValue) |
设置指定索引位置的元素的值为newValue。 |
E getAndSet(int index, E newValue) |
获取指定索引位置的元素的当前值,并将其设置为newValue。 |
案例代码:
packageExample2104; importjava.util.concurrent.atomic.AtomicReferenceArray; publicclassjavaDemo { publicstaticvoidmain(String[] args) { Stringdata[] =newString[]{"王二狗","180","130"}; // 初始化AtomicReferenceArray<String>array=newAtomicReferenceArray<String>(data); // 对象数组的操作System.out.println("身高是:"+array.get(1)); array.set(2,"150"); System.out.println(array.get(0)+"在拼命锻炼后体重变成:"+array.get(2)); // 筛选如果名字是王二狗的自动改名王二array.compareAndSet(0,"王二狗","王二"); System.out.println("改名后名字叫"+array.get(0)); } }
3.引用原子操作类
引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
其中AtomicReference是可以直接引用数据类型的原子性操作
下面是AtomicReference的常用方法:
方法 | 描述 |
AtomicReference() |
无参构造方法,创建一个初始值为null的AtomicReference对象。 |
V get() |
获取当前AtomicReference对象持有的值。 |
void set(V newValue) |
设置AtomicReference对象的值为newValue。 |
boolean compareAndSet(V expect, V update) |
将AtomicReference对象的值与期望值expect进行比较(==比较),如果相等,则将其更新为新值update。该操作是原子性的,返回是否更新成功。 |
V getAndSet(V newValue) |
先获取当前AtomicReference对象的值,然后将其设置为newValue,并返回原来的值。 |
案例代码:使用AtomicReference进行引用操作
packageExample2106; importjava.util.concurrent.atomic.AtomicReference; // 创建普通人类classPerson{ privateintage; privateStringname; privateintid; Person(intage,Stringname,intid){ this.age=age; this.name=name; this.id=id; } } publicclassjavaDemo { publicstaticvoidmain(String[] args) { Personperson1=newPerson(18,"张三",001); Personperson2=newPerson(20,"王思",1002); // 传入person1对象AtomicReference<Person>person=newAtomicReference<Person>(person1); // 输出对象地址System.out.println(person.get()); // 更改引用对象person.set(person2); System.out.println(person.get()); } }
AtomicStampedReference 基于版本号的数据引用。其中版本号是自己定义的int数据类型
下面是AtomicStampedReference的常用方法:
方法 | 描述 |
AtomicStampedReference(V initRef, int initStamp) |
构造一个AtomicStampedReference对象,初始引用值为initRef,初始标记值(戳)为initStamp。 |
V getReference() |
获取当前AtomicStampedReference对象持有的引用值。 |
void set(V newRef, int newStamp) |
设置AtomicStampedReference对象的引用值为newRef,标记值(戳)为newStamp。 |
boolean compareAndSet(V expectRef, V newRef, int expectStamp, int newStamp) |
将AtomicStampedReference对象的引用值与期望值expectRef、标记值(戳)与期望值expectStamp进行比较,如果相等,则将其更新为新值newRef和newStamp。该操作是原子性的,返回是否更新成功。 |
int attemptStamp(V expectedReference, int newStamp) |
如果当前引用值等于expectedReference,则尝试将标记值(戳)更新为newStamp。如果更新成功,返回新的标记值(戳),否则返回当前标记值。 |
int getStamp() |
获取当前AtomicStampedReference对象持有的标记值(戳)。 |
案例代码:
packageExample2107; importjava.util.concurrent.atomic.AtomicStampedReference; classDraw{ privateStringcontent=""; privateStringautor=""; privateStringtitle=""; Draw(Stringcontent,Stringautor,Stringtitle){ this.content=content; this.autor=autor; this.title=title; } publicvoidsetContent(Stringcontent) { this.content=content; } publicStringgetContent() { returncontent; } } publicclassjavaDemo { publicstaticvoidmain(String[] args) { Drawdraw1=newDraw("","alphaMilk","JUC并发编程原子操作类"); // 初始化内容,版本号为1AtomicStampedReference<Draw>atomicDraw=newAtomicStampedReference<Draw>(draw1,1); System.out.println(atomicDraw.getReference()); // 更新内容,版本号更改draw1.setContent("Hello,word"); atomicDraw.set(draw1,2); // 获取当前版本System.out.println(atomicDraw.getStamp()); } }
AtomicMarkableReference与AtomicStampedReference的区别在于,一个是设置boolean类型的初始化标记,一个多设置的是int类型版本号
下面是AtomicMarkableReference的常用方法:
方法 | 描述 |
AtomicMarkableReference(V initRef, boolean initMark) |
构造一个AtomicMarkableReference对象,初始引用值为initRef,初始标记值为initMark。 |
V getReference() |
获取当前AtomicMarkableReference对象持有的引用值。 |
boolean isMarked() |
判断当前AtomicMarkableReference对象是否被标记。 |
boolean compareAndSet(V expectRef, V newRef, boolean expectMark, boolean newMark) |
将AtomicMarkableReference对象的引用值与期望值expectRef、标记值与期望值expectMark进行比较,如果相等,则将其更新为新值newRef和newMark。该操作是原子性的,返回是否更新成功。 |
void set(V newRef, boolean newMark) |
设置AtomicMarkableReference对象的引用值为newRef,标记值为newMark。 |
boolean attemptMark(V expectedReference, boolean newMark) |
如果当前引用值等于expectedReference,则尝试将标记值更新为newMark。如果更新成功,返回true,否则返回false。 |
案例代码:
一个班统计同学是否交了班费
packageExample2108; importjava.util.concurrent.atomic.AtomicMarkableReference; classStudent{ privateStringname; privateintid; Student(Stringname,intid){ this.name=name; this.id=id; } } publicclassjavaDemo { publicstaticvoidmain(String[] args) { Studentstu1=newStudent("王一",001); Studentstu2=newStudent("张二蛋",002); // 王一交过班费AtomicMarkableReference<Student>atoStu=newAtomicMarkableReference<Student>(stu1,true); System.out.println(atoStu.getReference()); if (atoStu.isMarked()){ System.out.println("该同学交过班费"); }elseSystem.out.println("该同学尚未交过班费"); // 张二蛋没有交班费atoStu.set(stu2,false); System.out.println(atoStu.getReference()); if (atoStu.isMarked()){ System.out.println("该同学交过班费"); }elseSystem.out.println("该同学尚未交过班费"); } }
4.对象属性修改原子类
AtomicIntegerFieldUpdater;AtomicLongFiledUpdater;AtomicReferenceFieldUpdater;
这三个类的实现原理基本差不多,所以将用AtomicIntegerFieldUpdater举例:
以下是AtomicIntegerFieldUpdater类的常用方法:
int addAndGet(T obj, int data) |
将指定对象obj的字段值与data相加,并返回相加后的结果。 |
boolean compareAndSet(T obj, int expect, int update) |
将指定对象obj的字段值与期望值expect进行比较,如果相等,则将其更新为新值update。返回是否更新成功。 |
int get(T obj) |
获取指定对象obj的字段值。 |
int getAndSet(T obj, int newValue) |
获取指定对象obj的字段值,并将其设置为新值newValue。 |
int decrementAndGet(T obj) |
将指定对象obj的字段值减1,并返回减1后的结果。 |
int incrementAndGet(T obj) |
将指定对象obj的字段值加1,并返回加1后的结果。 |
案例代码:
packageExample2109; importjava.util.concurrent.atomic.AtomicReferenceFieldUpdater; classBook{ volatilelongid; volatileStringname; Book(longid, Stringname){ this.id=id; this.name=name; } } publicclassjavaDemo { publicstaticvoidmain(String[] args) { Bookbook1=newBook(114514,"Java从入门到入土"); AtomicReferenceFieldUpdater<Book,String>bookmanger=AtomicReferenceFieldUpdater.newUpdater(Book.class,String.class,"name"); System.out.println("更新前书本名称为:"+bookmanger.get(book1)); bookmanger.set(book1,"Java从入门到项目实战"); System.out.println("更新后书本名称为:"+bookmanger.get(book1)); } }
4.CAS 、AQS机制
CAS是一条CPU并发原语。它的功能是判断某个内存某个位置的值是否相等,如果是则改为新的值,这个操作过程属于原子性操作。
CAS是乐观锁,是一种冲突重试机制,在并发竞争不是很剧烈的情况下,其操作性能会好于悲观锁机制(Synchronization同步处理)
*面试题为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
- Synchronized的并发策略是悲观的,不管是否产生竞争,任何数据的操作都必须加锁。
- 乐观锁的核心是CAS,CAS包括内存值、预期值、新值,只有当内存值等于预期值时,才会将内存值修改为新值。
*面试题:乐观锁一定就是好的吗?
- 乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。
- 乐观锁没有加锁,但乐观锁引入了ABA问题,此时一般采用版本号进行控制;
- 也可能产生自旋次数过多问题,此时并不能提高效率,反而不如直接加锁的效率高;
- 只能保证一个对象的原子性,可以封装成对象,再进行CAS操作;
*面试题:volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。