黑马程序员——Java多线程1

来源:互联网 发布:淘宝为什么不卖烟 编辑:程序博客网 时间:2024/05/29 19:05

------- android培训、java培训、期待与您交流! ----------

概述

计算机用户理所当然的认为他们的操作系统一次同时可以做多件事情。他们可以一边在WORD中写文档,同时听着歌,下载着文件。其实一个单独的应用程序也可以一次做多件事情。例如,音乐播放器必须同时读取来自网络的音频数据,解压它,管理播放,更新界面显示。即使是像WORD一样的文字编辑器也时刻准备着响应键盘和鼠标事件,同时不耽误格式化文本和刷新屏幕显示。像这样可以一次做多件事的软件就叫做并发软件。

Java平台被设计成从底层支持并发编程。它通过Java编程语言和类库来实现基本的并发性。同时,从Java1.5以后,Java平台也包括高级的并发API。本文主要介绍Java平台的基本并发性支持,同时也总结一些java.util.concurrent包中的高级API.

在并发编程中,有两个基本的执行单元:进程和线程。在Java编程语言中,并发编程主要考虑的是线程,当然也要考虑一些进程。
计算机系统中通常有很多活动的线程和进程,这即使是在单核CPU系统上也是正确的。因为在某个给定的时刻,实际上只有一个线程执行。单个CPU核心的处理时间被很多的进程和线程共享,这是通过操作系统的一种特征实现的,这种特征叫时间片
进程
一个进程自己包含一个执行环境。一个 进程通常通常有一个完整的,私有的一套运行时资源;特别的,每个进程有它自己的内存空间。
进程常常被视作等同于一个程序或应用。但是用户看到的单独的一个程序可能实际上是由协同工作的一套进程组成。为了简化进程间通信,大部分操作系统都支持某种Inter Process Communication(IPC)资源,像管道和端口。IPC不仅用于同一操作系统的进程间通信,同时也用于两个不同系统的进程的通信。
线程
线程有时被称作轻量级进程。进程和线程都提供执行环境,但是创建一个新线程比进程需要更少的资源。
线程存在于进程中,每个进程都至少有一个线程。一个进程内的多个线程共享这个进程的资源,包括内存,占有的文件等等。这使线程工作很有效率,但是也造成了潜在的问题——线程间通信。

多线程执行是Java平台的基本特征。每一个应用程序都至少有一个线程,也可以说至少有n个(如果把内存管理和信号处理的系统线程算在内)。但是从应用程序编写者的角度,你只启动了一个线程,叫做主线程。这个线程有能力创建其他的线程。

线程生命周期

新建:创建线程对象

就绪:有执行资格,没有CPU执行权

运行:有执行资格,有CPU执行权

阻塞:没有执行资格,没有CPU执行权。类似暂停,之后还可以继续。

死亡:线程对象编程垃圾,等待被回收。

线程状态图如下:


上图中,当一个线程对象a调用join,实际上是使a获取CPU执行权,直到运行结束。之所以把setDeamon()方法运行到死亡的连接线上,因为,setDeamon()的意思是使调用这个方法的线程对象守护这行代码所在的线程,例如,t1.setDeamon();如果这行代码位于主线程,则意味着,t1线程必须守护主线程,也就是说,当主线程结束,t1线程在执行完最后一个时间片后,也要立即死亡,而不管run()方法中的代码是否执行完。

创建线程

第一种方式:继承Thread类
步骤:
1.定义一个类继承Thread
2.复写Thread类中的run方法。该方法用来存放想在新线程执行的代码。
3.调用线程的start方法。该方法有两个作用,使创建好的线程开始执行,调用run方法

代码如下:

class MyThread extends Thread{public void run(){for (int i=0;i<60 ;i++ ) {System.out.println("OtherThread"+i);}}}class MultiThread{public static void main(String[] args) {MyThread thead=new MyThread();thead.start();for (int i=0;i<60 ;i++ ) {System.out.println("MainThread"+i);}}}

运行后发现,每一次的执行结果都不相同,这就是多线程的一个特性:随机性。

可以形象的理解为多个线程在运行时互相抢夺CPU的使用权,谁抢到,谁执行。

 如何用这种方式创建两个线程并交替运行:

class Test extends Thread{private String name;public Test(String name){this.name=name;}public void run(){for (int i=0;i<60 ;i++ ) {System.out.println(this.name+" run "+i);}}}class MultiThread{public static void main(String[] args) {Test t1=new Test("Thread1");t1.start();Test t2=new Test("Thread2");t2.start();for (int i=0;i<60 ;i++ ) {System.out.println("MainThread"+i);}}}

上面的代码中,我们自己定义了一个变量来标识不同的线程,其实Thread类里会自动分配给线程名字,获取的方法是getName().

Thread类里还有一个静态方法currentThread(),它用于获取当前执行线程的对象,和getName()方法配合代替println里的this.name方法,可以使代码更规范更具可读性。在Test类构造函数里可以调用父类构造函数,修改Thread类里分配给线程的默认名字。

优化后的代码如下:

class Test extends Thread{public Test(String name){super(name);}public Test(){}public void run(){for (int i=0;i<5 ;i++ ) {//下面三句代码在这里是等价的System.out.println(Thread.currentThread().getName()+" run "+i);// System.out.println(currentThread().getName()+" run "+i);// System.out.println(this.getName()+" run "+i);}}}class MultiThread{public static void main(String[] args) {// Test t1=new Test("Thread1");Test t1=new Test();t1.start();// Test t2=new Test("Thread2");Test t2=new Test();t2.start();for (int i=0;i<5 ;i++ ) {System.out.println("MainThread"+i);}}}
上面的程序运行结果如下图:


从运行结果可以看出:3个线程交替运行,Thread类默认分配给线程的名字是Thread-x,x为从0开始的整数。


第二种方式:实现Runnable接口

步骤:
1. 定义一个类(假设名字为A)实现Runnable接口
2.在A类中覆盖Runnable接口的run()方法。将线程要运行的代码存放在该run方法中。
3.创建一个A类的实例a。
4.将a作为参数传递给Thread类的构造函数。这么做是为了让Thread类的start方法明确知道要调用哪一个对象的run方法。
5.调用Thread类的start方法开启线程。该方法会内部调用A类的run方法。

例子:创建一个Java程序,让3个窗口同时卖同一节车厢的100张票。
//定义一个类实现Runnable接口class MyRunnableThread implements Runnable{//把票数定义在这里可以使三个线程同时卖相同的100张票,而相互不重复,没有遗漏。//如果把票数写到run方法里则会卖出300张票private int tickets=1;//卖出的票数public void run(){while(tickets<=100){System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets++)+"张票");}}}class MultiThread{public static void main(String[] args) {MyRunnableThread myThread=new MyRunnableThread();Thread t1=new Thread(myThread);Thread t2=new Thread(myThread);Thread t3=new Thread(myThread);t1.start();t2.start();t3.start();}}
最后几个执行结果如下:

从图中可以看出100张票成功售出,并且无重复无遗漏。如果有重复,负值,或遗漏,说明出现了多线程安全问题详见多线程安全章节。

继承Thread方式与实现Runnable方式区别

实现Runnable方式避免了单继承的局限性。就是说当需要多线程执行的代码位于的类已经继承了一个类,就不可能再继承Thread类实现多线程,而只能通过实现Runnable接口方式。实现Runnable方式还有个好处,就是如上面的卖票程序一样,多个线程可以访问一个类对象的变量。 通常情况下使用多线程推荐此种方式。
而Thread方式好处在于代码量少,易于理解。
还有个区别就是继承Thread方式将要多线程执行的代码放在Thread类的run方法中,而实现Runnable方式将要多线程执行的代码放在了实现Runnable接口的类的run方法中。

多线程安全

当你执行上面的卖票程序时,有一定概率会出现买票结果出现错误(重复,负值,或遗漏),为了增大这种概率,我们将卖票程序更改如下:
//定义一个类实现Runnable接口class MyRunnableThread implements Runnable{//把票数定义在这里可以使三个线程同时卖相同的100张票,而相互不重复,没有遗漏。//如果把票数写到run方法里则会卖出300张票private int tickets=100;//剩下的票数public void run(){try{Thread.sleep(10);}catch(Exception e){}while(tickets>0){System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets--)+"张票");}}}class MultiThread{public static void main(String[] args) {MyRunnableThread myThread=new MyRunnableThread();Thread t1=new Thread(myThread);Thread t2=new Thread(myThread);Thread t3=new Thread(myThread);t1.start();t2.start();t3.start();}}
运行上面的程序可能会出现以下结果,您的结果可能与我不同:

注意到画红线的两个线程都卖出了第99张票,这就是错误的运行结果,可能出现错误结果的程序就不安全的,而这个安全问题是由于使用了多线程导致的。那么有没有一种机制解决这种问题,当然有,那就是java同步代码块。

同步代码块

语法:
synchronized(对象){
    需要同步的代码
}

其中的对象如同锁(也叫监视器)。持有锁的线程可以再同步中执行。没有锁的线程即使获得cpu的执行权,也进不去,因为没有获取锁。要使用线程同步,有两个前提,一个是必须要有两个以上线程,一个是多个线程使用同一个锁。必须保证同步代码块中只有一个线程在运行。使用同步代码块的好处是:解决了多线程的安全问题,弊端:多个线程需要判断锁,较为消耗资源。


例子,上面的卖票程序使用同步代码块后的代码如下:
class MyRunnableThread implements Runnable{private int tickets=100;//卖出的票数Object obj=new Object();public void run(){try{Thread.sleep(10);}catch(Exception e){}while(true){synchronized(obj){//此处同步代码块中的代码越少越好,所以讲while循环中的条件挪出来放在if中,这样可以避免把这个while循环包括在内if(tickets>0)System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets--)+"张票");}}}}class MultiThread{public static void main(String[] args) {MyRunnableThread myThread=new MyRunnableThread();Thread t1=new Thread(myThread);Thread t2=new Thread(myThread);Thread t3=new Thread(myThread);t1.start();t2.start();t3.start();}}
这段代码运行后,结果是卖出的票不重不漏,但是线程交替的现象大大减少,甚至可能100张票都由一个线程来完成,这取决与你的CPU速度,速度越快,一个时间片可执行的一个线程的代码越多,一个线程卖出的票数也就越多,要想看到交替线程,需要把票数调到足够大才行。

同步函数


同步函数就是在函数名前加上synchronized,这样整个函数就成为临界资源,也就是同一时间点(精确到微秒),只能有一个线程访问。作用和同步代码块相同。
上面的卖票程序用同步函数实现如下:
class MyRunnableThread implements Runnable{private int tickets=100;//卖出的票数Object obj=new Object();public void run(){try{Thread.sleep(10);}catch(Exception e){}while(true){sellTicket();}}public synchronized void sellTicket(){if(tickets>0){String str=Thread.currentThread().getName()+"卖出了第"+(tickets--);System.out.println(str);try{Thread.sleep(10);}catch(Exception e){}}}}class MultiThread{public static void main(String[] args) {MyRunnableThread myThread=new MyRunnableThread();Thread t1=new Thread(myThread);Thread t2=new Thread(myThread);Thread t3=new Thread(myThread);t1.start();t2.start();t3.start();}}
同步函数用的锁是this.运行函数会发现先启动的线程会多卖出一些票这是正常现象。

静态同步函数

静态同步函数就是在同步函数的关键词前面加上static。形式如下:

public synchronized void sellTicket(){……}

静态同步方法的锁是方法所在类的字节码文件对象


支持多线程的懒汉式单例设计模式

传统的懒汉式在多线程访问时会出现对象创建多次的问题,如下所示:
class Single{public static Single s=null;private Single(){}public static Single getInstance(){if(s==null)//线程有可能在此挂起s=new Single();return s;}}
当多个线程访问getInstance()方法时,多个线程有可能同时在执行到第6行的位置挂起,导致对象被重复创建,产生潜在的问题。应用本节所学习的多线程安全知识,可以妥善解决此问题,代码如下:
class Single{public static Single s=null;private Single(){}public static Single getInstance(){if(s==null){synchronized(Single.class){if (s==null) {s=new Single();}}}return s;}}
其中在synchronized外面加的判断语句是为了提高性能,防止多线程每一次访问都对锁进行一次判断。

死锁

所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

最简单的制造死锁的java程序如下:

class DeadLock{public static void main(String[] args) {Thread thread1=new Thread(new Thread1());Thread thread2=new Thread(new Thread2());thread1.start();thread2.start();}}class Thread1 implements Runnable{public void run(){synchronized(Locks.locka){System.out.println("Thread1 access locka");//Asynchronized(Locks.lockb){System.out.println("Thread1 access lockb");//C}}}}class Thread2 implements Runnable{public void run(){synchronized(Locks.lockb){System.out.println("Thread2 access lockb");//Bsynchronized(Locks.locka){System.out.println("Thread2 access locka");//D}}}}class Locks{static Object locka=new Object();static Object lockb=new Object();}

运行结果如下


从结果可以看出,线程1和线程2都没有访问到内层嵌套的同步代码块,线程1访问到A语句时将锁locka关闭,这样当线程2想访问D语句时,被锁locka锁死无法继续,此时线程2挂起,等待locka解锁。而线程2访问到B语句时,又把lockb锁死,这样线程1无法访问语句C,挂起,开始等待锁lockb打开。两个线程由于互相等待对方解锁,所以都在等待,从而导致锁解不开,程序像死了一样。

0 0
原创粉丝点击