JAVA线程与多线程

去安卓面试的时候通常会问一些java问题,所以呢你可能觉得答问题时答案很蛋疼,今天来介绍一下线程。

先看几个概念:

线程:进程中负责程序执行的执行单元。一个进程中至少有一个线程。

多线程:解决多任务同时执行的需求,合理使用CPU资源。多线程的运行是根据CPU切换完成,如何切换由CPU决定,因此多线程运行具有不确定性。

● 线程

java中的线程

使用java.lang.Thread类或者java.lang.Runnable接口编写代码来定义、实例化和启动新线程。

一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。

Java中,每个线程都有一个调用栈,即使不在程序中创建任何新的线程,线程也在后台运行着。

一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。

一旦创建一个新的线程,就产生一个新的调用栈。

线程总体分两类:用户线程和守候线程。

当所有用户线程执行完毕的时候,JVM自动关闭。但是守候线程却不独立于JVM,守候线程一般是由操作系统或者用户自己创建的

创建线程的两种方式:

一、继承Thread类,扩展线程。

class MyThread extends Thread {

    @Override
    public void run() {
        super.run();
        // Perform time-consuming operation...
    }
}

MyThread t = new MyThread();
t.start();
  • 继承Thread类,覆盖run()方法。
  • 创建线程对象并用start()方法启动线程。

面试题

  • 1)线程和进程有什么区别?

一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

  • 2)如何在Java中实现线程?

创建线程有两种方式:
一、继承 Thread 类,扩展线程。
二、实现 Runnable 接口。

  • 3)Thread 类中的 start() 和 run() 方法有什么区别?

调用 start() 方法才会启动新线程;如果直接调用 Thread 的 run() 方法,它的行为就会和普通的方法一样;为了在新的线程中执行我们的代码,必须使用 Thread.start() 方法。

扩展

Android 系统接口 HandlerThread 继承了 Thread,它是一个可以使用 Handler 的 Thread,一个具有消息循环的线程。run()方法中通过 Looper.prepare() 来创建消息队列,通过 Looper.loop() 来开启消息循环。可以在 run() 方法中执行耗时的任务,而 HandlerThread 内部创建了消息队列外界需要通过 Handler 的方式来通知 HandlerThread 执行一个具体任务;HandlerThread 的 run() 方法是一个无限的循环,可以通过它的 quite() 或 quitSafely() 方法来终止线程的执行;

二、实现Runnable接口。

public class DemoActivity extends BaseActivity implements Runnable {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Thread t = new Thread(this);
        t.start();
    }

    @Override
    public void run() {

    }
}

面试题

  • 1)用 Runnable 还是 Thread ?

我们都知道可以通过继承 Thread 类或者调用 Runnable 接口来实现线程,问题是,创建线程哪种方式更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口更好了。

  • 2)Runnable 和 Callable 有什么不同?

Runnable 和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。

⚠注意:这面第二个面试题主要是为了引出下面的扩展,原谅我这样为难人的出场。

扩展

先看一下 Runnable 和 Callable 的源码

public interface Runnable {
    public void run();
}

public interface Callable<V> {
    V call() throws Exception;
}

可以得出:

1)Callable 接口下的方法是 call(),Runnable 接口的方法是 run()。
2)Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。
3)call() 方法可以抛出异常,run()方法不可以的。
4)运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

但是,但是,凡事都有但是嘛…

单独使用 Callable,无法在新线程中(new Thread(Runnable r))使用,Thread 类只支持 Runnable。不过 Callable 可以使用 ExecutorService (又抛出一个概念,这个概念将在下篇的线程池中说明)。

上面又提到了 Future,看一下 Future 接口:

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future 定义了5个方法:

1)boolean cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel() 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。此方法返回后,对 isDone() 的后续调用将始终返回 true。如果此方法返回 true,则对 isCancelled() 的后续调用将始终返回 true。
2)boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。
3)boolean isDone():如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
4)V get()throws InterruptedException,ExecutionException:如有必要,等待计算完成,然后获取其结果。
5)V get(long timeout,TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException: 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。

看来 Future 接口也不能用在线程中,那怎么用,谁实现了 Future 接口呢?答:FutureTask。

public class FutureTask<V> implements RunnableFuture<V> {
    ...
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

FutureTask 实现了 Runnable 和 Future,所以兼顾两者优点,既可以在 Thread 中使用,又可以在 ExecutorService 中使用。

看完上面啰哩啰嗦的介绍后,下面我们具体看一下具体使用的示例:

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "Callable 的使用";
    }
};

FutureTask<String> task = new FutureTask<String>(callable);

Thread t = new Thread(task);
t.start(); // 启动线程
task.cancel(true); // 取消线程

使用 FutureTask 的好处是 FutureTask 是为了弥补 Thread 的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。FutureTask 是一种可以取消的异步的计算任务,它的计算是通过 Callable 实现的,它等价于可以携带结果的 Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。

线程状态

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

● 多线程

多线程的概念很好理解就是多条线程同时存在,但要用好多线程确不容易,涉及到多线程间通信,多线程共用一个资源等诸多问题。

使用多线程的优缺点:
优点:
1)适当的提高程序的执行效率(多个线程同时执行)。
2)适当的提高了资源利用率(CPU、内存等)。
缺点:
1)占用一定的内存空间。
2)线程越多CPU的调度开销越大。
3)程序的复杂度会上升。

下面我主要列出一些多线程的技术点。

synchronized

同步块大家都比较熟悉,通过 synchronized 关键字来实现;所有加上 synchronized 的方法和块语句,在多线程访问的时候,同一时刻只能有一个线程能够访问。

wait()、notify()、notifyAll()

这三个方法是 java.lang.Object 的 final native 方法,任何继承 java.lang.Object 的类都有这三个方法。它们是Java语言提供的实现线程间阻塞和控制进程内调度的底层机制,平时我们会很少用到的。

wait():
导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒,该方法只能在同步方法中调用。

notify():
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,该方法只能在同步方法或同步块内部调用。

notifyAll():
解除所有那些在该对象上调用wait方法的线程的阻塞状态,同样该方法只能在同步方法或同步块内部调用。

wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。

调用这三个方法中任意一个,当前线程必须是锁的持有者,如果不是会抛出一个 IllegalMonitorStateException 异常。

wait() 与 Thread.sleep(long time) 的区别

sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),该线程不丢失任何监视器的所属权,sleep() 是 Thread 类专属的静态方法,针对一个特定的线程。
wait() 方法使实体所处线程暂停执行,从而使对象进入等待状态,直到被 notify() 方法通知或者 wait() 的等待的时间到。sleep() 方法使持有的线程暂停运行,从而使线程进入休眠状态,直到用 interrupt 方法来打断他的休眠或者 sleep 的休眠的时间到。
wait() 方法进入等待状态时会释放同步锁,而 sleep() 方法不会释放同步锁。所以,当一个线程无限 sleep 时又没有任何人去 interrupt 它的时候,程序就产生大麻烦了,notify() 是用来通知线程,但在 notify() 之前线程是需要获得 lock 的。另个意思就是必须写在 synchronized(lockobj) {…} 之中。wait() 也是这个样子,一个线程需要释放某个 lock,也是在其获得 lock 情况下才能够释放,所以 wait() 也需要放在 synchronized(lockobj) {…} 之中。

volatile 关键字

volatile 是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile 变量可以保证下一个读取操作会在前一个写操作之后发生。线程都会直接从内存中读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。

ThreadLocal 变量

ThreadLocal 是Java里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

join() 方法

join() 方法定义在 Thread 类中,所以调用者必须是一个线程,join() 方法主要是让调用该方法的 Thread 完成 run() 方法里面的东西后,再执行 join() 方法后面的代码,看下下面的”意思”代码:

Thread t1 = new Thread(计数线程一);  
Thread t2 = new Thread(计数线程二);  
t1.start();  
t1.join(); // 等待计数线程一执行完成,再执行计数线程二
t2.start();  

启动 t1 后,调用了 join() 方法,直到 t1 的计数任务结束,才轮到 t2 启动,然后 t2 才开始计数任务,两个线程是按着严格的顺序来执行的。如果 t2 的执行需要依赖于 t1 中的完整数据的时候,这种方法就可以很好的确保两个线程的同步性。

Thread.yield() 方法

Thread.sleep(long time):线程暂时终止执行(睡眠)一定的时间。
Thread.yield():线程放弃运行,将CPU的控制权让出,暂停当前正在执行的线程对象,并执行其他线程。

这两个方法都会将当前运行线程的CPU控制权让出来,但 sleep() 方法在指定的睡眠时间内一定不会再得到运行机会,直到它的睡眠时间完成;而 yield() 方法让出控制权后,还有可能马上被系统的调度机制选中来运行,比如,执行yield()方法的线程优先级高于其他的线程,那么这个线程即使执行了 yield() 方法也可能不能起到让出CPU控制权的效果,因为它让出控制权后,进入排队队列,调度机制将从等待运行的线程队列中选出一个等级最高的线程来运行,那么它又(很可能)被选中来运行。

扩展

线程调度策略

(1) 抢占式调度策略

Java运行时系统的线程调度算法是抢占式的。Java运行时系统支持一种简单的固定优先级的调度算法。如果一个优先级比其他任何处于可运行状态的线程都高的线程进入就绪状态,那么运行时系统就会选择该线程运行。新的优先级较高的线程抢占了其他线程。但是Java运行时系统并不抢占同优先级的线程。换句话说,Java运行时系统不是分时的。然而,基于Java Thread类的实现系统可能是支持分时的,因此编写代码时不要依赖分时。当系统中的处于就绪状态的线程都具有相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。

(2) 时间片轮转调度策略

有些系统的线程调度采用时间片轮转调度策略。这种调度策略是从所有处于就绪状态的线程中选择优先级最高的线程分配一定的CPU时间运行。该时间过后再选择其他线程运行。只有当线程运行结束、放弃(yield)CPU或由于某种原因进入阻塞状态,低优先级的线程才有机会执行。如果有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。

线程的同步与锁

1、锁的原理

Java中每个对象都有一个内置锁

当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

1)、只能同步方法,而不能同步变量和类;

2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?

3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。

4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。

6)、线程睡眠时,它所持的任何锁都不会释放。

7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:

    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }

当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:

    public synchronized int getX() {
        return x++;
    }

    public int getX() {
        synchronized (this) {
            return x;
        }
    }

效果是完全一样的。

静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。

例如:

public static synchronized int setName(String name){

      Xxx.name = name;

}

等价于

public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

如果线程不能不能获得锁会怎么样

如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。

当考虑阻塞时,一定要注意哪个对象正被用于锁定:

1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。

2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

何时需要同步

在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。

对于非静态字段中可更改的数据,通常使用非静态方法访问。

对于静态字段中可更改的数据,通常使用静态方法访问。

 JAVA学习网站:

http://www.journaldev.com/

Java线程与多线程教程

Java线程:概念与原理

更多精彩内容:

我的个人站点:https://zilianliuxue.github.io/

猜你喜欢

转载自blog.csdn.net/u013278099/article/details/52317355