今天被工程师问到了多线程的问题,哎从来没用过多线程直接回答了个不会,感觉很难堪啊,从java多线程开始吧。
一、多线程的概念
其实就是操作系统里面的线程和进程的关系,一个进程有很多线程,线程是进程的执行单元,线程不携带资源,只携带自己的堆栈,计数器和局部变量,多线程编程无非就是编程的时候考虑到了线程的层次,各个线程各自执行一段代码。过去写程序只有一个main函数,程序根据main函数的内容依次顺序执行,而引入多线程编程之后我们的程序就可以做到并发执行了!各个线程之间的调度由进程自己完成,从java的概念来讲线程就是独立并发的执行流。
二、多线程的优点
1. 线程之间可以共享内存,而进程之间不可以共享内存;
2. 线程创建的代价要小的多;
3. java语言内置了多线程的支持,而不是单纯的作为底层操作系统的调用方式;
三、java多线程的实现机制
java的多线程主要是通过Thread()类来实现,多线程程序的生成有以下几种方法:
1. 继承thread类,示例代码如下:
public class FirstThread extends Thread {
private int i;
public void run() {
for(i = 0; i < 100; i++)
{
// 因为FirstThread继承了thread的方法,所以这里可以直接用getName;
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 50)
{
// 虽然启动了两个线程但实际上是3个线程再跑,main函数自带一个main线程;
new FirstThread().start();
new FirstThread().start();
}
}
}
}
2. 重写runable接口,示例代码如下:
public class SecondThread implements Runnable{
private int i;
public void run() {
for(i = 0; i < 100; i++)
{
// 用runnable接口就不能直接用getName()方法了;
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 90)
{
SecondThread st = new SecondThread();
new Thread(st, "线程1").start();
new Thread(st, "线程2").start();
}
}
}
}
两个方法的不同之处在于:方法1是继承的thread类,所以firstThread可以使用Thread的所有方法,因此在重写run()方法的时候就可以直接用thread的方法;重写runnable接口的话因为runnable只有一个函数run(),所以必须要重写run(),还有就是在运行的时候必须把st用thread类的实例来运行。
还有一种接口实现多线程的方式就是callable(),从功能上看callable更像是runable的加强版,它可以使线程具有返回值并抛出异常,在实现的机制上实现callable的类实例化的对象不能直接被系统thread类来调用,他需要事先转化为FutureTask的对象才行。
示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThirdThread implements Callable<Integer>{
private int i;
public Integer call() {
for(i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ThirdThread tt = new ThirdThread();
FutureTask<Integer> ft = new FutureTask<Integer>(tt);
for(int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 50)
{
new Thread(ft, "线程1").start();
}
}
try {
System.out.println("子线程的返回值: " + ft.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
四、java多线程实现方式的比较
从大体来讲多线程实现的方式有两种:继承方式和接口实现方式,下面比较这两种方式的不同。
接口方式:很显然定义类只是实现了runable或callable接口的方法,这样定义类还可以继承其他类,使用更灵活;接口定义方式在实现的时候由系统实现target对象,非常适合多个线程同时处理一份资源的情况。
继承方式:显然继承了Thread类后不能再继承其他的类了;编写比较简单(直接getName()的操作就ok)。
五、线程的生命周期
线程的生命周期内的各状态如下图所示:
关于生命周期的介绍基本和操作系统的无状态间的转换一致。
ps:线程死亡状态包括线程的正常结束(run或call执行完毕),异常抛出和stop强行结束,所以说线程死亡意味着异常抛出这么说是不对的。
六、线程控制
Thread类提供了让一个线程等待另一个线程的方法——join()。示例代码如下:
package firstThread;
public class JoinThread extends Thread{
private int i;
public JoinThread(String name) {
super(name);
}
public void run() {
for(i = 0; i < 100; i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
new JoinThread("新线程").start(); // 这个属于新线程;
for(int i = 0; i < 100; i++) // 这个属于系统的main线程,因为jt.join()发生在main线程里面,所以被挂起的是main线程;
{
if(i == 50)
{
JoinThread jt = new JoinThread("被join的线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
程序的执行过程:开始系统两个线程“新线程”和main线程在并发执行,当执行到i=20时,线程jt启动,因为jt.join()方法所以main线程不能和jt并发将被挂起,所以接下来并发的线程为“新线程”和jt,等到jt执行完后被挂起的main线程才能继续执行。
从线程调度来看,设计多线程程序还真是一门艺术。
join()方法有以下3种重载形式:
1. join() 无限期等待,直到被join的线程结束为止;
2.join(long millis)设置最长等待时间为xxx毫秒;
3.join(long millis, int nanos)基本没什么用,因为操作系统本身也无法精确到毫秒;
七、后台线程
后台线程的概念:专门为其他线程提供服务的线程就是后台线程(JVM的垃圾回收线程就是典型的后台线程)。
特征:如果前台线程全部死亡,那么后台线程自动死亡。
示例代码如下:
package firstThread;
public class DaemonThread extends Thread{
public DaemonThread(String name) {
super(name);
}
public void run() {
for(int i = 0; i < 1000; i++)
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
DaemonThread dt = new DaemonThread("后台线程");
dt.setDaemon(true);
dt.start();
for(int i = 0; i < 10; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
执行结果如下:
main 0
main 1
后台线程 0
后台线程 1
后台线程 2
后台线程 3
后台线程 4
后台线程 5
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
后台线程 6
后台线程 7
后台线程 8
后台线程 9
后台线程 10
后台线程 11
后台线程 12
后台线程 13 // 为什么这里在main执行完之后后台线程还执行了几条呢?
main线程是默认的前台线程,dt被设置为后台线程,dt本来应该执行到999结束但是因为前台线程执行到9就结束之后JVM会主动退出使得整个进程结束,前台线程结束后通知后台线程,这个通知是有一定时延的,所以在前台线程执行完后又执行了几条后台线程。
ps: 后台线程应该是常用先设置后start(), 否则会抛出IllegalThreadStatusException的异常。
八、线程睡眠(sleep)
这个很简单,顾名思义线程睡眠就是让当前的线程阻塞一段时间,由Thread类的sleep()方法来是实现。
常用的sleep()方法的重载形式为static void sleep(long millis),示例代码如下:
import java.util.Date;
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
for(int i = 0; i < 10; i++)
{
System.out.println("当前时间: " + new Date());
Thread.sleep(2000);
}
}
}
执行结果如下:
当前时间: Wed Apr 23 16:29:22 CST 2014
当前时间: Wed Apr 23 16:29:24 CST 2014
当前时间: Wed Apr 23 16:29:26 CST 2014
当前时间: Wed Apr 23 16:29:28 CST 2014
当前时间: Wed Apr 23 16:29:30 CST 2014
当前时间: Wed Apr 23 16:29:32 CST 2014
当前时间: Wed Apr 23 16:29:34 CST 2014
当前时间: Wed Apr 23 16:29:36 CST 2014
当前时间: Wed Apr 23 16:29:38 CST 2014
当前时间: Wed Apr 23 16:29:40 CST 2014
我们可以看到程序中得main线程每隔2s就执行一次。
八、线程让步yield()
yield()和sleep()的功能类似,都是让当前的线程停下来,只不过区别在于yield()让当前线程回到runable状态,而sleep直接使线程blocked。yield()的目的是使cpu重新调度一次,看看有没有比当前线程优先级更高的线程执行。
示例代码如下:
package firstThread;
public class YieldTest extends Thread{
public YieldTest(String name) {
super(name);
}
public void run() {
for(int i = 0; i < 50; i++)
{
System.out.println(getName() + " " + i);
if(i == 10)
{
Thread.yield();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
YieldTest yt1 = new YieldTest("高级");
yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
从理论上讲,yield()只是当前线程放弃cpu,使cpu重新调度,调度的结果只会让相同优先级或者更高优先级的获取cpu,不会使较低线程获得cpu调度。而sleep则能够使任何级别的线程都能得到调度。
sleep()方法具有更好的移植性,一般不建议采用yield来控制线程的并发执行。
九、改变线程的优先级
每个线程的优先级都与创建其的父线程相同,默认情况下main线程具有普通的优先级,其创建的子线程也具有普通的优先级。
示例代码如下:
package firstThread;
public class PriorityTest extends Thread{
public PriorityTest(String name) {
super(name);
}
public void run() {
for(int i = 0; i < 30; i++)
{
System.out.println(getName() + ", 当前的优先级是:" + getPriority() + ", 循环变量的值为:" + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread.currentThread().setPriority(6);
for(int i = 0; i < 30; i++)
{
if(i == 10)
{
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:" + low.getPriority());
low.setPriority(Thread.MIN_PRIORITY);
}
if(i == 20)
{
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:" + high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
java本身提供线程10个优先级别,但是这些优先级别需要得到操作系统的支持,不同的os与java本身的优先级未必匹配和适用,我们应该尽量避免直接设置优先级,应该适用系统提供的MAX_PRIORITY,NORM_PRIORITY和MIN_PRIORITY来设置优先级,以使得java程序能够更好的进行移植。
十、线程同步
先阅读下面的代码,定义个两个用户同时对一个银行账户的取钱操作,这是银行里最简单的一个业务。
package firstThread;
public class Account {
private String accountNO;
private double balance;
public Account() {}
public Account(String accounyNO, double balance) {
this.accountNO = accounyNO;
this.balance = balance;
}
public int hashCode() {
return accountNO.hashCode();
}
public String getAccountNO() {
return accountNO;
}
public void setAccountNO(String accountNO) {
this.accountNO = accountNO;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if(obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNO().equals(accountNO);
}
return false;
}
}
package firstThread;
public class DrawThread extends Thread{
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
public void run() {
if (account.getBalance() > drawAmount) {
System.out.println("取钱成功!吐出钞票:" + drawAmount);
// 如果程序总是在这里停,那么输出的一定是错误的;
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
}
else {
System.out.println("取钱失败,余额不足!");
}
}
}
package firstThread;
public class DrawTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account("123", 1000);
new DrawThread("甲", account, 800).start();
new DrawThread("乙", account, 800).start();
}
}
执行结果如下:
取钱成功!吐出钞票:800.0
取钱成功!吐出钞票:800.0
余额为:200.0
余额为:-600.0
出现了这种意外的错误和情况,这里就是操作系统里学过的多个线程对同一个缓冲区操作造成了结果的不可预知性,我们知道在os里面加入了信号量机制,在java中我们有专门的同步监视器synchronized(),只需要在临界区域加上即可,修改的部分如下:
public void run() {
synchronized (account) {
if (account.getBalance() > drawAmount) {
System.out.println("取钱成功!吐出钞票:" + drawAmount);
// 如果程序总是在这里停,那么输出的一定是错误的;
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
}
else {
System.out.println("取钱失败,余额不足!");
}
}
}
这样运行的结果就没问题了,判断出临界区域加上同步监视器就ok!
同步锁:
java在5之后提供了更为强大的Lock方法,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显示的加锁和释放锁。ReentrantLock可重复锁的意思是可以对上过锁后继续上锁,上锁可以进行嵌套,会有一个计数器来计算套用的层次,上锁的线程会形成一个等待线程队列。
十一、线程池
为什么会有线程池?因为系统启动一个线程的成本比较高,涉及与操作系统的交互。当程序中需要创建大量生存期很短的线程时更应该考虑线程池。
线程池在系统启动的时候会创建大量的空闲线程程序将一个Runable对象或者Callable对象传送给线程池,线程池就会启动一个run()或call()方法,当run()或者call()方法结束后该线程并不会死亡,只是返回线程池重新成为空闲状态,等待下次被启动。
使用线程池执行线程任务如下:
(1)调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;
(2)创建Runable实现类或Callable实现类的实例,作为线程的执行任务;
(3)调用ExecutorsService对象的submit()方法来提交Runable实例或者Callable实例;
(4)当不想提交任何任务时,调用ExecutorsService对象的shutdown()来关闭线程池;
示例代码如下:
package firstThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + "的i值为:" + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
// 去创建一个pool;
ExecutorService pool = Executors.newFixedThreadPool(6);
// 向线程池提交两个线程
pool.submit(new MyThread());
pool.submit(new MyThread());
// 关闭线程池
pool.shutdown();
}
}
ForkJoinPool类支持将一个任务拆分成多个“小任务”并行计算再把多个小任务的结果合并成总的计算结果。
ForkJoinPool类提供了两个常用的构造器,一种是人为指定并行线程个数的ForkJoinPool(int parallelism),一种是以Runtime.availableProcessors()方法作为parallelism参数创建ForkJoinPool。
使用ForkJoinPool类的过程如下:
(1)创建ForkJoinPool类的实例,调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务了,ForkJoinTask有两个抽象子类RecursiveAction和RecursiveTask类,后者代表有返回值的任务,前者有返回值的任务。
以下面打印0-300数值为例加以说明:
package firstThread;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
class PrintTask extends RecursiveAction{
private static final int THRESHOLD = 50;
private int start;
private int end;
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
// TODO Auto-generated method stub
if(end - start < THRESHOLD)
{
for(int i = start; i < end; i++)
{
System.out.println(Thread.currentThread().getName() + "的i值:" + i);
}
}
else {
// 任务数超过50,需要把大任务分解成两个小任务
int middle = (start + end) / 2;
PrintTask leftPrintTask = new PrintTask(start, middle);
PrintTask righPrintTask = new PrintTask(middle, end);
leftPrintTask.fork();
righPrintTask.fork();
}
}
}
public class ForkJoinPoolTest {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0, 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
pool.shutdown();
}
}