Java中给多线程编程提供了内置的支持,多线程是多任务的一种特别形式,它使用了更小的资源开销。这里需要知道两个术语及其关系:进程和线程。
进程:进程是系统进行资源分配和调度的一个独立单位。一个进程包括由操作系统分配的内存空间,包含一个或多个线程。
线程:线程是进程的一个实体,是CPU调度和分派的基本单位。它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
(一)线程的生命周期
线程是一个动态执行的过程,从产生到死亡,这个过程称为线程的生命周期。线程的状态有:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,如下图所示,注意整个执行过程的实现:
- 新建状态(New):当线程对象对创建后,即进入了新建状态;
- 就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态,等待CPU调度执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态;
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期;
(二):线程的创建
Java提供了三种创建线程的方法:
- 实现Runnable接口;
- 继承Thread类;
- 通过Callable和Future创建;
在开发中,前两种是常用的线程创建方式,下面来简单说下:
(1)通过实现Runnable接口创建线程
public class RunnableDemo implements Runnable{
private Thread t;
private String threadName;
//构造方法
public RunnableDemo(String name) {
this.threadName = name;
System.out.println("创建线程:"+threadName);
}
//重写run()方法
@Override
public void run() {
System.out.println("运行线程:"+threadName);
try {
for(int i=4;i>0;i--){
System.out.println("Thread: "+threadName+","+i);
Thread.sleep(50);
}
}catch (Exception e) {
System.out.println("Thread "+threadName+" 阻塞");
}
System.out.println("Thread "+threadName+" 终止");
}
//调用方法(为了输出信息,可以忽略)
public void start(){
System.out.println("启动线程:"+threadName);
if(t == null){
t = new Thread(this,threadName);
t.start();
}
}
}
测试类:
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r1 = new RunnableDemo("T1");
r1.start();
RunnableDemo r2 = new RunnableDemo("T2");
r2.start();
}
}
输出为:
创建线程:T1
启动线程:T1
创建线程:T2
启动线程:T2
运行线程:T1
Thread: T1
运行线程:T2
Thread: T2,4
Thread: T2,3
Thread: T1,3
Thread: T2,2
Thread: T1,2
Thread: T2,1
Thread: T1,1
Thread T2 终止
Thread T1 终止
从上面的实例可以看出,通过实现Runnable接口创建线程的几个要点:
- 构造方法来创建线程对象,有参或无参看自己需要;
- 重写run()方法,这里写入自己需要实现的代码;
- 启动start()方法,这里可以直接调用;
- run()方法是线程的入口点,必须通过调用start()方法才能执行;
(2)通过继承Thread类创建线程
它本质上也是实现了 Runnable 接口的一个实例,所以这里就不贴出代码了,可以按照上面的实例,更改class为继承即可,如下:
public class ThreadDemo extends Thread{}
Thread类的常用且重要的方法有:
- public void start():使该线程开始执行;Java虚拟机调用该线程的 run 方法,对象调用;
- public void run():对象调用;
- public static void sleep(long millisec):在指定的毫秒数内让当前正在执行的线程休眠,静态方法,直接调用;
注意:Java虚拟机允许应用程序并发的运行多个执行线程,利用多线程编程可以编写高效的程序,但线程太多,CPU 花费在上下文的切换的时间将多于执行程序的时间,执行效率反而降低,所以,线程并不是创建的越多越好好,一般来说小到1个,大到10左右基本就够用了。
当然,关于线程的其他知识,如优先级、休眠、终止等,这里就不做介绍了。
(三)synchronized关键字
Java提供了很多方式和工具来帮助简化多线程的开发,如同步方法,即有synchronized关键字修饰的方法,这和Java的内置锁有关。每个Java对象都有一个内置锁,若方法用synchronized关键字声明,则内置锁会保护整个方法,即在调用该方法前,需要获得内置锁,否则就处于阻塞状态。一个简单的同步方法声明如下:
public synchronized void save(){}
synchronized关键字也可以修饰静态方法,此时若调用该静态方法,则会锁住整个类。下面通过实例来说明下具体的使用:
同步线程类:
public class SyncThread implements Runnable {
//定义计数变量并在构造函数中初始化
private static int count;
public SyncThread(){
count = 0;
}
@Override
public synchronized void run() {
for(int i=0;i<5;i++){
//打印当前count值并进行累加操作,可分开写
System.out.println(Thread.currentThread().getName() +":"+ (count++));
try {
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getCount(){
return count;
}
}
测试类:
public class SyncTest {
public static void main(String[] args) {
SyncThread sThread = new SyncThread();
//创建线程对象的同时初始化该线程的名称
Thread t1 = new Thread(sThread,"SThread1");
Thread t2 = new Thread(sThread,"SThread2");
t1.start();
t2.start();
}
}
输出为:
SThread1:0
SThread1:1
SThread1:2
SThread1:3
SThread1:4
SThread2:5
SThread2:6
SThread2:7
SThread2:8
SThread2:9
从上面可以看出:一个线程访问一个对象中的synchronized同步方法时,其他试图访问该对象的线程将被阻塞。当然,大家可以去掉synchronized关键字,看看会有什么不同。这里必须要注意:是访问同一个对象的不同方法,如上面的对象sThread,若是不同的对象,则不受阻塞。这里不做介绍了,大家可以参考:Java中synchronized的用法,好好理解下。
(四)volatile关键字
相比较synchronized而言,volatile关键字是Java提供的一种轻量级的同步机制,为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。
如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。简单的定义如下:
private volatile int count = 0;
volatile不具备原子特性,也不能用来修饰final类型的变量。要使volatile修饰的变量提供理想的线程安全,必须满足两个条件:
- 对变量的写操作不依赖于当前值;
- 变量没有包含在具有其他变量的不变式中;
这里不做详述了,但需要注意一点:避免volatile修饰的变量用于复合操作,如 num++,这个复合操作包括三步(读取->加1->赋值),所以,在多线程的环境下,有可能线程会对过期的num进行++操作,重新写入到主存中,而导致出现num的结果不合预期的情况。
线程间还可以实现通信,这里不做介绍。
Java中的对象使用new操作符创建,若创建大量短生命周期的对象,则性能低下。所以才有了池的技术,如数据库连接有连接池,线程则有线程池。
使用线程池创建对象的时间是0毫秒,说明其高效性。大家感兴趣的可自行查看、了解该块的知识点。
好了,以上概括的就是多线程的基本知识点了,希望帮到大家。