0%

面试题-多线程知识

1、线程同步,你了解哪几种方式?

点击看答案
  1. 数据同步:可以使用Android 提供的同步容器。如: CopyOnWriteArrayList(当某个线程要修改list的元素时,首先copy一份出来,然后在修改前加锁,防止多个线程同时修改而copy多个副本,读的时候无需加锁,读的是副本)、concurrentHashMap(分段锁)、BlockingQueue(队列满了,在调用put,会阻塞,直到不再是满的;同理,取也是一样)。
  2. 用锁。同步锁:synchronize(无论synchronized关键字加在方法上还是对象上,他锁的都是对象,而不是把一段代码或函数当作锁)、ReentrantLock

2、synchronized 与 Lock。讲下 RecentLock 可重入锁? 什么是可重入锁?为什么要设计可重入锁?

点击看答案

介绍:synchronized:无论synchronized关键字加在方法上还是对象上,他锁的都是对象,而不是把一段代码或函数当作锁。
Lock:一般使用ReentrantLock类做为锁,需要显式指出加锁与释放锁的位置,在加锁和解锁处通过lock()和unlock()显示指出,所以一般会在finally块中写unlock()以防死锁。

区别:

  • synchronized 是个关键字,而 Lock 是个接口
  • synchronized 使用简单,会自动释放;Lock需要显式加锁与释放,要防止死锁
  • synchronized是悲观锁,其他线程只能阻塞来等待线程释放锁;Lock是乐观锁。
  • lock可以使我们多线程交互变得方便,而使用synchronized则无法做到这点。如:某个线程在等待锁的过程中需要中断、或者获知某个线程有没有获取到锁、或者需要指定notify哪个线程

3、为什么wait、notify、notifyAll 方法在object 类中,而sleep 在Thread 类中?

点击看答案

一个例子,假如要做的事情是“生火-烧水-煮面”。sleep 就是,当我生火之后,觉得有点累,要休息一段时间,所以并不立即烧水,等过会再烧,休息的这段时间是由我自己来控制的,这个灶台我也要一直占用;而对于wait,首先,wait 是由某个object 来调用的,这个object 类似于监督人的角色,当我点火在烧水的过程中,监督人要求我停下来,不允许我继续烧水了,同时剥夺我灶台使用权,让其他人先用灶台,我在旁边等着,直到这个监督人通知(notify/notifyAll)我可以继续使用灶台了,我才能继续。

通过这个例子,我们可以知道:

  • 首先,sleep 和 其他的是有本质区别的:sleep 是一个线程的运行状态控制,所以交给Thread 自己更合适;而wait 是线程之间的通讯工具,交给object更合适,这样各个竞争线程不需要知道彼此的存在。
  • 其次,wait、notify与notifyAll 是锁级别的操作,而锁属于对象,每个对象都可能作为锁,所以它们定义在Object 类中。

从另一个角度来说是,假如不这样做的话,即如果wait、notify和notifyAll 都在Thread 中,会有什么问题:

首先,wait 方法仍然可以使当前线程挂起,但是挂起后怎么被其他线程唤起呢?因为唤起时需要知道要唤起哪个线程。
其次,notify 与 notifyAll 都需要知道目前需要唤醒哪些线程。

当然你可以说我们使用共享变量或者其他方式,这无疑会增加线程间通信的复杂性,并带来安全隐患,所以并没有必要。

值得注意的是:必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()、notify()或notifyAll()方法。原因很简单,如果这个代码块或者方法不是同步的,那么进入其中必然是不需要获取锁的,所以释放锁和等待锁就无从谈起,这时候调用的话会报 IllegalMonitorStateException。Java 给报这个错,其实就是不想让我们的程序在不经意间出现 Lost Wake-Up 问题。

以上内容参考csdn的这篇文章

4、延伸-深究 notify/wait 方法为什么一定要放在同步块中?

点击看答案

前面谈到的是没有同步就没必要释放或者等待锁,这里深究如果不放在同步块中会出现什么问题。这主要是涉及到 “Lost Wake-Up Problem”问题。

我们notify/wait 很典型的一个用途就是生产者-消费者 场景,伪代码如下:

生产者:

count ++;
notify();

消费者:

while(count <= 0) wait();
count –;

生产者和消费着都有两个步骤,如果不在同步块中的话,多线程情况下执行顺序很难保证,有可能会出现如下图所示的执行顺序:

生产消费乱序

