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);
禁止指令重排:编译器和处理器为了优化性能,常常会进行指令重排,在单线程中无需考虑指令重排,处理器在进行重新排序的时候必须要考虑指令之间的数据依赖性,多线程中线程交替执行由于指令重排的存在导致结果无法预测。通过内存屏障禁止内存屏障前后的指令执行重排序优化。
多线程下的单例模式:
public class InstallCert {
public static volatile InstallCert instance = null; // 需要禁止指令重排
private InstallCert(){
System.out.println("2333");
}
public 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问题。
原子引用类:
AtomicReference userAtomicReference = new AtomicReference<>(); // 创建一个原子引用类
user zhangsan = new user("zhangsan", 11);
user li = new user("li", 11);
userAtomicReference.set(zhangsan);
userAtomicReference.compareAndSet(zhangsan, li);// 如果期望值是张三那么就换成里斯
4、ABA问题
ABA问题的由来:当有两个线程都从主内存中读取到变量的值,A线程进行挂起,B线程先将变量值修改为其他值,然后又将变量的值修改回来,当A挂起结束以后,不会知道这个变量被修改过,所出现的这种情况就是ABA问题。一般的原子引用类无法解决这种问题,所以AtomicStampedReference 时间戳类的原子引用可以很好的解决ABA问题。
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();
}
5、集合类的不安全问题
ArrayList是一个非线程安全的数组,如何解决这种非线程安全的问题
1. Vector
HashSet:多线程下如何使用HashSet
Set objects = Collections.synchronizedSet(new HashSet<>());
CopyOnWriteArraySet objects = new CopyOnWriteArraySet<>();
/**底层原理**/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList(); // 底层初始化的为一个数组
}
HashSet的初始化:
/**在Hashset的底层初始化是创建了一个HashMap**/
public HashSet() {
map = new HashMap<>();
}
/**当hashSet进行添加元素时**/
private transient HashMap 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也是非公平锁。
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();
}
}