您的位置:首页 > 职场人生

黑马程序员——Java基础---多线程

2015-06-26 18:46 537 查看
-----------android培训java培训、java学习型技术博客、期待与您交流!------------

一、多线程概述

要理解多线程,就必须理解线程。而要理解线程,就必须知道进程。

1、 进程

是一个正在执行的程序。

每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。

2、线程

就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。

一个进程中至少有一个线程。

3、多线程

在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。像种在一个进程中有多个线程执行的方式,就叫做多线程。

4、多线程存在的意义

多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。

例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。如果垃圾过多就有可能是堆内存出现内存不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。

5、计算机CPU的运行原理

我们电脑上有很多的程序在同时进行,就好像cpu在同时处理这所以程序一样。但是,在一个时刻,单核的cpu只能运行一个程序。而我们看到的同时运行效果,只是cpu在多个进程间做着快速切换动作。

而cpu执行哪个程序,是毫无规律性的。这也是多线程的一个特性:随机性。哪个线程被cpu执行,或者说抢到了cpu的执行权,哪个线程就执行。而cpu不会只执行一个,当执行一个一会后,又会去执行另一个,或者说另一个抢走了cpu的执行权。至于究竟是怎么样执行的,只能由cpu决定。

二、创建线程的方式

创建线程共有两种方式:继承方式和实现方式(简单的说)。

1、 继承方式

通过查找java的帮助文档API,我们发现java中已经提供了对线程这类事物的描述的类——Thread类。这第一种方式就是通过继承Thread类,然后复写其run方法的方式来创建线程。

创建步骤:

a,定义类继承Thread。

b,复写Thread中的run方法。

目的:将自定义代码存储在run方法中,让线程运行。

c,创建定义类的实例对象。相当于创建一个线程。

d,用该对象调用线程的start方法。该方法的作用是:启动线程,调用run方法。

注:如果对象直接调用run方法,等同于只有一个线程在执行,自定义的线程并没有启动。

覆盖run方法的原因:

Thread类用于描述线程。该类就定义了一个功能,用于存储线程要执行的代码。该存储功能就run方法。也就是说,Thread类中的run方法,用于存储线程要运行的代码。

程序示例:

[java] view
plaincopy

/*

小练习

创建两线程,和主线程交替运行。

*/

//创建线程Test

class Test extends Thread

{

// private String name;

Test(String name)

{

super(name);

// this.name=name;

}

//复写run方法

public void run()

{

for(int x=0;x<60;x++)

System.out.println(Thread.currentThread().getName()+"..run..."+x);

// System.out.println(this.getName()+"..run..."+x);

}

}

class ThreadTest

{

public static void main(String[] args)

{

new Test("one+++").start();//开启一个线程

new Test("tow———").start();//开启第二线程

//主线程执行的代码

for(int x=0;x<170;x++)

System.out.println("Hello World!");

}

}

结果:

如图,执行是随机、交替执行的,每一次运行的结果都会不同。



2、 实现方式

使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。这样就有了第二种创建线程的方式:实现Runnable接口,并复习其中run方法的方式。

创建步骤:

a,定义类实现Runnable的接口。

b,覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。

c,通过Thread类创建线程对象。

d,将Runnable接口的子类对象作为实参传递给Thread类的构造方法。

为什么要将Runnable接口的子类对象传递给Thread的构造函数?

因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。

e,调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

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

程序示例:

[java] view
plaincopy

/*

需求:简单的卖票程序。

多个窗口卖票。

*/

class Ticket implements Runnable//extends Thread

{

private int tick = 100;

public void run()

{

while(true)

{

if(tick>0)

{

//显示线程名及余票数

System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);

}

}

}

}

class TicketDemo

{

public static void main(String[] args)

{

//创建Runnable接口子类的实例对象

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();

}

}

三、两种方式的区别和线程的几种状态

1、两种创建方式的区别

继承Thread:线程代码存放在Thread子类run方法中。

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

2、几种状态

被创建:等待启动,调用start启动。

运行状态:具有执行资格和执行权。

临时状态(阻塞):有执行资格,但是没有执行权。

冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。

消忙状态:stop()方法,或者run方法结束。

注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。

图解:



四、线程安全问题

1、导致安全问题的出现的原因:

当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没用执行完,另一个线程参与进来执行。导致共享数据的错误。

简单的说就两点:

a、多个线程访问出现延迟。

b、线程随机性 。

注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

2、解决办法——同步

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

在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)

这里也有两种解决方式,一种是同步代码块,还有就是同步函数。都是利用关键字synchronized来实现。