初始的时候,count == 0,消费者发现条件成立,此时,发生上下文切换,生产者线程一顿操作执行了 count ++,之后发出了通知准备唤醒一个线程,这时候消费者刚决定wait,但是还没进入wait,此时,由于消费者线程还未进入wait状态,因此在等待队列中还找不到消费者线程,这个notify就被丢掉了。

以上内容参考自360linker的博客

5、两个线程交替打印,一个输出偶数,一个输出奇数

点击看答案

第一种,使用synchronize 关键字,锁住对象,每次只能一个线程进入。每打印一个数,就释放锁,然后挂起自己,如此往复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MainClass {

public static void main(String[] args) {
MainClass obj = new MainClass();

Thread t1 = new Thread(obj::printOdds);
Thread t2 = new Thread(obj::printEven);

t2.start();
t1.start();

}

public synchronized void printOdds() {
for (int i = 1; i <= 100; i += 2) {
System.out.println("t1---" + i);
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public synchronized void printEven() {
for (int i = 0; i <= 100; i += 2) {
System.out.println("t2---" + i);
this.notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

第二种,使用volatile 来保证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MainClass {

static volatile int num = 0;
static volatile boolean flag = false;

public static void main(String[] args) {

Thread t1 = new Thread(){
@Override
public void run() {
super.run();
for (;num<=100;){
if (!flag && (num == 0 || ++num %2 == 0)){//打印偶数
System.out.println(num);
flag = true;
}
}
}
};

Thread t2 = new Thread(){
@Override
public void run() {
super.run();
for (;num<100;){
if (flag && ++num %2 != 0){//打印奇数
System.out.println(num);
flag = false;
}
}
}
};

t1.start();
t2.start();
}
}

上述代码中 num 和flag 都必须为volatile ,根据短路机制,if 语句中基本上只需要管 flag的值,所以即使 ++num 这个操作是非原子性的,也无妨。

以上内容参考莫那.鲁道的博客

如何控制某个方法允许并发访问线程的个数?

6、Java创建线程的三种方式

点击看答案
  • 继承Thread类。优点:使用简单 缺点:已经继承Thread 类,不能继承其它类。
  • 实现runnable 接口,创建线程类。
  • 通过Callable 和 Future 创建线程。

以上内容参考自:Java创建线程的三种方式

7、synchronized原理

点击看答案

JVM基于进入和退出Monitor对象来实现 代码块同步 和 方法同步 ,两者实现细节不同。

代码块同步: 在编译后通过将monitorenter指令插入到同步代码块的开始处,将monitorexit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个monitor与之关联,线程执行monitorenter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁。

方法同步: synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。

两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。

对象、监视器、同步队列和执行线程间的关系

以上内容参考自:synchronized原理

8、线程间通信?

点击看答案

1、通过内存共享,共享内存中的信息是公共可见的,但是需要显式地进行同步,可以使用Synchronize 和 Lock 来进行同步。

2、notify/wait 方法

3、Condition 实现等待/通知

以上内容部分参考自: 线程通信

9、单例中,为什么要使用volatile 修饰instance?

点击看答案

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

10、有三个线程T1,T2,T3,怎么确保它们按顺序执行?

点击看答案

参考2个线程的即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class MainClass1 {

static volatile int num = 0;
static volatile int flag = 1;

public static void main(String[] args) {

Thread t1 = new Thread(){
@Override
public void run() {
super.run();
for (;num<=100;){
if (flag == 1){
++num;
System.out.println("t1-"+ num);
flag = 2;
}
}
}
};

Thread t2 = new Thread(){
@Override
public void run() {
super.run();
for (;num<100;){
if (flag == 2){
++num;
System.out.println("t2-"+ num);
flag = 3;
}
}
}
};

Thread t3 = new Thread(){
@Override
public void run() {
super.run();
for (;num<100;){
if (flag == 3){
++num;
System.out.println("t3-"+ num);

flag = 1;
}
}
}
};

t1.start();
t2.start();
t3.start();
}
}

11、Java停止一个正在运行的线程

点击看答案
  • 采用经过volatile 标记的变量来控制
  • Thread.stop() ,但是不推荐
  • 使用FutureTash 时,可以使用其 cancel() 方法来取消任务
  • 使用return 关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Thread(){
@Override
public void run() {
super.run();

System.out.println("开始执行");
if (true){
return;
}

System.out.println("return 之后");
}
}.start();

}

代码只会打印 “开始执行” ,代码中if是需要的,不然会提示编译不通过。

以上部分内容参考自:Java如何停止线程

12、Java线程池工作原理

点击看答案

线程池好处

  • 复用已经创建的线程,降低线程创建和销毁带来的损耗
  • 提高响应速度。当任务到达时,可以复用已有线程,无需等到线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制创建,不仅消耗系统资源,延长线程等待时间,还可能降低系统稳定性。

线程的几种状态

  1. 新建状态(New):新建一个线程对象
  2. 就绪状态(Runnable):调用线程的 start() 方法,线程变成可运行状态,其他资源都已经获取,只等cpu了
  3. 运行状态(Running):就绪状态的线程获取CPU,执行
  4. 阻塞状态(Blocked):由于某种原因放弃cpu使用权。可能是调用了线程的 wait() 方法、或者等待获取同步锁、或者是执行了sleep()或者其他线程执行了 join()
  5. 死亡状态(Dead):线程执行完了或者因为一场退出run()

线程生命周期

以上代码参考自:java线程池

13、死锁条件

点击看答案

产生死锁的四个条件(满足才会死锁):

  1. 互斥使用
  2. 不可剥夺
  3. 请求保持
  4. 循环等待

破坏任意一个条件即可解锁

14、引申-写个简单的死锁程序

点击看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();

Thread thread1 = new Thread("thread1"){
@Override
public void run() {
super.run();
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "进入第1层锁");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "进入第2层锁");
}
}
}
};


