天天看点

Java笔记:多线程一些概念线程状态图Thread 和 Runnable线程的同步与锁

现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。

<code>进程</code> 是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在 windows 系统中,一个运行的 exe 就是一个进程。

<code>线程</code> 是指进程中的一个执行流程,一个进程中可以运行多个线程。比如 <code>java.exe</code> 进程中可以运行很多线程。<code>线程总是属于某个进程,进程中的多个线程共享进程的内存</code>。

<code>同时</code> 执行是人的感觉,在线程之间实际上轮换执行。

创建线程两种方式:<code>继承thread类</code> 或 <code>实现runnable接口</code>。

thread 对象代表一个线程,一个 thread 类实例只是一个对象,像 java 中的任何其他对象一样,具有变量和方法,生死于堆上。

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

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

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

多线程共同访问的同一个对象(临界资源),如果破坏了不可分割的操作(原子操作),就会造成数据不一致的情况。

线程总体分两类:<code>用户线程</code> 和<code>守候线程</code>。

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

Java笔记:多线程一些概念线程状态图Thread 和 Runnable线程的同步与锁

说明: 线程共包括以下5种状态。

<code>新建状态(new)</code>: 线程对象被创建后,就进入了新建状态。例如,<code>thread thread = new thread()</code>。

<code>就绪状态(runnable)</code>: 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的 <code>start()</code> 方法,从而来启动该线程。例如,<code>thread.start()</code>。运行中的线程调用 <code>yield()</code> 之后也会进入就绪状态。处于就绪状态的线程,随时可能被 cpu 调度执行。

<code>运行状态(running)</code>: 线程获取cpu权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

<code>阻塞状态(blocked)</code>: 阻塞状态是线程因为某种原因放弃 cpu 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

a) 等待阻塞 – 通过调用线程的 <code>wait()</code> 方法,让线程等待某工作的完成。

b) 同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

b) 其他阻塞 – 通过调用线程的 <code>sleep()</code> 或 <code>join()</code> 或发出了 i/o 请求时,线程会进入到阻塞状态。当 <code>sleep()</code> 状态超时、<code>join()</code> 等待线程终止或者超时、或者 i/o 处理完毕时,线程重新转入就绪状态。

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

runnable 是一个接口,该接口中只包含了一个 <code>run()</code> 方法。它的定义如下:

我们可以定义一个类 a 实现 runnable 接口;然后,通过 <code>new thread(new a())</code> 等方式新建线程。

thread 是一个类。thread 本身就实现了 runnable 接口。它的声明如下:

相同点:

都是“多线程的实现方式”。

不同点:

thread 是类,而 runnable 是接口;thread 本身是实现了 runnable 接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此 runnable 具有更好的扩展性。

此外,runnable 还可以用于“资源的共享”。即,多个线程都是基于某一个runnable对象建立的,它们会共享这个runnable对象上的资源。

<code>start()</code>:它的作用是启动一个新线程,新线程会执行相应的 <code>run()</code>方法。<code>start()</code> 不能被重复调用。

<code>run()</code>:和普通的成员方法一样,可以被重复调用。单独调用 <code>run()</code> 的话,会在当前线程中执行 <code>run()</code>,而并不会启动新线程!

在调用 <code>start()</code>方法之前,线程处于新状态中,新状态指有一个 thread 对象,但还没有一个真正的线程。

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

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

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

当该线程获得机会执行时,其目标 <code>run()</code>方法将运行。

在 object.java 中,定义了 <code>wait()</code>, <code>notify()</code> 和 <code>notifyall()</code> 等接口。<code>wait()</code> 的作用是让当前线程进入等待状态,同时,<code>wait()</code> 也会让当前线程释放它所持有的锁。而 <code>notify()</code> 和 <code>notifyall()</code> 的作用,则是唤醒当前对象上的等待线程;<code>notify()</code> 是唤醒单个线程,而 <code>notifyall()</code>是唤醒所有的线程。

<code>notify()</code>,<code> wait()</code> 依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!

在 java 中,任何对象都有一个锁池,用来存放等待该对象锁标记的线程,线程阻塞在对象锁池中时,不会释放其所拥有的其它对象的锁标记。

在 java 中,任何对象都有一个等待队列,用来存放线程,线程 t1对(让)o调用 wait 方法,必须放在对 o 加锁的同步代码块中!

t1 会释放其所拥有的所有锁标记;

t1会进入 o 的等待队列

t2 对(让)o调用 notify/notifyall 方法,也必须放在对 o 加锁的同步代码块中! 会从 o 的等待队列中释放一个/全部线程,对 t2 毫无影响,t2 继续执行。

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

<code>yield()</code> 应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用 <code>yield()</code> 的目的是让相同优先级的线程之间能适当的轮转执行。

但是,实际中无法保证 <code>yield()</code> 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

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

wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

<code>sleep()</code> 的作用是让当前线程休眠,即当前线程会从 <code>运行状态</code> 进入到 <code>休眠(阻塞)状态</code> 。<code>sleep()</code> 会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由 <code>阻塞状态</code> 变成 <code>就绪状态</code>,从而等待 cpu 的调度执行。

wait()会释放对象的同步锁,而sleep()则不会释放锁。

thread的非静态方法 <code>join()</code> 让一个线程 b “加入” 到另外一个线程 a 的尾部。在 a 执行完毕之前,b 不能工作。例如:

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

interrupt()的作用是中断本线程。

本线程中断自己是被允许的;其它线程调用本线程的 interrupt() 方法时,会通过 checkaccess() 检查权限。这有可能抛出 securityexception 异常。

如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或 wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int) 也会让它进入阻塞状态。若线程在阻塞状态时,调用了它的 <code>interrupt()</code>方法,那么它的“中断状态”会被清除并且会收到一个 interruptedexception 异常。

例如,线程通过 wait() 进入阻塞状态,此时通过 interrupt() 中断该线程;调用 interrupt() 会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个 interruptedexception 的异常。

如果线程被阻塞在一个 selector 选择器中,那么通过 interrupt() 中断它时;线程的中断标记会被设置为 true,并且它会立即从选择操作中返回。

如果不属于前面所说的情况,那么通过 interrupt() 中断线程时,它的中断标记会被设置为“true”。中断一个“已终止的线程”不会产生任何操作。

interrupt()常常被用来终止“阻塞状态”线程。

interrupted() 和 isinterrupted()都能够用于检测对象的“中断标记”。

区别是,interrupted()除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false);而isinterrupted()仅仅返回中断标记。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

举例: