理解线程

cpu线程和操作系统线程有什么区别?

两者都叫Thread是因为他们都是调度的基本单位,软件操作系统调度的基本单位是OS的Thread,硬件的调度基本单位是CPU中的Thread。我们常见的几核几线程,说的就是CPU中的Thread。操作系统中的Thread有自己的栈空间,和同一进程中的其他线程共享地址空间。

进程和线程有什么区别?

进程之间不共享资源,而线程之间共享资源。

那为什么这么设计呢?

因为每个进程内部是自己完整的程序逻辑,不同的程序之间就不应该共享资源(例如变量、常量等);而同一个进程的多个不同线程,需要都能操作到这个进程的资源,程序才能正常运行。

一个线程调用两次start方法会出现什么?

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start() 被认为是编程错误。在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。

实现一个 Runnable去构建线程有什么好处?

Runnable 的好处是,不会受 Java 不支持类多继承的限制,重用代码实现,当我们需要重复执行相应逻辑时优点明显。而且,也能更好的与现代 Java 并发库中的 Executor 之类框架结合使用。

多线程安全

多线程安全的本质是什么?

线程安全的本质问题是多个线程去操作共同资源的问题。锁机制是为了解决在多线程下,对共享资源的操作进行控制。所以锁机制的本质还是对资源,而非某个方法或者代码块。

synchnorized 是怎么实现多线程安全的?

线程同步存在监视器Monitor,Monitor 对象是同步的基本实现单元

synchnorized 同步方法的Monitorthis,所有被同一个synchnorized标记的同步方法,拥有同一个Monitor,它们同时都不能被同一个线程访问。如果需要多个方法允许同时被多个线程访问,那就需要用synchnorized包代码块,并准备不同的对象,这些不同的对象标记就表示不同的Monitor

Object monitor = new Object()
synchnorized(monitor){
  //...
}

synchnorized和ReentrantLock有什么区别?

synchnorized是在Java5之前的同步机制,它可以锁方法,也可以锁住代码快。当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

也正是因为synchnorized实现同步,写起来很简单,一个关键字就够了,与此同时它也失去了一定的灵活性。

ReentrantLockJava5之后的出现解决了问题,ReentrantLock,通常翻译为再入锁。

再入锁通过代码直接调用lock()方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制fairness,也就是公平性,或者利用定义条件等。

所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。但保证公平性则会引入额外开销,自然会导致一定的吞吐量下降。

ReentrantLock 相比 synchronized,因为可以像普通对象一样使用,所以可以利用其提供的各种便利方法,进行精细的同步操作,甚至是实现 synchronized 难以表达的用例,比如,可以判断是否有线程,或者某个特定线程,在排队等待获取锁、又或者可以响应中断请求。

那synchnorized是最慢的吗?

关于二者的性能,synchronizedReentrantLock的性能不能一概而论,早期版本synchronized在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock

stop()与interrupte()

什么 Thread的stop() 是危险的,该方法被标记为过时?

因为它会直接杀死线程,有几率让线程在某些工作运行到一半的时候被结束,从而系统被强制停留在某种中间状态,导致出现问题。

而 Thread.interrupt() 并不是直接杀死线程,而是告诉线程「外界希望你停止」,具体的结束工作由线程自己来完成,所以会更安全。

interrupte方法只是会加个标记,表示有地方中断了该线程,该线程的程序还是会正常执行。所以需要开发者去判断这个中断标记位,在判断出中断的时候做结束收尾工作。那如何判断呢,可通过Thread.isInterrupte这个静态方法或者isInterrupte方法?

  • Thread.isInterrupte这个静态方法会改变它自身的值(true -> false),一般情况下就只能用一次
  • isInterrupte方法不会改变值,可以多次使用。

说说对InterruptedException的理解

如果在sleep过程的时候,线程被中断了,就会抛出InterruptedException,让你捕获。这也是为了节省资源,线程都中断了,sleep()是无意义的,一般情况需要去做线程终止后的收尾工作。

Thread thread = new Thread(new Runnable() {
       @Override
       public void run() {
           try {
               Thread.sleep(2000);
           } catch (InterruptedException e) {
               e.printStackTrace();
               //... TODO收尾工作
           }
       }
   });

线程间通信

考虑一个场景: 有两个线程去操作一个共享资源,假设这个共享资源是一个字符串,其中线程1要去写这个字符串,它是个耗时任务,线程2要去读这个字符串,怎么保证线程2读到的是线程1写入后的值?

