一、进程和线程的概念

进程:是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。

线程:是指进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

 

 

二、线程状态

一般来说,线程包括以下这几个状态:

创建(new)、就绪(runnable)、运行(running)、阻塞(blocked、time waiting、waiting)、消亡(dead)。

以下是每个状态的流转过程说明: 

1. 创建到就绪阶段:首先创建一个线程,不会立刻进入就绪状态,而是需要先进行资源分配(内存、CPU等),待资源准备完成之后,就进入线程的就绪状态。

2. 就绪到运行阶段:当线程进入就绪状态之后,不一定会立即被执行,需要等待CPU的执行时间,如果此时CPU正在处理其他任务,需要等到CPU执行完其他任务之后,线程才能进入运行状态。

3. 运行到阻塞阶段,到再次进入就绪阶段:在线程运行过程中,可能由于某种原因导致线程执行不下去了,这时线程就进入阻塞状态。当不再满足阻塞的条件时,线程会再次进入就绪状态。具体分以下几种情况:

3.1. 如果是用户让线程主动等待(调用了wait方法),则线程进入waiting状态,此时如果等待被唤醒(调用了notify方法),线程会再次进入就绪状态;


3.2. 如果是用户让线程主动睡眠(调用了sleep方法),则线程进入time waiting状态,此时如果睡眠时间到了,线程会再次进入就绪状态;


3.3. 如果是线程执行到同步代码块时被其他进程锁住,则线程进入blocked状态,此时如果同步代码块已经执行完毕,线程会再次进入就绪状态。

4. 运行到消亡阶段:如果线程突然被中断(调用了interrupt方法)或者任务执行完毕,线程就会进入消亡状态。

线程的几个状态流转示意图:

java线程知识总结 java线程的理解_同步方法

 

 

 

三、线程创建

创建线程有两种方式:

1. 继承Thread类,重写它的run()方法;

2. 实现Runnable接口,实现它的run()方法。

两种方式比较:实现Runnable接口相对于扩展Thread类来说,具有无可比拟的优势。这种方式不仅有利于程序的健壮性,使代码能够被多个线程共享,而且代码和数据资源相对独立,从而特别适合多个具有相同代码的线程去处理同一资源的情况。这样一来,线程、代码和数据资源三者有效分离,很好地体现了面向对象程序设计的思想。因此,几乎所有的多线程程序都是通过实现Runnable接口的方式来完成的。

 

 

四、线程启动

启动线程是通过调用Thread类的start方法来实现的。

在调用start()方法之后:发生了一系列复杂的事情:

1. 启动新的执行线程(具有新的调用栈);

2. 该线程从新状态转移到可运行状态;

3. 当该线程获得机会执行时,其目标run()方法将运行。

 

 

五、线程调度方式一:睡眠

线程休眠的方法是调用Thead的静态方法:Thread.sleep(long millis)和Thread.sleep(long millis, int nanos),那调用sleep休眠的哪个线程呢?简单说,哪个线程调用sleep,就休眠哪个线程。

 

 

六、线程调度方式二:让步

首先来看线程的优先级相关概念:

1.  线程的优先级:线程总是存在优先级,优先级范围在1~10之间(默认的优先级是5)。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下,当前运行的线程优先级将大于或等于线程池中任何线程的优先级。但这仅仅是大多数情况。注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。

 

2.  设置线程的优先级方法:线程默认的优先级是创建它的执行线程的优先级。

可以通过setPriority(int newPriority)更改线程的优先级。例如:

        Thread t = new MyThread();

        t.setPriority(8);

        t.start();

Thread类中有三个常量,定义线程优先级范围:

        static int MAX_PRIORITY  线程可以具有的最高优先级。

        static int MIN_PRIORITY  线程可以具有的最低优先级。

        static int NORM_PRIORITY 分配给线程的默认优先级。

 

3. 线程让步:线程的让步是通过静态方法Thread.yield()来实现的。

yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

 

 

七、线程调度方式三:合并

Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。

例如:

     

Thread t = new MyThread();
        t.start();
        t.join();

另外,join()方法还有带超时限制的重载版本。例如t.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。

线程的加入join()对线程栈导致的结果是线程栈发生了变化,当然这些变化都是瞬时的。

 

 

八、线程同步

1. 线程同步的目的?

为了防止多个线程访问一个数据对象时,对数据造成的破坏。

 

2. 线程同步的方法?

首先把竞争访问的资源类Foo变量x标识为private;然后使用synchronized关键字同步方法或代码。

 

3. 对象加锁的概念?

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

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

 

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

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

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

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

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

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

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

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

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

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

10)注意:在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep或者yield方法,因为synchronized程序块占有着对象锁,你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率,也不合逻辑。同样,在同步程序块内调用yeild方法让出CPU资源也没有意义,因为你占用着锁,其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。

 

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

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

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

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

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

 

 

 九、线程交互

void notify() :唤醒在此对象监视器上等待的单个线程。

void notifyAll():唤醒在此对象监视器上等待的所有线程。

void wait():导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。

当然,wait()还有另外两个重载方法:

         void wait(long timeout):导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。  

         void wait(long timeout, int nanos):导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

 当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程荣然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用。

 

       综上所述,上图的改进版如下图:

java线程知识总结 java线程的理解_就绪状态_02