Java多线程编程
来源:互联网 发布:3306端口攻击 编辑:程序博客网 时间:2024/05/29 06:56
Java多线程编程
概述
对于一个初学者,可能问的最多的就是在我刚学Java的时候,没有用到线程,但是为什么我还能执行程序呢?一个Java程序最少会含有2个线程,怎么解释这个问题呢?首先,main方法是每一个程序都必须有的函数,此时JVM会为其创建一个线程,Java是一个面向对象的编程语言,并且Java提供了垃圾回收机制,此时垃圾回收也会占用一个线程,这样的话有两个线程也是可以理解的。在操作系统课程中,我们同样接触过线程的概念,在Java中线程和操作系统中线程的概念是一致的,在此不在重复说明概念,下面的文章中将会用编程语言实现Java的线程机制,每个知识点都是根据一个具体的需求实现的,便于理解。请注意:本文实际上全部是使用java.lang.Thread类的方法,请学会参考JDK。
线程的两种创建方式
- 继承Thread类,并重写run方法
- 继承Thread
- 重写run方法,将线程中将要执行的代码放到run方法中
- 调用Thread类的start方法开启线程
- 实现Runnable接口
- 首先定义一个类实现Runnable接口
- 重写Runnable接口的run方法
- 创建Runnable实现类的对象
- 创建Thread类的对象,并使用构造函数:
Thread(Runnable target,String name)
将第一步创建的Runnable实现类的对象传入到Thread构造函数中 - Thread类的对象调用start方法,在这里有一个疑问,并没有重写Thread类的run方法,只是重写了Runnable接口的run方法,这个调用关系是怎样解释的?看下面的代码:
public void run() { if (target != null) { target.run(); //这的target实际上就是Runnable接口实现类的对象,实际上是Thread类的run方法调用了Runnable接口实现类的run方法 }}
- 推荐使用实现Runnable类的接口,原因:在Java中继承是单继承,比如现实开发中我们必须去继承其他的类,比如项目经理或者以前的设计人员写的代码,但是由于你已经继承了Thread,所以就不能继承其他的类,这样直接导致我们需要重复的写一些已经被别人实现的类的方法,重复率很大,不科学。但是下文全部是以第一种方式去创建的线程,因为实现方式简单。
Java中的线程类
Java提供了一个类java.lang.Thread类提供给我们一种简单的定义,简单的代码如下:
class Test extends Thread{//继承Thread类,并重写run()方法 @Override public void run(){//重写run()方法 for(int i = 0 ; i < 100 ; i++){ System.out.println("子线程"+ (i+1)); } } public static void main(String[] args){ Test test = new Test(); test.start();//开启线程必须调用start()方法,不能直接调用run()方法 for(int i = 0;i < 100 ; i++){ System.out.println("main线程"+(i+1)); } }}
现实编程中,很多时候需要同时开启多个线程,Java支持吗?回答是肯定的,像下面的代码:
class VideoThread extends Thread{ @Override public void run(){ while(true){ System.out.println("咱们聊天吧!"); } }}class TalkThread extends Thread{ @Override public void run(){ while(true){ System.out.println("视频好不好?"); } }}class Test{ public static void main(){ TalkThread talkThread = new TalkThread(); talkThread.start();//开启聊天线程 VideoThread videoThread = new VideoThread(); videoThread.start();//开启视频线程 }}
Thread类常用方法
- 构造函数
在JDK 1.7中,Thread类给我们提供给了7种不同的构造函数,其中我个人觉得用的最多的是Thread(String name)
,该构造函数能帮助我们在创建线程的时候自定义线程的名字,但是我们不是直接使用该方法去构造线程的名字,因为在这之前我们采用的创建线程的方法是通过继承Thread类实现,所以我们使用super关键字调用父类构造函数,代码如下:
class MyThread extends Thread{ public MyThread(String name){ super(name); } @Override public void run(){ System.out.println("自定义线程"); }}class TestThread{ public static void main(String[] args){ MyThread myThread = new MyThread("该线程的名字"); myThread.start();//开启线程,调用run方法 }}
- 获取当前线程的信息
- currentThread方法,该方法是一个静态方法,直接就能使用类名调用,并且返回的是执行该方法的进程,测试代码:
class MyThread extends Thread{ public MyThread(String name){ super(name); } @Override public void run(){ System.out.println("自定义线程"); }}class TestThread{ public static void main(String[] args){ MyThread myThread = new MyThread("该线程的名字"); myThread.start();//开启线程,调用run方法 //获取当前进程的引用 Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用 System.outprintln(thread.toString()); }}
- getId
- getName
- getPriority,获取当前进程的优先级
- setName,设置线程的名字,但是我们一般会在初始化线程时使用构造函数创建,这个函数通常情况下供我们使用去修改线程的名字
- setPriority,设置线程的优先级,优先级从1到10,默认情况下优先级为5
- start,该函数开启线程去调用run方法
- sleep函数,该函数也是静态函数,但是sleep方法会抛出一个InterruptedException异常,当sleep方法写在重载的run方法中时不能抛出异常只能捕获异常。
package thread;class MyThread extends Thread{ public MyThread(String name){ super(name); } @Override public void run(){ Thread thread = MyThread.currentThread(); System.out.println(thread.toString()); System.out.println("自定义线程"); }}public class MainThread { public static void main(String[] args)throws InterruptedException{ MyThread myThread = new MyThread("线程名字"); myThread.start();//开启线程,调用run方法 //获取当前进程的引用 Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用 System.out.println(thread.toString()); //try{ //Thread.currentThread().sleep(1000); //sleep函数必须捕获异常 //}catch(InterruptedException e){ //e.printStackTrace(); //} Thread.currentThread().sleep(1000); }}
在重载的run方法中使用sleep方法,代码如下:
package thread;class MyThread extends Thread{ public MyThread(String name){ super(name); } @Override public void run(){ Thread thread = MyThread.currentThread(); System.out.println(thread.toString()); System.out.println("自定义线程"); try{ Thread.currentThread().sleep(1000); //当在run方法中时只能捕获异常,抛出异常时错误的 }catch(InterruptedException e){ e.printStackTrace(); } }}public class MainThread { public static void main(String[] args)throws InterruptedException { MyThread myThread = new MyThread("线程名字"); myThread.start();//开启线程,调用run方法 //获取当前进程的引用 Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用 System.out.println(thread.toString());// try{// Thread.currentThread().sleep(1000); //sleep函数必须捕获异常// }catch(InterruptedException e){// e.printStackTrace();// } Thread.currentThread().sleep(1000); }}
为什么在run方法中只能捕获异常,但是在其他方法中使用时既可以抛出异常也可以捕获异常呢?我们看一下在Thread类中run方法的定义,这个问题就解决了。
public void run() { if (target != null) { target.run(); } }
可以看出在Thread中父类方法没有抛出异常,根据方法重写的要求,子类抛出的异常应该比父类的异常要小,如果父类不抛出异常,那么子类也必定不能抛出异常。
同步代码块与线程安全问题
所谓线程安全问题,是指不同的线程之间争用同一个临界区时,由于CPU的调度,可能会使临界区(mutex)出现脏数据的情况,这在操作系统课程中已经学习过,在这里不再赘述,本文的重点是怎么解决线程安全问题,采用两种方案:同步代码块、同步函数,两种方法,下面先将同步代码块的使用。synchronized(同步),在同步的时候需要一个共享的锁,这个锁与临界区一样,必须是所有线程共享的,所以,临界区和锁被实现为static,但是对于锁有一个特例就是String,比如”锁”,这个字符串被创建,并且能够保证,当使用的是同一个字符串时,这种形式的字符串能够保证不会创建重复的副本,实际上也保证锁的一致性,那么在Java编程中,究竟什么能作为锁呢?回答是:任何的对象都能作为锁,但是该对象必须被修饰为static,那么为什么任何的对象都能作为锁呢?原因是:每一个对象内部都维护了一个state,Java同步机制便是利用了对象的状态作为锁的标识。简单的举一个现实生活中的例子,我们的需求是:在车站售票时,总是同时几个售票窗口在售票,我假设目前有3个售票窗口,并且只有50张票,怎么编码保证线程安全?
/*** @author:junpeng zhu* 功能:用同步代码块实现线程之间的同步,解决线程安全问题*/package thread;class SaleTickets extends Thread{ public SaleTickets(String name){ super(name); } static int tickets = 50 ;//50张票作为临界资源 @Override public void run(){ while(true){ synchronized("锁"){ if(tickets > 0){ System.out.println("售票窗口"+Thread.currentThread().getName()+"售出第"+tickets+"张票"); tickets--; }else{ System.out.println("票卖完了"); break; } } } }}public class MainThread { public static void main(String[] args){ //创建三个售票窗口,也即是三个线程,并且分别赋值为窗口1、窗口2、窗口3 SaleTickets saleTickets1 = new SaleTickets("窗口1"); saleTickets1.start(); SaleTickets saleTickets2 = new SaleTickets("窗口2"); saleTickets2.start(); SaleTickets saleTickets3 = new SaleTickets("窗口3"); saleTickets3.start(); }}
同步函数与线程安全问题
同步函数就是使用synchronized去修饰一个函数,如果是一个非静态的同步函数的锁,锁对象是this对象,也就是当前调用该同步函数的对象,这样的话实际上锁不是多个线程共享的,这样的话根本不能达到同步的效果,所以只能使用静态函数,如果是静态同步函数的锁,锁对象是当前函数所属的类的字节码文件(class对象),所以同步函数的锁对象不能由自己指定,它是固定的。
package thread;class SaleTickets extends Thread{ public SaleTickets(String name){ super(name); } static int tickets = 50 ;//50张票作为临界资源 @Override public void run(){ getMoney(); } public synchronized static void getMoney(){ while(true){ if(tickets > 0){ System.out.println("售票窗口"+Thread.currentThread().getName()+"售出第"+tickets+"张票"); tickets--; }else{ System.out.println("票卖完了"); break; } } }}public class MainThread { public static void main(String[] args){ //创建三个售票窗口,也即是三个线程,并且分别赋值为窗口1、窗口2、窗口3 SaleTickets saleTickets1 = new SaleTickets("窗口1"); saleTickets1.start(); SaleTickets saleTickets2 = new SaleTickets("窗口2"); saleTickets2.start(); SaleTickets saleTickets3 = new SaleTickets("窗口3"); saleTickets3.start(); }}
在同步代码块和同步函数解决线程安全问题时,推荐使用同步代码块,因为同步代码块的控制很灵活,而使用同步函数的话,整个函数的所有代码都被同步,并且由于其锁对象不能由自己指定,缺乏灵活性。上面的代码提供给了一种罪常见的使用同步函数的例子,其结果一旦被一个对象掌握,就会一直执行结束,其它对象不能获得临界区。
死锁
同步解决了线程安全问题,但是却引起了另外一个更加重要的问题死锁,死锁也是在操作系统中存在的,详细问题,请看操作系统相关部分。死锁现象出现的根本原因是:互相等待对方的资源,也就是存在两个或者两个以上的共享资源。出现死锁怎么解决呢?没有方案,死锁只能尽量去避免,但是不能解决,在写代码的过程中要注意。
线程的通讯
一个线程完成自己的任务时,这时要通知另外一个线程去做另外的任务,官方给出的例子是生产者、消费者问题。首先定义需求:生产者生产一个产品,消费者接着就去消费一个产品,如果消费者没有消费那么生产者等待消费者消费,如果生产者没有生产,此时消费者应该等待生成。
线程通讯用到的三个方法
- wait方法,等待,如果线程执行了wait方法,线程就会进入等待状态,此时必须要有另外的线程调用notify方法去唤醒这个处于等待状态的线程
- notify方法,唤醒,唤醒等待的线程
- notifyAll方法,唤醒线程池中所有等待的线程
- 三个方法需要注意的事项
- java.lang.Object中才有wait方法和notify方法、notifyAll方法
- 为什么要把wait方法和notify方法放在Object类中?原因是锁对象可以是任意类型的对象,并且wait方法和notify方法只能用锁对象进行调用,出于这种原因只能利用Java的多态性才能实现。
- wait方法和notify必须在同步代码块或者同步函数中才能使用
- 因为这个两个方法必须由锁对象调用,而锁对象只能出现在同步函数或者同步代码块中。
- 这个两个方法必须由锁对象调用,否则会报错
- 因为设置为等待和唤醒的线程是用锁对象为标识放在线程池中的,换句话说就是以锁对象为标识创建的线程池。
- java.lang.Object中才有wait方法和notify方法、notifyAll方法
生产者消费者实例
//产品类class Product{ String name; //名字 double price; //价格 boolean flag = false; //产品是否生产完毕的标识,默认情况是没有生产完成。}//生产者class Producer extends Thread{ Product p ; //产品 public Producer(Product p) { this.p = p ; } @Override public void run() { int i = 0 ; while(true){ synchronized (p) { if(p.flag==false){ if(i%2==0){ p.name = "苹果"; p.price = 6.5; }else{ p.name="香蕉"; p.price = 2.0; } System.out.println("生产者生产出了:"+ p.name+" 价格是:"+ p.price); p.flag = true; i++; p.notifyAll(); //唤醒消费者去消费 }else{ //已经生产 完毕,等待消费者先去消费 try { p.wait(); //生产者等待 } catch (InterruptedException e) { e.printStackTrace(); } } } } }}//消费者class Customer extends Thread{ Product p; public Customer(Product p) { this.p = p; } @Override public void run() { while(true){ synchronized (p) { if(p.flag==true){ //产品已经生产完毕 System.out.println("消费者消费了"+p.name+" 价格:"+ p.price); p.flag = false; p.notifyAll(); // 唤醒生产者去生产 }else{ //产品还没有生产,应该 等待生产者先生产。 try { p.wait(); //消费者也等待了... } catch (InterruptedException e) { e.printStackTrace(); } } } } }}public class Demo5 { public static void main(String[] args) { Product p = new Product(); //产品 //创建生产对象 Producer producer = new Producer(p); //创建消费者 Customer customer = new Customer(p); //调用start方法开启线程 producer.start(); customer.start(); }}
线程知识杂烩
学习任何的编程语言,实际上首先应该明确的就是应该认真的学习编程语言自带的API,其实本文大部分都是对java.lang.Thread类的方法的使用,只是其中渗透了自己在编程中的一些经验,本文还属于Java SE部分,其中很多的功能都只是模拟,用输出语句代替现实功能,这样做的好处是可以隐藏功能本身带来的复杂性,只关注API本身的特性和功能,下面再讲三个在Java.lang.Thread类中常用的方法,均是以代码去呈现的。顺便说一个可能被大家忽略的问题:线程函数中通常都有循环,并且我们通常用的是死循环,如果需要,使用break停止线程。
停止线程
在有些情况下我们需要去停止一个线程,这种停止线程的需求也是很常见的,怎么去停止线程呢?停止一个线程,我们通常会使用一个变量去控制,如果停止一个处于等待状态的线程,我们需要使用notify方法去控制或者使用interrupt方法,但是Interrupt方法会抛出一个异常。
package thread;class TestThread extends Thread{ public TestThread(String name){ super(name); } boolean flag = true; @Override public void run(){ int i= 0; while(flag){ synchronized(this.currentThread()){ System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次"); i++; } } }}public class StopThread { public static void main(String[] args){ TestThread thread = new TestThread("张三"); thread.start(); //给一个循环,当循环到第80次时停止线程 for(int i = 0 ; i < 100 ; i++){ System.out.println("执行了main线程第"+i+"次"); if(i == 80){ thread.flag = false; break; } } }}
当线程处于等待状态时,怎么办呢?看下面的代码:
package thread;class TestThread extends Thread{ public TestThread(String name){ super(name); } boolean flag = true; @Override public void run(){ int i= 0; while(flag){ this.setPriority(10); synchronized(this.currentThread()){ try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次"); i++; } } }// public synchronized void run(){// int i= 0;// while(flag){// this.setPriority(10);// try {// this.wait();// } catch (InterruptedException e) {// // TODO Auto-generated catch block// e.printStackTrace();// }// System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");// i++;// }// }}public class StopThread { public static void main(String[] args){ TestThread thread = new TestThread("李四"); thread.start(); for(int i = 0 ; i < 100 ; i++){ System.out.println("执行了main线程第"+i+"次"); if(i == 80){ synchronized(thread){ thread.notify(); } thread.flag = false; break; } } }}
用interrupted方法终止线程:
package thread;class TestThread extends Thread{ public TestThread(String name){ super(name); } boolean flag = true; @Override public void run(){ int i= 0; while(flag){ this.setPriority(10); try { this.wait(); } catch (InterruptedException e) { System.out.println("线程异常终止了..."); } synchronized(this.currentThread()){ System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次"); i++; } } }// public synchronized void run(){// int i= 0;// while(flag){// this.setPriority(10);// try {// this.wait();// } catch (InterruptedException e) {// // TODO Auto-generated catch block// e.printStackTrace();// }// System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");// i++;// // }// }}public class StopThread { public static void main(String[] args){ TestThread thread = new TestThread("李四"); thread.start(); for(int i = 0 ; i < 100 ; i++){ System.out.println("执行了main线程第"+i+"次"); if(i == 80){ thread.flag = false; thread.interrupted(); break; } } }}
守护线程
所谓守护线程,其实就是在背后干一些偷鸡摸狗的事情,比如当你下载一个Java设计的游戏时,通常情况下会有很多的广告,但是你肯定是不会去点击那些广告的,这样的话程序的钱从哪里来,所以通常情况下,软件编写人员会在后台开启一个守护线程去点击广告,这是一个最贴近现实的例子。在一个进程中,如果只剩了守护线程,那么守护线程也会死亡。设置守护线程的方法是setDaemon(boolean on),当on为true时,设置为守护线程,当on为false时,设置为非守护线程。默认情况下,线程是非守护线程,我们可以使用isDaemon方法去判断一个线程是否是守护线程。需求提出:在使用QQ的时候,经常会发现在我们不注意的时候QQ在进行版本更新,当只有守护线程在运行时,守护线程必须结束执行,偷鸡摸狗现象最常见,下面的代码模拟这个功能。
package thread;class TestThread extends Thread{ public TestThread(String name){ super(name); } @Override public void run(){ for(int i = 0 ; i < 100 ; i++){ if(i==99){ System.out.println("完成下载"); break; } System.out.println("已经下载"+(i+1)+"%"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }}public class StopThread { public static void main(String[] args){ TestThread thread = new TestThread("王五"); thread.setDaemon(true);//将该进程设置为守护进程,该方法必须在线程启动之前调用(在JDK中有说明) thread.start(); for(int i = 0 ; i < 10 ; i++){ System.out.println(Thread.currentThread()+"正在执行"); } }}
线程让步
所谓的线程让步就是在执行一个线程的同时,调用了另外的一个线程,并且,必须等待这个被调用线程结束才能执行调用者线程,此时就需要使用线程让步方法join,该方法也出现在java.lang.Thread类中。需求提出:小时候,当妈妈做饭的时候,想要炒菜但是发现没有酱油了,于是妈妈叫我们去打酱油,同时她做饭也停止了,等打完酱油回来再继续做饭,这就是典型的线程让步,妈妈做饭是一个线程,我们打酱油是一个线程,当出现这种情况时,妈妈线程必须停下来等待打酱油线程结束。
package thread;class TestThreadMum extends Thread{ public TestThreadMum(String name){ super(name); } @Override public void run(){ System.out.println("妈妈正在做菜"); System.out.println("但是没有酱油了"); TestThreadSun testthreadsun = new TestThreadSun("儿子线程"); testthreadsun.start(); try { testthreadsun.join(); //必须执行完TestThreadSun线程,才能执行TestThreadMum线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("妈妈继续做菜"); System.out.println("全家人一起快乐的吃饭"); }}class TestThreadSun extends Thread{ public TestThreadSun(String name){ super(name); } @Override public void run(){ System.out.println("我要替妈妈去打酱油啦!"); System.out.println("打完酱油回到家!"); }}public class StopThread { public static void main(String[] args){ TestThreadMum testthreadmum = new TestThreadMum("妈妈进程"); testthreadmum.start(); }}
总结
这些方法都是在平时的线程编程中经常用到的,在java.lang.Thread中还有很多方法在本文没有涉及到,这些不常见,使用时可以查看JDK文档。
- 【JAVA】JAVA多线程编程
- 【java】:java多线程编程
- Java多线程编程初步
- Java 多线程编程
- Java多线程编程详解
- Java多线程编程经验谈
- Java 多线程编程
- Java多线程编程详解
- Java多线程编程详解
- Java 5.0多线程编程
- Java 5.0多线程编程
- Java 5.0多线程编程
- Java多线程编程详解
- Java多线程编程详解
- Java 5.0多线程编程
- Java多线程编程详解
- java基础教程-多线程编程
- Java多线程编程详解
- 打印*号
- 第三周项目3—输出星号图
- PHP表单传值
- Linux 文件传输 lrzsz,scp
- android studio + bluestack
- Java多线程编程
- Android Studio安装指南及genymotion配置
- eclipse 出现 adb-Adb connection Error 解决方式
- js中的正则表达式
- mingw 编译c++ 最简单的程序出现Program received signal SIGSEGV, Segmentation fault.
- 关于学习的思考
- jdk环境变量配置
- java基础之Classloading and class objects
- Ajax and php 2.5