黑马程序员_多线程

来源:互联网 发布:发票数据怎么导出电子 编辑:程序博客网 时间:2024/05/22 17:24

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

1线程和进程

说到线程,首先要了解什么是进程。

Windows操作系统是多任务操作系统,它是以进程为单位。      一个进程是一个包含有自身地址的程序,每个独立执行的程序都称为进程,也就是正在执行的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。而线程:就是进程中的一个独立的控制单元,也可以说一个线程就是进程中的执行流程。一个进程中可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程。在单线程中,程序代码按调用顺序依次往下执行,如果需要一个进程同时完成多段代码的操作,就需要产生多线程。注意:一个进程中至少有一个线程。

比如:Java JVM  启动的时候会有一个进程java.exe.该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。

概括总结线程和进程就是:

1)进程是静态的,其实就是指开启的一个程序;而线程是动态的,是真正执行的单元,执行的过程。其实我们平时看到的进程,是线程在执行着,因为线程是作为进程的一个单元存在的。

2)同样作为基本的执行单元,线程是划分得比进程更小的执行单位。

3)每个进程都有一段专用的内存区域。与此相反,线程却共享内存单元(包括代码和数据),通过共享的内存单元来实现数据交换、实时通信与必要的同步操作。

2,多线程存在的意义

一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

3,线程的创建方式

如何在自定义的代码中,自定义一个线程呢?
通过对api的查找,java已经提供了对线程这类事物的描述。即Thread类。

1)创建线程的第一种方式:继承Thread

步骤:
1,定义类继承Thread。
2,复写Thread类中的run方法。目的:将自定义代码存储在run方法中。让线程运行。

为什么要覆盖run方法呢?Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
3,调用线程的start方法,该方法两个作用:启动线程,调用run方法。

可以通过下面的一段代码和注释来了解:

class Demo extends Thread{ public void run() { for(int x=0; x<60; x++)System.out.println("demo run----"+x); }}class ThreadDemo { public static void main(String[] args) { //for(int x=0; x<4000; x++) //System.out.println("Hello World!"); Demo d = new Demo();//创建好一个线程。 d.start();//开启线程并执行该线程的run方法。 //d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。而且由主线程执行//主线程执行完run方法才回去执行下面的代码。 for(int x=0; x<60; x++)    System.out.println("Hello World!--"+x); /* 总结: 主函数一运行JVM就会创建一个线程,并依次向下执行main中的方法 Demo d =new Demo(); d.start(); 到这里会创建一个线程,执行start方法中的run()方法,并开始打印demo run   但是main函数的线程还是会依次向下执行 for(int x=0; x<60; x++) System.out.println("Hello World!--"+x);读到这里开始打印hello world 所以程序运行后两条线程会交替进行打印, CPU在执行的时候只会执行一个程序(多核除外),但是看上去好像这两线程都是在同时进行运行,其实不是 只是CPU在不断的切换运行线程,但是切换速度可以几乎忽略,所以看上去像是同时在运行。 CPU真正切换的是进程中的线程,速度快的感觉不到,所以你开的进程越多,电脑会反应越慢越卡这个道理。 注意:当主函数这个线程将hello world 打印完线程结束,但是d这个线程还在运行,JVM还在运行那么这个 进程就还在。 主线程存储的是main方法中的代码, */  }}

通过以上代码我们可以总结出多线程的特性:

并发性:

程序运行结果每一次都不同。因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。

明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。也可以说是多线程具有并发性。

随机性:谁抢到谁执行,至于执行多长,cpu说的算。

2)创建线程的第二种方式:实现Runable接口

步骤:
1,定义类实现Runnable接口
2,覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
3,通过Thread类建立线程对象。
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去执行指定对象的run方法。就必须明确该run方法所属对象。
5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。


可以通过下面的代码来详细分步了解实现Runnable接口的过程:

class Ticket implements Runnable//1,定义Ticket类实现Runnable接口{ private int tick = 100; public void run()//2,覆盖Runnable接口中的run方法。 {while(true) { if(tick>0) { System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } }}class TicketDemo{ public static void main(String[] args) { Ticket t = new Ticket();//创建Ticket对象t也就是Runnable的子类对象       //3,通过Thread类建立线程对象。   //4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。       Thread t1 = new Thread(t);//创建了一个线程;       Thread t2 = new Thread(t);//创建了一个线程;       Thread t3 = new Thread(t);//创建了一个线程;       Thread t4 = new Thread(t);//创建了一个线程;        //5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。         t1.start();        t2.start();    t3.start();    t4.start(); }}

说明:其实通过调查API我们可以发现,实质上Thread类实现了Runnable接口,其中的run()方法正是对Runnable接口中的run()方法的具体实现。
两种创建线程的方式都要注意两点:
1,如果start()方法调用一个已经启动的线程,系统将会抛出IllegalThreadStateException异常。
2,启动一个新的线程,不是直接调用Thread子类对象的run()方法,而是调用Thread子类的start()方法,Thread类的start()方法产生一个新的线程,该线程运行Thread子类的run()方法。

3)两种方式的区别

实现方式好处:避免了单继承的局限性。在定义线程时,建立使用实现方式。

继承Thread:线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。

3,线程的运行状态

用一个图可以表示:


4,线程安全问题

问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。注意:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方式:就是同步代码块。

格式为:

synchronized(对象)
{
需要被同步的代码
}
说明:对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。打个比方,好比说火车上的卫生间:当一个人进去上厕所,关上门后,门上显示有人,那么另一个想上厕所的人看见是有人就会等待,等另一个人出来后门上显示无人才可以进去。

同步的前提:
1,必须要有两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。

必须保证同步中只能有一个线程在运行。

同步的好处与弊端:

好处:解决了多线程的安全问题。

弊端:多个线程需要判断锁,较为消耗资源,(这个消耗是在允许范围内的)
比如下面的代码:

class Ticket implements Runnable//Runnable实现{ private int tick = 1000;    Object obj = new Object(); public void run() { while(true) { synchronized(obj)//加上锁,任何对象进来都会先判断锁 { //下面是需要同步的代码块 if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } } }}class TicketDemo2{ public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); }}

5,同步函数

同步函数就是在方法前面修饰synchronized关键字的函数

格式为:Synchronized void f(){}

说明:当某个对象调用了同步函数时,该对象上的其他同步函数必须等待该同步函数执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。

对于多线程安全问题解决时应该做到:

1,目的:该程序是否有安全问题,如果有,如何解决?

2,如何找问题:

1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。

比如下面的代码:

