几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个执行流就是一个线程。
所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程时系统进行资源分配和调度的一个独立单元。
进程包含3个特征:1、独立性 2、动态性 3、并发性
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程拥有的全部资源。
线程是独立运行的,线程的执行是抢占性的。
操作系统无需将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成。
归纳:操作系统可以执行多个任务,每个任务就是进程,但至少要包含一个线程。
线程在执行过程中拥有独立的内存单元,而多个线程共享内容,从而极大的提高了程序的运行效率
线程比进程拥有更高的性能,这是由于同一个进程中的线程都有共性-多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现互相之间的通信。
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建一个线程则简单的多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得多。
使用多线程编程有如下优点:
* 进程之间不能共享内存,但线程之间共享资源非常容易
* 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高
* Java语言内置了多线程功能的支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java的多线程编程。
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或者其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码),Java使用线程执行体来代表这段程序流。
1、继承Thread类来创建线程类
进行多线程编程时不要忘记了Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体
使用继承Thread类方法来创建线程类时,多个线程之间无法共享线程类的实例变量
2、实现Runnable接口创建线程类
这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例属性
callback提供一个call方法可以作为线程执行体
* call()方法可以有返回值
* call()方法 可以声明抛出异常
创建并启动有返回值的线程步骤如下:
因为实现Runnable接口与实现Callable接口的方式基本相同,只是Callback接口里定义的方法有返回值,可以声明抛出异常而已,因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下:
采用实现Runnable、Callback接口的方式创建多线程
采用继承Thread类的方式创建多线程
线程的生命周期中要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dend)5种状态
1、新建和就绪状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。
2、运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run方法的执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任意时刻只有一个线程处于运行状态
发生如下情况下,线程会进入阻塞状态
发生如下特定的情况下可以解除上面的阻塞,让该线程重新进入就绪状态
注意:调用yield方法可以让运行状态的线程转入就绪状态
线程会以如下3种方式结束,结束后就处于死亡状态
不要试图对一个已经死亡的线程调用start()方法使它重新启动、死亡就是死亡,该线程不可再次作为线程执行
1、join线程
Thread提供了让一个线程等待另一个线程完成的方法-join()方法。
join()方法有如下三种重载形式:
在后台运行,它的任务是为其他的线程提供服务,这种线程被称为”后台线程(Daemon Thread)”,又称为”守护线程”或”精灵线程”。JVM的垃圾回收线程就是典型的后台线程
特征:如果所有的前台线程都死亡,后台线程会自动死亡,调用Thread对象的setDaemon(true)方法可以指定线程设置成后台线程。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就推出了。
将某个线程设置为后台线程,必须在start()方法之前调用,否则会引发IllegalThreadStateException
sleep()方法有两种重载形式:
yield()方法可以让当前正在执行的线程暂停,但是不会阻塞线程,只是将该线程转入就绪状态。实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同、或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
sleep()方法和yield()方法的区别如下:
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程控制较少的执行机会
每个线程默认的优先级都与创建它的父线程的优先级相同,再默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以是使用Thread类的如下三个静态常量
1、当多个线程来访问同一个数据时,很容易偶然出现线程安全问题
2、同步代码块
当两个进程并发修改同一个文件时就有可能造成异常
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:
“
synchronised(obj)
{
...
//此处的代码就是同步代码块
}
“
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得 对同步监视器的锁定
注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成之后,该线程会释放对该同步监视器的锁定
3、同步方法
与同步代码块对应,Java的多线程安全支持了还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法来说,去须显示指定同步监视器,同步方法的同步监视器是this,也就是该对象本身
通过使用同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征:
为了减少线程安全所带来的负面影响,程序可以采用如下策略:
提示:JDK所提供的StringBuilder和StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBuilder来保证较好的性能;当需要保证多线程安全时,就应该使用StringBuilder
4、释放同步监视器的锁定
线程会在如下几种情况下释放对同步监视器的锁定
在如下所示的情况下,线程不会释放同步监视器
5、同步锁
从Java5开始,Java提供了一种功能更加强大的线程同步机制-通过显示定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性。并且支持多个相关的Condition对象
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock进行加锁。线程开始访问共享资源之前应先获得Lock对象
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5新提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantWriteLock实现类
6、死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,只是所有线程处于阻塞状态,无法继续
1、使用Condition控制线程通信
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象的却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程
Condition将同步监视器(wait()、notify()和notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能
Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下3个方法:
BlockingQueue具有一个特征,当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好的控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法
BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法。
BlockingQueue包含如下5个实现类:
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组
ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程
1、当程序中需要创建大量生存期很短暂的线程时,应该考虑线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态,等待下一个Runnable对象的run()或call()方法
除此之外,使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数
Java5新增了一个Executors工厂类来创建线程池,该工厂类包含如下几个静态工厂方法来创建线程池
2、Java7新增的ForkJoinPool
Java7提供了ForkJoinPool来支持将一个任务拆分为多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池,提供了两个常用的构造器
1、通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以很简洁的隔离多线程程序的竞争资源
线程局部变量(ThreadLocal)的功用就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程可以独立的改变自己的副本,而不会和其他线程的副本冲突。
ThreadLocal类只提供了3个public方法
ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突,在普通的访问机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的。
ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLoacl是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。
如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal
2、包装线程不安全的集合
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,当多个线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性
3、线程安全的集合类
线程安全的集合类可分为如下两类: