1、JMM(Java内存模型)
JMM是一种抽象概念并不是真实存在的,是一组规范,有三个特性:原子性、有序性、可见性,JMM关于同步的规定:
- 线程解锁前必须把共享变量的值刷新回主内存。
- 线程解锁前必须从主内存读取最新的值到自己的工作空间。
- 加锁和解锁是同一把锁。
每个线程对变量的操作(读取赋值)都必须在工作内存中进行,首先将变量从主内存拷贝到自己的工作空间,然后进行操作,操作完成后将变量写回主内存,不能直接操控主内存的变量,各个线程无法访问对方的工作空间。
2、对volatile的理解
volatile是Java虚拟机提供的轻量级的同步机制,具有三个特性:禁止指令重排、变量的可见性、不保证原子性。
原子性是指不可分割,某个线程在做某个具体业务的时候,中间不可以被加塞或者分割。如何验证不保证原子性,可以通过一个for循环进行对变量的累加,发现最后的结果并不是预期的结果。
for(int k=0;k<=100;k++){
new Thread(() -> {
i = i+1;
}).start();
}
对于变量的可见性,是指当一个线程修改了变量的值以后立刻刷新到主内存,其他线程在读取的时,发现被修改过直接去主内存中读取最新的值。如何证明:
new Thread(() -> {
for(int i = 0; i<40;i++){
k = k+1;
}}).start();
while(k != 40); // 这里一直夯住,知道等于40才放行
System.out.println(k);
禁止指令重排:编译器和处理器为了优化性能,常常会进行指令重排,在单线程中无需考虑指令重排,处理器在进行重新排序的时候必须要考虑指令之间的数据依赖性,多线程中线程交替执行由于指令重排的存在导致结果无法预测。通过内存屏障禁止内存屏障前后的指令执行重排序优化。
volatile通过内存屏障保证可见性:
内存屏障节点之前所有写操作都要会写到主内存中,节点之后所有的读操作都能获得最新的结果,实现了可见性。
在每个volatile读操作后面插入
loadload屏障 (禁止下面所有的普通读和上面的volatile读重排序)
loadStore屏障 (禁止把volatile读和下面的写重排序)
在每个volatile写操作
后面插入 storeLoad屏障(禁止把volatile写和下面的读重排序)
之前插入 storestore屏障(禁止把volatile写和下面的写重排序) 写入之前所有的值已经刷新回了主内存。
即 volatile写之前的操作都不能重排到 volatile写之后
即 volatile读之后的操作都不能重排到 volatile读之前 注意内存屏障的位置
多线程下的单例模式:
public class InstallCert {
public static volatile InstallCert instance = null; // 需要禁止指令重排
private InstallCert(){
System.out.println("2333");
}
public static InstallCert getInstance(){
if(instance == null){
synchronized (InstallCert.class){
if(instance == null){
instance = new InstallCert(); // 在这里分为三步骤:为对象分配空间,初始化对象,将该地址指向该对象
//由于第二步和第三步没有数据依赖,很可能导致指令重排所以需要加上volatile
}
}
}
return instance;
}
}
3、CAS
CAS指的是compare-and-swap,是一条指令原语,,它的功能是判断内存的某个位置的值是否为期望值,如果是则更新。CAS是一条原子性的指令,不会造成所谓的数据不一致的情况,在Java的unsafe类中的CAS方法进行了体现。
AtomicInteger number = new AtomicInteger(); // 声明一个原子整形,默认初始值为0
number.compareAndSet(expect, update); // 如果number符合期望值,则修改成更新值
/**compareAndSet底层实现**/
// u是unsafe类
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
// value是获取value属性的内存偏移量
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue); // 当前对象的内存偏移量的值是否是期望值。
}
/**底层调用**/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 通过获取当前的值并通过volatile进行修饰
} while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 比较当前对象的偏移地址的值是否还是这个值
return v;
}
CAS的缺点:1.循环时间长开销大。2. 只能保证一个共享变量的原子操作。3. 不能解决ABA问题。
4、原子引用类
引用类型原子引用类:
User zhangsan = new User("zhangsan", 11);
// 创建一个原子引用类,需要传入默认值
AtomicReference<User> refer = new AtomicReference<>(zhangsan);
User li = new User("li", 11);
refer.compareAndSet(zhangsan, li); // 如果期望值是张三那么就换成里斯
数组类型原子引用类:
// 两种初始化的方式
AtomicIntegerArray array = new AtomicIntegerArray(new int[]{1, 2, 3});
AtomicIntegerArray array1 = new AtomicIntegerArray(new int[5]);
array.compareAndSet(0, 1, 10); // 多传一个索引值,进行compareAndSet
System.out.println(array.get(0));
ABA问题的由来:当有两个线程都从主内存中读取到变量的值,A线程进行挂起,B线程先将变量值修改为其他值,然后又将变量的值修改回来,当A挂起结束以后,不会知道这个变量被修改过,所出现的这种情况就是ABA问题。一般的原子引用类无法解决这种问题,所以AtomicStampedReference 时间戳类的原子引用可以很好的解决ABA问题。
// 初始化AtomicStampedReference 两个参数:当前值,版本号
static AtomicStampedReference<Integer> instance = new AtomicStampedReference(100, 1);
public void test2(){
new Thread(() -> {
int stamp = instance.getStamp(); // 获取版本号
System.out.println(stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// compareAndSet(期望值,更新值,期望版本号,更新版本号)
instance.compareAndSet(100,101, instance.getStamp(), instance.getStamp()+1);
instance.compareAndSet(101,100, instance.getStamp(), instance.getStamp()+1);
System.out.println("t1"+instance.getStamp());
}, "t1").start();
new Thread(() -> {
int stamp = instance.getStamp();
System.out.println("t2"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(instance.compareAndSet(100, 101, stamp, stamp+1));
}, "t2").start();
}
※:AtomicStampedReference可以通过版本还解决修改几次的问题,AtomicMarkableReference可以解决数据是否被修改过。
// 设置初始值并设置标记位状态
AtomicMarkableReference<User> markRefer = new AtomicMarkableReference<User>(User.One, false);
boolean mark = markRefer.isMarked(); // 获取标识
markableReference.compareAndSet(User.One, User.two, mark, !mark);
System.out.println(markRefer.getReference()); // 获取引用对象
原子字段更新:以前对某个对象的属性进行并发加锁需要加对象锁,AtomicFieldUpdater 可以 通过反射某个字段进行加锁,使锁的粒度更小。
// 无法通过new进行创建,通过静态方法进行创建初始化
// 参数 [类的class, 字段的类型class, 字段名], 需要注意字段需要volatile进行修饰
AtomicReferenceFieldUpdater<User, Integer> age = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
// 将User.One 的age的值修改为19
age.getAndSet(User.One, 19);
// 将User.One 的age的值修改为36,期望值为19
age.compareAndSet(User.One, 19, 36);
// 输出修改后对象的age值
System.out.println(age.get(User.One));
※:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater没有上面的使用灵活,但是功能是一样的。
5、集合类的不安全问题
ArrayList是一个非线程安全的数组,如何解决这种非线程安全的问题
1. Vector<Object> objects = new Vector<>(); 使用vector,但是vector的诞生要比ArrayList的要早,所以不推荐使用
2. List<Integer> results = Collections.synchronizedList(new ArrayList<>()); Collection是个接口,Collections是个类,其下面有一个同步数组的方法。
3. CopyOnWriteArrayList<Object> objects = new CopyOnWriteArrayList<>(); 写时复制数组,就是先将容器进行复制,然后写一个进行对容器的复制,然后再把容器写回去。
/**底层原理**/
public boolean add(E e) {
synchronized (lock) { // 同步代码块
Object[] es = getArray(); // 获取到当前的数组
int len = es.length;
es = Arrays.copyOf(es, len + 1); // 在当前数组的len+1的位置进行填写
es[len] = e;
setArray(es); // 再将数组进行写回
return true;
}
}
HashSet:多线程下如何使用HashSet
Set<Object> objects = Collections.synchronizedSet(new HashSet<>());
CopyOnWriteArraySet<Object> objects = new CopyOnWriteArraySet<>();
/**底层原理**/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>(); // 底层初始化的为一个数组
}
HashSet的初始化:
/**在Hashset的底层初始化是创建了一个HashMap**/
public HashSet() {
map = new HashMap<>();
}
/**当hashSet进行添加元素时**/
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null; // 将值存到了hashmap的key中
}
HashMap:多线程下使用HashMap应该使用ConcurrentHashMap。
6、公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,有先来后到之分。
非公平锁:不按照申请锁的顺序,有可能后申请的比先申请的线程先获取锁,在高并发的情况下有可能造成优先级反转或饥饿现象。
/**关于Reentranlock**/
/**可以通过构造函数指定是否是公平锁,默认是非公平锁**/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
此外,Synchronized也是非公平锁。
TIPS:Synchronized底层是monitor enter和 monitor exit 实现的,一般退出命令要比进入命令多一个,是因为怕中途发生异常,从而引发死锁。synchronized 加在方法上就是会编译成ACC_synchronized,底层也是一样的原理。
// 当锁的目标是实例对象的时候或者是this的时候,多次调用会存在竞争。
synchronized (testA){
...
}
// 当锁的目标是类,并且synchronized 加在方法上,也会存在竞争
public static synchronized void doGet(){...}
对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。
7、可重入锁(递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,也就是说,线程可以进入任何一个它以拥有的锁同步着的代码块。
public void test4(){
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock();
/**do something .... **/
lock.unlock();
lock.unlock();
}
8、自旋锁
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少了上下文的切换的消耗,缺点是会消耗CPU。
9、读写锁
独占锁:该锁只能被一个线程所持有,对Synchronized和Reentranlock都是独占锁。共享锁:该锁可被多个线程所持有。
ReentrantReadWriteLock的读锁是共享锁,写锁是独占锁。
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); // 创建读写锁
reentrantReadWriteLock.writeLock().lock(); // 写锁
reentrantReadWriteLock.writeLock().unlock();
reentrantReadWriteLock.readLock().lock(); // 读锁
reentrantReadWriteLock.readLock().unlock();
10、CountDownLatch
public void test2() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5); // 倒计数,直到0
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"走了");
countDownLatch.countDown();
String name = User.for_each(temp).getName();
System.out.println(name);
},String.valueOf(i)).start();
}
countDownLatch.await(); // 会在这里进行阻塞
System.out.println("班长走人");
}
11、CyclicBarrier(可循环使用的屏障)
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,才会解除屏障。
public void test3() throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("我是被阻塞的线程");
}); // 当阻塞的5个线程条件的时候才会,执行被阻塞的线程
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
System.out.println("我是"+Thread.currentThread().getName());
cyclicBarrier.await(); // 进行阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
12、Semaphore(信号量)
public void test4(){
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 展位
System.out.println(Thread.currentThread().getName());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}