1进程和线程
进程:一个进程就是一个执行中的程序。每一个进程都有自己独立的一块内存空间,一组系统资源。
线程:线程就是进程中的一个负责程序执行的控制单元(执行路径)。同类的多个线程是共享一块内存空间和一组系统资源。所以系统在各个线程之间切换时,开销要比进程小得多,正因如此,线程被称为轻量级进程。一个进程中可以包含多个线程。
Java程序至少会有一个线程,这就是主线程,程序启动后由JVM创建主线程,程序结束时由JVM停止主线程。主线程负责管理子线程,即子线程的启动,挂起,停止等操作。
获取主线程示例代码如下:
package duoxiancheng;
public class HelloThread {
public static void main(String[] args) {
//获取主线程
Thread mainThread=Thread.currentThread();
System.out.println("主线程名:"+mainThread.getName());
}
}
Thread.currentThread()可以获得当前线程对象,getName()是Thread类的实例方法,可以获得线程的名字。
2创建子线程
Java中创建一个子线程涉及java.lang.Thread类和java.lang.Runnable接口。
Thread是线程类,创建一个Thread对象就会产生一个新的线程。
实现Runnable接口对象是线程执行对象,需要实现run()方法。子线程处理代码放到run()方法中,run()方法称为线程体。
有两种方式可以创建子线程:
①实现Runnable接口,实现run()方法。
②继承Thread类,重写run()方法。
2.1实现Runnable接口
创建线程Thread对象时,可以将线程执行对象传递给它,这需要用到Thread类的两个构造方法:
Thread(Runnable target):target是线程执行对象,实现Runnable接口。线程名字是由JVM分配的。
Thread(Runnable target, String name) :target是线程执行对象,实现Runnable接口。name是线程名字。
下面看一个具体示例。实现Runnable接口的线程执行对象Runner代码如下:
package duoxiancheng;
//线程执行对象
public class Runner implements Runnable {
//编写执行线程代码
@Override
public void run() {
for(int i=1;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) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成!"+Thread.currentThread().getName());
}
}
代码Thread.sleep(sleepTime)是休眠当前线程,sleep是静态方法。它有两个版本:
static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
static void sleep(long millis, int nanos):导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行)。
测试程序HelloThread代码如下:
package duoxiancheng;
public class HelloThread {
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();
}
}
线程创建完成还需要调用start()方法才能执行,start()方法一旦调用,线程进入可以执行状态,可以执行状态下的线程等待CPU调度执行,CPU调度后线程进入执行状态,运行run()方法。
运行结果:
2.2继承Thread线程类
事实上,Thread类也实现了Runnable接口,所以Thread类也可以作为线程执行对象,这需要继承Thread类覆盖run()方法。
采用继承Thread类重新实现2.1节示例。自定义线程类MyThread代码如下:
package duoxiancheng;
//线程执行对象
public class MyThread extends Thread {
public MyThread()
{
super();
}
public MyThread(String name)
{
super(name);
}
//编写执行线程代码
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=1;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) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成!"+Thread.currentThread().getName());
}
}
测试程序HelloThread代码如下:
package duoxiancheng;
public class HelloThread {
public static void main(String[] args) {
//创建线程t1
Thread t1=new MyThread();
//开始线程t1
t1.start();
//创建线程t2
Thread t2=new MyThread("MyThread");
//开始线程t2
t2.start();
}
}
2.3使用匿名内部类和Lambda表达式实现线程体
如果线程体使用的地方不是很多,可以不用单独定义一个类。可以使用匿名内部类或Lambda表达式实现Runnable接口。
函数式接口,匿名内部类可以使用lamda表达式。
重新实现2.1节示例.代码如下:
package duoxiancheng;
public class HelloThread {
public static void main(String[] args) {
//创建线程t1,参数是实现Runnable接口的匿名内部类
Thread t1=new Thread(new Runnable() { //①
//编写执行线程代码
@Override
public void run() {
for(int i=1;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) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成!"+Thread.currentThread().getName());
}
});
//开始线程t1
t1.start();
//创建线程t2,参数是实现Runnable接口的Lambda表达式
Thread t2=new Thread(()->{ //②
for(int i=1;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) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成!"+Thread.currentThread().getName());
},"MyThread");
//开始线程t2
t2.start();
}
}
上述代码第①行采用匿名内部类实现Runnable接口,覆盖run()方法。这里使用的是Thread(Runnable target)构造方法。代码第②行采用Lambda表达式实现Runnable接口,覆盖run()方法。这里使用的是Thread(Runnable target, String name)构造方法,Lambda表达式是它的第一个参数。
3线程状态
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
1.新建状态
新建状态(new)是用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码,它仅仅是一个空的线程对象。
2.就绪状态
当主线程调用新线程的start()方法后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行run()方法,它必须等待CPU的调度。
3.运行状态
CPU调度就绪状态的线程,线程进入运行状态(running),处于运行状态的线程独占CPU,执行run()方法。
4.阻塞状态
因为某种原因进入运行状态的线程会进入不可运行状态,即阻塞状态(blocked),处于阻塞状态的线程JVM系统不能执行,即使CPU空闲,也不能执行该线程。线程运行过程中,可能由于各种原因进入阻塞状态:
①当前线程调用sleep()方法,进入睡眠状态。
②当前线程调用wait()方法。除非线程收到nofify()或者notifyAll()消息,否则不会变成就绪态。
③被其他线程调用了join()方法,等待其他线程结束。
④发出I/O请求,等待I/O操作完成期间。
处于阻塞状态可以重新回到就绪状态,如休眠结束,其他线程加入,I/O操作完成,调用notify()或notifyAll()唤醒wait线程。
5.死亡状态
线程退出run()方法后,就会进入死亡状态(dead)。线程进入死亡状态有可能是正常执行完成run()方法后进入,也有可能是由于发生异常而进入的。
4线程管理
4.1线程优先级
Java提供了10种优先级,分别用1~10整数表示,最高优先级是10,用常量MAX_PRIORITY表示;最低优先级是1,用常量MIN_PRIORITY表示;默认优先级是5,用常量NORM_PRIORITY表示。
Thread类提供了setPriority(int newPriority)方法用以设置线程优先级,通过getPriority()方法可以获得线程优先级。
设置线程优先级示例代码如下:
package duoxiancheng;
public class HelloThread2 {
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());
t2.setPriority(Thread.MIN_PRIORITY);
//开始线程t2
t2.start();
}
}
多次运行上面的示例会发现,t1线程经常先运行,但是偶尔t2线程也会先运行。这些现象说明,影响线程获得CPU时间的因素,除了线程优先级外,还与操作系统有关。
4.2等待线程结束
在介绍线程状态时提到过join()方法,当前线程调用t1线程的join()方法,则阻塞当前线程,等待t1线程结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。
Thread类提供了多个版本的join(),其定义如下:
void join() :等待这个线程结束。
void join(long millis) :等待这个线程结束的时长最多 millis毫秒。
void join(long millis, int nanos) :等待该线程结束的时间最长为 millis毫秒加上 nanos纳秒。
使用join()方法的场景是,一个线程依赖于另外一个线程的运行结果,所以调用另一个线程的join()方法等它运行完成。
4.3线程让步
线程类Thread还提供一个静态方法yield(),调用yield()方法能够使当前线程给其他线程让步。它类似于sleep()方法,能够使运行状态的线程放弃CPU使用权,暂停片刻,然后重新回到就绪状态。与sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级高低都有机会运行。而yield()方法只给相同优先级或更高优先级线程机会。yield()方法在实际开发中很少使用,大都使用sleep()方法,sleep()方法可以控制时间,而yield()方法不能。
4.4线程停止
线程体中的run()方法结束,线程进入死亡状态,线程就停止了。但是有些业务比较复杂,例如想开发一个下载程序,每隔一段执行一次下载任务,下载任务一般会由子线程执行,休眠一段时间再执行。这个下载子线程中会有一个死循环,为了能够停止子线程,设置一个结束变量。
示例代码如下:
package duoxiancheng;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class HelloThread3 {
public static String command=""; //①
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建线程t1,参数是一个线程执行对象Runner
Thread t1=new Thread(()->{
//一直循环,直到满足条件再停止线程
while(!command.equalsIgnoreCase("exit")) //②
{
//线程开始工作
//TODO
System.out.println("下载中......");
try {
//线程休眠
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//线程执行结束
System.out.println("执行完成!");
});
//开始线程t1
t1.start();
try(InputStreamReader ir=new InputStreamReader(System.in); //③
BufferedReader in=new BufferedReader(ir)){
//从键盘接收了一个字符串的输入
command=in.readLine(); //④
} catch (IOException e) {
e.printStackTrace();
}
}
}
上述代码第①行是设置一个结束变量。代码第②行是在子线程的线程体中判断用户输入的是否为exit字符串,如果不是则进行循环,否则结束循环,结束循环就结束了run()方法,线程就停止了。
代码第③行中的System.in是一个很特殊的输入流,能够从控制台(键盘)读取字符。代码第④行是通过流System.in读取键盘输入的字符串。
运行结果:
注意:控制线程的停止有人会想到使用Thread提供的stop()方法,这个方法已经不推荐使用,因为这个方法有时会引发严重的系统故障,类似还有suspend()和resume()挂起方法。Java现在推荐的做法就是采用本例的结束变量方式。
5线程安全
5.1临界资源问题
多个线程间共享的数据称为共享资源或临界资源,由于是CPU负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
5.2多线程同步
为了防止多线程对临界资源的访问有时会导致数据的不一致性,Java提供了“互斥”机制,可以为这些资源对象加上一把“互斥锁”,在任一时刻只能由一个线程访问,即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象,这就是多线程同步。线程同步是保证线程安全的重要手段,但是线程同步客观上会导致性能下降。
可以使用synchronized关键字通过两种方式实现线程同步:一种是synchronized方法,使用synchronized关键字修饰方法,对方法进行同步;另一种是synchronized语句,将synchronized关键字放在对象前面限制一段代码的执行。
1.synchronized方法
synchronized关键字修饰方法实现线程同步,方法所在的对象被锁定。
以售票系统为例。TicketDB文件代码如下:
package duoxiancheng;
//机票数据库
public class TicketDB {
//机票的数量
private int ticketCount=5;
//获得当前机票数量
public synchronized int getTicketCount()
{
return ticketCount;
}
//销售机票
public synchronized void sellTicket()
{
try {
//线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("第%d号票,已经售出\n",ticketCount);
ticketCount--;
}
}
调用代码如下:
package duoxiancheng;
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();
}
}
2.synchronized语句
synchronized语句方式主要用于第三方类,不方便修改它的代码情况。其TicketDB.java类方法可以不用加synchronized关键字,但调用代码HelloWorld.java需做修改:
package duoxiancheng;
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();
}
}
6线程间通信
如果两个线程之间有依赖关系,线程之间必须进行通信,互相协调才能完成工作。
例如有一个经典的堆栈问题,一个线程生成了一些数据,将数据压栈;另一个线程消费了这些数据,将数据出栈。这两个线程互相依赖,当堆栈为空,消费线程无法取出数据时,应该通知生成线程添加数据;当堆栈已满,生产线程无法添加数据时,应该通知消费线程取出数据。
为了实现线程间通信,需要使用Object类中声明的5个方法:
void wait():等待当前线程释放对象锁,然后当前线程处于对象等待队列中阻塞状态,如下图所示,等待其他线程唤醒。
void wait(long timeout):同wait()方法,等待timeout毫秒时间。
void wait(long timeout,int nanos):同wait()方法,等待timeout毫秒加nanos纳秒时间。
void notify():当前线程唤醒此对象等待队列中的一个线程,如下图所示,该线程将进入就绪状态。
void notifyAll():当前线程唤醒此对象等待队列中的所有线程,如下图所示,这些线程将进入就绪状态。
下面是消费和生产示例中堆栈类代码:
package duoxiancheng;
//堆栈类
public 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) {
e.printStackTrace();
}
}
//通知其他线程把数据出栈
this.notify();
//数据压栈
data[pointer]=c;
//指针向上移动
pointer++;
}
//出栈方法,加上互斥锁
public synchronized char pop()
{
//堆栈无数据,不能出栈
while(pointer==0)
{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知其他线程压栈
this.notify();
//指针向下移动
pointer--;
//数据出栈
return data[pointer];
}
}
调用代码如下:
package duoxiancheng;
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) {
e.printStackTrace();
}
}
});
//消费者线程
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) {
e.printStackTrace();
}
}
});
producer.start(); //启动生产者线程
consumer.start(); //启动消费者线程
}
}