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);

禁止指令重排:编译器和处理器为了优化性能,常常会进行指令重排,在单线程中无需考虑指令重排,处理器在进行重新排序的时候必须要考虑指令之间的数据依赖性,多线程中线程交替执行由于指令重排的存在导致结果无法预测。通过内存屏障禁止内存屏障前后的指令执行重排序优化。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值