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前缀指令会引发两件事情:
- 将当前处理器缓存行的数据协会到系统内存。
- 这个协会内存的操作会使在其他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类。(代码略)