目录
一、原子性
1 什么是原子性
JMM(Java Memory Model):不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定!)(虽然理论上每次修改都应该立即反映在主内存中,但实际上由于上述原因,修改后的值可能不会立即写回到主内存,而是会在某个适当的时机(如缓存满、同步点到达或其他JMM规则触发)才进行写回)。
原子性的定义:原子性指一个操作是不可分割的,不可中断的,不受其他线程影响。
由一个例子来阐述
private static int count;
public static void increment(){try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {
increment();
}
});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
当前的代码:多线程操作共享变量,预期结果与实际结果不符。
原子性:多线程操作临界资源(共享变量),预期结果与最终结果一致。
2 保证并发编程的原子性
2.1 synchronized(上锁)
上面代码中++操作,编译后的代码,看看其是怎么处理的。
可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性
synchronized可以让避免多线程同时操作临界资源(在操作资源前后添加标识,进行锁定资源),同一时间点,只会有一个线程正在操作临界资源
2.2 CAS
CAS:CAS是compare and swap也就是比较和交换简称,是一条CPU的并发原语。
CAS的操作是在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。
Unsafe的类提供了对CAS的操作的方法,JVM将方法实现CAS汇编指令。
下面是一个简单的例子:
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
在CAS的基础上实现了一些原子类,其中就包括上方用到的AtomicInteger ,还有很多其他原子类
CAS的缺点:只能保证一个变量的操作是原子性,无法实现对多行代码实现原子性。
CAS存在的问题:
- ABA问题:可以从下图了解ABA问题,可以通过引入版本号的方式来解决。AtomicStampeReference原子类,就是通过该方法,解决ABA问题。
- 下面是AtomicStampedReference的使用例子,在CAS时,不仅比较原值,还比较版本信息。
public static void main(String[] args) {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
System.out.println("修改1版本的:" + b);
boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
System.out.println("修改2版本的:" + c);
}
- 自旋时间过长问题:
- 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
- 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。
2.3 Lock锁
Lock锁初次出现于JDK1.5,其性能比同版本的时期的synchronized好得多,但是在JDK1.6对synchronized进行之后,性能已经相差不大,但是在并发较高的时候建议使用ReentrantLock,下面是Lock的一个使用例子:
private static int count;
private static ReentrantLock lock = new ReentrantLock();
public static void increment() {
lock.lock();
try {
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
ReentrantLock与synchronized对比,在功能上来说,都是锁。但是ReentrantLock的使用场景比synchronized更加丰富。
ReentrantLock底层基于AQS实现,有一个基于CAS维护的state变量来实现锁操作。
2.4 ThreadLocal
Java中的四种引用类型
Java中的使用引用类型分别是强,软,弱,虚。
强引用:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用:对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
弱引用:比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
虚引用:不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。
本地线程变量ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。
例子:
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get());
System.out.println("t1:" + tl2.get());
});
t1.start();
System.out.println("main:" + tl1.get());
System.out.println("main:" + tl2.get());
}
ThreadLocal实现原理:
- 每个Thread中都存储着一个成员变量,ThreadLocalMap
- ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
- ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
- 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
- ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收
ThreadLocal内存泄漏问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
- 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可、