名词解释
1. 主线程:
JVM调用程序main()所产生的线程。
2. 当前线程:
这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
3. 守护线程:
指为其他线程提供服务的线程,称为守护线程。JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
4. 用户线程:
是指接受守护线程服务的线程,用户线程和守护线程,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
生命周期
上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括:
1.新建(New)
当线程对象对创建后,即进入了新建状态。
如:Thread t = new MyThread();
2.就绪(Runnable)
当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3.运行(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
重要说明 :
就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4.阻塞(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
(1).等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
(2).同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
(3).其他阻塞: 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.销毁(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
实现方式
1. Thread类
通过扩展java.lang.Thread类来实现多线程的方式一直是比较传统的实现方案,此方案的特点是简单明了。扩展类很好的继承了Thread的所有属性和方法。
class TheadA extends Thread {
public TheadA(String tname) {
super(tname);
}
@Override
public void run() {
System.out.print("执行内容:");
for (int i = 0; i < 10; i++) {
System.out.print(i);
}
System.out.println();
}
}
@Test
public void testStart() throws InterruptedException {
Thread t = new TheadA("T1");
t.start();
Thread.sleep(1000);
}
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。实际上所有的多线程代码执行顺序都是随机的。
重要的方法
1) Start线程准备就绪
Thread类的start()方法用于开始执行线程。
2) Sleep线程阻塞
Thread.sleep(time); 当前线程会休息指定的时间段,进入Time_Waiting状态,不会释放锁。
3) isAlive() 获取当前线程是否被激活。
线程已经调用了start方法且没有结束称为被激活。
如果当前线程处于被激活,线程State可不一定对应Runnable,还有可能是Waiting,Timed_Waiting,Blocked等。如果当前线程处于未激活,线程State对应于New,Terminated两种状态。
4) isInterrupted()当前线程中断标志
调用线程此方法只是为当前线程打上可中断标志,但并不会对线程产生任何影响,更不会影响线程运行。一般此方法需要结合业务使用。
public void run() {
while(!isInterrupted()){
//do task
}
}
如上形式如果调用interrupt()方法就中断线程运行。
**5) join()、join(1000) **
调用该方法后,当前线程会阻塞直到线程t结束或者等待指定时间.但当前线程被interrupt(调用join前或者后)会抛出InterruptedException异常。该方法可以用于等待若干个线程执行完毕后再进行后续动作。
6) setDaemon
设置线程为守护线程或者用户线程,必须在线程调用start前设置.
当只剩下守护线程时,JVM可以exit。
7) Interrupted
Thread.interrupted() 当前线程的是否interrupted 状态为true.并且该方法会清空该状态。
8) Yield()释放CPU资源。
yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。因为在简单测试环境下CPU资源很快就又分配给此线程。
9) 其它方法
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
2. Runnable接口
采用实现Runnable接口的多线程实现方式是当前比较主流的方式。在实际项目中通过实现Runnable接口将需要在子线程中运行的逻辑与主线程分离,这样可以充分利用服务器的CPU运算能功。达到提升应用系统性能的目标。
class TheadB implements Runnable {
private String name;
private boolean interrupted=false;
private boolean isInterrupted() {
return interrupted;
}
public TheadB(String tname) {
this.name = tname;
}
@Override
public void run() {
int i = 1;
try {
while (!isInterrupted()) {
System.out.println(String.format("%s.线程:%s>%s", i++, name, StringUtil.toDateString()));
Thread.currentThread().join(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Test
public void testStart() throws InterruptedException {
Thread t2 = new Thread(new TheadB("TB"));
t2.start();
Thread.sleep(3000);
}
3. Callable接口
Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。获取子线程执行结果时会阻塞主进程的继续往下执行,当然如果不调用就不会阻塞。
class TheadC implements Callable<String> {
private String name;
public TheadC(String tname) {
this.name = tname;
}
@Override
public String call() throws Exception {
try {
for (int i = 0; i < 10; i++) {
System.out.println(String.format("%s.线程:%s>%s", i + 1, name, StringUtil.toDateString()));
Thread.currentThread().join(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return "finished";
}
}
@Test
public void testStart() throws Exception {
// 1.执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
FutureTask<String> result = new FutureTask<String>(new TheadC("TC"));
Thread t3 = new Thread(result);
t3.start();
//等所有线程执行完,获取值,因此FutureTask 可用于 闭锁
String ss = result.get();
System.out.println("线程执行结果:" + ss);
}
Thread和Runnable的区别
实现Runnable接口比继承Thread类所具有的优势:
1)适合多个相同的程序代码的线程去处理同一个资源
2)可以避免java中的单继承的限制
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4)线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
线程同步
多线程应用在实际运行中由于可对共享数据区产生并发影响,可能会导致数据脏读。例如银行业务中“A线程”负责定期增加账户余额,“B线程”负责定期减账户余额。假如现有余额为100元,A、B线程同时获取现有余额100,A增加10元后得到新余额为110元。B减少10元后得到新余额90元。这就说明A、B线程在此过程中都获取到了脏数据。最后计算的结果也是错误的。要解决此问题必须要想办法让多线程进行数据同步。
public class TestBankThread {
/**
* @余额
*/
private int balance = 100;
///处理存款
class ThreadA implements Runnable {
@Override
public void run() {
try {
for (int i=0;i<5;i++) {
System.out.print(Thread.currentThread().getName() + " -> 余额:" + balance);
balance = balance -10;
System.out.print(", 最新:" + balance);
System.out.println();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
///处理取款
class ThreadB implements Callable {
@Override
public Object call() throws Exception {
for (int i=0;i<5;i++) {
if (balance > 10) {
System.out.print(Thread.currentThread().getName() + " -> 余额:" + balance);
balance = balance -10;
System.out.print(", 最新:" + balance);
System.out.println();
}
Thread.sleep(500);
}
return null;
}
}
@Test
public void testBalance() throws Exception {
Thread t1=new Thread(new ThreadA(),"Thread1");
t1.start();
Thread t2=new Thread(new FutureTask(new ThreadB()),"Thread2");
t2.start();
Thread t3=new Thread(new ThreadA(),"Thread3");
t3.start();
t1.join();
t2.join();
System.out.println("主线程计算完毕!");
}
}
以上代码运行结果如下:
可以发现Thread1线程和Thread3线程对balance数据产生了严重脏读,各自的计算结果都不对。这就是多线程应用的弊端。好在Java已经为我们提供了解决方案就是线程同步。
同步代码块
针对生产并生的代码块进行加锁,这样线程要访问共享区的数据时就会先尝试获取锁,如果共享区的数据正在被其它线程访问,哪么当前线程就会进入锁等待区。等到共享区的被释放时由CPU调度机制决定哪个线程开始访问数据。同时再次锁住共享区的数据。
我们对事例程序做如下修改,增加一个负责数据计算方法。
public class TestBankThread {
/**
* @余额
*/
private int balance = 100;
private void compute(int v) {
synchronized (this) {
System.out.print(Thread.currentThread().getName() + " -> 余额:" + balance);
balance = balance + v;
System.out.print(", 最新:" + balance);
System.out.println();
}
}
///处理存款
class ThreadA implements Runnable {
@Override
public void run() {
try {
for (int i=0;i<5;i++) {
compute(10);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
///处理取款
class ThreadB implements Callable {
@Override
public Object call() throws Exception {
for (int i=0;i<5;i++) {
if (balance > 10) {
compute(-10);
}
Thread.sleep(500);
}
return null;
}
}
@Test
public void testBalance() throws Exception {
Thread t1=new Thread(new ThreadA(),"Thread1");
t1.start();
Thread t2=new Thread(new FutureTask(new ThreadB()),"Thread2");
t2.start();
Thread t3=new Thread(new ThreadA(),"Thread3");
t3.start();
t1.join();
t2.join();
System.out.println("主线程计算完毕!");
}
}
再次运行测试程序的结果如下:
可以发现所有线程对数据的作用都正确了。
此解决方案需要注意的是加锁的代码块要最小,因为所有线程都在等待执行共享代码块,如果代码块太大可能会影响应用的性能。
同步方法
此解决方案效果和同步代码块一样,只是需要在并发操作的方法上增加“synchronized”关键字,告诉编译器此方法是多线程同步执行方法在执行的时候需要自动加锁。
private synchronized void compute(int v) {
System.out.print(Thread.currentThread().getName() + " -> 余额:" + balance);
balance = balance + v;
System.out.print(", 最新:" + balance);
System.out.println();
}
同步变量
在多线程应用中为了提高效率,数据变量会被线程进行复制使用,还是拿上面的实例代码分析,如表示余额的成员变量balance,线程Thread1和线程Thread2各自对balance的访问其实是访问的一个副本。只在某些动作时才进行同步。因此存数据不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存。
private volatile int balance = 100;
知识总结
多线程应用可以充分利用服务器CPU资源,整体提高应用处理性能,但开发思路与传统应用有很大区别,并发思维更像一种立体思维。需要我们慢慢品味。