文章目录
进程和线程
进程: 具有一定独立功能的程序关于某个数据集合的一次运行活动,操作系统动态执行的基本单元
线程: 一个进程中可以包含一个或若干个线程
并行: 同一时刻多个线程在访问同一个资源,多个线程对一个点,
并发: cpu在多个调度之间来回切换,从宏观来看是一起在执行,多项工作一起执行,之后再汇总.
wait/sleep的区别
wait: 释放锁,Object的方法
sleep: 结束睡眠后,手里依然有锁,Thread的方法
常用创建线程的方式
方式1:继承Thread
public class Demo extends Thread{
@Override
public void run() {
System.out.println("hello world!");
}
public static void main(String[] args) {
new Demo().start();
}
}
方式2: 实现Runnable接口
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world!");
}
},"线程名称(可选)");
thread.start();
}
方式3: lambda表达式
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println("hello");
},"线程名称(可选)(不选:系统会自动分配))");
thread.start();
}
判断锁是什么
Java中的每一个对象都可以作为锁:
普通方法: 锁是当前实例对象
静态方法: 锁是当前类的Class对象
代码块: 锁是Synchonized括号里配置的对象
通俗理解:
普通方法:
一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁.
不同实例对象的非静态同步方法用的是不同对象的锁,无需等待其他实例对象的非静态同步方法释放锁,就可以获取自己的锁
静态方法:
所有的静态同步方法用的是同一把锁–类本身。只要是一个类的对象,一个静态同步方法获取锁之后,其他对象的静态同步方法,都必须等待该方法释放锁之后,才能获取锁。
Lock锁
相比同步锁,JUC包中的Lock锁的功能更加强大(公平锁,非公平锁,共享锁,独占锁……)
Lock是一个接口
主要有三个实现:ReentrantLock、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock
ReentrantLock可重入锁
private ReentrantLock lock = new ReentrantLock();
lock.lock();//加锁
....功能代码
lock.unlock();//解锁
synchronized是可重入锁
class A{
public synchronized void aa{
......
bb();
......
}
public synchronized void bb{
......
}
}
A a = new A();
a.aa();
ReentrantLock也是可重入锁
public void func(){
lock.lock();
lock.lock();
if(num == 0){
return;
}
System.out.println(Thread.currentThread().getName()+": " +num);
num--;
lock.unlock();
lock.unlock();
}
@Test
public void main() {
new Thread(()->{
for (int i = 0;i< 20;i++){
this.func();
}
},"A").start();
new Thread(()->{
for (int i = 0;i< 20;i++){
this.func();
}
},"B").start();
}
公平锁
ReentrantLock还可以实现公平锁。在锁上等待时间最长的线程将获得锁的使用权。就是谁排队时间最长谁先执行获取锁。
在ReentrantLock的构造器中填入true就是公平锁
private ReentrantLock lock = new ReentrantLock(true);
限时等待
tryLock方法来实现,传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。
//表示当前线程一秒钟没有取到锁,就返回false
boolean b = lock.tryLock(1, TimeUnit.SECONDS);
ReentrantLock和synchronized区别
1:synchronized是独占锁,加锁和解锁的过程自动进行
ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行
2:synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁
ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁
3:synchronized不可响应中断,一个线程获取不到锁就一直等着
ReentrantLock可以响应中断(tryLock)
ReentrantReadWriteLock读写锁
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
读写锁的特点:
- 写写不可并发
- 读写不可并发
- 读读可以并发
private volatile Map<String, String> cache= new HashMap<>();
// 加入读写锁
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void put(String key, String value){
// 加写锁
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
Thread.sleep(500);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
rwl.writeLock().unlock();
}
}
public void get(String key){
// 加入读锁
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
Thread.sleep(500);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
锁降级
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级
简单说: 一个线程一个线程拿到写锁,又拿到读锁,随后释放写锁,锁降级为读锁.
public void test(){
rwlock.writeLock().lock();
System.out.println("获取到写锁。。。。");
rwlock.readLock().lock();
System.out.println("获取到读锁----------");
rwlock.writeLock().unlock();
System.out.println("释放写锁==============");
rwlock.readLock().unlock();
System.out.println("释放读锁++++++++++++++++");
}
读写锁总结
1.支持公平/非公平策略
2.支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁
3.支持锁降级,不支持锁升级
4.读写锁如果使用不当,很容易产生“饥饿”问题:在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
5.Condition条件支持
写锁可以通过newCondition()方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()
方法会直接抛出UnsupportedOperationException。
线程间通信
如果使用synchronized加锁可以使用
object.wait()
object.notify()
objext.notifyAll();
使用Condition
Lock lock = new ReentrantLock(); // 加锁
Condition condition = lock.newCondition();
lock.lock();//加锁
lock.unlock();//解锁
condition.await();//线程等待
condition.signalAll();//唤醒所有线程
虚假唤醒
多个线程等待一个可以拿到锁的条件,条件满足时唤醒所有线程,但由于所有方法加了synchronized ,导致只有一个线程能抢到锁往下执行,这个线程执行完后,第二个线程拿到锁,但是此时可能这个线程已经不满足拿到锁的条件,就不应该往下执行,而是应该在重新判断这个线程是否具备拿到锁的条件.此时如果没有重新判断,就是虚假唤醒.
在下面例子中应该将if换成while,让第二个拿到锁的线程重新判断,而不是直接往下执行
private Integer number = 0;
public synchronized void increment() throws InterruptedException {
//if换成while
if(number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
//if换成while
if(number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
this.notifyAll();
}
public static void main(String[] args) {
Demo3 demo3 = new Demo3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
demo3.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
demo3.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
demo3.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"CCC").start();
}
定制化调用通信
启动3个线程,每个线程调用一种方法,输出AAA,BBB,CCC,交替3次,每次输出结果顺序一致
public class Demo4 {
private Integer flag = 1; // 线程标识位,通过它区分线程切换
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName());
flag = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName());
flag = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName());
flag = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Demo4 demo4 = new Demo4();
new Thread(()->{
for (int i = 0; i < 3; i++) {
demo4.print5();
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 3; i++) {
demo4.print10();
}
}, "BBB").start();
new Thread(()->{
for (int i = 0; i < 3; i++) {
demo4.print15();
}
}, "CCC").start();
}
}
并发容器类
ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常)
List接口有很多实现类,除了常用的ArrayList之外,还有Vector和SynchronizedList。
Vector和Synchronized的缺点:
vector:内存消耗比较大,适合一次增量比较大的情况
SynchronizedList:迭代器涉及的代码没有加上线程同步代码
synchronizedList保证list是同步线程安全的
CopyOnWrite容器
通俗的理解:往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器复制一份,然后往新的容器里添加元素之后,再将原容器的引用指向新的容器。
CopyOnWrite并发容器用于读多写少的并发场景
**CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。**所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
扩展类比:Set和Map
JUC提供的CopyOnWrite容器实现类有:CopyOnWriteArrayList和CopyOnWriteArraySet,
线程安全的map: Map<String, String> map = new ConcurrentHashMap<>();
Collections安全处理工具
Collections.synchronizedMap(new HashMap<>());
Collections.synchronizedSet(new HashSet<>());
Collections.synchronizedList(new ArrayList<>());
JUC强大的辅助类
1. CountDownLatch(倒计数器)
让主线程等待直到count为0后,往下执行
new CountDownLatch(int count) //实例化一个倒计数器,count指定初始计数
countDown() // 每调用一次,计数减一
await() //等待,当计数减到0时,阻塞线程(可以是一个,也可以是多个)并行执行
调用await()方法的线程(一般是主线程),等待所有调用countDown()的线程,直到count为0
主线程才能接着往下执行,否则阻塞
CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束
调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕
6个同学陆续离开教室后值班同学才可以关门。
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,初始计数为6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// 每个同学墨迹几秒钟
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + " 同学出门了");
// 调用countDown()计算减1
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
// 调用计算器的await方法,等待6位同学都出来
countDownLatch.await();
System.out.println("值班同学锁门了");
}
2. CyclicBarrier(循环栅栏)
控制线程间的相互等待
CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数
await() 被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,所有线程调用await()后(即最后一个线程调用await()后),当前所有线程才会被唤醒.接着往下执行
barrierAction一个可选的Runnable命令,该命令由最后一个调用await()方法的线程运行一次.
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println(Thread.currentThread().getName() + " 过关了");
});
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + " 开始第一关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第二关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第三关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
3. Semaphore(信号量)
限制访问资源的线程数.
Semaphore可以控制同时访问的线程个数,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续。
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
public static void main(String[] args) {
// 初始化信号量,3个车位
Semaphore semaphore = new Semaphore(3);
// 6个线程,模拟6辆车
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// 抢占一个停车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 抢到了一个停车位!!");
// 停一会儿车
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
System.out.println(Thread.currentThread().getName() + " 离开停车位!!");
// 开走,释放一个停车位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
Callable接口
Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
FutureTask充当中间人的角色
FutureTask:未来的任务,用它就干一件事,异步调用。通常用它解决耗时任务,挂起堵塞问题。
FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask仅在call方法完成时才能get结果;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
为了防止主线程阻塞,建议get方法放到最后
只计算一次,FutureTask会复用之前计算过得结果
如果想打印threadName2的结果,即不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可。
FutureTask futureTask = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
return 123;
}
});
Thread thread = new Thread(futureTask,"abc");
thread.start();
System.out.println(thread.getName());
System.out.println(futureTask.get());//获取返回值
阻塞队列(BlockingQueue)
被阻塞的情况主要有如下两种:
-
当队列满了的时候进行入队列操作
-
当队列空了的时候进行出队列操作
BlockingQueue接口主要有以下7个实现类: -
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
-
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
-
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
-
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
-
LinkedTransferQueue:由链表组成的无界阻塞队列。
-
LinkedBlockingDeque:由链表组成的双向阻塞队列。
ThreadPool线程池
创建一个连接池对象
//创建单一线程连接池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//创建固定线程数线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//自动创建线程数,可扩容,遇强则强
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
threadPool.shutdown();
三个方法的本质都是ThreadPoolExecutor的实例化对象,只是具体参数值不同。
线程池的7个重要参数
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳同时 执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂, 用于创建线程,一般默认的即可
- handler:拒绝策略,表示当队列满了,并且工作线程大于 等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝 请求执行的runnable的策略
线程池底层工作原理
-
在创建了线程池后,线程池中的线程数为零。
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
拒绝策略
务队列已满且线程池创建的线程数达到你设置的最大线程数时**,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。
ThreadPoolExecutor自带的拒绝策略如下:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略
自定义线程池
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建固定数线程的连接池
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 可扩容连接池
// ExecutorService threadPool = Executors.newCachedThreadPool();
-------------------------------------------------------------------------------------
// 自定义连接池
ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
//new ThreadPoolExecutor.DiscardPolicy()
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定义拒绝策略");
}
}
);
多线程高并发底层原理
java内存模型(JMM)
JMM规定了内存主要划分为主内存和工作内存两种。
主内存:保存了所有的变量。
共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。
工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。
主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域.
从更底层的来说:主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JMM对共享内存的操作做出了如下两条规定:
- 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
- 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。
内存模型的三大特性:
原子性: 需要使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作
可见性: 在 Java 中 volatile、synchronized 和 final 实现可见性,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
有序性: 因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性
volatile关键字
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性。
2. 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。
3.不保证变量的原子性
volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
CAS
CAS是解决多线程并发安全问题的一种乐观锁算法:在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。
// 对象、对象的属性地址偏移量、预期值、修改值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe简单demo:
private int number = 0;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
UnsafeDemo unsafeDemo = new UnsafeDemo();
System.out.println(unsafeDemo.number);// 修改前
unsafeDemo.compareAndSwap(0, 30);
System.out.println(unsafeDemo.number);// 修改后
}
public void compareAndSwap(int oldValue, int newValue){
try {
// 通过反射获取Unsafe类中的theUnsafe对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 设置为可见
Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 获取Unsafe对象
// 获取number的偏移量
long offset = unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("number"));
// cas操作
unsafe.compareAndSwapInt(this, offset, oldValue, newValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
在JUC下有个atomic包,有很多原子操作的包装类:
AtomicInteger
public class CasDemo {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(1);
System.out.println("第一次更新:" + i.compareAndSet(1, 200));
System.out.println("第一次更新后i的值:" + i.get());
System.out.println("第二次更新:" + i.compareAndSet(1, 300));
System.out.println("第二次更新后i的值:" + i.get());
System.out.println("第三次更新:" + i.compareAndSet(200, 300));
System.out.println("第三次更新后i的值:" + i.get());
}
}
第一次更新:true
第一次更新后i的值:200
第二次更新:false
第二次更新后i的值:200
第三次更新:true
第三次更新后i的值:300
第一次更新:i的值(1)和预期值(1)相同,所以执行了更新操作,把i的值更新为200
第二次更新:i的值(200)和预期值(1)不同,所以不再执行更新操作
第三次更新:i的值(200)和预期值(1)相同,所以执行了更新操作,把i的值更新为300
缺点
开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。
不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。