Java常见问题总结一

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 objects = new Vector<>(); 使用vector,但是vector的诞生要比ArrayList的要早,所以不推荐使用

2. List results = Collections.synchronizedList(new ArrayList<>()); Collection是个接口,Collections是个类,其下面有一个同步数组的方法。

3. CopyOnWriteArrayList 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 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();
    }
}

 

你可能感兴趣的:(Java)