0
点赞
收藏
分享

微信扫一扫

并发编程之多线程基础知识


概念整理

在学习多线程之前,我们需要知道什么是线程,当然也少不了需要知道什么是进程。这里仅仅是放两句比较简单的话,有个印象就行,毕竟线程和进程的概念网上一大堆,记大多其实也没啥用。

但是呢,面试的时候又喜欢问一些概念性的东西,所以这里还是需要把比较重要的概念记录一下。

进程和线程

  • 进程:运行中的程序,是资源分配的最小单位。进程相当于是一个容器,里面可以包含许多线程。
  • 线程:是CPU调度的最小单位,线程相当于轻量级的进程。

知道了进程和线程的概念之后,我们在看一下什么是并发,什么是并行。

并行和并发

  • 并行:指在同一时刻,多个任务同时执行。
  • 并发:指在同一时间段内,多个任务同时执行。

至于我们说多线程就是并发,那我们来看一下并发中的三个问题:

原子性 、可见性 和 有序性

  • 原子性:是指一个操作是不可中断的,即使是在多个线程一起执行的时候,一个操作一旦开始,就不能被其他程序干扰。(要么不做,要么做完)
  • 可见性:是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个值被修改了。(多线程情况,①线程可能将变量缓存,② 指令重排)
  • 有序性: 在并发情况下,程序的执行可能就会出现乱序(给人的感觉就是:写在前面的代码会在后面执行。有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令和原指令的顺序未必是一致的。)

临界区

用来表示一种公共资源或是共享数据,可以被多个线程使用。同一时间只能有一个线程访问临界区(阻塞状态),其他资源必须等待。

死锁,活锁 和 饥饿

  • 死锁:都等待对方的资源让给自己,导致没有任何一个线程可以获取到所需的所有资源。(哲学家就餐问题)
  • 活锁:都将自己所持有的资源谦让给对方,导致没有任何一个线程可以获取到所需要的所有资源。(面对面让路)
  • 饥饿:是由于优先级的问题,总是先执行优先级(priority)比较高的,导致优先级低的线程一直得不到执行,处于饥饿状态。饥饿状态可以使用队列来解决。

Thread 类的run方法和start方法的区别:

  • 使用Thread对象去调用run方法,相当于普通方法的调用,不会开启一个新的线程去执行run方法,它只会在当前线程中串行执行run方法中的代码。
  • 如果需要在子线程中执行run方法的业务逻辑,则需要使用start方法来启动一个新的线程。start() 方法底层调用 start0() 方法,而start0()方法是一个native方法,底层实现会去调用run方法。

wait方法和notify方法为什么要设计到Object类中

首先,在解释这个问题之前,我们需要知道wait方法和notify方法的用法。

我们都知道,wait和notify方法都需要在synchronized关键字包裹的代码中执行才可以。那在此之前我们看一下synchronized关键字的用法:

// 第一种
public class App{
public synchronized void method1(){

}
}
// 第二种
public class App{
public static synchronized void mothod2(){}
}
// 第三种
public class App{
public void method3(){
synchronized(this){}
}
}
// 第四种
public class App{
public void method4(){
synchronized(App.class){}
}
}

我们需要明确,synchronized关键字锁的是什么?锁的是对象,不管是以上哪种方式出现,锁的都是对象。

  • 第一种和第三种。锁的是当前对象,即当前调用的对象
  • 第二中和第四种。锁的是当前的类对象。即类名.class.

当然,除了上述四种使用,我们可以使用任何对象当锁。比如​​Object o = new Object()​​ 是用o当锁对象。即(monitor).

如下:

public class App{
private final Object o = new Object();
public void method(){
synchronized(o){}
}
}

到这里,synchronized 内置锁的用法就差不多了,我们看一下wait方法和notify方法。为什么在说wait方法和notify方法之前需要介绍synchronized关键字的用法呢,是因为wait方法和notify方法是需要在synchronized关键字作用的代码中使用的。如下:

public synchronized void method(){
try {
this.wait();
System.out.println("running.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WaitAndNotifyApp andNotifyApp = new WaitAndNotifyApp();
new Thread(){
@Override
public void run() {
andNotifyApp.method();
}
}.start();

/** 等待 new Thread 执行*/
Thread.sleep(1000);

synchronized (andNotifyApp){
andNotifyApp.notify();
}
System.out.println("running here.");
}

通过上面代码可以看到,wait方法和notify方法都是作用在synchronized关键字的代码中。我们使用哪个对象去做锁对象(monitor), 就需要使用哪个对象去调用wait方法,否则会抛出​​IllegalMonitorStateException​​ 非法monitor状态异常,其实就是monitor和调用的方法不一致。

同样,如果是static方法,则需要使用类对象来wait和notify:

public static synchronized void method() {
try {
WaitAndNotifyApp.class.wait();
System.out.println("run.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {

new Thread(){
@Override
public void run() {
WaitAndNotifyApp.method();
}
}.start();
/** 等待 new Thread 执行*/
Thread.sleep(1000);

synchronized (WaitAndNotifyApp.class) {
WaitAndNotifyApp.class.notify();
}
System.out.println("running here.");
}

总结:

  • wait方法和notify方法都需要放在synchronized关键字作用的代码中。
  • wait方法和notify方法的调用者都需要与锁对象monitor 一致。
  • 想要唤醒某个monitor下wait的线程,就需要使用对应的monitor去调用notify方法。

到这里我们就可以回答这个问题了:

Q: 为什么wait方法和notify方法会设计到Object类中。

A: 因为锁对象monitor可以是任何对象,而wait方法和notify方法 又只能被monitor对象调用。所以wait方法和notify方法一定要设计到Object类中。

wait方法和sleep方法的不同点

  • sleep 是Thread类的方法,而wait方法是Object类的方法
  • wait方法只能在synchronized关键字作用的代码中使用,即只能同步代码块或同步方法中使用,而sleep可以在任何地方调用。
  • 使用wait方法,线程在等待的时候会释放它占用的资源和锁。而sleep只会释放资源,不会释放锁。
  • wait方法如果没有指定时间,则会一致等待,直至被唤醒,而sleep睡眠时间到了就会自动继续执行。


举报

相关推荐

0 条评论