a、同步代码块

用法:

synchronized(对象)

{需要被同步的代码}

同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

示例:

[java] view
plaincopy

/*

给卖票程序示例加上同步代码块。

*/

class Ticket implements Runnable

{

private int tick=100;

Object obj = new Object();

public void run()

{

while(true)

{

//给程序加同步,即锁

synchronized(obj)

{

if(tick>0)

{

try

{

//使用线程中的sleep方法,模拟线程出现的安全问题

//因为sleep方法有异常声明,所以这里要对其进行处理

Thread.sleep(10);

}

catch (Exception e)

{

}

//显示线程名及余票数

System.out.println(Thread.currentThread().getName()+"..tick="+tick--);

}

}

}

}

}

b,同步函数

格式:

在函数上加上synchronized修饰符即可。

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

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

拿同步代码块的示例:

[java] view
plaincopy

class Ticket implements Runnable

{

private int tick=100;

Object obj = new Object();

public void run()

{

while(true)

{

show();

}

}

//直接在函数上用synchronized修饰即可实现同步

public synchronized void show()

{

if(tick>0)

{

try

{

//使用线程中的sleep方法,模拟线程出现的安全问题

//因为sleep方法有异常声明,所以这里要对其进行处理

Thread.sleep(10);

}

catch (Exception e)

{

}

//显示线程名及余票数

System.out.println(Thread.currentThread().getName()+"..tick="+tick--);

}

}

}

3、同步的前提

a,必须要有两个或者两个以上的线程。

b,必须是多个线程使用同一个锁。

4、同步的利弊

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

弊端:多个线程需要判断锁,较为消耗资源。

5、如何寻找多线程中的安全问题

a,明确哪些代码是多线程运行代码。

b,明确共享数据。

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

五、静态函数的同步方式

如果同步函数被静态修饰后,使用的锁是什么呢?

通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:

类名.class 该对象的类型是Class

这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class

经典示例:

[java] view
plaincopy

/*

加同步的单例设计模式————懒汉式

*/

class Single

{

private static Single s = null;

private Single(){}

public static void getInstance()

{

if(s==null)

{

synchronized(Single.class)

{

if(s==null)

s = new Single();

}

}

return s;

}

}

六、死锁

当同步中嵌套同步时,就有可能出现死锁现象。

示例:

[java] view
plaincopy

/*

写一个死锁程序

*/

//定义一个类来实现Runnable,并复写run方法

class LockTest implements Runnable

{

private boolean flag;

LockTest(boolean flag)

{

this.flag=flag;

}

public void run()

{

if(flag)

{

while(true)

{

synchronized(LockClass.locka)//a锁

{

System.out.println(Thread.currentThread().getName()+"------if_locka");

synchronized(LockClass.lockb)//b锁

{

System.out.println(Thread.currentThread().getName()+"------if_lockb");

}

}

}

}

else

{

while(true)

{

synchronized(LockClass.lockb)//b锁

{

System.out.println(Thread.currentThread().getName()+"------else_lockb");

synchronized(LockClass.locka)//a锁

{

System.out.println(Thread.currentThread().getName()+"------else_locka");

}

}

}

}

}

}

//定义两个锁

class LockClass

{

static Object locka = new Object();

static Object lockb = new Object();

}

class DeadLock

{

public static void main(String[] args)

{

//创建2个进程,并启动

new Thread(new LockTest(true)).start();

new Thread(new LockTest(false)).start();

}

}

结果:程序卡住,不能继续执行



七、线程间通信

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

1、使用同步操作同一资源的示例:

[java] view
plaincopy

/*

有一个资源

一个线程往里存东西,如果里边没有的话

一个线程往里取东西,如果里面有得话

*/

//资源

class Resource

{

private String name;

private String sex;

private boolean flag=false;

public synchronized void setInput(String name,String sex)

{

if(flag)

{

try{wait();}catch(Exception e){}//如果有资源时,等待资源取出

}

this.name=name;

this.sex=sex;

flag=true;//表示有资源

notify();//唤醒等待

}

public synchronized void getOutput()

{

if(!flag)

{

try{wait();}catch(Exception e){}//如果木有资源,等待存入资源

}

System.out.println("name="+name+"---sex="+sex);//这里用打印表示取出

flag=false;//资源已取出

notify();//唤醒等待

}

}

//存线程

class Input implements Runnable

{

private Resource r;

Input(Resource r)

{

this.r=r;

}

public void run()//复写run方法

{

int x=0;

while(true)

{

if(x==0)//交替打印张三和王羲之

{

r.setInput("张三",".....man");

}

else

{

r.setInput("王羲之","..woman");

}

x=(x+1)%2;//控制交替打印

}

}

}

//取线程

class Output implements Runnable

{

private Resource r;

Output(Resource r)

{

this.r=r;

}

public void run()//复写run方法

{

while(true)

{

r.getOutput();

}

}

}

class ResourceDemo2

{

public static void main(String[] args)

{

Resource r = new Resource();//表示操作的是同一个资源

new Thread(new Input(r)).start();//开启存线程

new Thread(new Output(r)).start();//开启取线程

}

}

结果:部分截图



几个小问题:

1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?

a,这些方法存在与同步中。

b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。

c,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。

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

wait():释放cpu执行权,释放锁。

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

3)为甚么要定义notifyAll?

因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。

2、JDK1.5中提供了多线程升级解决方案。

将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。

升级解决方案的示例:

[java] view
plaincopy

/*

生产者生产商品,供消费者使用

有两个或者多个生产者,生产一次就等待消费一次

有两个或者多个消费者,等待生产者生产一次就消费掉

*/

import java.util.concurrent.locks.*;

class Resource

{

private String name;

private int count=1;

private boolean flag = false;

//多态

private Lock lock=new ReentrantLock();

//创建两Condition对象,分别来控制等待或唤醒本方和对方线程

Condition condition_pro=lock.newCondition();

Condition condition_con=lock.newCondition();

//p1、p2共享此方法

public void setProducer(String name)throws InterruptedException

{

lock.lock();//锁

try

{

while(flag)//重复判断标识,确认是否生产

condition_pro.await();//本方等待

this.name=name+"......"+count++;//生产

System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);//打印生产

flag=true;//控制生产\消费标识

condition_con.signal();//唤醒对方

}

finally

{

lock.unlock();//解锁,这个动作一定执行

}

}

//c1、c2共享此方法

public void getConsumer()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;

}

//复写run方法

public void run()

{

while(true)

{

try

{

res.setProducer("商品");

}

catch (InterruptedException e)

{

}

}

}

}

//消费者线程

class Consumer implements Runnable

{

private Resource res;

Consumer(Resource res)

{

this.res=res;

}

//复写run

public void run()

{

while(true)

{

try

{

res.getConsumer();

}

catch (InterruptedException e)

{

}

}

}

}

class ProducerConsumer

{

public static void main(String[] args)

{

Resource res=new Resource();

new Thread(new Producer(res)).start();//第一个生产线程 p1

new Thread(new Consumer(res)).start();//第一个消费线程 c1

new Thread(new Producer(res)).start();//第二个生产线程 p2

new Thread(new Consumer(res)).start();//第二个消费线程 c2

}

}

运行结果:部分截图



八、停止线程

在JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。

那么现在我们该如果停止线程呢?

只有一种办法,那就是让run方法结束。

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

如:run方法中有如下代码,设置一个flag标记。

[java] view
plaincopy

public void run()

{

while(flag)

{

System.out.println(Thread.currentThread().getName()+"....run");

}

}

那么只要在主函数或者其他线程中,在该线程执行一段时间后,将标记flag赋值false,该run方法就会结束,线程也就停止了。

2、上面的1方法可以解决一般情况,但是有一种特殊情况:就是当线程处于冻结状态。就不会读取到标记。那么线程就不会结束。

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

如:

[java] view
plaincopy

class StopThread implements Runnable

{

private boolean flag =true;

public void run()

{

while(flag)

{

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 num = 0;

while(true)

{

if(num++ == 60)

{

t1.interrupt();//清除冻结状态

t2.interrupt();

st.changeFlag();//改变循环标记

break;

}

System.out.println(Thread.currentThread().getName()+"......."+num);

}

System.out.println("over");

}

}

结果:



九、什么时候写多线程?

当某些代码需要同时被执行时,就用单独的线程进行封装。

示例:

[java] view
plaincopy

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().toString()+"....."+x);

}

}

}.start();

//又是一条线程

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();

//可以看作主线程

for (int x=0;x<1000 ;x++ )

{

System.out.println("Hello World!");

}

}

}

扩展小知识:

1、join方法

当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。

2、setPriority()方法用来设置优先级

MAX_PRIORITY 最高优先级10

MIN_PRIORITY 最低优先级1

NORM_PRIORITY 分配给线程的默认优先级

3、yield()方法可以暂停当前线程,让其他线程执行。

-----------android培训java培训、java学习型技术博客、期待与您交流!------------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: