Java多线程

1.线程的创建与启动

在Java中,多线程的实现有两种方式:
继承java.lang.Thread类
实现java.lang.Runnable接口

1.1继承Thread类创建线程

继承Thread类创建并启动线程的步骤:
1、定义Thread的子类,并重写该类的run()方法,run()方法的方法体就表示线程需要完成的任务。run()被称为线程执行体。
2、创建Thread的子类的实例,即创建线程对象。
3、调用线程对象的start()方法启动该线程。

1.2实现Runnable接口创建线程

实现Runnable接口创建并启动线程的步骤:
1、定义Runnable接口的实现类,并重写该接口的run()方法,run()方法的方法体就表示线程需要完成的任务。run()被称为线程执行体。
2、创建Runnable的实现类的实例,并以此实例作为Thread的target来实现Thread对象,该Thread对象才是真正的线程对象。
3、调用线程对象的start()方法启动该线程。
PS:使用Runnable创建的多个线程对象可以共享Runnable的实现类的实例属性。

2.线程的生命周期

在线程的生命周期中有五种状态。

2.1新建状态和就绪状态

当程序使用new关键字创建一个线程之后,该线程就处于新建状态。
当线程对象调用了start()方法之后,该线程就处于就绪状态。处于就绪状态的线程并没有开始运行,只是可以运行而已,,至于该线程何时开始运行就取决于JVM里线程调度器的调度。
PS:不能直接调用线程的run()方法,不然就变成了普通方法的调用,而不是执行线程执行体。

2.2运行状态和阻塞状态

处于就绪状态的线程获得了CPU,开始执行run()方法的方法体,则该线程处于运行状态。
当一个线程开始运行后,在某个时刻失去了CPU,则该线程处于阻塞状态。
当发生如下情况时,线程会进入阻塞状态(从运行状态转入阻塞状态):
1、线程调用sleep()方法主动放弃占用的处理器资源
2、线程调用了一个阻塞式IO方法
3、线程试图获取一个同步监视器,但是该同步监视器正被其他线程所拥有。
4、线程在等待某个通知
5、线程调用suspend()方法将该线程挂起。这个方法容易造成死锁。
针对上面几种情况,当发生如下情况时可以解除阻塞(从阻塞状态转入就绪状态):
1、调用sleep()方法的线程过了指定的时间
2、线程调用的阻塞式IO方法已经返回
3、线程成功获取它试图获取同步监视器
4、线程正在等待某个通知时,其他线程发出了一个通知
5、处于挂起状态的线程调用了resume()方法
PS:线程调用yield()方法可以从运行状态转入就绪状态

2.3死亡状态

线程会以三种方式结束,结束后就处于死亡状态:
1、线程的run()执行完成,线程正常结束
2、线程抛出一个未捕获的Exception或Error
3、线程调用stop()方法来结束该线程,该方法容易造成死锁
为了测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞三种状态时,返回true,当线程处于新建,死亡两种状态时,返回false。

3.线程控制 

Java的线程提供一些便捷的工具,通过这些工具可以很好地控制线程的执行。

3.1join线程

Thread提供让一个线程等待另一个线程完成的方法——join()方法。当在某个线程的执行过程中调用另一个线程的join()方法时,调用线程将被阻塞,直到被join()方法的线程执行完为止。

3.2后台线程

有一种线程是在后台运行的,它的任务是为其他线程提供服务,这种线程叫做后台线程(Daemon Thread),又称为守护线程或精灵线程。
后台线程有个十分重要的特点:如果所有的前台线程死亡,那么后台线程自动死亡。
调用线程对象的setDaemon(true)方法可将指定线程设置为后台线程。
Thread类还提供一个isDaemon()方法,用来判断指定线程是不是后台线程,是后台线程就返回true,否则返回false。
主线程(main线程)默认是前台前程。
前台,后台线程有一个重要的特点:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
PS:setDaemon(true)设置线程为后台线程必须在start()方法之前调用。

3.3线程睡眠

如果需要让当前正在后执行的线程暂停一段时间,并进入阻塞状态,可以通过调用Thread类的静态sleep()方法实现。

3.4线程让步

yield()方法是一个和sleep()方法相似的方法,它也是Thread类的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是让该线程转入就绪状态。
当某个线程调用了yield()方法暂停后,只有优先级与该线程相同,或者优先级高于该线程的处于就绪状态的线程才会获得执行的机会。

3.5线程的优先级

每个线程都具有一定的优先级,优先级高的线程有更多的机会获得执行,优先级低的有更少的机会获得执行。
主线程(main线程)默认优先级是普通优先级,每个线程的默认优先级都与创建它的父线程相同。
Thread类提供setPriority(int newPriority)方法和getPriority()方法来设置和获取指定线程的优先级,其中setPriority(int newPriority)方法中的参数取值范围是1-10,也可以使用Thread类的三个静态常量
MAX_PRIORITY:值是10
MIN_PRIORITY:值是1
NORM_PRIORITY:值是5
PS:Java提供10个线程优先级,但不是所有的操作系统都支持这10个优先级,所以我们应该尽量避免直接有数字来设置优先级,应该多用三个静态常量来设置优先级,这样程序的移植行更好。

4.线程同步

4.1线程安全问题

多个线程同时访问一个对象时,可能会出现线程安全问题

4.2同步监视器

为了解决线程安全问题,Java的多线程支持引入了同步监视器。

4.2.1同步代码块

使用同步监视器的一个方法是同步代码块。
synchronized(obj){
...//此处代码就是同步代码块
}
synchronized后面圆括号里面的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一个线程获取对同步监视器的锁定。

4.2.2同步方法

使用同步监视器的一个方法是同步方法。
对同步方法而言,无须显示地指定同步监视器,同步方法的同步监视器是this,也就是同步方法本身。
public synchronized void play(){
...//同步方法体
}
使用同步方法可以非常方便地实现线程安全类,线程安全类具有以下特征:
1、该类的对象可以被多个线程安全访问
2、每个线程调用该对象的任意方法后将得到正确的结果
3、每个线程调用该对象的任意方法后,该对象依然保持合理状态

4.2.3释放同步监视器的锁定

释放同步监视器的锁定的方法:
1、当前线程的同步方法、同步代码块执行结束
2、当前线程的同步方法、同步代码块中遇到break、return终止了该方法、该代码块的继续执行
3、当前线程的同步方法、同步代码块中出现了未处理的Error或Exception,导致该方法、该代码块异常结束
4、当前线程执行同步方法、同步代码块时,程序执行了同步监视器对象的wait()方法,则当前线程暂停
如遇到以下几种情况,线程不会释放同步监视器
1、当前线程执行同步方法、同步代码块时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行
2、当前线程执行同步代码块时,其他线程调用该线程的suspend()方法将该线程挂起。这个方法容易造成死锁。

4.3同步锁

从Java5开始,Java提供了一种功能强大的心爱你策划给你同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。
Lock提供比同步方法、同步代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个多个相关的Condition对象。
Lock是充值多线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。
Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
使用ReentrantLock的常用格式如下
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
	private final ReentrantLock lock=new ReentrantLock();
	public void play(){
		lock.lock();
		try{
			//需要保证线程安全的代码					
		}finally{
			lock.unlock();
		}
	}
}

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已经被加锁的ReentrantLock锁再次加锁,一段被锁保护的代码可以调用另一个被相同锁保护的方法。

4.4死锁

当两个线程互相等待对方释放同步监视器时,就会出现死锁。所以多线程编程时要采取措施避免死锁。

5.线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行。

5.1传统的线程通信

为了实现线程间的通信,可以借助Object类提供的wait()、notify()、notifyAll()3个方法,但这3个方法必须有同步监视器对象来调用。
1、对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步对象中直接调用这3个方法
2、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这3个方法
关于这3个方法的含义
1、wait()方法:导致当前线程等待,知道其他线程调用该同步监视器的notify()方法或notifyAll()方法
2、notify()方法:唤醒在此同步监视器上等待的单个线程
3、notifyAll()方法:唤醒在此同步监视器上等待的所有线程

5.2使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也不能使用前面的3个方法来进行通信了。
当使用Lock对象来保证同步时,Java提供一个Condition类来保证协调,使用Condition可以让那些得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
这时,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获取特定Lock实例的Condition实例,调用Lock对象的newCondition方法即可。Condition提供了如下3个方法。
1、await()方法:类似于隐式同步监视器上的wait()方法,导致当前小昵称等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程
2、signal()方法:唤醒在此Lock对象上等待的单个线程
3、signalAll()方法:唤醒在此Lock对象上等待的所有线程

5.3使用阻塞队列控制线程通信

Java5提供了一个BlockingQueue接口,虽然它也是Queue的子接口,但它的主要作用不是作容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中加入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,就可以很好地控制线程的通信了。
BlockingQueue提供如下两个支持阻塞的方法,
1、put(E e)方法:尝试把E元素放入BlockingQueue中
2、take()方法:尝试从BlockingQueue的头部取出元素

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。