黑马程序员–Java之多线程09
一、线程和进程
在Java中,并发机制非常重要,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其他线程并发执行,这种机制被称为多线程。多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此间可以进行通信。多线程是非常复杂的机制,在每个操作系统中的运行方式也存在差异,window操作系统是多任务操作系统,它以进程为单位。一个进程是一个包含有自身地址的程序,每个独立执行的程序都称为进程,也就是正在执行的程序。系统可以分配给每个进程一段有限的使用CPU的时间(也可以称为CPU时间片),CPU在这段时间中执行某个进程,然后下一时间片又跳至另一个进程中去执行。由于CPU转换较快,所以使得每个进程好像是同时执行一样。一个线程则是进程中的执行流程,一个进程中可以同时包含多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程。在单线程中,程序代码按调用顺序依次往下执行,如果需要一个进程同时完成多段代码的操作,就需要产生多个线程。
二、实现线程的两种方式
Java提供了两种多线程实现方式,一种是继承Java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。
2.1、继承Thread类
Thread类是java.lang包中的一类,从这个类中实例化的对象代表线程,程序员启动一个新线程需要建立Thread实例。Thread类中常用的两个构造方法如下:
public Thread(String threadName)
public Thead()
- 1
- 2
其中,第一个构造方法是创建一个名称为threadName的线程对象。
继承Thread类创建一个新的线程的语法如下:
public class ThreadTest extends Thread{ }
- 1
- 2
完成线程真正功能的代码放在类的run()方法中,当一个类继承Thread类后,就可以在该类中覆盖run()方法,将实现该线程功能的代码定入run()方法中,然后同时调用Thread类中的start()方法执行线程,也就是调用run()方法。
Thread对象需要一个任务来执行,任务是指线程在启动时执行的工作,该工作的功能代码被写在run()方法中。run()方法必须使用以下语法格式:
public void run(){
}
- 1
- 2
注意:如果start()方法调用一个已经启动的线程,系统将会抛出IllegalThreadStateException异常。
当执行一个线程程序时,就自动产生一个线程,主方法正是在这个线程上运行的。当不再启动其他线程时,该程序就为单线程程序,主方法线程启动由Java虚拟机负责,程序员负责启动自己的线程,语法为:
public sstatic void main(String[] agrs){
new ThreadTest().start();
}
- 1
- 2
- 3
实例:
public class Test{
public static void main(String[] args){ MyThread myThread = new Thread(); //创建线程MyThread的线程对象 myThread.start();//启动线程 while(true){ System.out.println("main()方法在运行:"); } } } class MyThread extends Thread{//继承Thread类 public void run(){//覆盖run()方法 while(true){ System.out.println("MyThread类的run()方法在运行:); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
两个while循环中的打印语句轮流执行说明该例程实现了多线程。继承了Thread类,然后在类中覆盖了run()方法。在main方法中,使线程执行需要调用Thread类中的start()方法,start()方法调用被覆盖的run()方法,如果不调用start()方法,线程 永远都不会启动,在主方法没有调用start()方法之前,Thread对象只是一个实例,而不是一个真正的线程。
2.2、实现Runnable接口
通过继承Thread类实现多线程的方法有一定的局限性,因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,为了克服这种弊端,Thread类提供了另一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需要给该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
public class Test2{
public static void main(String[] args){ MyThread myThread = new MyThrad();//创建MyThread的实例对象 Thread thread = new Thread(myThread);//创建线程对象 thread.start();//开启线程,执行线程中的run()方法 while(true){ System.out.println("main方法在运行:"); } } } class MyThread extends Runnable{//实现Runnable接口 public void run(){//重写run()方法,线程的代码段,当调用start()方法时,线程从此处开始执行 while(true){ System.out.println("MyThread类的run()方法在运行:"); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
三、对比分析两种方式
实现Runnable接口相对于继承Thread类来说有如下好处:
1. 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
2. 可以避免由于Java的单继承带来的局限性。
事实上,大部分的应用程序都采用第二种方式来创建多线程,即实现Runnable接口。
四、线程的生命周期及状态转换
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中的代码正常执行完毕或线程抛出一个未捕获的异常或者错误时,线程的生命周期便会结束。线程的整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。在程序中,我们可以通过一些操作使线程在不同状态之间转换器。
五、多线程同步
5.1、线程安全
实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。这种多线程的程序通常会发生问题,以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出结论票数大于0,于是它也执行售出操作,这样就会产生负数。所以编写多线程程序时,应该考虑线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。
public class ThreadSafeTest implements Runnable{ int num = 10;//设置当前票数 public void run(){ while(true){ try{ if(num > 0){ Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); } System.out.println("tickets" + num--); } } public static void main(String[] args){ ThreadSafeTest t = new ThreadSafeTest();//实例化类对象 Thread t1 = new Thread(t);//以该类对象分别实例化4个线程 Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start();//分别启动线程 t2.start(); t3.start(); t4.start(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
运行上述程序可以看到,最后打印出售的票号为负值,这样就出现了问题。这是由于同时创建了四个线程,这四个线程执行run()方法,在num变量为1时,线程1、线程2、线程3、线程4都对num变量有存储功能,当线程1执行run()方法时,还没来得及做递减操作,就指定它调用sleep方法进入就绪状态,这时其他线程进入run()方法发现num变量依然大于0,但此时线程1休眠时间已到,将num变量值递减,同时其他线程也都对num变量进行递减操作,从而产生了负值。
为了解决资源共享的安全问题,基本上所有方法都是采用给定时间只允许一个线程访问共享资源,这时就需要给共享资源上一道锁。这就好比一个人上洗手间时,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。
5.2、同步块
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作为同步代码块,语法如下:
synchronized(lock){
操作共享资源代码块
}
- 1
- 2
- 3
其中lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。
//定义Ticket类实现Runnable接口
class Ticket implements Runnable{
private int tickets = 10;//定义变量tickets,并赋值10 Object lock = new Object();//定义任意一个对象,用作同步代码块的锁 public void run(){ while(true){ synchronized(lock){定义同步代码块 try{ Thread.sleep(100);//经过的线程休眠100毫秒 }catch(Exception e){ e.printStackTrace(); } if(tickets > 0){ System.out.println(Thread.currentThread().getName()+"---卖出的票为:" + tickets--); }else{//如果tickets小于0,跳出循环 break; } } } } } public class Test{ public static void main(String[] args){ Ticket ticket = new Ticket();//创建Ticket对象 //创建并开启四个线程 new Thread(ticket,"线程一").start(); new Thread(ticket,"线程二").start(); new Thread(ticket,"线程三").start(); new Thread(ticket,"线程四").start(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可知售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新的对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位。线程之间便不能产生同步的效果。
5.3、同步方法
在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,语法如下: synchronized 返回值类型 方法名([参数1,....]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。
//定义Ticket类实现Runnable接口
class Ticket implements Runnable{
private int tickets = 10;
public void run(){ while(true){ saleTicket();//调用售票方法 if(ticket <= 0){ break; } } } //定义一个同步方法saleTicket() private synchronized void saleTicket(){ if(tickes > 0){ try{ Thread.sleep(100);//经过的线程休眠100毫秒 }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--); } } } public class Test{ public static void main(String[] args){ Ticket ticket = new Ticket();//创建Ticket对象 new Thread(ticket,"线程一").start(); new Thread(ticket,"线程二").start(); new Thread(ticket,"线程三").start(); new Thread(ticket,"线程四").start(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
同步代码块的锁是自己定义的任意类型的对象,同步方法的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。
有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时候我们就会有一个疑问,如果不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?java中静态方法的锁是该方法所在烦的class对象,该对象可以直接用“类名.class”的方式获取。
同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但线程在执行同步代码时会每次都判断锁的状态,非常消耗资源,效率较低。
5.4、死锁问题
class DeadLockThread implements Runnable {
static Object chopticks = new Object();//定义Object类型的chopticks锁对象
static Object knifeAndFork = new Object();//定义Object类型的knifeAndFork锁对象 private boolean flag;//定义boolean类型的变量flag DeadLockThread(boolean flag){//定义有参的构造方法 this.flag = flag; public void run(){ if(flag){ while(true){ synchronized(chopticks){ //chopticks锁对象上的同步代码块 System.out.println(Thread.currentThread().getName() + "---if---chopticks"); synchronized(knifeAndFork){ //knifeAndFork锁对象上的同步代码块 System.out.println(Thread.currentThread().getName() + "---if---knifeAndFork"); } } } while(true){ synchronized(knifeAndFork){ //knifeAndFork锁对象上的同步代码块 System.out.println(Thread.currentThread().getName() + "---else---knifeAndFork"); synchronized(chopticks){ //chopticks锁对象上的同步代码块 System.out.println(Thread.currentThread().getName() + "---else---chopticks"); } } } } } } public class Test2{ public static void main(String[] args){ //创建两个DeadLockThread对象 DeadLockThread d1 = new DeadLockThread(true); DeadLockThread d2 = new DeadLockThread(false); //创建并开启两个线程 new Thread(d1,"Chinese").start();//创建开启线程Chinese new Thread(d2,"American").start();//创建开启线程American } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
创建了Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕,而American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕,两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了死锁现象。