java 之线程
一、线程与进程
在众多的开发语言中,Java 是其中一个支持多线程的编程语言。
进程指的是程序的一次完整运行过程,调动 CPU、内存、IO 等计算机资源为进程服务。在计算机发展的早期,计算机病毒主要是抢占计算机资源,让其他的程序无法使用计算机资源,从而影响计算机的运行,死循环便是其中的一个方式。DOS 操纵系统时代,计算机处于死机状态;进入 Windows 操作系统时代,计算机不会死机,而是运行极慢,这与 Windows 操作系统是多进程系统有关。
线程是在进程的基础上,进一步细分,一个进程包含多个线程。线程是运行更快的处理单元,所占资源小,多线程的应用提高了程序性能。多线程可以实现数据共享,也就是多个线程去抢占计算机的同一资源。
二、多线程的实现
对于多线程如何实现,Java 提供了以下方式:
继承 Thread 类;
实现 Runnable 接口;
实现 Callable 接口;
以下便具体介绍如何实现多线程。
1、继承 Thread 类
需要覆写线程的主体方法 run(),这是线程程序执行的起点。但是,单纯执行 run()方法并不能够实现多线程,从实际运行结果得知,程序仍然是顺序执行。而多线程是需要同时运行去抢占计算机资源执行程序代码,故需要使用 start()方法开启多线程,其中 start()方法运行 run()方法体程序。
问题:为什么覆写的是 run()方法,而执行多线程需要使用 start()方法?回答这个问题,需要查看源代码。
源码如下
run()方法:
start()方法:
从源代码可以发现,start0()方法使用的 JNI(Java Native Interface)技术,从而调用操作系统中提供的函数方法。这也是为什么要使用 start()方法去通过 JVM 调用操作系统,进行资源的分配。
2、实现 runnable 接口
通过继承 Thread 类,会受到单继承的局限影响。当发生继承关系时,首选使用接口,故 Java 提供了 Runnable 接口实现多线程。
Runnable 接口结构定义如下:
通过类实现 Runnable 接口,覆写 run()方法,但是实际上,并没有 start()方法启动多线程,因此,仍然需要借助 Thread 类进行多线程的启动。其中,Thread 类中提供了支持 Runnable 接口对象的构造方法。这样,既避免了单继承的局限,同时又实现了多线程。
Runnable 与 Thread 的区别
图 Runnable 与 Thread 的区别
多线程可以实现数据共享,即多个线程抢占同一资源。建议子类实现 Runnable 接口,这样可以通过启动多个线程,实现对 Runnable 接口的子类对象这一资源共享,这也是因为 Runnable 接口没有提供线程 start()启动方法。
而如果子类继承 Thread 类,虽然也可以使用其他多个线程启动,去共享这个子类对象资源(Thread 类是 Runnable 接口的子类),但是,Thread 的子类也具有线程 start()启动方法,再使用其他线程去启动非常不合适。
3、实现 Callable 接口
原有线程方法执行使用 run()没有返回结果,为了解决这一问题,jdk1.5 提供了 Callable 接口。
Callable 接口定义如下:
jdk1.5 提供了可以获取 Callable 接口 call()方法返回值的 java.util.concurrent.FutureTask 类。同时,这个类的父接口 java.util.concurrent.RunnableFuture 又是 Runnable 接口的子接口,所以,综合下来,可以通过 FutureTask 接收 Callable 接口对象,再通过 Thread 类进行多线程启动。
Callable 接口不是 Runnable 接口的子接口,无法像 Runnable 接口那样的方式单独作为共享的资源一方,让其他线程去抢占资源。
范例:
三、多线程的常用方法
对于多线程而言,主要的方法都是在 Thread 类中定义的,这是需要注意的,这也不奇怪,因为 runnable 接口只是定义了一个抽象方法,表示一种能力。
以下介绍多线程的常用方法:
1、线程的命名
1、通过 Thread 的构造方法命名
2、public final synchronized void setName(String name){}
2、线程名的取得
public final String getName(){}
3、当前运行线程的取得(在 Runnable 接口子类中运用非常合适)
public static native Thread currentThread()
补充:主方法也是一个线程,叫做主线程,通过主线程再创建子线程。每个 JVM 进程启动的时候,会启动至少两个线程:
main 线程:启动主线程,再由主线程再去启动子线程
gc 线程:负责垃圾回收
4、线程的休眠
public static native void sleep(long millis) throws InterruptedException;
5、线程优先级(优先级越高,越有可能先执行)
设置优先级
public final void setPriority(int newPriority)
取得优先级
public final int getPriority()
最高优先级
public static final int MAX_PRIORITY
中等优先级
public static final int NORM_PRIORITY
最低优先级
public static final int MIN_PRIORITY
6、join()方法
join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了 join()方法后面的代码,只有等到子线程结束了才能执行。
四、线程的生命周期
对于每一个线程而言,都有各自的状态,可以通过以下图示进行理解。
其中 wait()、notify()、notifyAll()方法是继承父类 Object 的方法,但并没有覆写新方法体。
图 线程的生命周期
五、线程的同步与死锁
同步的问题,也就是为了解决多个线程访问同一资源时异步造成的数据不统一问题。不同步(也就是异步)的问题,会造成数据的不一致。就如同购买火车票,如果不同地方购买同一车次的票,剩余票数不统一,就会造成重复购买相同票的情况。
为了实现同步的问题,引入了锁的概念,在 Java 中提供了 synchronized 关键字实现同步。使用 synchronized 有两种方式:同步代码块和同步方法。
同步代码块必须要锁当前对象。
同步是一个线程等待另一个线程执行完毕之后再执行。但是会出现两个线程都互相等待彼此的过程,此时便会出现死锁的问题。
死锁问题如何破?
1、当还未进入到代码运行阶段,编写代码过程中,使用 synchronized、wait、notify 编写同步代码时,减少出现死锁条件发生,也即线程之间互相等待的状态;
2、在编写程序代码过程中,可以在程序内部,增加等待状态的判断机制,如果等待时间超出配置时间范围,可直接进行异常抛出,执行事务处理;
3、对于指定运行的线程,增加线程队列,通过对线程队列中线程的等待状态判断机制,进行死锁处理,一旦超出预定等待时间,则可以停止线程,进行线程事务处理。
六、生产者与消费者模式
生产者和消费者模式,也就是不同类型的线程类对象,针对同一资源所采取的不同操作。
生产者负责产生数据,消费者负责消耗数据;
生产者产生一组数据,消费者取走一组数据。
图 生产者与消费者模式
基于以上提供的模型,编写的简单代码,数据会容易产生以下问题:
数据错位,不再是一个完整正确的数据;
重复取出数据,重复设置数据。
问题出现的关键在于异步处理,故需要使用同步进行处理。
生产者与消费者模式,主要问题就是解决统一资源操作的问题。对于同一资源,一定用到同步思路。增加一个控制“开关”,生产时,不可消费,消费时,不可生产,也不能重复生产和重复消费。同时,可以利用 Object 类实现等待与唤醒。Object 类提供了以下方法:
等待:
public final void wait()throws InterruptedException
唤醒第一个等待线程:
public final void notify()
唤醒所有等待线程,优先级高的先被唤醒抢占资源:
public final void notifyAll()
七、线程局部变量
线程局部变量就是存在线程内部的变量,为每个线程所独享,是线程安全的。Java 提供了 ThreadLocal 类实现线程局部变量。
当使用 ThreadLocal 维护变量时,每一个线程都会拥有一个自己独立的副本,相互之间不会影响。每一个线程都会有自己的 ThreadLocalMap 对象,该 Map 存储一个该线程下的以 hashcode 为健,线程局部变量为值的键值对。
常用方法
protected T initialValue():初始化线程局部变量,只可通过匿名实现类完成
public T get():获取线程局部变量
public void set(T value):设置线程局部变量
评论