Thread thread2 = new Thread("thread1"){
@Override
public void run() {
super.run();
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "进入第1层锁");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "进入第2层锁");
}
}
}
};

thread1.start();
thread2.start();
}

15、synchronized 关键字

点击看答案

当执行如下代码:

1
2
3
4
5
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}

反编译之后能看到 monitorenter 和 monitorexit 字样,关于这两条指令,jvm中的解释如下:

每个对象都有一个监视器锁(monitor),当monitor 被占用时就处于锁定状态,线程执行 monitorenter 指令时,就尝试获取 monitor的所有权,过程如下:

1、如果 monitor 的进入数为 0 ,则该线程进入 monitor,然后将进入数设置为 1,即该现成为 monitor 的所有者。

2、如果有线程已经占有 monitor ,之后又重新进入,则 monitor 的进入数加 1。

3、如果其他线程已经占用了 monitor ,则当前线程进入阻塞状态,直到 monitor 的进入数为 0(之后被唤醒),再重新获取 monitor 的所有权。

反之,monitorexit 则是线程退出的一个过程。

所以我们知道,修饰代码块时,synchronized 底层是通过 monitor 对象来完成的,其实,wait/notify方法也是依赖于 monitor 对象的,这也是为什么只有再同步块中才能调用 wait/notify 等方法(因为wait 等待的是啥?其实等待的就是对象的 monitor,由于所有类都是object ,里面内置有一个 monitor,因此自然所有类都应该有 wait/notify 方法)。

在修饰成员方法时,如下代码所示:

1
2
3
public synchronized void method() {
System.out.println("Hello World!");
}

反编译过后就会有 ACC_SYNCHRONIZED 标识符(没有用 monitorenter 和 monitorexit,其实理论上也是可以的),jvm 根据这个标识符来实现方法同步:当调用方法时,首先检查方法的 ACC_SYNCHRONIZED 标志是否被设置,如果设置了,则线程首先获取 monitor,获取成功后才能执行方法,方法执行完成后再释放 monitor,方法执行期间,其他对象无法再获得同一个 monitor。所以与修饰 代码块 的时候本质上是没有区别的。

最后,如果修饰的是静态方法,则锁定的是 class 对象。

以上内容参考自:Synchronized及其实现原理

16、synchronized 锁持有的object 是Thread 对象

点击看答案

看过上面synchronized原理,我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Thread threadTest = new Thread() {
public void run() {
System.out.println("执行线程中方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

threadTest.start();

synchronized (threadTest) {//这里不明白可以想象单例的 synchronized 用法
try {
threadTest.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println("执行到了这里");
}

我们知道,wait 必须在同步块中执行,上述代码也是这样的;同时我们也知道,wait() 方法后,必须收到 notify/notifyAll 之后才能结束等待状态。但是上述代码首先输出 “执行线程中方法” ,之后输出 “执行到了这里” ,明明没有notify ,怎么就能结束 wait 状态呢?其实这是因为synchronized 获得对象锁是 Thread 对象的锁时,当该线程执行完成后,会调用线程自身的 notifyAll() 方法,通知所有等待在该线程对象上的线程。所以,用这种方式能够实现 join 的功能,使得线程依次执行。

以上内容参考自csdn上的博客

17、引申-理解Java 中的 join

点击看答案

如果要用join 方法实现上述功能,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Thread threadTest = new Thread() {
public void run() {
System.out.println("执行线程中方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

threadTest.start();

try {
threadTest.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行到了这里");
}

join 方法从字面上理解就是新线程加入进来,等新线程执行完后,老的线程才继续执行。我们日常使用 join 方法是通过 join(0) 来实现的,我们看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {             //如果时执行的join(0)
while (isAlive()) { //如果线程是运行状态,就会执行下面的等待
wait(0);
}
} else {                       //如果是执行的join(time)
while (isAlive()) { //如果线程时运行状态
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);                 //等待delay时间后自动返回继续执行
now = System.currentTimeMillis() - base;
}
}
}

其中 while (isAlive()) { wait(0) } ,我们日常调用的 wait() 方法就是调用的 wait(0) 实现,因此这里本质还是 执行了 wait 方法,就是让其一直等待。所以我们上述代码: threadTest.join() 本质是利用 threadTest 对象作为对象锁,当线程终止时,会调用线程自身的 notifyAll ,因此这个 wait 就解除了。

再说下 while (isAlive()),这是在判断线程是否线程是否已经执行了start() 方法,因此如果还没有start() 则不会执行wait() 方法。可以用如下代码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});

t2.start();//先启动t2

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

t1.start();//延迟2秒后启动t1
}

这段代码看起来执行了 t1.join() ,应该要等t1执行完了才会执行t2,但是呢,这里最终输出: t2 t1 ,即 t2 还是会先执行。这是因为在执行 t1.join() 的时候,t1并没有执行 start() ,isAlive() 为false,因此不生效。

以上内容参考自csdn上的博客

18、wait/sleep/yield/join 方法

点击看答案

wait() 方法的作用是将当前线程挂起,让其进入阻塞状态,直到 notify/notifyAll 方法来唤醒线程,很容易理解,wait 是释放了锁资源的。

wait(long timeout)方法与 wait 相似,区别在于如果再指定时间内没有被notify/notifyAll ,则自动唤醒。

sleep方法只是暂时让出cpu执行权,并不释放锁,而 wait 是释放锁的。

yield 方法只是暂停当前线程,以便其他线程有机会运行,不过不能指定暂停时间,也不能保证当前线程立即停止。yield 方法只是将thread 的状态由 Running 转变为 Runable 状态。不过调度器可能会忽略这个方法,并且Java官方建议只用这个方法用于调试和测试。

join 方法是父线程等待子线程执行完之后再执行。

以上内容参考自cnblogs的博客

19、实现线程安全的方法(三种)

点击看答案
  • 互斥同步

指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(再使用信号量的时候可以是一些)线程使用,Java中典型的互斥同步手段是 synchronized 关键字,以及 ReentrantLock 类。

  • 非阻塞同步

互斥同步主要的问题就是线程阻塞和唤醒带来的性能问题,因此也称为阻塞同步。非阻塞同步就是先进行操作,如果没有其它线程争用共享数据,操作就成功了,否则产生冲突再补偿(比如自旋不断重试,直到成功为止),这种乐观方式无需把线程挂起和唤醒,CAS 的出现也为非阻塞同步提供了条件。

  • 无同步方案

如果方法本来就没有涉及共享数据,每次输入相同的数据就能得到相同的输出,这种情况当然无需进行同步。

20、死锁

点击看答案

产生死锁的四个条件(满足才会死锁):

  1. 互斥使用
  2. 不可剥夺
  3. 请求保持
  4. 循环等待

破坏任意一个条件即可解锁

21、引申-写个简单的死锁程序

点击看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();

Thread thread1 = new Thread("thread1"){
@Override
public void run() {
super.run();
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "进入第1层锁");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "进入第2层锁");
}
}
}
};


Thread thread2 = new Thread("thread1"){
@Override
public void run() {
super.run();
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "进入第1层锁");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "进入第2层锁");
}
}
}
};

thread1.start();
thread2.start();
}

22、synchronized 关键字

点击看答案

当执行如下代码:

1
2
3
4
5
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}

反编译之后能看到 monitorenter 和 monitorexit 字样,关于这两条指令,jvm中的解释如下:

每个对象都有一个监视器锁(monitor),当monitor 被占用时就处于锁定状态,线程执行 monitorenter 指令时,就尝试获取 monitor的所有权,过程如下:

1、如果 monitor 的进入数为 0 ,则该线程进入 monitor,然后将进入数设置为 1,即该现成为 monitor 的所有者。

2、如果有线程已经占有 monitor ,之后又重新进入,则 monitor 的进入数加 1。

3、如果其他线程已经占用了 monitor ,则当前线程进入阻塞状态,直到 monitor 的进入数为 0(之后被唤醒),再重新获取 monitor 的所有权。

反之,monitorexit 则是线程退出的一个过程。

所以我们知道,修饰代码块时,synchronized 底层是通过 monitor 对象来完成的,其实,wait/notify方法也是依赖于 monitor 对象的,这也是为什么只有再同步块中才能调用 wait/notify 等方法(因为wait 等待的是啥?其实等待的就是对象的 monitor,由于所有类都是object ,里面内置有一个 monitor,因此自然所有类都应该有 wait/notify 方法)。

在修饰成员方法时,如下代码所示:

1
2
3
public synchronized void method() {
System.out.println("Hello World!");
}

反编译过后就会有 ACC_SYNCHRONIZED 标识符(没有用 monitorenter 和 monitorexit,其实理论上也是可以的),jvm 根据这个标识符来实现方法同步:当调用方法时,首先检查方法的 ACC_SYNCHRONIZED 标志是否被设置,如果设置了,则线程首先获取 monitor,获取成功后才能执行方法,方法执行完成后再释放 monitor,方法执行期间,其他对象无法再获得同一个 monitor。所以与修饰 代码块 的时候本质上是没有区别的。

最后,如果修饰的是静态方法,则锁定的是 class 对象。

以上内容参考自:Synchronized及其实现原理

23、synchronized 锁持有的object 是Thread 对象

点击看答案

