并发面试题(01) :volatile和synchronized 相关面试题

1、synchronized 锁的是对象还是代码块?

class TestSynchronizedMethod {
    public synchronized void method01() {
        // do sth
    }
    public void method02() {
        synchronized(this) {
        	// do sth
        }
    }
}

解 : synchronized 锁住的是对象,事实上,锁存在于每个对象之中(详见JVM内存),synchronized 获得到了对象的锁,其他锁想要获取对象的锁时会失败,Java通过对象锁的获取保证线程安全

解析 : synchronized 用的锁是存在Java对象头里的。

Java 对象头里的 Mark Word 里默认存储对象的HashCode,分代年龄和锁标志位。

锁标志位通常是两字节,数组对象是三字节。

32 位Mark Word的状态变化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9fnby9P-1572232278637)(C:\Users\Administrator\Pictures\Mark Word的状态变化.png)]

可以看到:锁的升级就是对象头中的Mark Word的锁标志位的改变。–《13p》

2、volatile和synchronized的区别.

解 :

特性 volatile synchronized
可见性
原子性 ×
修饰字段
修饰方法 ×
执行成本 极小 较大(1.6之后,锁逐步升级)
很大(1.6之前)

解析 : volatile和synchronized在多线程并发编程中都扮演着重要的角色,volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性",它比synchronized 的使用和执行成本更低,因为他不会因为线程上下文的切换和调度。

volatile是通过Lock指令,使CPU的缓存数据无效,从而使每个线程获取到最新值。—《8p》

3、同步方法和非同步方法是否可以同时调用?

可以,线程在执行同步方法前,需要获取对应的对象锁,然后才能执行。

非同步方法不需要获取到对象锁,所以与同步方法不矛盾。

例:

class Test {
    synchronized void m1 () {
        Test t = new Test();
        // 调用非同步方法
        t.m2();
    }
    void m2() {
        System.out.println("m2");
    }
}

4、什么是脏读,脏写?

5、一个同步方法是否可以调用另外一个同步方法?

解: 可以。因为当线程去执行另一个同步方法时仍需要获取锁,然后获取时发现:自己已经获得了对象锁的所有权,然后执行第二个同步方法。

例:

class T {
    // 锁的深度++
    synchronized void m1 () {
        Test t = new Test();
        // 调用同步方法
        t.m2();
    }// 深度--
    
    // 调用时会发现,自己已经获得了当前 T 的实例锁,所以直接执行,这就是可重入锁(锁的深度++)。
    synchronized void m2() {
        System.out.println("m2");
    }// 执行完毕后,锁的深度--
}// if (锁的深度==0) 释放锁;

同步方法可以调用另一个同步方法时,说明锁是可重入的。

可重入锁往往是通过一个深度计数器,来实现的。

6、程序执行中出现异常,锁会释放吗?这会造成什么影响?怎样去避免?

当程序执行中出现异常,锁会释放,这可能造成代码执行到一半。

例:

class T {
    int a = 0;
  	int aCopy = 0;
    synchronized void m() {
        a++;
        // do sth
        aCopy++;
    } 
    public static void main(String[] args) {
        T t = new T();
        // 这个线程执行后抛出异常。
        new Thread(t::m, "thread-1").start();
        // 这个线程正常执行。
        new Thread(t::m, "thread-2").start();
    }
}// a = 2; aCopy = 1;

7、以下代码会出现问题吗?

public class T {
    static boolean flag = true;
    void m() {
        System.out.println("m start");
        while (flag) {
            
        }
        System.out.println("m end");
    }
    public static void main (String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            Thread.sleep(1000);
        }catch (Exception e) {
            e.printStackTrace();
        }
        flag = false;
    }
}// 结果: m start

虽然看似一切正常,但是控制台不会打印 m end。

因为t1中的flag是CPU的缓存,只有等到缓存过期才能拿到false。

修改方案 :

volatile boolean flag = true;

也可以通过在死循环中做一些费时操作:如print(),sleep()等,现代JVM会自动的刷新最新值到CPU缓存中,但是不建议这样做,因为不确定性较大。

8、volatile是如何实现可见性的?

volatile在编译后变为Lock前缀指令,Lock前缀指令会引发两件事情:

  1. 将当前处理器缓存行的数据协会到系统内存。
  2. 这个协会内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

也就是缓存一致性协议

缓存一致性协议: 每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当CPU发现自己缓存行对应的内存地址被修改,就会将当前CPU的缓存行设置为无效状态,当CPU对这个数据进行修改操作的时候,会重新从系统内存中把数据读到CPU缓存中

9、以下代码的输出是什么?

public class T {
    volatile int count = 0;
    void m() {
        for(int i = 0; i < 10000; i++) {
            count++;
        }
    }
    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for(int i = 0; i< 10; i++) {
            // t::m 的意思是 t 的 m 将被作为 thread.run();
            threads.add(new Thread(t::m, "thread-" + i));
        }
        thread.forEach((o)->o.start());
        
        threads.forEach((o)->{
            try {
                o.join();
            }catch(Exception e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}// 三次运行结果:99028,82472,72664

小于100000的原因:volatile只保证了count 的可见性,但是不保证原子性,cnt++是两步操作,出现了脏写。

volatile只避免了脏读。

修改:

常规答案:count++代码块加锁。太沉重了。

优秀答案:将count值变为AtomicInteger,“++”换为count.incrementAndGet();

10、 以下代码输出的结果是什么?

public class T {
	Object o = new Object();
    void m(){
        while (true) {
            synchronized (o) {
                Thread.yield();
                System.out.println(Thread.currentThread.getName());
            }
        }
    }
    public static void main(String[] args) {
    	T t = new T();
        new Thread(t::m, "t1").start();
        try {
            	Thread.sleep(1000);
        }catch (Exception e) {
            	e.printStackTrace();
        }
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object();
        t2.start();
    }
}/**
t1
t2
t1
t2
t1
t1
*/

// 在于o 的指向内存改变了,所以t1和t2获取的锁不矛盾了,两者交互yield(),所以是两者不严格的交互执行。

11、观察代码,回答问题

class T {
    String s1 = "str";
    String s2 = "str";
    
    void m1() {
        synchronized(s1) {
            while (true) {
                // sleep一秒
                System.out.println("m1");
            }
        }
    }
    void m2() {
        synchronized(s2) {  
            while (true) {
                // sleep一秒
                System.out.println("m2");
            }
        }
    }
    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start();
        // sleep一秒.
        new Thread(t::m2, "t2").start();
    }
}
// 会打印出m2吗?为什么?

不会,因为str1 == str2,指向的是同一块内存区域,其对象头中的锁相同,所以t1和t2处于竞争关系。

12、写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数达到五个时,线程2给出提示并结束。(淘宝面试题)

// 可以通过wait-notify模仿生产者–消费者模式。(较复杂,面试原题就是填空wait—notify)

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.locks.Lock;

public class MyCollection {
    private Collection<Integer> c = new ArrayList<>();

    void add(int v) {
        c.add(v);
    }

    int size() {
        return c.size();
    }

    public static void main(String[] args) {
        MyCollection myCollection = new MyCollection();
        Object lock = new Object();
        Thread t2 = new Thread(() -> {
            if (myCollection.size() != 5) {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("t2 end");
        });
        t2.start();
        
        Thread.yield();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                myCollection.add(i);
                System.out.println(i);
                if (i == 5) {
                    synchronized (lock) {
                        lock.notify();
                    }
                    Thread.yield();
                }
            }
            System.out.println("t1 end");
        });
        t1.start();
    }
}

// 也可以用CountDownLatch类。(代码略)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章