1. 前言
今天主要是从实战 + 浅谈原理的角度来说下,并发线程下原子性的几种处理方式,好了废话不多说了,接下来让我们看看吧
2. 开始
在开始之前需要提问下大家, 代码
i ++;
能保持原子性吗??? 是不是一句话就执行ok了呢??? 接下来看下字节码分析:。
public class T01_Thread_Test01 {
static int i = 0;
public static void main(String[] args) {
i ++;
}
}
通过上述的截图可以看到,短短一句话
i++
会被翻译为很多条语句,在多线程的情况下,执行到任何语句时都会被别的线程打断,所以很难保证执行结果的一致性。
所以为了保证原子性,我们就要代码执行过程中不可拆分。
2.1 synchronized
public class T03_Thread_Synchronized {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (T03_Thread_Synchronized.class) {
for (int i = 0; i < 100000; i++) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
synchronized (T03_Thread_Synchronized.class) {
for (int i = 0; i < 100000; i++) {
count ++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
上锁
是一定能保证原子性的。在保证某个线程拿到锁后,别的线程只能处于blocked
状态。是无法构成资源竞争的。
其实锁的本质
也是并发线程的序列化操作
通过上述的截图中也可以看出。添加synchronized
之后,从代码层面就相当于上锁了。等语句执行结果后会自动释放锁。
2.2 CAS
CAC
全称是compare and swap. 也称为自旋锁,无锁,乐观锁等。- 其大致的原理是:在替换内存的某个位置的值时,会判断下运算之前的值 跟 内存的值是否保持一致(为了避免别的线程也修改过此值)。 如果一致的话,直接替换。反之拿到新值继续
比较-替换
。
那CAS
在Java代码中如何体现呢??? Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令
public class T02_Thread_CAS {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
atomicInteger.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
atomicInteger.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger);
}
}
- 通过上述的代码我们会发现,整个过程中我们并没有用到锁。而是使用Java提供的API
incrementAndGet
。而incrementAndGet
其实就是基于类unsafe
来实现的。
- 通过下列字节码可以看到。以下的代码其实就是自旋的过程
2.2.1 CAS
不足之处:
2.2.1.1 ABA
问题
ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference
2.2.1.2 自旋时间过长的问题
- 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
- 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果
2.3 lock锁
Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好
public class T05_Thread_ReentrantLock {
static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void add() {
try {
lock.lock();
for (int i = 0; i < 100000; i++) {
count ++;
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t01 = new Thread(T05_Thread_ReentrantLock::add);
Thread t02 = new Thread(T05_Thread_ReentrantLock::add);
t01.start();
t02.start();
t01.join();
t02.join();
System.out.println(count);
}
}
注意
:因为lock锁无法自动释放锁。所以unlock
释放锁的从操作必须用finally
包裹下,表示释放锁的操作一定会执行。
2.4 ThreadLocal
其他方式都是允许线程操作共享资源,才会形成了资源竞争。但是
ThtreadLocal
是避免操作临界值的,每个值只单独在自己的线程中。这样就避免了资源竞争了
public class T06_Thread_ThreadLocal {
static ThreadLocal t1 = new ThreadLocal();
static ThreadLocal t2 = new ThreadLocal();
public static void main(String[] args) {
t1.set(123);
t2.set(456);
t1.set(345);
new Thread(() -> {
System.out.println("线程:" + t1.get());
System.out.println("线程" + t2.get());
}).start();
System.out.println("main线程" + t1.get());
System.out.println("main线程" + t2.get());
}
}
2.4.1 ThreadLocal实现原理
- 每个Thread中都存储着一个成员变量,ThreadLocalMap
- ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
- ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
- 每一个线程都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
- ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收
2.4.2 ThreadLocal内存泄漏问题
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
- 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可
3. 结束
关于保证原子性的几种方式就列举这么多了。如果大家不同的看法 欢迎请在评论区内留言,欢迎一起进步