文章目录
JUC并发编程(基础认识)
1、什么是JUC
java.util.concurrent工具包。
Callable、Lock等。
2、线程和进程
进程:一个静态程序代码运行一次称为一次进程,进程里面可以有很多的线程组成。进程是操作系统分配资源的最小单位。
线程:开一个进程Typora,你在上面输入文字,过一段时间自动保存,这个自动保存就是线程负责的。线程是调度的最小单位。
Java默认有两个线程。main和GC(垃圾回收)。
Java真的可以开启线程吗? 不可以,深入底层是调用本地方法(native)。
并发和并行区别
并发:多线程操作同一个资源
- 单核CPU,多个线程快速交替运行
并行:多个线程一起走
- CPU多核,多个线程可以同时进行
获取CPU核数。
System.out.println(Runtime.getRuntime().availableProcessors());
并发编程的本质:充分利用CPU资源。
线程状态
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待,死等
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
sleep和wait区别
- 来自不同的类。wait来自Object类,sleep来自Thread类。
- 锁的释放。wait会释放锁,sleep不会释放锁。
- 适用范围。wait必须在同步代码块中,sleep可以用在任何地方。
- 是否需要捕获异常。wait不需要捕获异常,sleep需要捕获异常。
3、Lock锁(重点)
传统synchronized
synchronized本质:队列+锁。
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket { //共同资源类单独写出来,不要继承任何类或者实现接口。OOP思想。分离出来。这个类仅有相关资源成员和操作方法构成。
private int count = 50;
public synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + count-- +"张票,还剩" + count +"张票");
}
}
}
Lock接口
class Ticket { //共同资源类单独写出来,不要继承任何类或者实现接口。OOP思想。分离出来。这个类仅有相关资源成员和操作方法构成。
private int count = 50;
private Lock lock = new ReentrantLock(); //创建锁
public void sale() {
lock.lock(); //加锁
try {
//业务代码被try包围
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + count-- +"张票,还剩" + count +"张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
公平锁和非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:先来后到。
非公平锁:可以插队,完全看CPU调度。
默认false,非公平锁。为什么默认非公平锁,因为把权限打开,谁抢时间片段到就是谁的。例如,两个线程一个线程1花费3h,一个线程2花费3min。如果用公平锁,用非公平锁给线程2创造可能抢夺到时间片段的机会。
synchronized和lock锁区别
- synchronized是内置的java的关键字,lock是一个Java接口。
- synchronized无法判断获取锁的状态,lock可以判断是否获取到了锁。
- synchronized会自动释放锁,lock必须手动释放锁!如果不释放锁,会导致死锁问题。
- synchronized线程1获得了锁,然后自身阻塞了,其他属于这个锁的线程就会一直傻傻的等;lock就不一定会等待下去(tryLock)。
- synchronized可重入锁,不可以中断,非公平的;lock可重入锁,可以进行判断,公平可以设置。
- synchronized适合少量代码的同步问题,lock适合锁大量的同步代码。
4、生产者消费者
synchronized版
public class ProducerConsumer {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
}
}
},"B").start();
}
}
//生产者消费者口诀。 判断等待,业务,通知。
class Data { //资源类。独立耦合的。只有属性和方法。
private int num = 0;
public synchronized void increment() throws InterruptedException {
if(num != 0) {
wait();//等待
}
//业务操作
num++;
System.out.println(Thread.currentThread().getName() + "==>" + num);
//通知其他线程,我完毕了
//notify(); //notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。
notifyAll();//notifyAll,那么等待该锁的所有线程都会被唤醒,然后重新开始争夺。
}
public synchronized void decrement() throws InterruptedException {
if(num == 0){
wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "==>" + num);
notifyAll();
}
}
输出是正确的,交替加减1。
但是如果多加几个线程进行,就会出现错误。
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
}
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
}
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
}
}
},"D").start();
}
多几个线程就会出现问题了。这就是虚假唤醒问题。
虚假唤醒
问题描述为多个(大于2)线程交替操作同一共享资源。问题就出在if判断,因为wait()操作唤醒后的线程会从wait之后的代码开始运行。而if仅进行判断了一次,而while循环就会继续进行判断。
lock版
synchronized是搭配wait和notify实现线程间通信。
lock是通过await和signal实现的。
class Data {
private int num = 0;
private final Lock lock = new ReentrantLock();
private final 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();
}
}
}
始终记得lock的操作模板
lock.lock(); //同步代码块开始。加锁
try {
while (条件表达式) { //条件表达式,判断什么情况下该等待,while循环防止虚假唤醒。
condition.await();
}
//业务代码
condition.signalAll(); //业务代码做完,唤醒锁上其他线程。
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //finally内同步代码块结束。解锁。
}
Condition(监视器)实现精准通知唤醒
public class ProducerCondition {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printC();
}
}, "C").start();
}
}
class Data3 {
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition(); //每个监视器监视自己不同的对象。
private int num = 2; //条件表达式进行判断的依据
public void printA() {
lock.lock();
try {
while (num != 1){
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName() + "==>" + "aaaaaa");
condition2.signalAll(); //可以唤醒指定的人。唤醒所有锁在监视器2的线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num != 2){
condition2.await();
}
num = 3;
System.out.println(Thread.currentThread().getName() + "==>" + "bbbbb");
condition3.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num != 3){
condition3.await();
}
num = 1;
System.out.println(Thread.currentThread().getName() + "==>" + "cccc");
condition1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5、synchronized锁对象和锁类
synchronized 普通方法{ <=> synchronized(this)锁的是调用该方法时的对象
}
synchronized static方法 <=> synchronized(.class)锁的是类
然后记住共享同一把锁的多线程,必须等锁释放了之后,然后再去抢夺。
6、线程不安全的类
写入时复制(CopyOnWrite),其核心思想是不同线程在访问同一资源时,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一资源。
CopyOnWriteArrayList正是采用了这个思想,平时查询的时候,都不需要加锁,任意访问,只有在更新数据的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。
可以看源码
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。
7、Callable
Callable后面有泛型,Runnable里的run方法在Callable里面是call()。
需要借助FutureTask。因为线程启动只能(其实还可以用线程池)借助new Thread(线程).start()
,Runable和Callable有关系只能借助FutureTask。
FutureTask的run方法只执行一次
private volatile int state; //volatile 拒绝内存优化的变量
public void run() {
if (state != NEW ||
!RUNNER.compareAndSet(this, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
FutureTask的run()仅执行一次的原因: 1. state != NEW表示任务正在被执行或已经完成, 直接return 2. 若state==NEW, 则尝试CAS将当前线程 设置为执行run()的线程,如果失败,说明已经有其他线程 先行一步执行了run(),则当前线程return退出
FutureTask特性
- 异步执行,可执行多次(通过
runAndReset()
方法),也可仅执行一次(执行run()
即可) - 可获取线程执行结果
应用场景
- 长时间运行的任务,包含远程调用的任务
- 数据量大的查询,划分为多个小查询,每个FutureTask 仅执行一次 的特性能有效避免重复的查询
- 计算密集型的任务
8、常用辅助类
8.1、CountDownLatch
允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。辅助工具类。用来计数的。减法计数器。
public class CountDownDemo {
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() + "getOut");
countDownLatch.countDown(); //计数器进行--操作
}, String.valueOf(i)).start();
}
countDownLatch.await(); //只有当计数器归零,才可以继续往下执行。
System.out.println("close door");
}
}
8.2、CyclicBarrier
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,new Thread(()-> {
System.out.println("集齐了七颗龙珠召唤神龙");
})); //定义加法计数器增加到第一个参数的数值就执行第二个参数定义的线程。
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println("集齐了第" + temp + "颗龙珠");
//lambda操作不了i,因为i是局部变量,而lambda表达式相当于一个新类(匿名),只能引用final修饰的
try {
cyclicBarrier.await(); //每执行完一个加法计数器+1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
lambda表达式不能操局部变量,借助final
需要注意的是:lambda表达式操作不了i,因为i是局部变量,而lambda表达式相当于一个新类(匿名),只能引用final修饰的。再回顾下final修饰的变量,相当于申请的那段空间定了,所以可以在类外取得到。
8.3、Semaphore
这个东西在并发问题用的十分多,译为信号量。
用在限流问题上。
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量。停车位:3。限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire(); //得到,占一个空位
System.out.println(Thread.currentThread().getName() + "抢到车位");
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、读写锁
private final ReadWriteLock lock = new ReentrantReadWriteLock();
ReadWriteLock.writeLock().lock() ReadWriteLock.writeLock().unlock()
ReadWriteLock.readLock().lock() ReadWriteLock.readLock().unlock()//和lock操作一样的
不加读锁,就随便读了,一个人写的时候也可以读,不安全。(与写锁互斥)
写的时候只能一个人写(银行例子),读的时候可以多个人读(银行例子),但对于同一数据,读的时候不能写,写的时候也不能读(数据库例子),所以引入了读锁。
独占锁(写锁):一次只能被一个线程占有。
共享锁(读锁):多个线程可以同时占有。
10、阻塞队列
写入:如果队列满了,就必须阻塞等待
取:如果队列是空的,必须阻塞等待生产
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer()(满为false,不满为true) | put | offer(,) |
移除 | remove | poll()(空为null) | take | poll(,) |
检测队首元素 | element | peek | - | - |
SynchronousQueue同步队列
没有容量。
进去一个元素,必须等待取出来后,才能往里面再放一个元素。
和其他的BlockingQueue不一样,SynchronousQueue不存储元素,put一个元素,必须从里面先take取出来,否则不能再put进去值!
11、线程池(重点)
池化技术
程序的运行,,本质:占用系统资源!我们需要优化资源的使用!===>提出池化技术。
线程池、数据库连接池、内存池、对象池。
池化技术:例如数据库连接。每次连接和关闭的时候十分消耗资源,为了不让它频繁开和关,所以提出池。一次创建,多次使用。
事先准备好一些资源,有人要用,就来我这里来拿,用完之后还给我。
线程池的好处:
- 降低资源的消耗
- 提高响应速度
- 方便管理
线程复用,可控制最大并发数、易于管理线程。
线程池可以参照这篇博文。
12、四大函数式接口
lambda表达式、链式编程、函数式接口、Stream流式计算。
函数式接口:只有一个方法的接口
函数式编程,简化代码,工程上可读性不是很好。
13、Stream流式计算
想去学先把Java8新特性了解下。
我对这里没有兴趣,普通的写Java代码我有时候都会出错,更别将什么高级简洁的写法了。先把最基础的搞好,以后再做这些锦上添花的事情不算迟。
14、ForkJoin
什么是ForkJoin
ForkJoin(分支合并)在JDK1.7,并行执行任务!提高效率,大数据量!
把大任务拆分成小任务,分而治之的思想。
ForkJoin特点:工作窃取
维护的双端队列,因为要从队尾去窃取任务。
ForkJoin仅在大数据量表现优秀,小数据量还是按基本来吧。
不是算法,是分治思想。
15、Volatile
Volatile是Java虚拟机提供轻量级的同步机制。
1.保证可见性(工作内存和主存)
2.不保证原子性
即不能起到synchronized,lock那种重量级锁的效果。
public class AtomicDemo {
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { //GC和main
Thread.yield(); //main和GC进行礼让,重新争取CPU
}
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
加了volatile会拒绝内存优化,去主存读。首先线程A去主存读取了num的值100,然后放入自己的寄存器进行+1操作得到101,还未来得及将值赋值给主存,这时候时间片段到了。切换到线程B,B从主存中读取num的值为100,然后+1操作,然后赋值101主存,结束。这时候轮到线程A运行了,将101赋值给主存。 始终需要记住,i++ 的操作是3步骤(取值,操作,赋值)!
不加锁可以不可以解决原子性问题?可以!用JUC包提供的原子类。
AtomicInteger等类,追溯到最深是Unsafe类,其中它的方法都是native的底层操作代码,可以保证原子操作。
3.禁止指令重排(博文)
16、单例模式
单例模式(Singleton Pattern),在工程上还是很常见的,意思需要一个对象的值申请赋值一次就够了,以后都不变了。最主要的就是将构造方法私有,给特定方法获取对象。
饿汉式
public class HungrySingle {
private static final HungrySingle me = new HungrySingle();
private HungrySingle() {
}
public static HungrySingle getInstance() {
return me;
}
}
饿汉式浪费内存空间,一旦加载这个类进行了申请空间的操作。
优点:简单。且不会出现多线程问题,因为饿汉是ClassLoad的时候,JVM级别保证只来一次。
懒汉式
区别于饿汉式,是需要的时候才去申请,而不是一开始就进行相关值空间的申请和赋值。
public class LazySingle {
private static LazySingle me;
private LazySingle() {
}
public static LazySingle getInstance() {
if(me == null) {
me = new LazySingle();
}
return me;
}
}
单线程下,该操作没有问题。但是多线程就遇见问题了。如果两个线程同时运行判断me
是否为null
的语句,并且确实没有创建时,那么两个线程都会创建一个实例,此时就不满足单例了。为了多线程下还是单例,我们需要加一个同步锁。
单锁情况下,每次调用方法instance()
时候,都会加上一个锁,而加锁是一个十分耗时的操作,没有必要的时候尽量应该避免。我们只是在实例还没有创建之前时才需要锁,当实例已经存在后就不需要加锁了,于是我们可以继续优化,于是就有了DCL。
DCL(Double Check Lock双重检测锁)单例
public class DCLSingle {
private static DCLSingle me;
private DCLSingle() {
}
public static DCLSingle getInstance() {
if(me == null) {
synchronized (DCLSingle.class) {
if(me == null) {
me = new DCLSingle();
}
}
}
return me;
}
}
第一个if是为了效率,第二个if是为了安全。第一个if是为新来的线程,如果得到了相关对象(me),直接返回;如果没有就开始等锁。第二个if是为了那种没感知到对象(me)已经赋值的线程;如果对象为null,说明还没赋值,那就这个线程来;如果不为null说明前面有个线程已经给它赋值了,为了避免重复赋值破坏单例,直接return。所以DCL使用两个if和锁保证了只有一个线程在创建对象实例。
再来分析代码,有没有问题。还是有的。me = new DCLSingle();
这个语句在底层不是一个原子性操作。它可能分为三步。
- 初始化内存空间,赋默认初始值(0、false、null等)。
- 调用构造方法,赋正确的值。
- 连接堆栈(实际数据空间)和栈(对象空间)的映射。
所以可能出现指令重排这个问题。即假设第一个线程1刚上来用DLC单例模式,只执行到第一步,算半初始化(相关数值还没填好),这个时候发生了指令重排序,2和3交换了,首先进行连接,是半初始化对象进行连接,假设正好执行到这里这线程1停下来了。线程2来了,第一个if (me == null),此时me已经经过连接了,它是实际指向堆空间的值不为null,直接就返回了,返回了个什么?返回了个半初始化的对象,这当然不可以,显然不是线程2想要的(相关数值没填好)。
所以需要给DCL模式需要给单例对象加上volatile修饰,禁止指令重排。(再底层原理是volatile内存屏障,写操作时禁止读)
public class DCLSingle {
private volatile static DCLSingle me;
private DCLSingle() {
}
public static DCLSingle getInstance() {
if(me == null) {
synchronized (DCLSingle.class) {
if(me == null) {
me = new DCLSingle();
}
}
}
return me;
}
}
静态内部类
public class InnerClassSingle {
private InnerClassSingle() {
}
public static InnerClassSingle getInstance() {
return InnerClass.me;
}
static class InnerClass {
private static InnerClassSingle me = new InnerClassSingle();
}
}
静态内部类线程安全且懒加载。
外部类加载时不会立即加载内部类,只有调用getInstance()
第一次被调用方法,才会加载这个内部类。
线程安全原因:点击查看。
但这还不是最完美的单例,因为如果涉及到传参问题。同时上述方法无论如何都是构造方法私有化形成的,Java存在反射,黑科技setAccessible(true)
这个黑科技就打破了private的束缚。
枚举
最佳的单例实现模式就是枚举模式。利用枚举的特性,JVM来帮我们保证线程安全和单一实例的问题。
public enum EnumSingle {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
class Demo {
public static void main(String[] args) {
EnumSingle.INSTANCE.doSomething();
}
}
17、深入理解CAS
CAS(compare and swap),比较并交换。CAS是CPU的并发原语,是操作系统层上的原子性操作。
去找AtomicInteger下的compareAndExchange(int expectedValue, int newValue)
或者compareAndSet(int expectedValue, int newValue)
。其中两个重要参数期望的值,和更新的值。其再底层都是调用native方法。
再看atomicInteger.getAndIncrement()的方法。
这个do while就是Java层面模拟底层实现的自旋锁。
CAS:比较当前工作内存和主内存中的值,如果这个值是期望的,那么执行操作!如果不是就一直循环!如果与我期望的值相同就更新,否则,就不更新。
缺点:
- 循环会耗时
- 一次只能保证一个共享变量的原子性
- ABA问题。
ABA问题
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//==========捣乱的线程
new Thread(()->{ //B线程相较于A线程执行速度快,很快的进行修改然后再改回来
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(Thread.currentThread().getName() + ":" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(Thread.currentThread().getName() + ":" + atomicInteger.get());
}, "B").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2); //模拟A线程执行的逻辑语句多,慢。
System.out.println(atomicInteger.compareAndSet(2020, 66666));
System.out.println(Thread.currentThread().getName() + ":" + atomicInteger.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
}
}
这样肯定不行的,你只要进行数据改动我就一定需要获知。
乐观锁是一种锁类型,CAS是乐观锁的一种具体实现方式。
18、原子引用
AtomicStampedReference的使用。
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<Integer>(2020, 1);
new Thread(()->{ //ABA问题中,执行速度慢的那个线程
int orignStamp = integerAtomicStampedReference.getStamp();
System.out.println("A:orignStamp:" + orignStamp);
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(integerAtomicStampedReference.compareAndSet(2020, 666, orignStamp,
orignStamp + 1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(()->{ //ABA问题中,执行速度快的那个线程,将值改了又改回来那个
int orignStamp = integerAtomicStampedReference.getStamp();
System.out.println("B:orignStamp:" + orignStamp);
System.out.println(integerAtomicStampedReference.
compareAndSet(2020, 2021, orignStamp, orignStamp + 1));
System.out.println("B:2020->2021 stamp:" + integerAtomicStampedReference.getStamp());
System.out.println(integerAtomicStampedReference.
compareAndSet(2021, 2020, integerAtomicStampedReference.getStamp(),
integerAtomicStampedReference.getStamp() + 1));
System.out.println("B:2021->2020 stamp:" + integerAtomicStampedReference.getStamp());
}, "B").start();
}
}
不知道这里为什么版本号一直为1,没变过。
输出修改stamp的语句,发现一直是false;什么意思?难道我期待的2020,和初始值的2020不一样?再深入源码compareAndSet()
方法进去看。
是==
比较啊,期待的“2020”和初始值的“2020”难道数值不一样吗?等等,==
比较。我们要始终记得==
比较根据类型不一样是不同的比较原则,八大基本类型确实是比较值,但是对象就不是了,它比较的是内存地址!然后再看参数类型,一下就想通了,V泛型,此时泛型就是Integer。
最后找到问题原因,与类型Integer的一个知识点,在此叙述铭记。
Integer类型的坑
Integer取得大于127或小于-128的数值都是重新new出来的对象。
我们知道八大基本类型都有对应的类类型。写小写的八大基本类型是关键字,这无可厚非。如果写成它们相对应的类,这时候就要注意一个问题,自动拆箱装箱问题。
我们去看Integer的源码。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到定义了Integer[]的cache数组,保存了-128到127 的整数 ,可以直接使用数组中的元素,不需要去new。主要目的就是提高效率。
而这-128到127都是一个个对象,用则直接取。这就是为什么-128到127之间的数值对象都可以进行==
比较,因为直接取申请好的对象。而超过这个区间就是不同的对象了。
我们看阿里巴巴开发手册也说的很明白。
所以我们刚才问题出在这里比较的是两个Integer对象的内存地址,而不是值,因此一直是false。
进行代码更改。
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<Integer>(10, 1);
new Thread(()->{ //ABA问题中,执行速度慢的那个线程
int orignStamp = integerAtomicStampedReference.getStamp();
System.out.println("A:orignStamp:" + orignStamp);
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(integerAtomicStampedReference.compareAndSet(10, 666, orignStamp,
orignStamp + 1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(()->{ //ABA问题中,执行速度快的那个线程,将值改了又改回来那个
int orignStamp = integerAtomicStampedReference.getStamp();
System.out.println("B:orignStamp:" + orignStamp);
System.out.println(integerAtomicStampedReference.
compareAndSet(10, 11, orignStamp, orignStamp + 1));
System.out.println("B:2020->2021 stamp:" + integerAtomicStampedReference.getStamp());
System.out.println(integerAtomicStampedReference.
compareAndSet(11, 10, integerAtomicStampedReference.getStamp(),
integerAtomicStampedReference.getStamp() + 1));
System.out.println("B:2021->2020 stamp:" + integerAtomicStampedReference.getStamp());
}, "B").start();
}
}
因为版本号发生了变动,所以线程A没有进行修改。
19、各种锁的理解
1、公平锁、非公平锁
公平锁:非常公平。不能插队,必须先来后到
非公平锁:不公平,可以插队(看CPU调度,默认都是非公平)。
2、可重入锁
可重入就是说某个线程已经获得到某个锁,可以再次获取锁而不会出现死锁。
例如:synchronized和ReentrantLock都是可重入锁。
不同就是synchronized是自动释放锁,而ReentrantLock需要在try catch finally语句中的finally最后释放锁(unlock),需要手动释放。而且加锁次数要和释放次数保持一致。
3、自旋锁
spinLock。不断去尝试,直到成功为止。
4、死锁
解决问题
日志和堆栈信息。
堆栈信息:使用jps - l
命令定位进程号与jstack 进程号
命令找到死锁问题。
堆栈经常出(StackOverFlow和OOM错误)