一、排队等待
1、下面的这个简单的 Java 程序完成四项不相关的任务.这样的程序有单个控制线程,控制在这四个任务之间线性地移动.此外,因为所需的资源 ― 打印机、磁盘、数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间.因此,程序在访问数据库之前必须等待打印机完成打印文件的任务,等等.如果 您正在等待程序的完成,则这是对计算资源和您的时间的一种拙劣使用.改进此程序的一种方法是使它成为多线程.
class myclass { static public void main(String args[]) { print_a_file(); manipulate_another_file(); access_database(); draw_picture_on_screen(); } }
2、在本例中,每项任务在开始之前必须等待前一项任务完 成,即使所涉及的任务毫不相关也是这样.但是,在现实生活中,我们经常使用多线程模型.我们在处理某些任务的同时也可以让孩子、配偶和父母完成别的任务. 例如,我在写信的同时可能打发我的儿子去邮局买邮票.用软件术语来说,这称为多个控制(或执行)线程.
3、可以用两种不同的方法来获得多个控制线程:
-
多个进程
在 大多数操作系统中都可以创建多个进程.当一个程序启动时,它可以为即将开始的每项任务创建一个进程,并允许它们同时运行.当一个程序因等待网络访问或用户 输入而被阻塞时,另一个程序还可以运行,这样就增加了资源利用率.但是,按照这种方式创建每个进程要付出一定的代价:设置一个进程要占用相当一部分处理器 时间和内存资源.而且,大多数操作系统不允许进程访问其他进程的内存空间.因此,进程间的通信很不方便,并且也不会将它自己提供给容易的编程模型. -
线程
线 程也称为轻型进程 (LWP).因为线程只能在单个进程的作用域内活动,所以创建线程比创建进程要廉价得多.这样,因为线程允许协作和数据交换,并且在计算资源方面非常廉 价,所以线程比进程更可取.Java 编程语言,作为相当新的一种语言,已将线程支持与语言本身合为一体,这样就对线程提供了强健的支持.
二、进程和线程概念
1、进程:独立运行的程序,在某种程度上相互隔离.
2、线程:就是进程中的一个独立的控制单元,程序的一条执行路径,一个进程中至少有一个线程,线程有时称为轻量级进程.与进程一样,它们拥有通过程序运行的独立的并发路径,并且每个线程都有自己的程序计数器,称为堆栈和本地变量.然而,线程存在于进程中,它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态.因为一个进程中的线程是在同一个地址空间中执行的,所以多个线程可以同时访问相同对象,并且它们从同一堆栈中分配对象.虽然这使线程更易于与其他线程共享信息,但也意味着您必须确保线程之间不相互干涉.
3、.class编译的时候有一个javac.exe,JVM启动也就是程序运行的时候有一个进程java.exe.java.exe进程中至少一个线程负责java程序的执行这个线程运行的代码存在于main方法中,该线程称之为主线程.其实jvm启动不止一个线程,还有负责垃圾回收机制的线程.
4、正确使用线程时,线程能带来诸多好处,其中包括更好的资源利用、简化开发、高吞吐量、更易响应的用户界面以及能执行异步处理.
5、线程的运行状态图
三、使用Java编程语言实现多线程
Thread 类
1、定义类继承Thread.
2、复写Thread类中的run方法,将自定义代码存储在run方法,让线程运行,此处有模板方法设计模式的思想.
3、调用线程的start方法,该方法两个作用:启动线程,线程进入就绪状态,调用run方法.
class Demo extends Thread{ @Override public void run(){ //或者把run方法写在ThreadDemo1中,ThreadDeom1继承Thread. for(int x=0; x<100; x++) System.out.println("demo run----"+x); } } public class ThreadDemo1 { public static void main(String[] args) { Demo d = new Demo();//创建好一个线程. d.start();//开启线程并执行该线程的run方法. //d.run();//仅仅是对象调用方法.而线程创建了,并没有运行. for(int x=0; x<100; x++) System.out.println("Hello World!--"+x); } }
运行结果分析:
发现运行结果每一次都不同,因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行,明确一点,在某一个时刻,只能有一个程序在运行(多核除外).cpu在做着快速的切换,以达到看上去是同时运行的效果,我们可以形象把多线程的运行描述为在互相抢夺cpu的执行权.这就是多线程的一个特性:随机性.谁抢到谁执行,至于执行多长,cpu说了算.
4、线程命名
public class ThreadDemo2 extends Thread{ @Override public void run() { for(int i=0;i<100;i++) System.out.println(getName()+"---"+i); } public static void main(String[] args) { ThreadDemo2 t1 = new ThreadDemo2(); ThreadDemo2 t2 = new ThreadDemo2(); t2.setName("t2"); t1.start(); t2.start(); for(int i=0;i<100;i++) System.out.println("main---"+i); } } public class ThreadDemo3 extends Thread{ public ThreadDemo3(String name) { super(name); } @Override public void run() { for(int i=0;i<100;i++) System.out.println((Thread.currentThread()==this)+"-"+currentThread().getName()+"-"+i); } public static void main(String[] args) { new ThreadDemo3("t1").start(); new ThreadDemo3("t2").start(); for(int i=0;i<100;i++) System.out.println("main---"+i); } }
运行结果分析:
1、线程都有自己默认的名称,Thread-编号,该编号从0开始.
2、setName()或者构造函数,设置线程名称,getName()获取线程名称.
3、static Thread currentThread(),获取当前线程对象.
4、有时要作为线程运行的类可能已经是某个类层次的一部分,所以就不能再按这种机制创建线程.虽然在同一个类中可以实现任意数量的接口,但 Java 编程语言只允许一个类有一个父类.同时,某些程序员避免从 Thread 类导出,因为它强加了类层次.对于这种情况,就要 runnable 接口.
Runnable 接口
例:简单的卖票程序,多个窗口同时卖票.
class Tick extends Thread{ public Tick(String name) { super(name); } private int num=100; @Override public void run() { while(true) if(num>0)System.out.println(getName()+"--sale--"+num--); } } public class TicketDemo2 { public static void main(String[] args) { new Tick("t2").start(); new Tick("t1").start(); } }
运行结果分析:
无法实现,没有共享num,因为不是同一个线程对象,各自有各自的堆栈空间,引出自定义线程的第二种方式.此处类比servlet中实现SingleThreadModel标记接口,会创建多个servlet,从而没有线程问题.但其实servlet一般不采用这种方式解决线程问题.
第二种方式步骤:
1、定义类实现Runnable接口
2、覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中.
3、通过Thread类建立线程对象.
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数.
5、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法.
第二种方式解决方案:
class Ticket implements Runnable{ private int tick= 100; public void run(){ while(true) if(tick>0)System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } public class TicketDemo{ public static void main(String[] args) { Ticket t = new Ticket();//此处并没有创建线程 new Thread(t).start();//创建并启动了一个线程 new Thread(t).start();//创建并启动了一个线程 new Thread(t).start();//创建并启动了一个线程 } }
运行结果分析:
发现实现了num的共享,虽然也不是同一个线程对象,观察源码发现,调用的都是t对象的run方法,只有一个t对象,所以实现了共享,但是打印出0,-1,-2等错票,多线程的运行出现了安全问题.
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误.
解决办法:
对多条操作共享数据的语句,只能保证里面有一个线程执行,在执行过程中,其他线程不可以参与执行.
Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块.
synchronized(对象){
需要被同步的代码
}
此处的对象就是锁(可以是任意对象),持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁,同步代码块底层其实有一个标志变量,比如0标志有线程,1标志没有线程,每个线程要进入前先检查标志.
好处:解决了多线程的安全问题.
弊端:多个线程需要判断锁,较为消耗资源,但是必须的,为了安全.
6、同步代码块解决方案:
class Ticket3 implements 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--); } } } } } public class TicketDemo3{ public static void main(String[] args) { Ticket3 t = new Ticket3(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } }
7、面向对象的思想模拟火车站买票,结果做到最后发现,也是生产者消费者问题,购票人是消费者,售票员是生产者,票是生产的产品,售票的窗口是装产品的容器.生活中大部分并发操作都是生产者消费者问题.
8、同步函数实例:
银行有一个金库,有两个储户分别存300元,每次存100,存3次.
目的:该程序是否有安全问题,如果有,如何解决?
同步函数,可以将操作共享数据的代码抽出封装到同步函数中,同步函数默认锁匙this,同步函数和同步代码块可以相互转换.
如何找问题:
1,明确哪些代码是多线程运行代码.
2,明确共享数据.
3,明确多线程运行代码中哪些语句是操作共享数据的.
class Bank{ //模型很好,将共享数据单独封装到类中,和线程分开 private int sum; //Object obj = new Object(); public synchronized void add(int n){ //synchronized(obj){ sum = sum + n; try{Thread.sleep(10);}catch(Exception e){} System.out.println("sum="+sum); //} } } class Cus implements Runnable{ private Bank b = new Bank(); public void run(){ for(int x=0; x<3; x++) b.add(100); } } class BankDemo{ public static void main(String[] args) { Cus c = new Cus(); new Thread(c).start(); new Thread(c).start(); } }
9、同步的前提:
1、必须要有两个或者两个以上的线程.
2、必须是多个线程使用同一个锁,也就是锁定的是同一个对象.
例:只需明白概念,代码无实际意义
class Ticket implements Runnable{ private int tick = 100; Object obj = new Object(); boolean flag = true; public void run(){ if(flag) while(true){ synchronized(obj){ //此处不是同一锁 if(tick>0){ try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....code : "+ tick--); } } } else while(true) show(); } public synchronized void show(){//this if(tick>0){ try{Thread.sleep(10);}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--); } } } public class TicketDemo3{ public static void main(String[] args) { Ticket t = new Ticket(); new Thread(t).start(); Thread t2 = new Thread(t); try{Thread.sleep(10);}catch(Exception e){} t.flag = false; t2.start(); } } 运行结果分析: Thread-0....code : 4 Thread-0....code : 3 Thread-1....show.... : 2 Thread-1....show.... : 1 Thread-0....code : 0
结尾处出现0的错误票数,表明还是使用了同步还是存在安全问题,是同步函数和同步代码块没有使用同一个锁,将同步代码块中的锁改为this,则运行结果正常.
10、如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this.因为静态方法中也不可以定义this.
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象.
静态的同步方法,使用的锁是该方法所在类的字节码文件对象. 类名.class
四、死锁
同步中嵌套同步时,会发生死锁.
class Test implements Runnable{ private boolean flag; Test(boolean flag){ this.flag = flag; } public void run(){ if(flag){ while(true){ synchronized(MyLock.locka){ Thread.sleep(10);synchronized(MyLock.lockb){ System.out.println(getName()+"..if lockb"); } } } } else{ while(true){ synchronized(MyLock.lockb){ Thread.sleep(10);synchronized(MyLock.locka){ System.out.println(getName()+".....else locka"); } } } } } } class MyLock{ static Object locka = new Object(); static Object lockb = new Object(); } public class TicketDemo3{ public static void main(String[] args) { new Thread(new Test(true)).start(); new Thread(new Test(false)).start(); } }
死锁可能是多线程程序最常见的问题.当一个线程需要一个资源而另一个线程持有该资源的锁时,就会发生死锁.这种情况通常很难检测.但是,解决方案却相当好:在所有的线程中按相同的次序获取所有资源锁. 例如,如果有四个资源 ―A、B、C 和 D ― 并且一个线程可能要获取四个资源中任何一个资源的锁,则请确保在获取对 B 的锁之前首先获取对 A 的锁,依此类推.如果"线程 1"希望获取对 B 和 C 的锁,而"线程 2"获取了 A、C 和 D 的锁,则这一技术可能导致阻塞,但它永远不会在这四个锁上造成死锁.
五、生产者消费者问题 **经典代码
class Product{ private int id; Product(int id){ this.id=id; } public String toString() { return "Product [id=" + id + "]"; } } class Box{ Product[] p=new Product[10]; //此处可以定义为一个队列 int index=0; public synchronized void put(Product pro){ while(index==p.length){ try {wait();} catch (InterruptedException e) {} } notifyAll(); p[index]=pro; index++; } public synchronized Product pop(){ while(index==0){ try {wait();} catch (InterruptedException e) {} } notifyAll(); index--; return p[index]; } } class Producter implements Runnable{ Box box=null; Producter(Box box){ this.box=box; } public void run(){ for(int i=0;i<20;i++){ //每个生产者生产20个 Product pro = new Product(i); box.put(pro); System.out.println(Thread.currentThread().getName()+"生产了:"+pro); } } } class Customer implements Runnable{ Box box=null; Customer(Box box){ this.box=box; } public void run(){ while(true){ Product pro = box.pop(); System.out.println(Thread.currentThread().getName()+"消费了:"+pro); } } } public class ProducerAndCustomer { public static void main(String[] args) { Box box = new Box();//必须传入同一个box Producter p = new Producter(box); Customer c = new Customer(box); new Thread(p).start(); new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); new Thread(c).start(); } }
运行结果分析:
wait()函数必须拿到对象锁才能调用,wait()以后这个锁不在归当前线程所持有,必须等待被叫醒.sleep()的话还持有锁,notify()叫醒一个正在当前对象锁上等待的线程.
六、其它函数的使用
1、join
public class TestJoin { public static void main(String args[]){ Runner r = new Runner(); Thread t = new Thread(r);t.start(); try{t.join();}catch(InterruptedException e){} //把当前线程合并到主线程 for(int i=0;i<50;i++) System.out.println("主线程:" + i); } } class Runner implements Runnable { public void run() { for(int i=0;i<50;i++) System.out.println("SubThread: " + i); } }
2、线程优先级
public class TestPriority { public static void main(String[] args) { Thread t1 = new Thread(new T1()); Thread t2 = new Thread(new T2()); t1.setPriority(Thread.NORM_PRIORITY + 3); //Thread类有且仅有三个关于线程优先级的字段 t1.start(); t2.start(); } } class T1 implements Runnable { public void run() { for(int i=0; i<1000; i++) { System.out.println("T1: " + i); } } } class T2 implements Runnable { public void run() { for(int i=0; i<1000; i++) { System.out.println("------T2: " + i); } } }
3、yield
public class TestYield { public static void main(String[] args) { new MyThread3("t1").start(); new MyThread3("t2").start(); } } class MyThread3 extends Thread { MyThread3(String s){ super(s); } public void run(){ for(int i =1;i<=100;i++){ System.out.println(getName()+": "+i); if(i%10==0) yield(); } } }
4、sleep,interrupt
如果线程在调用Object类的wait(),或者该类的join()或sleep()方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptException.如果线程没有被这三个方法阻塞,调用interrupt将不起作用.
public class TestInterrupt { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); try {Thread.sleep(10000);} catch (InterruptedException e) {} thread.interrupt(); } } class MyThread extends Thread { boolean flag = true; public void run(){ while(flag){ System.out.println("==="+new Date()+"==="); try { sleep(1000); } catch (InterruptedException e) { System.out.println("xxs"); } } } }
ps:匿名内部类在多线程代码中使用
class ThreadTest { public static void main(String[] args) { new Thread(){ public void run(){ for(int x=0; x<100; x++) System.out.println(Thread.currentThread().getName()+"....."+x); } }.start(); for(int x=0; x<100; x++) System.out.println(Thread.currentThread().getName()+"....."+x); Runnable r = new Runnable(){ public void run(){ for(int x=0; x<100; x++) System.out.println(Thread.currentThread().getName()+"....."+x); } }; new Thread(r).start(); } }
小结
本文说明了在 Java 程序中如何使用线程.像是否 应该使用线程这样的更重要的问题在很大程序上取决于手头的应用程序.决定是否在应用程序中使用多线程的一种方法是,估计可以并行运行的代码量.并记住以下几点:
-
使用多线程不会增加 CPU 的能力.但是如果使用 JVM 的本地线程实现,则不同的线程可以在不同的处理器上同时运行(在多 CPU 的机器中),从而使多 CPU 机器得到充分利用.
-
如果应用程序是计算密集型的,并受 CPU 功能的制约,则只有多 CPU 机器能够从更多的线程中受益.
-
当应用程序必须等待缓慢的资源(如网络连接或数据库连接)时,或者当应用程序是交互式的时,多线程通常是有利的.
基于 Internet 的软件有必要是多线程的;否则,用户将感觉应用程序反映迟钝.例如,当开发要支持大量客户机的服务器时,多线程可以使编程较为容易.在这种情况下,每个线程可以为不同的客户或客户组服务,从而缩短了响应时间.