一般可以在同一时间内执行多个程序的操作系统都有进程的概念。
一个进程就是一个执行中的程序, 而每一个进程都有自己独立的一块内存空间、一组系统资源
每一个进程的内部数据和状态都是完全独立的。
在Windows操作系统下通过Ctrl+Alt+Del组合键查看进程,在UNIX和 Linux操作系统下通过ps命令查看进程的。
在Windows操作系统中一个进程就是一个exe或者dll程序,它们相互独立,互相也可以通信。
线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序控制的流程。
但与进程不同的是,同类的多个线程是共享一块内存空间和一组系统资源。所以系统在各个线程之间切换时,开销要比进程小的多,线程被称为轻量级进程。
一个进程中可以包含多个线程。
Java程序至少会有一个线程,这就是主线程,程序启动后是由JVM创建主线程,程序结束时由JVM停止主线程。
负责管理子线程,即子线程的启动、挂起、停止等等操作。
public class HelloWorld {
public static void main(String[] args) {
// 获取主线程
Thread mainThread = Thread.currentThread();
System.out.println("主线程名:" + mainThread.getName());
}
}
第5行Thread.currentThread()获得当前线程,在main()方法中当前线程就是主线程
Thread是Java线程类,位于java.lang包中。getName()方法获得线程的名字,主线程名是main,由JVM分配。
Java中创建一个子线程涉及到java.lang.Thread类和java.lang.Runnable接口。
Thread是线程类,创建一 个Thread对象就会产生一个新的线程。
而线程执行的程序代码是在实现Runnable接口对象的run()方法中编写的,实现Runnable接口对象是线程执行对象。
线程执行对象实现Runnable接口的run()方法,run()方法是线程执行的入口,该线程要执行程序代码都在此编写,run()方法称为线程体。
主线程中执行入口是main(String[] args)方法,这里可以控制程序的流程,管理其他的子线程等。
子线程执行入口是线程执行对象(实现Runnable接口对象)的run()方法,在这个方法可以编写子线程相关处理代码。
创建线程Thread对象时,可以将线程执行对象传递给它。
Thread构造方法:
Thread(Runnable target, String name) target是线程执行对象,实现Runnable接口。name为线程指定一个名字。
Thread(Runnable target) target是线程执行对象,实现Runnable接口。线程名字由JVM分配。
实现Runnable接口的线程执行对象Runner:
// 线程执行对象
public class Runner implements Runnable {
// 编写执行线程代码
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n", i,
Thread.currentThread().getName());
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
}
15行的Thread.sleep(sleepTime),休眠当前线程:
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠。
static void sleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠。
public class HelloWorld {
public static void main(String[] args) {
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(new Runner());
// 开始线程t1
t1.start();
// 创建线程t2,参数是一个线程执行对象Runner
Thread t2 = new Thread(new Runner(), "MyThread");
// 开始线程t2
t2.start();
}
}
7行,线程创建完成还需要调用start()方法才能执行,start()方法一旦调用线程进入可以执行状态,可以执行状态下的线程等待CPU调度执行,CPU调用后线程进行执行状态,运行run()方法。
运行结果:
第 0次执行 - MyThread
第 0次执行 - Thread-0
第 1次执行 - Thread-0
第 1次执行 - MyThread
第 2次执行 - MyThread
第 2次执行 - Thread-0
第 3次执行 - MyThread
第 3次执行 - Thread-0
第 4次执行 - Thread-0
第 5次执行 - Thread-0
第 6次执行 - Thread-0
第 4次执行 - MyThread
第 7次执行 - Thread-0
第 5次执行 - MyThread
第 8次执行 - Thread-0
第 6次执行 - MyThread
第 9次执行 - Thread-0
第 7次执行 - MyThread
执行完成! Thread-0
第 8次执行 - MyThread
第 9次执行 - MyThread
执行完成! MyThread
分析运行结果,发现两个线程是交错运行的,感觉就像是两个线程在同时运行。但是实际上一台PC通常就只有一颗CPU,在某个时刻只能是一个线程在运行,而Java语言在设计时就充分考虑到线程的并发调度执行。对于程序员来说,在编程时要注意给每个线程执行的时间和机会,主要是通过让线程休眠的办法(调用sleep()方法)来让当前线程暂停执行,然后由其他线程来争夺执行的机会。如果上面的程序中没有用到sleep()方法,就是第一个线程先执行完毕,然后第二个线程再执行完毕。所以用活sleep()方法是多线程编程的关键。
Thread类也实现了Runnable接口,也可以作为线程执行对象,需要继承Thread类,覆盖run()方法。
自定义线程类MyThread:
// 线程执行对象
public class MyThread extends Thread {
public MyThread() {
super();
}
public MyThread(String name) {
super(name);
}
// 编写执行线程代码
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n", i, getName());
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
sleep(sleepTime);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + getName());
}
}
Thread类构造方法:
Thread(String name) name为线程指定一个名字
Thread() 线程名字是JVM分配的。
public class HelloWorld {
public static void main(String[] args) {
// 创建线程t1
Thread t1 = new MyThread();
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new MyThread("MyThread");
// 开始线程t2
t2.start();
}
}
由于Java只支持单继承,继承Thread类就不能再继承其他父类。当开发一些图形界面的应用时,需要一个类既是一个窗口(继承JFrame)又是一个线程体,那么只能采用实现Runnable接口方式。
如果线程体使用的地方不是很多,可以不用单独定义一个类。可以使用匿名内部类或Lambda表达式直接实现Runnable接口。Runnable中只有一个方法是函数式接口,可以使用Lambda表达式。
public class HelloWorld {
public static void main(String[] args) {
// 创建线程t1,参数是实现Runnable接口的匿名内部类
Thread t1 = new Thread(new Runnable() {
// 编写执行线程代码
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n",
i, Thread.currentThread().getName());
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
});
// 开始线程t1
t1.start();
// 创建线程t2,参数是实现Runnable接口的Lambda表达式
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n",
i, Thread.currentThread().getName());
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}, "MyThread");
// 开始线程t2
t2.start();
}
}
匿名内部类和Lambda表达式代码虽然很多,但是它只是一个参数,实现了Runnable接口线程执行对象。
在线程的生命周期中,线程会有几种状态:
新建状态
新建状态(New)是通过new等方式创建线程对象,它仅仅是一个空的线程对象。
就绪状态
当主线程调用新建线程的start()方法后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行run()方法,它必须等待CPU的调度。
运行状态
CPU调度就绪状态的线程,线程进入运行状态(Running),处于运行状态的线程独占CPU,执行run()方法。
阻塞状态
因为某种原因运行状态的线程会进入不可运行状态,即阻塞状态(Blocked),处于阻塞状态的线程JVM系统不能执行该线程,即使CPU空闲,也不能执行该线程。
几个原因会导致线程进入阻塞状态:
当前线程调用sleep()方法,进入休眠状态。
被其他线程调用了join()方法,等待其他线程结束。
发出I/O请求,等待I/O操作完成期间。
当前线程调用wait()方法。
处于阻塞状态可以重新回到就绪状态,如:休眠结束、其他线程加入、I/O操作完成和调用notify或notifyAll唤醒wait线程。
死亡状态
线程退出run()方法后,就会进入死亡状态(Dead),线程进入死亡状态有可能是正常实现完成run()方法进入,也可能是由于发生异常而进入的。
线程的难点…
线程的调度程序根据线程决定每次线程应当何时运行,Java提供了10种优先级,分别用1~10整数表 示,
最高优先级是10,用常量MAX_PRIORITY表示;最低优先级是1,用常量MIN_PRIORITY;默认优先级是5,用常量NORM_PRIORITY表示。
Thread类提供了setPriority(int newPriority)方法来设置线程优先级,通过getPriority()方法获得线程优先级。
设置线程优先级实例:
public class HelloWorld {
public static void main(String[] args) {
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(new Runner());
t1.setPriority(Thread.MAX_PRIORITY);
// 开始线程t1
t1.start();
// 创建线程t2,参数是一个线程执行对象Runner
Thread t2 = new Thread(new Runner(), "MyThread");
t2.setPriority(Thread.MIN_PRIORITY);
// 开始线程t2
t2.start();
}
}
第6行设置线程t1优先级最高,第11行设置线程t2优先级最低。
多次运行上面的示例会发现,t1线程经常先运行,但是偶尔t2线程也会先运行。说明影响线程获得CPU时间的因素,除了线程优先级外,还与操作系统有关。
当前线程调用t1线程的join()方法,则阻塞当前线程,等待t1线程结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。
Thread类提供了多个版本的join():
void join() 等待该线程结束。
void join(long millis) 等待该线程结束的时间最长为millis毫秒。如果超时为0意味着要一直等下去。
void join(long millis, int nanos) 等待该线程结束的时间最长为millis毫秒加nanos纳秒。
public class HelloWorld {
//共享变量
static int value = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程 开始...");
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(() -> {
System.out.println("ThreadA 开始...");
for (int i = 0; i < 2; i++) {
System.out.println("ThreadA 执行...");
value++;
}
System.out.println("ThreadA 结束...");
}, "ThreadA");
// 开始线程t1
t1.start();
// 主线程被阻塞,等待t1线程结束
t1.join();
System.out.println("value = " + value);
System.out.println("主线程 结束...");
}
}
运行结果:
主线程 开始…
ThreadA 开始…
ThreadA 执行…
ThreadA 执行…
ThreadA 结束…
value = 2
主线程 结束…
23行在当前线程(主线程)中调用t1的join()方法,因此会导致主线程阻塞,等待t1线程结束。
若去掉23行,则输出结果为:
主线程 开始…
value = 0
主线程 结束…
ThreadA 开始…
ThreadA 执行…
ThreadA 执行…
ThreadA 结束…
使用join()方法的场景:一个线程依赖于另外一个线程的运行结果,所以调用另一个线程的join()方法等它运行完成。
线程类Thread提供静态方法yield(),调用yield()方法能够使当前线程给其他线程让步。它类似于 sleep()方法,能够使运行状态的线程放弃CPU使用权,暂停片刻,然后重新回到就绪状态。
与sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级高低都有机 会运行。而yield()方法只给相同优先级或更高优先级线程机会。
// 线程执行对象
public class Runner implements Runnable {
// 编写执行线程代码
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n",
i, Thread.currentThread().getName());
Thread.yield(); // 使当前线程让步
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
}
yield()方法只能给相同优先级或更高优先级的线程让步,yield()方法在实际开发中很少使用,大多都使用sleep()方法,sleep()方法可以控制时间,而yield()方法不能。
线程体中的run()方法结束,线程进入死亡状态,线程就停止了。但是有些业务比较复杂,例如想开发 一个下载程序,每隔一段执行一次下载任务,下载任务一般会由子线程执行的,休眠一段时间再执行。这个下载子线程中会有一个死循环,但是为了能够停止子线程,设置一个结束变量。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class HelloWorld {
private static String command = ""; // 设置结束变量
public static void main(String[] args) {
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(() -> {
// 一直循环,直到满足条件在停止线程
while (!command.equalsIgnoreCase("exit")) {
// 线程开始工作
// TODO
System.out.println("下载中...");
try {
// 线程休眠
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成!");
});
// 开始线程t1
t1.start();
try (InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(ir)) {
// 从键盘接收了一个字符串的输入
command = in.readLine();
} catch (IOException e) {
}
}
}
第12行是在子线程的线程体中判断,用户输入的是否为exit字符串,如果不是则进行循环,否则结束循环,结束循环就结束了run()方法,线程就停止了。
第30行中的System.in是一个很特殊的输入流,能够从控制台(键盘)读取字符。第33行是通过流System.in读取键盘输入的字符串。
测试时:在控制台输入exit,然后敲Enter键。
控制线程的停止有人会想到使用Thread提供的stop()方法,这个方法已经不推荐使用了,这个方法有时会引发严重的系统故障,类似还有suspend()和resume()挂起方法。Java现在推荐的做法就是采用本例的结束变量方式。
在多线程环境下,访问相同的资源,有可以会引发线程不安全问题。
多一个线程同时运行,有时线程之间需要共享数据,一个线程需要其他线程的数据,否则就不能保证 程序运行结果的正确性。
一个模拟销售机票系统(每一天机票数量是有限的,很多售票点同时销售这些机票)
//机票数据库
public class TicketDB {
// 机票的数量
private int ticketCount = 5;
// 获得当前机票数量
public int getTicketCount() {
return ticketCount;
}
// 销售机票
public void sellTicket() {
try {
// 等待用户付款
// 线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.printf("第%d号票,已经售出\n", ticketCount);
ticketCount--;
}
}
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 创建线程t1
Thread t1 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
});
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
});
// 开始线程t2
t2.start();
}
}
可能运行结果:(每次结果不同)
第5号票,已经售出
第5号票,已经售出
第3号票,已经售出
第3号票,已经售出
第1号票,已经售出
第0号票,已经售出
创建两个线程,模拟两个售票网点。
问题:同一张票重复销售、出现第0号票和5张票卖了6次。这些问题的根本原因是多个线程间共享的数据导致数据的不一致性。
多个线程间共享的数据称为共享资源或临界资源,由于是CPU负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
为了防止多线程对临界资源的访问有时会导致数据的不一致性,Java提供了“互斥”机制,可以为这些资源对象加上一把“互斥锁”,在任一时刻只能由一个线程访问,即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象,这就是多线程同步。
线程同步是保证线程安全的重要手段,但是线程同步客观上会导致性能下降。
可以通过两种方式实现线程同步,都涉及到synchronized关键字:
synchronized方法,使用synchronized关键字修饰方法,对方法进行同步;
synchronized语句,使用 synchronized关键字放在对象前面限制一段代码的执行。
synchronized关键字修饰方法实现线程同步,方法所在的对象被锁定。
// 机票数据库
public class TicketDB {
// 机票的数量
private int ticketCount = 5;
// 获得当前机票数量
public synchronized int getTicketCount() {
return ticketCount;
}
// 销售机票
public synchronized void sellTicket() {
try {
// 等待用户付款
// 线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.printf("第%d号票,已经售出\n", ticketCount);
ticketCount--;
}
}
8行13行方法前都使用了synchronized关键字,表明这两个方法是同步的,被锁定的,每一个时刻只能由一个线程访问。
并不是每一个方法都有必要加锁的,要仔细研究加上的必要性,上述代码第8行加锁可以防止出现第0号票情况和5张票卖出6次的情况;代码第13行加锁是防止出现销售两种一样的票。
synchronized语句方式主要用于第三方类,不方便修改它的代码情况。
可以不用修改TicketDB.java类,只修改调用代码HelloWorld.java实现同步。
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 创建线程t1
Thread t1 = new Thread(() -> {
while (true) {
synchronized (db) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
}
});
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new Thread(() -> {
while (true) {
synchronized (db) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
}
});
// 开始线程t2
t2.start();
}
}
10行28行,使用synchronized语句,将需要同步的代码用大括号括起来。synchronized 后有小括号,将需要同步的对象括起来。
如果两个线程之间有依 赖关系,线程之间必须进行通信,互相协调才能完成工作。
(例如有一个经典的堆栈问题,一个线程生成了一些数据,将数据压栈;另一个线程消费了这些数据, 将数据出栈。这两个线程互相依赖,当堆栈为空时,消费线程无法取出数据时,应该通知生成线程添 加数据;当堆栈已满时,生产线程无法添加数据时,应该通知消费线程取出数据)
为了实现线程间通信,需要使用Object类中声明的5个方法:
void wait() 使当前线程释放对象锁,然后当前线程处于对象等待队列中阻塞状态,等待其他线程唤醒。
void wait(long timeout) 同wait()方法,等待timeout毫秒时间。
void wait(long timeout, int nanos) 同wait()方法,等待timeout毫秒加nanos纳秒时间。
void notify() 当前线程唤醒此对象等待队列中的一个线程,该线程将进入就绪状态。
void notifyAll() 当前线程唤醒此对象等待队列中的所有线程,这些线程将进入就绪状态。
(线程有多种方式进入阻塞状态,除了通过wait()外,还有加锁的方式和其他方式,加锁方式是前面的使用synchronized加互斥锁;其他方式线程状态时介绍的方式。)
消费和生产示例中堆栈类:
同步堆栈类:
// 堆栈类
class Stack {
// 堆栈指针初始值为0
private int pointer = 0;
// 堆栈有5个字符的空间
private char[] data = new char[5];
// 压栈方法,加上互斥锁
public synchronized void push(char c) {
// 堆栈已满,不能压栈
while (pointer == data.length) {
try {
// 等待,直到有数据出栈
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他线程把数据出栈
this.notify();
// 数据压栈
data[pointer] = c;
// 指针向上移动
pointer++;
}
// 出栈方法,加上互斥锁
public synchronized char pop() {
// 堆栈无数据,不能出栈
while (pointer == 0) {
try {
// 等待其他线程把数据压栈
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他线程压栈
this.notify();
// 指针向下移动
pointer--;
// 数据出栈
return data[pointer];
}
}
10行压栈方法push(),同步方法,在该方法中首先判断是否堆栈已满,如果已满不能压栈,调用this.wait()让当前线程进入对象等待状态中。如果堆栈未满,程序会往下运行调用this.notify()唤醒对象等待队列中的一个 线程。
调用:
public class HelloWorld {
public static void main(String args[]) {
Stack stack = new Stack();
// 下面的消费者和生产者所操作的是同一个堆栈对象stack
// 生产者线程
Thread producer = new Thread(() -> {
char c;
for (int i = 0; i < 10; i++) {
// 随机产生10个字符
c = (char) (Math.random() * 26 + 'A');
// 把字符压栈
stack.push(c);
// 打印字符
System.out.println("生产: " + c);
try {
// 每产生一个字符线程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
char c;
for (int i = 0; i < 10; i++) {
// 从堆栈中读取字符
c = stack.pop();
// 打印字符
System.out.println("消费: " + c);
try {
// 每读取一个字符线程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
producer.start(); // 启动生产者线程
consumer.start(); // 启动消费者线程
}
}