1 线程的概念
多线程,就类似于操作系统的多进程。可以同时并发执行多个任务,处理多件事情。
线程是一个轻量级的进程,一个进程中可以分为多个线程。一个进程中至少需要有一个线程。
相对于进程,线程所使用的系统资源更少,切换更加容易。
2 实现多线程的方法
1 继承Thread类
Thread是java中的线程类,可以通过继承Thread类来实现多线程的操作。
继承Thread类,重写其中的run方法,run方法中的代码就是线程所要执行的任务。通过调用对象的start方法即可启动线程。
注意:要启动线程,需要调用对象的start方法,而不是它的run方法。调用run方法无法实现多线程的效果。
Main方法就是一个线程。这个线程的名字就叫main。
自己编写的线程类,如果没有使用setName方法设置线程的名字,jvm会将它们命名为thread-0,thread-1...
下面是一个以便洗澡,一边听音乐的示例,该示例使用继承Thread类的方法实现多线程:
package cn.ancony.thread; |
程序运行结果如下:
Thread-0 洗澡中1/20... Thread-1 听音乐中1/20... Thread-1 听音乐中2/20... Thread-0 洗澡中2/20... Thread-1 听音乐中3/20... Thread-0 洗澡中3/20... Thread-0 洗澡中4/20... Thread-1 听音乐中4/20... Thread-1 听音乐中5/20... Thread-0 洗澡中5/20... Thread-1 听音乐中6/20... Thread-0 洗澡中6/20... Thread-1 听音乐中7/20... Thread-0 洗澡中7/20... Thread-0 洗澡中8/20... Thread-1 听音乐中8/20... Thread-1 听音乐中9/20... Thread-0 洗澡中9/20... Thread-1 听音乐中10/20... Thread-0 洗澡中10/20... Thread-0 洗澡中11/20... Thread-1 听音乐中11/20... Thread-1 听音乐中12/20... Thread-0 洗澡中12/20... Thread-1 听音乐中13/20... Thread-0 洗澡中13/20... Thread-1 听音乐中14/20... Thread-0 洗澡中14/20... Thread-1 听音乐中15/20... Thread-0 洗澡中15/20... Thread-0 洗澡中16/20... Thread-1 听音乐中16/20... Thread-0 洗澡中17/20... Thread-1 听音乐中17/20... Thread-0 洗澡中18/20... Thread-1 听音乐中18/20... Thread-0 洗澡中19/20... Thread-1 听音乐中19/20... Thread-0 洗澡中20/20... Thread-1 听音乐中20/20... |
2实现Runnable接口
步骤:
1创建一个类,实现Runnable接口。Runnable接口是一个函数式接口,实现的时候,可以直接使用Lambda表达式。接口中只有一个run方法。我们需要重写该方法。
2创建该类的对象,将对象作为Thread的运行目标。
package cn.ancony.thread; |
使用Thread类和实现Runnable接口的区别:
A当一个类继承了其他的类,而这个类又要实现多线程的时候,就需要实现Runnable接口,而不能再继承Thread类,因为Java是单继承的。
B如果想在实现多线程的类里面使用一些Thread类里面的方法,比如getName()、sleep()等方法的时候,使用Thread类就可以使用这些方法,而不像实现Runnable接口。在实现Runnable接口的类中使用上述的方法时,需要使用Thread类的静态方法方法获得一个Thread的实例,然后通过该实例调用相应的方法,此时,直接使用Thread类可以减少程序的繁琐。
C当实现不同不同的任务时,在上面的例子中,通过第一种方式实现多线程,可以实现一边洗澡一边听音乐,而通过第二种实现,那就是一边音乐,一边还是听音乐。实现不同的任务,使用Thread类会更好。
D当需要共享变量的时候,使用实现Runnable接口的方式会更好。
3线程的生命周期
A新建--创建对象
B就绪--调用对象的start方法以后。
C运行--获得CPU的时间片
D阻塞(挂起)--失去CPU的时间片
E死亡--线程执行结束或者抛出未捕获的异常。
4 Thread类的常用方法
1 public static Thread currentThread();
获得当前的线程,任何类都可以直接使用Thread.currentThread这个静态方法获得当前的线程对象。
2 public String getName() 获得线程的名称,实例方法。
3 public final native boolean isAlive();判断线程是否存活。
在线程的start方法调用之后,在线程死亡之前,调用该方法返回true。
注意:在创建完线程对象,而没有调用start方法的时候,调用该方法返回的是false。
4 public void join()等待直到这个线程死亡。
如果在A线程中调用了B线程的join方法,则A线程会等到B线程执行结束,A线程才会继续执行。
join方法还有两个重载的方法:
public void join(long millis);
public void join(long millis,int nanos)
带参数的join方法会至多等待参数指定的时间。join方法很可能提前结束,而不会滞后。不带参数的join方法会无限等待。
5 public static void sleep(long millis);使当前的线程睡眠(暂时停止执行)millis毫秒。如果当前程序存在其他等待的线程,则其他线程会获得执行机会。该方法会使当前的线程阻塞。同时该方法如果在同步块中,不会释放已经获得的锁。调用了该方法,该线程就一定会让出自己的时间片。当参数的时间到达后,该线程会重新转为就绪状态。但就绪后,不意味着马上会得到执行。线程在sleep期间,如果其他线程调用了该线程的interrupt方法,则该线程就会中断,并产生InterruptException异常。
6 public static void yield();
当前运行的线程有意让出CPU资源,由线程调度器重新选择线程调度。不过,这仅仅是一个提示,线程调度器可能会忽略。
7 public void setDaemon(boolean on);
设置线程是否为后台线程。
只能在线程对象的start方法调用之前使用该方法。如果在调用了start方法之后使用,会抛出IllegalThreadStateException异常。
注意:当没有前台线程执行的时候,jvm就会退出。程序就会结束。jvm的垃圾回收器是一个典型的后台进程。
8 public void setPriority(int newPriority);
设置线程的优先级。优先级为1-10。优先级的数字越大,优先级越高。优先级高的线程仅仅意味这可能获得更多的执行机会,不表示一定会一直执行,优先级低的线程仍然有机会执行。
通常使用高中低三个优先级就够了。
Thread类中高中低优先级的定义是:
public final static int MIN_PRIORITY = 1; |
5线程同步
当多线程并发运行时,多线程间很可能操作共享成员变量,此时,就需要对共享成员变量的操作进行同步,避免出现多线程的并发修改而造成的意外错误。
同步的代码在同一时刻,至多只有一个线程执行,使用线程锁机制来保证。
线程同步的实现方式:
1使用同步块
因为线程的同步涉及到操作共享变量,所以使用实现Runnable接口的方式来实现多线程。
同步块的写法:
synchronized (锁) { 共享变量的访问 } |
任意的对象都可以充当锁对象。对于同步块,一般使用this。
下面是三个人抢100 张火车票的例子。
package cn.ancony.thread; |
这个时候运行结果很不尽人意,每次运行的结果都不一致。很可能一个人抢完了所有的票,也有可能一个人一直抢了几十张票。所以我们让线程sleep一下。
那么问题来了,sleep方法写在哪里呢?
一个是可以写在同步块里面,一个写在同步块外面。应该写在同步块外面。(为什么?)
更改后的代码如下:
@Override |
完成后运行,这段代码有的时候能正常运行,有的时候不能正常运行。主要出在最后一张票的时候。当三个线程有可能同时进入了while循环之后,同步块之前,这个时候,ticket的vi为1。但是三个线程已经全部执行完了while(ticket>0)这条语句。所以按顺序执行后面的语句,就出现了0和-1的情况。
程序需要进一步修改。那需要把ticke>0这个条件也放入同步块中。
但是像下面写可以吗?
@Override |
其实是不可以的,这完全是一个人抢完了所有的票,根本没有多线程的效果。
那怎么办呢?
@Override |
2使用同步方法
使用同步方法进行同步,整个方法都是同步区域。对于实例方法,this会充当同步方法的锁。对于静态方法,类的Class对象会充当锁。
class Method { |
在上面的类中,有四个同步方法,两个实例同步方法和两个静态同步方法。
instanceMethod1和instanceMethod2方法是互斥的。当一个线程在操作同步方法instanceMethod1时,其他的线程自然不能操作instanceMethod1,但是也不能操作instanceMethod2,因为instanceMethod1和instanceMethod1使用的是同一把锁!它们的锁都是当前的this对象。
staticMethod1和staticMethod2方法是互斥的。原理同上面一样。不能同时访问是因为它们的锁一样,都是这个类的Class对象。
3 synchronized和Lock
在JDK1.5之前,同步都是使用synchronized关键字来实现的,就像上面的例子一样。在JDK1.5之后,通过Lock接口来实现。该接口中有一个lock方法用来加锁,还有一个unlock方法用来解锁。
Lock的使用方式也很简单。将synchronized语句块中的所有语句,使用lock.lock()和lock.unlock包围即可。
代码如下:
class Ticket implements Runnable { |
但是这个时候其实还是有问题的。程序输出结果完成后,还会一直处于阻塞的状态。
这是为什么呢?因为使用lock来实现加锁的时候,如果加锁的语句块中出现了未捕获的异常或者使用了break之后,这个锁是释放不掉的。而使用synchronized则没有这个问题。
在synchronized语句块中,如果出现了未捕获的异常和使用了break语句之后,锁会自动释放掉。那如果还想使用lock的方式来同步怎么办呢?我们知道try...finally语句中,finally中的语句不管是有没有异常都会得到执行的。所以,我们可以将同步块的语句使用try...finally来包围。代码如下:
class Ticket implements Runnable { |
所以,我们在使用lock的时候,往往会配合try...finally来实现。
4 Interrupt
线程中断异常InterruptedException。
如果一个线程在sleep,join,或wait方法休眠或等待过程中,其他线程调用了该线程的interrupt方法来中断该线程,则会产生InterruptedException异常。
如果该线程没有处于sleep,join,或wait休眠或等待中,则不会产生该异常。
代码如下:
package cn.ancony.thread; |
像上面的代码,可以保证一定可以产生InterruptException异常吗?
不是的。如果在main方法中,已经执行完了a.interrupt()这句代码,而线程对象a还没有执行,就不能保证产生这个异常。为了确保产生这个异常,我们可以让main线程也sleep,保证main线程比线程对象a这个线程先醒来即可。
修改以后的代码如下:
package cn.ancony.thread; |
6 死锁
当两个或者多个线程同时拥有自己的资源,而相互等待获得对方资源,导致程序永远陷入僵持的状态,这就是死锁。
但多线程并发访问共享数据的时候,使用同步操作可以避免多线程并发修改带来的危害,但是同时也有可能会产生死锁。
一个典型的打架的例子。这个例子就会产生死锁。
package cn.ancony.test; |
7等待与唤醒
在多线程通信时,在某些特定条件下,我们需要线程做出一定的让步,否则就很容易造成双方(或者多方)进行僵持的状态,进而造成死锁。
当A线程运行一个同步方法的时候,发现需要B线程的一个执行结果,这个时候,A线程就应该主动让出自己的锁,等待B线程执行完成之后再执行。
那么怎么去实现呢?使用sleep方法可以吗?这个时候使用sleep方法是不合适的。Sleep方法一定会让出自己的时间片,CPU会去执行其他的任务,但是sleep方法并不会让出自己的锁。而且,也不能保证线程苏醒后,条件就一定会得到满足。这个时候我们应该使用wait方法。Wait会令当前线程等待,直到另一个线程调用该线程的notify或者notifyAll方法。当前线程必须要拥有该对象的锁。当调用wait方法后,线程会释放掉其占有的锁,并处于等待队列中。Wait方法是Object里面的方法,所以,所有的类都有这个方法。
那么A怎么知道B线程已经执行完了呢?这个时候最好的策略就是B线程执行完了以后通知A。使用notify或者notifyAll方法来实现。Notify唤醒等待该对象锁的一个进程,如果有多个线程处于等待中,仅唤醒一个。具体哪一个,取决于底层的实现。notifyAll会唤醒等待该对象锁的所有线程。这两个方法也是Object中声明的,所有的类都有这个方法。
注意:wait,notify和notifyAll方法调用时,当前线程一定要拥有对象的锁,否则将会引起IllegalMonitorStateException异常。
Sleep方法和wait方法有什么区别呢?
1sleep不会放弃已经占有的锁,而wait会。
2. Sleep是Thread线程里面的方法,只有Thread类及其子类才有这个方法。而wait方法,所有的java类中都有这个方法。
Notity和notifyAll方法的区别:
当调用了wait方法的时候,当前的线程就会让出自己所占有的锁资源,同时会处于等待之中,当前线程就会加入一个阻塞队列。Notity方法是从阻塞队列中随机通知一个线程,具体通知的哪个线程,并不确定。所以如果同时有多个线程在阻塞队列中,并不能确定到底会通知谁。而notifyAll是通知处于阻塞队列中的所有线程。每个线程收到通知后,会检查是不是符合自己的条件,如果符合自己的条件,那么它就会从阻塞的状态变为就绪的状态。
一个经典的等待和唤醒的例子是生产者和消费者的例子。在这个例子中,有一个公共的仓库,用于存放物品。生产者生产了产品以后,把产品放在仓库中。消费者去仓库中消费产品。生产者生产物品和消费者消费物品都是同步的方法。当生产者生产的速度大于消费者消费的速度时,物品就会堆满仓库。这个时候,生产者就不能再生产了,需要消费者给它创造生产的条件(消费者要把物品消费掉,腾出仓库的空间,这样生产者才可以继续生产),生产者就应该等着消费者去消费(如果消费者没有消费,就应该去通知消费者去消费)。相反也是,当消费者消费的速度大时,仓库就会变空,这时候,消费者就不能再消费了,需要等待生产者创造消费的条件(生产物品,使仓库不为空)。这时,消费者就等待生产者生产物品(如果生产者没有生产,那么就应该通知生产者生产)。
package cn.ancony.thread; |
以上是JDK1.5之前的方法。在JDK1.5之后,还可以使用Lock和Condition来实现。
代码如下:
package cn.ancony.thread; |