class Bank{  private int sum;//共享数据 //Object obj = new Object(); public synchronized void add(int n) //定义同步函数 { sum = sum + n; try{Thread.sleep(10);}catch(Exception e){} System.out.println("sum="+sum); }}class Cus implements Runnable//实现Runnable接口{ private Bank b = new Bank(); public void run()//复写run方法 { for(int x=0; x<3; x++) { b.add(100);//因为for循环中只有一句调用add方法,所以可以只用将add方法进行同步 //而不用再将run方法也同步 } }}class BankDemo{ public static void main(String[] args) { Cus c = new Cus(); //创建线程 Thread t1 = new Thread(c); Thread t2 = new Thread(c); //启动线程t1.start(); t2.start(); }}

同步函数用的是哪一个锁?

函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。

通过以下程序进行验证。使用两个线程来买票。一个线程在同步代码块中。一个线程在同步函数中。
都在执行买票动作。

class Ticket implements Runnable//实现Runnable接口{ private int tick = 100;//共享数据100张票 Object obj = new Object(); boolean flag = true; public void run() { if(flag) { // 线程一进来执行 while(true) { synchronized(this)//同步代码块 { if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....code : "+ tick--); } } } } else//线程二进来执行show方法 while(true) show(); } public synchronized void show()//同步函数  { if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--); }  }}class ThisLockDemo{ public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); try{Thread.sleep(10);}catch(Exception e){} t.flag = false; t2.start(); }}

以上程序如果同步代码块用的是obj锁,也就是说如果同步代码块和同步函数用的不是同一个锁,那么会出现安全问题,也就是最后居然打印出了0号票,这不符合多线程的安全,也同时违背了多线程安全问题解决方案的前提:必须是多个线程使用同一个锁。以上程序也同时验证了同步函数使用的锁是this

当同步函数被静态修饰后,使用的锁就不在是this了。

因为静态方法中不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class  该对象的类型是Class。所以:静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class

所以上面的程序代码中的同步代码块应该改为:

synchronized(Ticket.class)//这里为Ticket.class { if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....code : "+ tick--); } }

6,死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 程序也不会发出任何异常和任何提示。比如下面程序就是一个死锁的现象:

class Ticket implements Runnable{ private int tick = 1000; Object obj = new Object(); boolean flag = true; public void run() { if(flag) {//第一个线程进来 while(true) { synchronized(obj)//第一个线程拿到了obj这个锁 { show();//第一个线程调用show方法的时候要拿this的锁 } } } else//第二个线程进来 while(true) show(); } public synchronized void show()//this 第二个线程拿到了this这个锁 { synchronized(obj)//第二个线程运行要拿obj这个锁 { if(tick>0) { try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....code : "+ tick--); } } }}class DeadLockDemo{ public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); try{Thread.sleep(10);}catch(Exception e){} t.flag = false; t2.start(); }}

其实也就是说 第一个线程和第二个线程同时咬着自己的锁不放时,那么他们都会处于永久的阻塞,导致死锁。

7,线程间通信

其实就是多个线程在操作同一个资源,但是操作的动作不同。

wait:
notify();
notifyAll();
都使用在同步中,因为要对持有监视器(锁)的线程操作。
所以要使用在同步中,因为只有同步才具有锁。

为什么这些操作线程的方法要定义Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁,
只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。
不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。
而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。

wait(),sleep()有什么区别?

wait()释放CPU执行权,释放锁

sleep()释放CPU执行权,不释放锁。

下面一段代码:

class Resource{ private String name; private int count = 1; private boolean flag = false; // t1 t2 private Lock lock = new ReentrantLock(); private Condition condition_pro = lock.newCondition(); private Condition condition_con = lock.newCondition();  public void set(String name)throws InterruptedException  { lock.lock(); try { while(flag) condition_pro.await();//当标记为假 也就是资源不为空 那么生产者t1,t2就会等待 this.name = name+"--"+count++; //当标记不为假 生产者开始生产 System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); flag = true;//将标记置为真,也就是资源为空 condition_con.signal();//将消费者唤醒 } finally { lock.unlock();//释放锁的动作一定要执行。 }  } // t3 t4 public void out()throws InterruptedException  { lock.lock(); try { while(!flag)//当标记为真 也就是资源为空的时候 condition_con.await();//消费者等待 //当标记为假 也就是资源不为空 消费者开始一顿消费 System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); flag = false;//消费完将标记置为假 condition_pro.signal();//唤醒生产者开始生产 } finally { lock.unlock(); }  }}class Producer implements Runnable{ private Resource res; Producer(Resource res) { this.res = res; } public void run()//复写run方法 { while(true) { try { res.set("+商品+"); } catch (InterruptedException e) { } } }}class Consumer implements Runnable{ private Resource res; Consumer(Resource res) { this.res = res; } public void run()//复写run方法 { while(true) { try { res.out(); } catch (InterruptedException e) { } } }}

JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notifynotifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了Synchronized
lock 
unlock
newCondition()

Condition:替代了Object wait notifynotifyAll
await();
signal();
signalAll();

8,停止线程

如何停止线程?stop方法已经过时。

只有一种方法:run方法结束。开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。

比如:

class StopThread implements Runnable{ private boolean flag = true; public void run() { while (flag)//t1 t2进去无限循环 { System.out.println(Thread.currentThread().getName() + "----run"); } } public void changeFlag(){ flag = false; } }class StopThreadDemo{ public static void main(String[] args) { StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.start(); int n = 0; while (true){ if (n++ == 60) { st.changeFlag();//改变标记 让循环结束 break; } System.out.println(Thread.currentThread().getName()+"......."+n); } } }

特殊情况
当线程处于了冻结状态。
就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。
Thread类提供该方法 interrupt();

比如:

class StopThread implements Runnable{ private boolean flag = true; public synchronized void run() { while (flag) { try { wait();//t1等待 释放资格 t2进来也等待 释放资格 } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "----Exception"); flag =false;//当把线程interrupt改变标记 使循环结束 } System.out.println(Thread.currentThread().getName() + "----run"); } } public void changeFlag(){ flag = false; } } class StopThreadDemo{ public static void main(String[] args) { StopThread st = new StopThread();Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); t2.start(); int n = 0; while (true){ if (n++ == 60) { t1.interrupt(); t2.interrupt(); st.changeFlag();//改变标记 让循环结束 break; } System.out.println(Thread.currentThread().getName()+"......."+n); } } }

9join方法

join:
当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。
join可以用来临时加入线程执行。比如下面的代码:

class Demo implements Runnable{ public void run() { for(int x=0; x<70; x++) { System.out.println(Thread.currentThread().toString()+"....."+x); } }}class JoinDemo{ public static void main(String[] args) throws Exception { Demo d = new Demo(); Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.start(); t2.start(); t1.join();//t1调用join方法那么这时主线程和t2线程都进入等待状态 等t1执行完t2和主线程才会继续执行 for(int x=0; x<80; x++) { System.out.println("main....."+x); } System.out.println("over"); }}

10,守护线程

public final voidsetDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。(也可以称为后台线程。方便理解)
一句话:当前台线程结束时  后台线程也就自动结束

用一段代码体现:

class StopThread implements Runnable{ private boolean flag =true; public void run() { while(flag) { //t1和t2是守护线程 当主线程一结束 他们也就结束 System.out.println(Thread.currentThread().getName()+"....run"); } } public void changeFlag() { flag = false; }}class StopThreadDemo{ public static void main(String[] args) { StopThread st = new StopThread(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.setDaemon(true); t2.setDaemon(true); t1.start(); t2.start(); int num = 0; while(true) { if(num++ == 60) { break;    } System.out.println(Thread.currentThread().getName()+"......."+num);     } System.out.println("over");}}

10,线程的一些其他方法

比如:

publicstatic void yield()//暂停当前正在执行的线程对象,并执行其他线程。

public final void setPriority(int newPriority)//更改线程的优先级。setPriority这个方法用于设置线程的优先级,而线程的执行权有1~10这10个级别,最高是10也可以用

MAX_PRIORITY来表示 最低阶1也可以用MIN_PRIORITY ,默认优先级是 NORM_PRIORITY 为5

但是并不代表优先级越高就会一直执行这个线程,其他线程也会和它争抢CPU执行权,只是说优先级越高,会优先执行到这个线程。

可以用一段代码体现:

class Demo implements Runnable{ public void run() { for(int x=0; x<70; x++) { System.out.println(Thread.currentThread().toString()+"....."+x); Thread.yield();//暂停当前正在执行的线程对象,并执行其他线程。 // } }}class JoinDemo{ public static void main(String[] args) throws Exception { Demo d = new Demo();Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.start(); t1.setPriority(Thread.MAX_PRIORITY);//将t1线程的优先级设置为最大 t2.start(); for(int x=0; x<80; x++) { System.out.println("main....."+x); } System.out.println("over");}}