多线程
概述
操作系统几乎都支持多任务操作,例如:可以听歌,可以看电影,可以聊天。 听歌,看电影,聊天就是一个个的任务,每个运行中的任务都是一个进程。就听歌而言,可以唱,可以显示歌词,这便是一个个的线程,一个进程中包含了多个顺序执行流,每个顺序执行流就是一个线程。
进程的定义
进程执行的程序(程序是静态的,进程是动态的),或者是一个任务。 一个进程可以包含一个或多个线程。
线程的定义
线程就是程序中单独顺序的流控制。线程本身不能运行,它只能用于程序中。而多线程指的是单个程序中可以同时运行多个不同的线程执行不同的小任务。 线程是不拥有系统资源的,只能使用分配给程序的资源和环境。
进程可以看成是一个工厂,而线程可以看成是工厂中的工人,为了完成工厂的任务,每个工人都要去完成自己所负责的小任务, 从而使进程完成它的任务。 多进程允许多个任务同时运行,多线程是一个任务分成不同部分运行。
进程和线程的区别
- 多个进程的内部数据和状态都是完全独立的,不会互相影响。 而多线程是共享一块内存空间和一组系统资源,有可能互相影响。
- 线程本身的数据通常只有寄存器数据,以及一个程序执行使用过的堆栈,所以线程的切换比进程切换的负担要小。
- 一个进程由多个线程组成,线程是进程的进行单元。当进程被初始化后,主线程就被创建了。一个线程必须有一个父进程。
一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
线程的创建和启动
JAVA中使用Threadk类来代表线程,所有的线程对象都是Thread类或者子类的实例。每个线程的作用是完成一定的任务,实际上就是进行一段程序流,JAVA使用线程执行体来代表这段程序流。一个线程不能被重现启动在它执行完成之后。
线程创建的方式:
- 继承Thread类,然后重写run方法
- 实现Runable接口 ,实现其run方法
- 使用Callable和Future创建线程
将希望线程执行的代码放到run方法中,然后使用start方法来启动线程,start方法首先为线程的执行准备好系统资源,在去调用run方法。
下面看一下各个线程的实现代码
通过继承Thread类实现
public class ThreadTest {
public static void main(String[] args) {
Thread1 t = new Thread1() ;
t.start();
}
}
class Thread1 extends Thread{
@Override
public void run(){
for (int i = 0 ; i < 100 ; i++) {
System.out.println("hello thread " + i );
}
}
}
通过实现Runable接口实现:
public class Thread2Test {
public static void main(String[] args) {
Thread2 t = new Thread2();
Thread tt = new Thread(t) ;
tt.start();
}
}
class Thread2 implements Runnable{
@Override
public void run() {
for (int i = 0 ; i < 100 ; i++){
System.out.println("hello world " + i);
}
}
}
//通过静态内部类的方式创建多线程
public class StaticTest {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0 ; i <20 ; i++) {
System.out.println("hello world " + i );
}
}
}) ;
t.start();
for (int i = 0 ; i < 20 ; i++){
System.out.println("main " + i );
}
}
}
通过上面两种线程启动方式的比较,是否会有这样的疑问,继承Thread为什么要重写run方法,通过实现Runable接口的类,为什么要作为Thread类的构造参数,才能启动线程,取源码中寻找一下答案。
public
class Thread implements Runnable { //Thread类也实现了Runable接口
//无参构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
//参数为Runable类型的构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//线程默认的名字是Thread-number ,通过下面的代码实现, threadInitNumber静态成员变量,被所有的线程对象共享
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
public Thread(String name) {
init(null, null, name, 0); //指定线程的名字
}
//... 还有其他的构造方法,不再罗列
//私有初始化方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
init(g, target, name, stackSize, null);
}
public synchronized void start() {
if (threadStatus != 0)
group.add(this);
boolean started = false;
try {
start0(); //start0 为一个native方法,调用c来分配系统资源
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//run方法,如果target为空,则什么都不做,否则执行target的run方法。
public void run() {
if (target != null) {
target.run();
}
}
//调用getName方法可以返回线程的名字
public final String getName() {
return new String(name, true);
}
//由于主要介绍多线程,Thread的其他方法不再过多的罗列,可以自行查阅源码文件
}
当使用第一种方式来生成线程对象的时候,必须要重写run方法,因为Thread类中的run方法,当target为空时,并不做任何的操作。第二方式来生成线程对象时,需要实现run方法,然后使用new Thread(new myThread())来生成线程对象。target不为空,就会调用target类的run 方法 。
使用Callable和Future创建线程
public class Thread3Test {
public static void main(String[] args) {
ThirdThread t3 = new ThirdThread() ;
FutureTask<Integer> futureTask = new FutureTask<Integer>(t3);
ThirdThread1 t4 = new ThirdThread1() ;
FutureTask<Integer> futureTask1 = new FutureTask<Integer>(t4);
Thread t = new Thread(futureTask);
Thread t1 = new Thread(futureTask1);
t.start();
t1.start();
try {
System.out.println(futureTask.get());
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class ThirdThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0 ;
for ( ; i <5 ; i++){
System.out.println("hello " + i );
}
return i ;
}
}
class ThirdThread1 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0 ;
for ( ; i <5 ; i++){
System.out.println("world " + i );
}
return i ;
}
}
//运行结果 ,多线程的运行结果是不可预测的,可能回和我的运行结果不同
hello 0
world 0
hello 1
world 1
hello 2
world 2
world 3
world 4
hello 3
hello 4
5
5
main
为了说明效果,特写了两个类,启动了两个线程,从结果看,多线程的效果确实是产生了。但是通过上面的代码发现,使用了FutureTask对实现了Callable接口的类,进行了包装,然后将FutureTask实例传入了Thread类的构造方法中,这又是为啥? Thread的构造方法只能接受Runable的类型,Call接口并没有继承Runable接口,无法作为参数传入Thread构造方法中,所以使用了FutureTask来进行包装,因为FutureTask实现了Runable接口,并实现了run方法,在该方法中调用了Callable接口的call方法,并将call方法的返回值赋值给了result变量。调用get()方法便能获取返回值。
三种创建方式总结:
通过继承Thread类,实现Runable接口和Callable接口都可以创建多线程,继承Thread类,并重写run方法 ,可以直接通过调用start()方法实现多线程,实现Runable接口,则需要借助Thread类实现,将Runable实例,传入Thread的构造方法中,在调用start()方法实现多线程。实现Callable接口,则需要使用FutureTask来包装,并将FutureTask实例传入Thread的构造方法中实现多线程。实现Runable和Callable接口的不同之处是,Callable接口中的call方法可以返回值,并可以抛出异常。
线程的生命周期
线程要经过:新建、就绪、运行、阻塞、死亡五种状态。
生命周期的转化如下:
线程的生命周期.png
线程同步
在多线程的环境中,可能会有两个甚至多个的线程试图同时访问一个有限的资源。对于这种潜在的资源冲突进行预防。解决的方法便是在资源上为其加锁,对象加锁之后,其他线程便不能再访问加锁的资源。
线程安全的问题,比较突出的便是银行账户的问题,好多书中都是拿这个在说明多线程对有限资源的竞争问题。
为了解决对这种有限资源的竞争,java提供了synchronized关键字来解决这个问题。 synchronized可以修饰方法,被synchronized修饰的方法称为同步方法。
java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他的任何线程都无法在去访问该synchronized方法了,直到之前的那个线程执行完毕后(或者抛出异常),将该对象的锁释放掉,其他线程才有可能再去访问synchronized方法。
对于同步方法而言,无须显式的指定同步监视器,同步方法的同步监视器是this,也就是对象本身。线程在开始进行同步方法或者同步代码块之前,必须先获得对同步监视器的锁定。所以任意时刻只能有一个线程获得对对象的访问操作。
synchronized方法是一种粗粒度的并发控制,synchronized块是一种细粒度的并发控制,范围更小一点。
Java5之后,java提供了一种功能更加强大的线程同步机制,显示的定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。
代码如下:
class LockTest{
private final ReentrantLock lock = new ReentrantLock();
public void method(){
lock.lock();
try{
....
}finally{
lock.unlock() ;
}
}
}
使用Lock与使用同步方法有点相似,使用Lock时时显式使用Lock对象作为同步锁,同步方法是隐式使用当前对象作为同步监视器。
死锁
当两个线程相互等待对象释放同步监视器时就会发生死锁,一旦发生死锁,程序不会出现任何异常和任何提示,所以应该避免线程之间相互等待对方释放资源的情况出现。
传统的线程通信
传统的线程通信,借助了Object类中的wait() ,notify(),notifyAll()三个方法,这三个方法并不属于Thread类。
如果使用sychronized来保证同步,通信则依靠下面的方法:
wait():导致当前线程阻塞,知道其他线程调用该同步监视器的notify()或者notifyAll()方法,该线程才会被唤醒,唤醒后并不会立即执行,而是等待处理器的调度。
notify():唤醒此同步监视器中等待的单个线程。
notifyAll():唤醒同步监视器中等待的所有线程。
wait()和notify() 或 wait()和notifyAll()是成对出现的,因为他们依靠的都是同一个同步监视器。
使用Condition控制线程通信
如果程序没有使用synchronized来保证同步,而是使用了Lock对象来保证同步,则系统中就不存在隐式的同步监视器,也就不能使用wait(),notify(),notifyAll()方法进行通信了。 Java提供了Condition来进行线程之间的通信。
Condition提供了3个方法来进行线程通信:
await(): 导致当前线程等待,知道其他线程调用signal()或者signalAll()方法
signal(): 唤醒此Lock对象上的单个线程。
signalAll():唤醒此Lock对象上的所有线程。
线程池
创建一个新的线程成本还是比较高的,因为它涉及了程序与操作系统之间的交互,所以使用线程池可以很好的提高性能,尤其是程序的线程的生命周期很短,需要频繁的创建线程的时候。
Java5提供了内建的线程池支持,新增了一个Executors工厂类来产生线程。
- newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中 。
- newFixedThreadPool(int nThreads): 创建一个可重用,固定线程数的线程池。
- newSingleThreadPool():创建一个只有单线程的线程池
上面三个方法返回ExecutorService对象代表一个线程池,可以执行Runnable对象或者Callable对象所代表的线程
下面这两个方法返回一个ScheduledExecutorService线程池,是ExecutorService的子类,可以在指定的延迟后执行任务。
- newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务,corePoolSize指定池中保存的线程数,即使线程是空闲的也被保存在线程池中。
- newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
线程池的简单使用:
public class MyThread extends Thread{
public void run(){
for(itn i = 0 ; i <10 ; i++){
System.out.println("hello world" + i) ;
}
}
}
public class Test{
public static void main(String[] args){
MyThread t = new MyThread();
ExecutorService pool = Exectors.newFixedThreadPool(6);
pool.submit(t) ;
pool.shutdown();
}
}
少年听雨歌楼上,红烛昏罗帐。
壮年听雨客舟中,江阔云低,断雁叫西风。
感谢支持!
---起个名忒难