<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流!
一、线程的概念
进程是一个正在执行中的程序,每一个进程执行都有一个执行顺序,或者叫一个控制单元,线程就是进程中的一个独立的控制单元,线程在控制着进程的执行,一个进程中可能同时有多个线程在运行,比如迅雷下载。一个进程中至少有一个线程,JavaVM启动时会有一个进程java.exe,该进程至少有一个线程, 负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程 。(其实更细节说明JVM,JVM启动不至一个线程,还有负责垃圾回收机制的线程。)CPU某一时刻处理一个线程,并且在线程间做着高速的切换,让我们感觉各线程是在同时运行。多核可以实现真正的同时处理,其瓶颈就是内存大小。
二、线程的创建
1、第一种方式:继承Thread类
通过对API的查找,Java已经提供了对线程这类事物的描述,就是java.lang包中的Thread类,可以通过继承Thread类来创建线程。
步骤:
(1)定义类继承Thread
(2)复写Thread类中的run方法
(3)调用线程的start方法,该方法有两个作用:启动线程,调用run方法
之所以要在start方法中调用run来运行线程,我想应该是start方法在调用run方法之前,通过调用操作系统函数做了诸如分配线程资源、进入线程队列之类的操作,那么为什么JVM不直接在编译时直接在run方法代码中加上这些过程呢?我想可能是Java设计者们故意留下了一种可以直接调用run方法而不启动线程的方式。
class ThreadDemo extends Thread{
public void run(){
for(int i=0;i<100;i++)
System.out.println(getName()+"-----HelloWorld!-----"+i);
}
public static void main(String[] args){
Thread t1=new ThreadDemo();
Thread t2=new ThreadDemo();
t1.start();
t2.start();
}
}
程序运行结果:
多次运行后,发现运行结果每一次都不同,因为多个线程都可以获取CPU执行权,CPU执行到谁,谁就执行在某一时刻,只能有一个线程在运行,CPU在做着快速的切换,以达到看上去是同时运行的效果,我们可以形象地把多线程的运行行为看成是在互相抢夺CPU的执行权。这就是多线程的一个特性:随机性,谁抢到谁执行,至于执行多长,CPU说了算。
start方法与run方法的区别:start开启线程并执行该线程的run方法,run仅仅是对象调用方法,而线程创建了,并没有运行。
上个程序中,如果改成t1.run();t2.run(),程序结果变为t1先执行完输出100个“HelloWorld”,t2再输出100个“HelloWorld”,没有达到同步效果。
线程有如下四种状态:
new() start() sleep()或wait()
————>被创建————>运行————————>冻结
<———————
sleep时间到或notify()
stop()或run方法结束
——————————> 消亡
此外还可以有临时状态或叫做阻塞状态:具备运行资格,但没有执行权,线程冻结意味着线程放弃了执行资格,同时也就放弃了执行权,当sleep时间到或线程被notify后,线程转为临时状态,获得执行资格,等待执行权进入运行状态。
getName():获得线程名
线程有自己的默认名:Thread+编号,可以在线程初始化传递
Thread.currentThread();静态方法,获取当前线程对象
setName(String name)或构造函数设置线程名称
2、第二种方式:实现Runnable接口
例子:多个窗口同时卖票
class Ticket extends Thread{
private int n=100;
public void run(){
while(n>0)
System.out.println(getName()+"-----sales-----"+n--);
}
public static void main(String[] args){
Ticket t1=new Ticket(),
t2=new Ticket(),
t3=new Ticket(),
t4=new Ticket();
t1.start();t2.start();t3.start();t4.start();
}
}
程序运行结果:
结果发现0号窗口和2号窗口同时售出了61号票
因为主函数建立4个Ticket对象,结果各卖各的,重复卖票。如果建立1个Ticket对象,分四次启动start(),则线程状态异常,无法重复启动线程,从而引出创建线程的另一种方式:实现Runnable接口。
步骤:
(1)定义类实现Runnable接口
(2)覆盖Runnable接口中的run方法
(2) 继承Thread类建立线程对象
(4)将Runnable接口的子类对象作为实际参数传递给Thread子类的构造函数
(5)调用Thread子类的start方法开启线程并调用Runnable接口子类的run方法
实现方式和继承方式有什么区别呢?继承Thread线程代码存放在Thread子类run方法中,而实现Runnable线程代码存放在接口的子类的run方法,实现方式避免了单继承的局限性,在定义线程时,建议使用实现方式。
三、多线程的安全问题
修改上面卖票例子的代码:
class Ticket implements Runnable{
private int n=1000;
public void run(){
while(n>0)
System.out.println(Thread.currentThread().getName()+"-----sales----
"+n--);
}
public static void main(String[] args){
Ticket t=new Ticket();
Thread t1=new Thread(t),
t2=new Thread(t),
t3=new Thread(t),
t4=new Thread(t);
t1.start();t2.start();t3.start();t4.start();
}
}
运行后,可能会出现两种异常情况:
(1)0号窗口卖出了49号票,接着1号窗口卖出50号票(应该先卖大票,再卖小票)
(2)某个窗口卖出了0号票,甚至卖出了-1号票
(本人试了好多次,都没有出现上述情况,估计是CPU太强,或者是人品太差
)
通过分析,发现,System.out.println()语句其实要执行两个步骤,执行n--和调用DOS命令窗口输出。那么问题来了,假设t1某一时刻判断n=50进入循环体,n-1变成49,即将打印50,突然被冻结,t2获得CPU,判断n=49,n-1变成48,打印出了49,之后t1获得了CPU,将刚才没打印的50打印出来,这样就出现了情况(1)。假设某一时刻n=1,t1判断完n>0突然被冻结,t2获得CPU,判断n=1,进入循环体,n-1变成0,输出1号票,然后t1继续执行,n-1=-1,输出0号票,出现情况(2)。总结就是:当多条语句在操作同一个线程共享数据时,一个线程被多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据错误。
解决办法:对多条操作共享数据的语句只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。JAVA对于多线程的安全问题提供了专业的解决方式,这就是同步代码块
synchronized(对象){
需要被同步的代码
}
修改上面卖票的例子:
class Ticket implements Runnable{
private int n=1000;
private Object obj=new Object();
private boolean flag=true;
public void run(){
while(flag){
synchronized(obj){
if(n>0){
System.out.println(Thread.currentThread().getName()+"-----
sales-----"+n--);
}else{
flag=false;
}
}
}
}
public static void main(String[] args){
Ticket t=new Ticket();
Thread t1=new Thread(t),
t2=new Thread(t),
t3=new Thread(t),
t4=new Thread(t);
t1.start();t2.start();t3.start();t4.start();
}
}
同步代码块中锁可以是任意对象,但必须是同一个对象。
如何找出程序中有可能因同步出现的安全问题:
(1)明确哪些代码是多线程运行代码
(2)明确共享数据
(3)明确多线程运行代码中哪些语句是操作共享数据的
同步代码块封装了运行代码,而函数也具有封存代码的功能,于是可以用同步函数来代替同步代码块,同步函数使用的锁是this,即对象本身。
验证:使用两个线程来卖票,一个同步代码块,一个同步函数,都在执行卖票工作,用flag确定线程执行路径
class Ticket implements Runnable{
private int n=1000;
public boolean flag=true;
private Object obj=new Object();
private boolean over=false;
public void run(){
if(flag){
synchronized(this){
while(!over)
if(n>0){
System.out.println(Thread.currentThread().getName()+"-----sales-----"+n--);
flag=!flag;
}else{
over=!over;
}
}
} else{
show();
flag=!flag;
}
}
private synchronized void show(){
while(!over)
if(n>0)
System.out.println(Thread.currentThread().getName()+"-----sales-----"+n--);
else
over=!over;
}
public static void main(String[] args){
Ticket t=new Ticket();
Thread t1=new Thread(t),
t2=new Thread(t);
t1.start();
try{
Thread.currentThread().sleep(100);
}catch(Exception e){}
t.flag=false;
t2.start();
}
}
运行正常,如果将同步代码块中的this改成obj,就会出现问题,说明同步函数使用的锁是this,即对象本身。还可以验证静态同步函数的锁是Class对象(静态方法中也没有this),静态方法进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象
类名.class,该对象的类型是Class,静态同步方法使用的锁就是该方法所在类的字节码文件对象。
在单例设计模式中,如果涉及到多线程,也可能会出现安全问题。单例设计模式有懒汉式和饿汉式两种,先看懒汉式:
class Single{
private static s=null;
private Single(){
System.out.println("new Single()");
}
public static Single getInstance(){
if(s==null)
s=new Single();
return s;
}
}
class SingleDemo implements Runnable{
public void run(){
Single.getInstance();
}
public static void main(String[] args){
SingleDemo s=new SingleDemo();
Thread t1=new Thread(t),t2=new Thread(t),
t3=new Thread(t),t4=new Thread(t);
t1.start();t2.start();t3.start();t4.start();
}
}
如果多个线程创建Single对象,就会出现问题。假设t1某时刻进入getInstance(),判断s=null,突然被冻结,t2获得CPU执行权,进入getInstance(),判断s=null,new Single();打印信息,之后t1获得CPU执行权,继续执行new Single();打印信息,Single类就被new了两个对象出来。 解决办法就是加同步
class Single{
private static s=null;
private Single(){
System.out.println("new Single()");
}
public static Single getInstance(){
synchronized(Single.class){
if(s==null)
s=new Single();
}
return s;
}
}
如果t1先getInstance(),获得锁,t2只能等待,t1创建对象完毕,交出锁,t2获得锁,判断s!=null,返回,t3又重复获得锁,浪费资源,可在获得锁之前先判断是否已创建,避免反复判断锁
class Single{
private static s=null;
private Single(){
System.out.println("new Single()");
}
public static Single getInstance(){
if(s==null){
synchronized(Single.class){
if(s==null)
s=new Single();
}
return s;
}
饿汉式代码
class Single{
private static Single s=new Single();
private Single(){
System.out.println("new Single()");
}
public static Single getInstance(){
return s;
}
}
多线程中还可能会出现另一个安全问题,就是死锁,出现死锁的原因是同步代码中嵌套同步代码.
class DeadLock implements Runnable{
public boolean flag=true;
private Object locka=new Object(),lockb=new Object();
public void run(){
while(true){
if(flag){
synchronized(locka){
show1();
}
flag=!flag;
}else{
synchronized(lockb){
show2();
}
flag=!flag;
}
}
}
private void show1(){
synchronized(lockb){
}
}
private void show2(){
synchronized(locka){
}
}
public static void main(String[] args){
DeadLock d=new DeadLock();
Thread t1=new Thread(d),t2=new Thread(d);
t1.start();
try{
Thread.currentThread().sleep(100);
}catch(Exception e){}
d.flag=false;
t2.start();
}
}
上述代码中,t1获得locka,t2获得lockb,之后t1请求lockb,但同时t2请求locka,发生死锁,编程应避免出现这种情况。
四、多线程间的通信
以上例程中线程运行代码都一致,现在看代码不一致的。有两个线程Input和Output,有一个资源类Res,包括成员变量String name和String gender,Input负责往Res中存放name和gender,Output负责输出。
Input线程:
class Input implements Runnable{
private Res res;
private boolean flag=false;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
if(flag){
res.set("刘德华","男");
}else{
res.set("刘亦菲","女");
}
flag=!flag;
}
}
}
Output线程
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run(){
while(true)
res.get();
}
}
Res类
class Res{
private String name;
private String gender;
public String getName()
{
return name;
}
public String getGender()
{
return gender;
}
public void set(String name,String gender){
this.name=name;
this.gender=gender;
}
public void get(){
System.out.println("name:"+getName()+"-----gender:"+getGender());
}
public static void main(String[] args){
Res res=new Res();
Input in=new Input(res);
Output out=new Output(res);
Thread input=new Thread(in),output=new Thread(out);
input.start();output.start();
}
}
运行结果:
结果出现人妖刘德华,女和刘亦菲,男。出现这种错误的原因是线程间没有同步,当input线程存数据时,output不应输出,应等到input存完后才能输出
修改代码如下:
input线程
class Input implements Runnable{
private Res res;
private boolean flag=false;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
if(flag){
res.set("刘德华","男");
}else{
res.set("刘亦菲","女");
}
flag=!flag;
}
}
}
}
output线程
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
res.get();
}
}
}
}
运行结果如下:
程序流程应该是inout和output交替输入和输出,但运行结果不是这样,分析原因,是因为当input输入完释放锁后,可能下一个获得锁的依然是input,必须在输入数据前,判断res是否已被取走,引入等待唤醒机制,当数据没被取走,input就wait,直到某时刻被output用notify唤醒
修改代码:
input线程
class Input implements Runnable{
private Res res;
private boolean flag=false;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
if(!res.empty)
try{
res.wait();
}catch(Exception e){}
if(flag){
res.set("刘德华","男");
}else{
res.set("刘亦菲","女");
}
flag=!flag;
res.empty=!res.empty;
res.notify();
}
}
}
}
output线程
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
if(res.empty)
try{
res.wait();
}catch(Exception e){}
res.get();
res.empty=!res.empty;
res.notify();
}
}
}
}
Res类
class Res{
private String name;
private String gender;
public boolean empty=false;
public String getName()
{
return name;
}
public String getGender()
{
return gender;
}
public void set(String name,String gender){
this.name=name;
this.gender=gender;
}
public void get(){
System.out.println("name:"+getName()+"-----gender:"+getGender());
}
public static void main(String[] args){
Res res=new Res();
Input in=new Input(res);
Output out=new Output(res);
Thread input=new Thread(in),output=new Thread(out);
input.start();
output.start();
}
}
运行结果如下:
运行成功
以上的例程说明了同步线程间通讯的解决方案,线程间通讯其实就是多个线程在操作同一个资源,但是操作的动作不同。上面的例子其实是“生产者——消费者”同步通讯问题的一个特例,即生产者和消费者都只是一个人,下面分析多个生产者和多个消费者的情况。
假设有2个生产者0号线程,1号线程和2个消费者2号线程,3号线程,生产者即input,消费者即output,修改程序:
为了区分生产者和消费者,每次输入或输出时,同时输出当前线程的名称
input线程
class Input implements Runnable{
private Res res;
private boolean flag=false;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
if(!res.empty)
try{
res.wait();
}catch(Exception e){}
if(flag){
System.out.println(Thread.currentThread().getName()+"生产-----刘德华-----男");
res.set("刘德华","男");
}else{
System.out.println(Thread.currentThread().getName()+"生产-----刘亦菲-----女");
res.set("刘亦菲","女");
}
flag=!flag;
res.empty=!res.empty;
res.notify();
}
}
}
}
output线程
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
if(res.empty)
try{
res.wait();
}catch(Exception e){}
System.out.print(Thread.currentThread().getName()+"消费-----");
res.get();
res.empty=!res.empty;
res.notify();
}
}
}
}
Res类
class Res{
private String name;
private String gender;
public boolean empty=false;
public String getName()
{
return name;
}
public String getGender()
{
return gender;
}
public void set(String name,String gender){
this.name=name;
this.gender=gender;
}
public void get(){
System.out.println("name:"+getName()+"-----gender:"+getGender());
}
public static void main(String[] args){
Res res=new Res();
Input in0=new Input(res);
Thread input0=new Thread(in0);
Input in1=new Input(res);
Thread input1=new Thread(in1);
Output out2=new Output(res);
Thread output2=new Thread(out2);
Output out3=new Output(res);
Thread output3=new Thread(out3);
input0.start();input1.start();
output2.start();output3.start();
}
}
程序运行结果如下:
分析下这段代码,当某一时刻res没有新数据,此时output2执行,获得锁,判断没有新数据,等待,output3执行,获得锁,判断没有新数据,等待,input0执行,获得锁,输入新数据,唤醒等待队列中第一个线程output2,output2取走新数据,接着唤醒等待队列上第二个线程output3,output3继续取数据,这样output2之前已取走的数据被重复取走了。原因在于当output3被唤醒时,没有重新判断是否有新数据,修改代码仅需把判断语句if改成while,保证每次被唤醒时重新判断一次,修改后运行,结果程序进入无限等待状态。原因如下:当某一时刻res数据未被取走,此时input0执行,获得锁,判断数据没取走,等待,input1执行,同样等待,output2执行,取走数据,唤醒input0,等待,input0输入新数据,唤醒input1,input1发现新数据,等待,接下来没有任何线程可以获得执行资格,程序进入无限等待状态。解决办法是当input0输入新数据后,唤醒所有线程,让output2获得执行资格,将数据取走。
最终程序代码如下:
input线程
class Input implements Runnable{
private Res res;
private static boolean flag=false;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
while(!res.empty)
try{
res.wait();
}catch(Exception e){}
if(flag){
System.out.println(Thread.currentThread().getName()+"生产
-----刘德华-----男");
res.set("刘德华","男");
}else{
System.out.println(Thread.currentThread().getName()+"生产
-----刘亦菲-----女");
res.set("刘亦菲","女");
}
flag=!flag;
res.empty=!res.empty;
res.notifyAll();
}
}
}
}
oputput线程
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run(){
while(true){
synchronized(res){
while(res.empty)
try{
res.wait();
}catch(Exception e){}
System.out.print(Thread.currentThread().getName()+"消费-----");
res.get();
res.empty=!res.empty;
res.notifyAll();
}
}
}
}
Res类
class Res{
private String name;
private String gender;
public boolean empty=false;
public String getName()
{
return name;
}
public String getGender()
{
return gender;
}
public void set(String name,String gender){
this.name=name;
this.gender=gender;
}
public void get(){
System.out.println("name:"+getName()+"-----gender:"+getGender());
}
public static void main(String[] args){
Res res=new Res();
Input in0=new Input(res);
Thread input0=new Thread(in0);
Input in1=new Input(res);
Thread input1=new Thread(in1);
Output out2=new Output(res);
Thread output2=new Thread(out2);
Output out3=new Output(res);
Thread output3=new Thread(out3);
input0.start();input1.start();
output2.start();output3.start();
}
}
JDK5.0版本中对上述多生产者—多消费者问题给出了更优化的解决方案,在同步代码块中,可以利用condition选择性的唤醒线程。
代码如下:
input线程
import java.util.concurrent.locks.*;
class Input implements Runnable{
private Res res;
private static boolean flag=true;
public Input(Res res){
this.res=res;
}
public void run(){
while(true){
res.lock.lock();
try{
while(!res.empty)
res.con_in.await();
if(flag){
System.out.println(Thread.currentThread().getName()+"生
产-----刘德华-----男");
res.set("刘德华","男");
}else{
System.out.println(Thread.currentThread().getName()+"生
产-----刘亦菲-----女");
res.set("刘亦菲","女");
}
flag=!flag;
res.empty=!res.empty;
res.con_out.signal();
}catch(InterruptedException e){
}finally{
res.lock.unlock();
}
}
}
}
output线程
import java.util.concurrent.locks.*;
class Output implements Runnable{
private Res res;
public Output(Res res){
this.res=res;
}
public void run() {
while(true){
res.lock.lock();
try{
while(res.empty)
res.con_out.await();
System.out.print(Thread.currentThread().getName()+"消费-----");
res.get();
res.empty=!res.empty;
res.con_in.signalAll();
}catch(InterruptedException e){
}finally{
res.lock.unlock();
}
}
}
}
Res类
import java.util.concurrent.locks.*;
class Res{
public boolean empty=true;
private String name,gender;
public Lock lock=new ReentrantLock();
public Condition con_in=lock.newCondition(),con_out=lock.newCondition();
public void set(String name,String gender){
this.name=name;
this.gender=gender;
}
public void get(){
System.out.println("name:-----"+name+"gender:-----"+gender);
}
public static void main(String[] args){
Res res=new Res();
Input in0=new Input(res);
Thread input0=new Thread(in0);
Input in1=new Input(res);
Thread input1=new Thread(in1);
Output out2=new Output(res);
Thread output2=new Thread(out2);
Output out3=new Output(res);
Thread output3=new Thread(out3);
input0.start();input1.start();
output2.start();output3.start();
}
}
五、守护线程和几个常见的线程方法
1、线程停止
开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是让线程结束。我们可以在线程中设置结束标志位,循环判断,在主线程中确定标志位什么时候为假,从而结束线程。
class StopThread implements Runnable{
public boolean go=true;
private int x=0;
public void run(){
while(go){
System.out.println("go..."+x++);
}
System.out.println("stop!");
}
public static void main(String[] args){
StopThread stop=new StopThread();
Thread t=new Thread(stop);
t.start();
int i=0;
while(true)
if(i++==100)
stop.go=false;
}
}
但是如果线程run方法中执行了wait(),线程将处于冻结状态,不会读取到go标记,也就无法停止,可以通过调用线程的interrupt方法将已冻结的线程重新进入运行状态。调用interrupt方法将会使程序捕获到InterruptedException异常,通过在异常处理代码中设置标志位,可以让线程重新回到运行状态,线程继续判断标志位从而结束线程。
2、守护线程
守护线程也就是后台线程,开启后和前台线程共同争夺CPU执行权,当所有的前台线程都结束后,后台线程会自动结束,setDaemon()方法设置线程为守护线程,该方法必须在线程启动前即start前使用。
3、join方法
线程调用join方法后,当前线程放弃执行权,该线程获得CPU执行权,直到该线程运行结束,之前放弃执行权的线程才重新抢夺执行权,其他线程不受影响。
4、线程优先级和yield方法
可以通过setPriority(int)方法改变线程的优先级,优先级是建议CPU分配执行权的数值,范围为1—10,数值越大,优先级越高,默认值是Thread.NORM_PRIORITY=5,最大值是Thread.MAX_PRIORITY=10,最小值是Thread.MIN_PRIORITY=0。
yield()方法让线程暂时放弃执行权,让其他线程有机会执行,可以减缓线程的执行速度,让所有线程和谐运行。