看过上面synchronized原理,我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Thread threadTest = new Thread() {
public void run() {
System.out.println("执行线程中方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

threadTest.start();

synchronized (threadTest) {//这里不明白可以想象单例的 synchronized 用法
try {
threadTest.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println("执行到了这里");
}

我们知道,wait 必须在同步块中执行,上述代码也是这样的;同时我们也知道,wait() 方法后,必须收到 notify/notifyAll 之后才能结束等待状态。但是上述代码首先输出 “执行线程中方法” ,之后输出 “执行到了这里” ,明明没有notify ,怎么就能结束 wait 状态呢?其实这是因为synchronized 获得对象锁是 Thread 对象的锁时,当该线程执行完成后,会调用线程自身的 notifyAll() 方法,通知所有等待在该线程对象上的线程。所以,用这种方式能够实现 join 的功能,使得线程依次执行。

以上内容参考自csdn上的博客

24、引申-理解Java 中的 join

点击看答案

如果要用join 方法实现上述功能,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Thread threadTest = new Thread() {
public void run() {
System.out.println("执行线程中方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

threadTest.start();

try {
threadTest.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行到了这里");
}

join 方法从字面上理解就是新线程加入进来,等新线程执行完后,老的线程才继续执行。我们日常使用 join 方法是通过 join(0) 来实现的,我们看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {             //如果时执行的join(0)
while (isAlive()) { //如果线程是运行状态,就会执行下面的等待
wait(0);
}
} else {                       //如果是执行的join(time)
while (isAlive()) { //如果线程时运行状态
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);                 //等待delay时间后自动返回继续执行
now = System.currentTimeMillis() - base;
}
}
}

其中 while (isAlive()) { wait(0) } ,我们日常调用的 wait() 方法就是调用的 wait(0) 实现,因此这里本质还是 执行了 wait 方法,就是让其一直等待。所以我们上述代码: threadTest.join() 本质是利用 threadTest 对象作为对象锁,当线程终止时,会调用线程自身的 notifyAll ,因此这个 wait 就解除了。

再说下 while (isAlive()),这是在判断线程是否线程是否已经执行了start() 方法,因此如果还没有start() 则不会执行wait() 方法。可以用如下代码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});

t2.start();//先启动t2

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

t1.start();//延迟2秒后启动t1
}

这段代码看起来执行了 t1.join() ,应该要等t1执行完了才会执行t2,但是呢,这里最终输出: t2 t1 ,即 t2 还是会先执行。这是因为在执行 t1.join() 的时候,t1并没有执行 start() ,isAlive() 为false,因此不生效。

以上内容参考自csdn上的博客

25、wait/sleep/yield/join 方法

点击看答案

wait() 方法的作用是将当前线程挂起,让其进入阻塞状态,直到 notify/notifyAll 方法来唤醒线程,很容易理解,wait 是释放了锁资源的。

wait(long timeout)方法与 wait 相似,区别在于如果再指定时间内没有被notify/notifyAll ,则自动唤醒。

sleep方法只是暂时让出cpu执行权,并不释放锁,而 wait 是释放锁的。

yield 方法只是暂停当前线程,以便其他线程有机会运行,不过不能指定暂停时间,也不能保证当前线程立即停止。yield 方法只是将thread 的状态由 Running 转变为 Runable 状态。不过调度器可能会忽略这个方法,并且Java官方建议只用这个方法用于调试和测试。

join 方法是父线程等待子线程执行完之后再执行。

以上内容参考自cnblogs的博客

26、Java 中锁的种类

点击看答案

大概可以分类为:

  • 乐观锁、悲观锁
  • 独享锁、共享锁
  • 公平锁、非公平锁
  • 互斥锁、读写锁
  • 可重入锁
  • 分段锁

其中乐观锁认为线程基本上是没有竞争的,一般采用CAS机制实现,悲观锁认为一个线程获取数据时,一定会有其他数据对数据进行更改,它的实现就是加锁,比如 synchronize 关键字

至于独享锁:一次只能被一个线程拥有,ReentrantLock 这个可重入锁也是独享锁。而共享锁可以被多个线程持有,比如:ReentrantReadWrite 中的读锁ReadLock 是共享锁,写锁WriteLock 是独享锁。

互斥锁的具体实现就是 synchronized、ReentrantLock 等实现;读写锁具体实现就是 ReadWriteLock。

可重入锁:对于同一个线程,在外层方法获取锁的时候,在进入内层方法也会自动获取锁。

公平锁:多线程竞争时排队,按照申请顺序获取锁;非公平锁:竞争时,先尝试插队,失败再排队。

分段锁:并不是一种锁,只是细化锁的粒度。

以上内容参考自知乎上的内容

27、Java 中锁优化的方式

点击看答案
  • 自旋锁与自适应自旋(主要解决互斥同步挂起和恢复需要转入内核态的问题。引入的自适应自旋,意味着自旋时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定的,因为如果在同一个锁对象上,自旋等待刚获得过锁,并且持有锁的线程正在运行,那么虚拟机认为当前自旋也很有可能再次成功,进而允许自旋等待更长时间)
  • 锁消除(虚拟机对于检测到不可能存在共享的数据进行锁消除)
  • 锁消除(一般来说,加锁同步块区域尽量小,只锁真正共享的区域,以便在锁竞争的时候,能尽快释放。但是如果一系列的连续操作对同一个对象频繁反复加锁解锁,甚至是出现在循环体中,那就会导致不必要的性能损耗,典型的就是在 循环中执行StringBuilder 的append操作)
  • 轻量级锁(如果同步对象没有被锁定,虚拟机首先将将在当前线程的栈帧中建立一个名为锁记录-Lock Record 的空间,用于存储锁对象目前的 Mark Word的拷贝,然后虚拟机使用CAS操作尝试把对象的Mark Word 更新为指向 Lock Record 的指针,如果更新成功了,则代表线程拥有了这个对象的锁,对象Mark Word的锁标志也会标记为 “00”,即轻量级锁状态。如果这个cas操作失败,则意味着至少存在一条线程竞争,此时检查Mark Word 是否指向当前线程的栈帧,如果是,则说明已经拥,有了,直接进入同步块即可。否则,如果有两条以上线程争用一个锁,则必须膨胀微重量级锁。轻量级锁的依据是,“对于绝大部分锁,在整个同步周期内是不存在竞争的”,如果没有竞争,则轻量级锁通过CAS操作避免了使用互斥量的开销;当然,如果存在竞争,除了互斥量本身之外,还发生了CAS开销,反而更慢了)
  • 偏向锁(它的目的是在无竞争的情况下消除CAS操作,进一步提升性能。当锁对象第一次被线程获取的时候,就将对象头中的偏向模式置为 “1”,即偏向模式,同时使用CAS操作把获取这个锁的线程的ID记录在Mark word 中,如果操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块都不需要进行同步操作,不过,一旦出现另外一个线程尝试去获取这个锁的情况,偏向模式马上结束。根据锁对象目前是否处于锁定状态来决定是否撤销偏向、恢复到未锁定或者轻量级锁定状态)

以上内容参考自《深入理解Java虚拟机-第三版》

谢谢你的鼓励