Java 多线程
1、Java中实现多线程有多少种方式:
答:
- 继承Thread;
2)实现接口Runnable;
3)FutureTask
4)线程池
2、sleep()和wait() 有什么区别?
答:
sleep不会释放对象锁,wait会释放对象锁。
sleep会自我唤醒,wait需要notify/notifyAll。
3、volatile是什么?可以保存有序性吗?
答:
volatile是Java多线程解决可见性的问题提供的一个保留关键字,使用它可以保证多线程访问时该变量的可见性。可以保证部分有序性,但是二目操作比如++i,不能保证其有序性。
在线程通信的过程中,有三个问题:重排序、可见性、原子性。
在重排序即有序性方面:1)编译器不会对volatile读和读后面的任意内存操作重排序,
2)编译器不会对volatile写和写之前任意内存排序
happens-before法则关于volatile的法则是:volatile的写 happens-before volatile的读。
在可见性方面:
JMM定义了volatile语义:
1)在每次读volatile变量的时候,都置本地内存的volatile变量失效,读主内存的volatile变量。
2)在每次写volatile变量的之后,都会刷本地内存的volatile变量到主内存。
原子性方面:
对于单个变量的读写具有原子性。但是volatile对类似volatile++这种复合操作不具备原子性,因为底层也是使用了锁来实现原子性的。
4、volatile的原理是什么?
答:
分三个方面讲:
1)有序性是底层也使用了一个OrderAccess.fence()
,让程序运行到该变量之前/之后不会内存重排序。
2)可见性是使用了内存/CPU一致性协议,单核的StoreStore,LoadLoad,StoreLoad,LoadStore栅栏,多核的MEIS协议。
3)原子性,整个JVM最终都是使用总线锁Lock# 方式,多核的MEIS协议。
5、为什么wait, notify和notifyAll这些方法不在thread类里面?
答:
因为wait,notify和notifyAll是配套给synchronized使用的,synchronized睡对象锁,jdk希望提供给使用者的是一个方便使用的锁形式,Java是面向对象语言,所以提供一个面向对象的锁。
wait、notify和notifyAll其实是多线程通信机制的一种。更加方便的让对象可以更加方便的通讯,释放锁和获取锁。
6、为什么wait和notify方法要在同步块中调用?
答:
wait和notify其实是jdk提供的在线程锁中对象通信的函数。在调用notify/wait之前会代码会去检查该线程是否握有这把锁,如果这把锁不是它所有的,jvm就会抛出异常IllegalMonitorStateException异常,即必须有多想的锁才能调用wait,调用了wait和notify就是重量级的级别了,使用者可以根据自己的情况使用。
7、wait会挂起线程,让出cpu资源,notify会不会?
答:
不会,我们做个试验试试:
public class NotifySyncLockA {
private static int status = 0;
private static Object object1 = new Object();
static Runnable changeRunner = () -> {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + " - " +NotifySyncLockA.status++);
try {
object1.wait();
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
static Runnable showRunner = ()-> {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + " - " +NotifySyncLockA.status++);
try {
object1.notifyAll();
System.out.println("我不会释放资源,还是会继续进行下去");
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
public static void main(String[] args) {
new Thread(changeRunner).start();
new Thread(showRunner).start();
}
}
输出如下:
Thread-0 - 0
Thread-1 - 1
我不会释放资源,还是会继续进行下去
Thread-1 - 2
我不会释放资源,还是会继续进行下去
Thread-1 - 3
我不会释放资源,还是会继续进行下去
Thread-1 - 4
我不会释放资源,还是会继续进行下去
Thread-1 - 5
我不会释放资源,还是会继续进行下去
Thread-1 - 6
我不会释放资源,还是会继续进行下去
Thread-1 - 7
我不会释放资源,还是会继续进行下去
Thread-1 - 8
我不会释放资源,还是会继续进行下去
Thread-1 - 9
我不会释放资源,还是会继续进行下去
Thread-1 - 10
我不会释放资源,还是会继续进行下去
Thread-1 - 11
我不会释放资源,还是会继续进行下去
Thread-1 - 12
我不会释放资源,还是会继续进行下去
Thread-1 - 13
我不会释放资源,还是会继续进行下去
Thread-1 - 14
我不会释放资源,还是会继续进行下去
Thread-1 - 15
我不会释放资源,还是会继续进行下去
Thread-1 - 16
我不会释放资源,还是会继续进行下去
Thread-1 - 17
我不会释放资源,还是会继续进行下去
Thread-0 - 18
...
这里可以看出Thread-1 调用notify之后并没有退出执行,而是继续执行后面的步骤,并且大多数情况都是Thread-1在执行,Thread-0只是在Thread-1释放资源之后偶尔抢到锁,因为Thread-1只是在notify的位置唤醒Thread-0,让Thread-0起来争抢锁,并不是直接把锁让给Thread-1,在不公平锁中,for循环抢锁的效率比fifo队列的节点抢锁的效率高点。
这里稍微演变一下就是多线程入门面试题:
交替打印数字:多线程交替打印0~100
代码实现如下:
public class CrossPrint {
private static int status = 0;
private static Object object1 = new Object();
static Runnable changeRunner = () -> {
while (true) {
synchronized (object1) {
if (CrossPrint.status % 2 ==0) {
System.out.println(Thread.currentThread().getName() + " - " + CrossPrint.status++);
if(CrossPrint.status == 101) {
break;
}
} else {
try {
object1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
static Runnable showRunner = ()-> {
while (true) {
synchronized (object1) {
if (CrossPrint.status % 2 ==1) {
System.out.println(Thread.currentThread().getName() + " - " + CrossPrint.status++);
if(CrossPrint.status == 100) {
object1.notify();
break;
}
} else {
object1.notify();
}
}
}
};
public static void main(String[] args) {
new Thread(changeRunner).start();
new Thread(showRunner).start();
}
}
打印结果如下:
Thread-0 - 0
Thread-1 - 1
Thread-0 - 2
Thread-1 - 3
Thread-0 - 4
Thread-1 - 5
...
Thread-0 - 98
Thread-1 - 99
Thread-0 - 100
交替打印有很多中方式,这种互相通知是一种方式。
细心的童鞋可能会发现,根据happens-before原则,在锁内变量的可见性可以得知:即使我不用wait和notify我这样交替打印也没问题。确实是的,但是锁的竞争完全靠线程自己去争抢,有性能消耗。这个使用Thread.yield() 可以优化一点这种情况。
关于交替打印,更多内容,如果有时间,可以给出一片文章,这里不扩散了。总的来说交替打印就是线程交互的问题。
8、Thread.yield() 除了退让CPU资源,会释放锁吗?
答:
不会。可以参考下面的代码:
private static int status = 0;
private static Object object1 = new Object();
static Runnable changeRunner = () -> {
while (true) {
synchronized (object1) {
if (CrossPrint.status % 2 ==0) {
System.out.println(Thread.currentThread().getName() + " - " + CrossPrint.status++);
if(CrossPrint.status == 5) {
break;
}
} else {
System.out.println("000000000");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "执行yield 退让");
}
}
}
};
static Runnable showRunner = ()-> {
while (true) {
synchronized (object1) {
if (CrossPrint.status % 2 ==1) {
System.out.println(Thread.currentThread().getName() + " - " + CrossPrint.status++);
if(CrossPrint.status == 6) {
Thread.yield();
break;
}
} else {
System.out.println("111111111");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "执行yield 退让");
}
}
}
};
public static void main(String[] args) {
new Thread(changeRunner).start();
new Thread(showRunner).start();
}
}
输出的结果是:
Thread-0 - 0
000000000
Thread-0执行yield 退让
000000000
Thread-0执行yield 退让
... ...
如果是释放锁,输出就会想wait那样只是:
Thread-0 - 0
000000000
Thread-1 - 1
... ...
或者
Thread-0 - 0
000000000
000000000
... ...
9、sychronzied和ReentrantLock的区别
答:
- synchronized不能打断,ReentrantLock可以使用能被打断的锁,也可以使用不能被打断的锁。
- sychronized不需要自己释放锁,ReentrantLock需要自己手动释放锁。
- sychronized只有一个等待队列,ReentrantLock有多个等待队列。
- sychronized只能是独占锁,ReetrantLock可以是共享锁,并且分为公平锁和非公平锁。
10、Thread类中的yield有什么用?
答:
CPU退让,让出自己的CPU执行时间,但是只是建议,CPU有可能忽略这个,直接执行线程接下来的逻辑。