资源描述
Java多线程实现
线程及进程
Java是一门为数不多的支持多线程编程的语言。
进程?在操作系统的定义中,进程指的是一次程序的完整运行,这个运行的过程之中内存、处理器、IO等资源操作都要为这个进程服务。
在最早的dos系统的时代,有一个特点:如果你电脑病毒发作了,那么你电脑几乎就不能动了。因为所有的资源都被病毒软件所占用,其他的程序无法抢占资源。但是到了后来Windows时代,这一情况发生了改变,电脑即使有病毒,电脑也可以运行(就是慢点)。
Windows属于多进程的操作系统。但是有一个问题出现了:每一个进程都需要有资源的支持,那么多个进程怎么去分配资源呢?
在同一个时间段上,会有多个进程轮流去抢占资源。但是在某一个时间点上只能有一个进程。
线程?线程是在进程的基础上进一步划分的结果,即:一个进程上可以同时创建多个线程。
线程是比进程更快的处理单元,而且所占的资源也小。那么多线程的应用也是性能最高的应用。
总结:线程的存在离不开进程。进程如果消失后,线程一定会消失。反之线程消失了进程未必会消失。
Thread类实现
掌握java中3种多线程的实现方式(JDK1.5之后增加了第3种)。
如果想在java中实现多线程,有两种途径:
1. 继承Thread类;
2. 实现Runable接口(Callable接口)。
Thread类是一个支持多线程的功能类,只要有一个类继承了Thread那么它就是一个多线程的类。
Class MyThread extends Thread{}
所有程序的起点是main()方法。但是所有线程也有自己的起点,即:run()方法。在多线程的每个主体类之中都必须覆写Thread类中所提供的run()方法。
public void run(){}
这个方法没有返回值,那么也就表示线程一旦开始那么就要一直执行,不能够返回内容。
class MyThread extends Thread{
private String name;//定义属性
public MyThread(String name){//定义构造方法
this.name=name;
}
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<200;x++){
System.out.println(this.name+"-------"+x);
}
}
}
本线程类的功能是进行循环的输出操作。所有的线程跟进程一样的,都必须轮流抢占资源。所以多线程的执行应该是多个线程彼此交替执行,也就是说如果直接调用了run()方法,那么就不能够启动多线程,多线程启动的唯一方法就是Thread类中的start()方法:public void start(),调用此方法执行的方法体是run()方法定义的。
package cn.mldn.util;
class MyThread extends Thread{
private String name;//定义属性
public MyThread(String name){//定义构造方法
this.name=name;
}
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<100;x++){
System.out.println(this.name+"-------"+x);
}
}
}
public class TestDemo{
public static void main(String args[]){
MyThread mt1=new MyThread("线程A");
MyThread mt2=new MyThread("线程B");
MyThread mt3=new MyThread("线程C");
mt1.start();
mt2.start();
mt3.start();
}
}
此时每一个线程对象交替执行。
疑问?为什么多线程启动不是调用run(),而必须调用start()?
Thread的start()方法不仅仅要启动多线程的执行代码,还要去根据不同的操作系统进行资源的分配。
Runnable接口实现
虽然Thread类可以实现多线程的主体类定义,但是它有一个问题,java具有单继承局限,正因为如此,在任何情况下针对于类的继承都应该是回避的问题,那么多线程也一样,为了解决单继承的限制,在java里面专门提供了Runable接口:
@FunctionalInterface
public interface Runnable{//函数式接口,特征是一个接口只能有一个方法
public void run();
}
在接口里面任何的方法都是public定义的权限,不存在默认的权限。
那么只需要让一个类实现Runnable接口,并且也需要覆写run()方法。
及继承Thread类相比,此时的MyThread类在结构上是没有区别的,但是有一点是有严重区别的:如果此时继承了Thread类,那么可以直接继承start()方法;但如果实现的是Runnable接口,并没有start()方法可以被继承。
class MyThread implements Runnable{
private String name;//定义属性
public MyThread(String name){//定义构造方法
this.name=name;
}
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<100;x++){
System.out.println(this.name+"-------"+x);
}
}
}
不管何种情况下,如果要想启动多线程,一定依靠Thread类完成,在Thread类里定义有如下的构造方法:
public Thread(Runnable target),接收的是Runnable接口对象;
范例:
package cn.mldn.util;
class MyThread implements Runnable{
private String name;//定义属性
public MyThread(String name){//定义构造方法
this.name=name;
}
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<200;x++){
System.out.println(this.name+"-------"+x);
}
}
}
public class TestDemo{
public static void main(String args[]){
MyThread mt1=new MyThread("线程A");
MyThread mt2=new MyThread("线程B");
MyThread mt3=new MyThread("线程C");
Thread m1=new Thread(mt1);
Thread m2=new Thread(mt2);
Thread m3=new Thread(mt3);
m1.start();
m2.start();
m3.start();
}
}
此时就避免了单继承局限,那么也就是说在实际工作中使用接口是最合适的。
两种实现方式的区别(面试题)
通过讲解已经清楚多线程两种实现方式,这两种方式有哪些区别?
首先我们要明确的是使用Runnable接口及Thread相比,解决了单继承局限,所以不管后面的区别及联系,至少这一点就已经下了死定义。如果要使用,一定使用Runnable接口。
观察Thread类定义:
public class Thread
extends Object
implements Runnable
发现Thread类实现了Runnable接口。
此时整个的定义结构非常像代理设计模式,如果是代理设计模式,客户端调用的代理类的方法应该是接口里提供的方法,那么也应该是run()才对。
除了以上的联系之外,还有一点:使用Runnable接口可以比Thread类更好的描述出数据共享这一概念。此时的数据共享指的是多个线程访问同一资源的操作。
范例:观察代码Thread实现(每一个线程对象都必须通过start()启动)
package cn.mldn.util;
class MyThread extends Thread{
private int ticket=10;//定义属性
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<100;x++){
if(this.ticket>0){
System.out.println("卖票,ticket="+this.ticket--);
}
}
}
}
public class TestDemo{
public static void main(String args[]){
//由于Mythread类有start()方法,所以每一个MyThread类就是一个线程对象,可以直接启动
MyThread mt1=new MyThread();
MyThread mt2=new MyThread();
MyThread mt3=new MyThread();
mt1.start();
mt2.start();
mt3.start();
}
}
本程序声明了3个MyThread类对象,并且分别调用了3次start()方法启动线程对象。结果是每一个线程对象都在卖各自的票。此时并不存在数据共享。
范例:利用Runnable实现
package cn.mldn.util;
class MyThread implements Runnable{
private int ticket=10;//定义属性
@Override
public void run() {//覆写run方法,作为线程的主体方法
for(int x=0;x<100;x++){
if(this.ticket>0){
System.out.println("卖票,ticket="+this.ticket--);
}
}
}
}
public class TestDemo{
public static void main(String args[]){
MyThread mt1=new MyThread();
new Thread(mt1).start();
new Thread(mt1).start();
new Thread(mt1).start();
}
}
此时也属于3个线程对象,唯一的区别是这3个线程对象都占用了同一个MyThread类的对象引用,也就是这3个线程对象都直接访问同一个数据资源。
面试题:请解释Thread类及Runnable接口实现多线程的区别?(请解释多线程两种实现方式的区别?)
Thread类是Runnable接口的子类,使用Runnable接口实现可以避免单继承局限;
Runnable接口实现的多线程可以比Thread类实现的多线程更加清楚的描述数据共享的概念。
请写出多线程两种实现操作?
把Thread类继承的方式和Runnable接口实现的方式都写出来。
Callable接口(理解)
使用Runnable接口实现的多线程可以避免单继承局限,但是有一个问题,Runnable接口里面的run()方法不能返回操作结果。为了解决这个矛盾,提供了一个新的接口:java.util.concurrent.Callable接口。
public Interface Callable<V>{
public V call()throws Exception;
}
Call()方法完成线程的主体功能后可以返回一个结果,而这个结果的类型由Callable接口的泛型决定。
范例:定义一个线程主体类
import java.util.concurrent.Callable;
class MyThread implements Callable<String>{
private int ticket=10;//定义属性
@Override
public String call() throws Exception{
for(int x=0;x<100;x++){
if(this.ticket>0){
System.out.println("卖票,ticket="+this.ticket--);
}
}
return "票已卖光!!!";
}
}
此时观察Thread类里面并没有直接支持Callable接口的多线程应用。
从JDK1.5开始提供有java.util.concurrent.FutureTask<V>类。这个类主要是负责Callable接口对象的操作的,这个接口的定义结构:
public class FutureTask<V> extends Object implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask有如下的构造方法:FutureTask(Callable<V> callable)。
接收的目的只有一个,那么就是去的call()方法的返回结果。
package cn.mldn.util;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<String>{
private int ticket=10;//定义属性
@Override
public String call() throws Exception{
for(int x=0;x<100;x++){
if(this.ticket>0){
System.out.println("卖票,ticket="+this.ticket--);
}
}
return "票已卖光!!!";
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt1=new MyThread();
MyThread mt2=new MyThread();
FutureTask<String> fu1=new FutureTask<String>(mt1);//目的是取得call()的返回结果
FutureTask<String> fu2=new FutureTask<String>(mt2);//目的是取得call()的返回结果
new Thread(fu1).start();//启动多线程
new Thread(fu2).start();//启动多线程
//多线程执行完毕后可以取得内容,依靠FutureTask的父接口Future中的get()方法完成
System.out.println("A线程的返回结果"+fu1.get());
System.out.println("B线程的返回结果"+fu2.get());
}
}
第三种方法最麻烦的在于需要接收返回值信息,并且又要及原始的多线程的实现靠拢(向Thread类靠拢)。
总结:
1. 对于多线程的实现重点在于Runnable接口及Thread类启动的配合上。
2. 对于JDK1.5的新特性了解就行,知道区别在于返回结果上。
线程常用操作方法
多线程里有很多方法定义,但是大部分方法都在Thread里定义。强调几个及开发有关的方法。
命名和取得
所有的线程程序的执行,每一次都是不同的结果,因为它会根据自己的情况进行资源的抢占,所以如果要想区分每一个线程,那么就必须依靠线程的名字。对于线程名字一般而言会在启动之前定义,不建议对已经启动的线程进行更改名称、或者为不同的线程设置重名。
如果想进行线程名称的操作,可以使用Thread类如下的方法:
构造方法:public Thread(Runnable target,String name);
设置名字:public final void setName(String name);(final表示不能覆写)
取得名字:public final String getName();
对于线程名字的操作会出现一个问题,这些方法是属于Thread类的,可是如果回到线程类(Runnable子类),这个类并没有继承Thread类,如果要想取得线程名字,那么能够取得的就是执行本方法的线程名字。所以在Thread类里面提供有一个方法:
取得当前线程对象:public static Thread currentThread()。
范例:观察线程的命名
package cn.mldn.util;
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
如果再实例化Thread类对象的时候没有为其设置名字,那么会自动的进行编号命名,保证线程对象的名字不重复。
范例:观察设置名字
package cn.mldn.util;
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt,"自己的线程A").start();
new Thread(mt).start();
new Thread(mt,"自己的线程B").start();
}
}
观察:
package cn.mldn.util;
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt,"自己的线程").start();
mt.run();
}
}
结果:
main
自己的线程
原来主方法就是一个线程:main线程。
通过以上的代码发现:线程其实一直都存在,可是进程去哪里了?
每当使用java命令去解释一个程序类的时候,对于操作系统而言,都相当于启动了一个新的进程,而main只是新进程上的一个子线程而已。
提问:每个JVM进程启动的时候至少启动几个线程呢?
1. main线程:程序的主要执行,以及启动子线程;
2. gc线程:负责垃圾收集。
线程的休眠
所谓线程休眠,指的是让线程执行的速度变慢一点,休眠方法:
public static void sleep(long millis) throws InterruptedException
long:用来表示日期、时间及文件大小。
范例:观察休眠的特点
package cn.mldn.util;
class MyThread implements Runnable{
@Override
public void run(){
for(int x=0;x<10000;x++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",X="+x);
}
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt,"自己的线程").start();
}
}
因为每一次执行到run()方法的线程对象都会休眠,所以执行的速度就会变慢。
默认情况下在休眠的时候如果设置了多个线程对象,那么所有的线程对象将会一起进入到run()方法(所谓一起进入因为先后顺序实在太短,肉眼忽略)。
线程优先级
所谓优先级指的是越高的优先级越有可能先执行。在Thread类里提供有以下两个方法进行优先级的操作:
设置优先级:public final void setPriority(int newPriority);
取得优先级:public final int getPriority()。
发现设置和取得优先级都是使用int型数据,对于此内容有3种取值:
最高优先级:public static final int MAX_PRIORITY,10;
中等优先级:public static final int NORM_PRIORITY,5;
最低优先级:public static final int MIN_PRIORITY,1。
范例:
package cn.mldn.util;
class MyThread implements Runnable{
@Override
public void run(){
for(int x=0;x<20;x++){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",X="+x);
}
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
Thread mt1=new Thread(mt,"自己的线程A");
Thread mt2=new Thread(mt,"自己的线程B");
Thread mt3=new Thread(mt,"自己的线程C");
mt3.setPriority(Thread.MAX_PRIORITY);
mt1.start();
mt2.start();
mt3.start();
}
}
范例:主线程优先级是多少?
public class TestDemo{
public static void main(String args[]) throws Exception{
System.out.println(Thread.currentThread().getPriority());
}
}
结果:5
所以主线程是中等优先级。
总结:
1. Thread.currentThread()可以取得当前线程类对象;
2. Thread.sleep()主要是休眠,如果设置多个线程对象的话,感觉是一起休眠,但实际是由先后顺序的。
3. 清楚优先级越高的线程对象越有可能先执行。
线程的同步及死锁(了解)
线程的同步产生原因
线程的同步处理操作
线程的死锁
同步问题引出
所谓同步指的是多个线程访问同一资源时所需要考虑到的问题。
范例:观察非同步情况下的操作
package cn.mldn.util;
class MyThread implements Runnable{
public int ticket=5;
@Override
public void run(){
for(int x=0;x<20;x++){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+",卖票,ticket="+this.ticket--);
}
}
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt,"票贩子A").start();
new Thread(mt,"票贩子B").start();
new Thread(mt,"票贩子C").start();
new Thread(mt,"票贩子D").start();
}
}
此时4个线程对象一起出售5张票。此时没有出现问题是因为在一个JVM下运行,并且没有
受到任何影响。
class MyThread implements Runnable{
public int ticket=5;
@Override
public void run(){
for(int x=0;x<20;x++){
if(this.ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",卖票,ticket="+this.ticket--);
}
}
}
}
增加延迟之后发现执行的结果出现了负数,那么着就叫不同步的情况,到底如何造成不同步呢?整个卖票的步骤分为2步:
第一步:判断是否还有剩余票数;
第二步:减少剩余票数。
目前互联网上的应用都属于异步。异步操作就会存在数据的安全隐患。
同步操作
通过观察可以发现以上程序发生的最大问题是:判断和修改数据是分开完成的,即:某几个线程可以同时执行这些功能。
问题的解决:
在java里面如果想要实现线程的同步可以使用synchronized关键字。而这个关键字可以通过两种方式使用:
第一种:同步代码块;
第二种:同步方法。
在java里面有四种代码块:普通代码块、构造块、静态块和同步块。
范例:观察同步块。
package cn.mldn.util;
class MyThread implements Runnable{
public int ticket=54;
@Override
public void run(){
for(int x=0;x<200;x++){
synchronized(this){//当前操作每次只允许一个对象进入
if(this.ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",卖票,ticket="+this.ticket--);
}
}
}
}
}
public class TestDemo{
public static void main(String args[]) throws Exception{
MyThread mt=new MyThread();
new Thread(mt,"票贩子A").start();
new Thread(mt,"票贩子B").start();
new Thread(mt,"票贩子C").start();
new Thread(mt,"票贩子D").start();
}
}
范例:观察同步方法解决
class MyThread implements Runnable{
public int ticket=54;
@Override
public void run(){
for(int x=0;x<200;x++){
this.sale();//调用同步方法
}
}
public synchronized void sale() {
if(this.ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",卖票,ticket="+this.ticket--);
}
}
}
同步操作及同步操作相比,异步操作的执行速度要高于同步操作,但是同步操作的数据安全性更高,属于安全的线程操作。
死锁
通过分析可以发现,所谓同步指的是一个线程对象等待另一个线程对象执行完毕后的操作形式。线程同步过多就有可能造成死锁。
范例:没有任何意义,仅用观察。
以上的代码只是为了说明死锁而举的例子,死锁是程序开发之中由于某种逻辑上的错误所造成的问题,并且不会简单就出现的。
面试题:请解释多个线程访问同一资源时需要考虑哪些情况?有可能带来哪些问题?
多个线程访问同一资源时一定处理好同步,可以使用同步代码块或同步方法解决;
同步代码块:synchronized(同步对象){代码};
同步方法:public synchronized 返回值 方法名称(){代码};
但是过多的使用同步有可能造成死锁。
总结:
1. 最简单的理解同步和异步操作那么就可以通过:synchronized来实现;
2. 死锁是一种不定的状态。
“生产者-消费者”模型-多线程操作经典案例
问题引出
生产者和消费者指的是两种不同的线程类对象,操作同一资源的情况,具体的操作流程如下:
生产者负责生产数据,消费者负责取走数据;
生产者每生产完一组数据后,消费者就要取走一组数据。
那么假设要生产的数据如下:
第一组:title=王惊雷,content=好学生一枚;
第二组:title=可爱的萌动物,content=草泥马。
范例:程序基本模型。
package cn.mldn.util;
class Info{
private String title;
private String content;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
class Productor implements Runnable{
private Info info;
public Productor(Info info){
this=info;
}
@Override
public void run(){
for(int x=0;x<100;x++){
if(x%2==0){
this.setTitle("王惊雷");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.setContent("好学生一枚!");
}
else{
this.setTitle("可爱的萌动物");
try {
//Thread.sleep(100);
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
th
展开阅读全文