方法1: 使用join()方法

void runTest() {
     System.out.println("run");
     //开启一个线程去设置共享的值,假设2s后才能完成
     Thread thread1 = new Thread(new Runnable() {
         @Override
         public void run() {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
                 //... TODO收尾工作
             }
             setShareString("i am set value thread");
         }
     });
     thread1.start();
     //开启一个线程去打印共享的值,假设1s就可以读值
     Thread thread2 = new Thread(new Runnable() {
         @Override
         public void run() {
             try {
                 thread1.join();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             printShareString();
         }
     });
     thread2.start();
 }

方法2: 使用wait()和notify()/notifyAll()

在打印同步方法里面,打印前调用wait(),进入等待状态

在设置共享值同步方法里,设置完成后通知所有( notifyAll)正在等待的线程:你可以继续执行下去了。

private synchronized void printShareString() {
    try {
        wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("print: "+ mShareString);
}

private synchronized void setShareString(String s) {
    mShareString = s;
    notifyAll();
}

wait()notify()/notifyAll() 一定是成对出现的

Java的线程池

Java 并发类库提供的线程池有哪几种? 分别有什么特点?

1.newCachedThreadPool

它是一种用来处理大量短时间工作任务的线程池。它的特点是:

它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;

如果线程闲置的时间超过一定的时间(可以根据需要设置),则被终止并移出缓存;所以即使长时间闲置,这种线程池也不会消耗什么资源。

2.newFixedThreadPool(int nThreads)

fixed 翻译过来:固定

它可以重用指定数目(nThreads)的线程,任何时候最多有nThreads 个工作线程是活动的。

如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。

3.newSingleThreadExecutor

它的特点在于工作线程数目被限制为 1

所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。

4. newSingleThreadScheduledExecutor

创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度。

5.newWorkStealingPool(int parallelism)

这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

Android多线程

为什么 UI 线程是一个死循环但却没有把界面卡死

因为 UI 线程是一个大循环,每一圈都是一次界面刷新操作,而不是对某一次界面刷新过程进行内部的死循环,所以不会卡死界面

使用 AsyncTask 会导致内存泄露的根本原因是什么?

你可能在很多地方看到这个问题的解释:因为它是内部类,持有了外部类的引用,所以当外部类的内存资源在需要被释放时,不能被GC回收。

仔细想想,这个解释是很奇怪的。按这种解释的话,不仅仅是 AsyncTask,只有是内部类,都会出现这个问题。

其实 AsyncTask 的内存泄露可以说不是个问题,之所以会出现内存泄露的根本原因是, AsyncTask 的后台线程在做任务时,如果这个时候去释放外部类资源,由于被 AsyncTask 持有,就会导致它不能被释放。所以本质上是因为存在线程去引用它,这个时候GC是不会认为它需要被回收。但当 AsyncTask 的后台任务做完,线程就会挂掉,之后,GC就会去回收。这个场景,和new一个匿名内部类Thread对象的是一样的。

这个后台任务一般是耗时很短的(如果耗时过长,要考虑的是 AsyncTask是否真的合适你的场景 ?),所以 AsyncTask 的内存泄露在某种程度上说并不需要用很多网文说的弱引用等方法去特意解决。简而言之,它是会出现内存泄漏,但时间非常短,没有必要一定去解决它。

进阶Q&A

1.自旋锁的出现是为了解决什么问题?

因为互斥锁,在线程的睡眠和唤醒都是复杂而昂贵的操作,需要大量的CPU指令。如果互斥仅仅被锁住是一小段时间, 用来进行线程休眠和唤醒的操作时间比睡眠时间还长,很浪费性能。

自旋锁的出现可以解决这一场景的性能问题:

自旋锁采用让当前线程不停循环体内执行实现,当循环条件被其他线程改变时,才能进入临界区。由于自旋锁只是将当前线程不停执行循环体,不进行线程状态的改变,所以响应会更快。但当线程不停增加时,性能下降明显。

在单核CPU上,自旋锁是无用,因为当自旋锁尝试获取锁不成功会一直尝试,这会一直占用CPU,其他线程不可能运行,同时由于其他线程无法运行,所以当前线程无法释放锁。

2.什么是锁的升级和降级

锁的升级和降级是JVM优化synchronized运行的机制,当JVM检测到不同的竞争程度,就会切到合适的锁实现。主要有三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。

CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。

最后修改:2021 年 01 月 18 日 03 : 20 PM
如果觉得我的文章对你有用,请随意赞赏