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

黑马程序员---多线程

2015-07-01 22:12 603 查看
-----------android培训java培训、java学习型技术博客、期待与您交流!---------

多线程

要理解多线程就要先了解线程,了解线程就要先了解进程,下面跟着我的博客学习多线程吧

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

线程:就是进程中的一个独立的公职单元,线程在控制着进程的执行。

只要进程中有一个线程在执行,进程就不会结束,一个进程中至少有一个线程。

多线程:在java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行。

而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,

还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程执行的方式,就叫做多线程。

多线程存在的意义

多线程的出现能让程序出现同时运行效果(原来没学多线程的时候我以为程序是同时运行的,

学完之后才发现不是,等一下我会讲解CPU的运行原理)可以提高程序执行效率。

例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象。

而对象调用完后,就成了垃圾。如果垃圾过多可能发生内存溢出的情况,如果只有一个线程工作的话,程序的执行将会很低效。

如果有另一个线程帮助处理的话,比如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。

创建线程有两种方式

继承方式和实现方式,继承就是继承Thread类,实现就是实现Runnable接口,我们先来看继承方式。

继承方式:

通过对API的查找,java已经提供了对线程这类事物的描述,就是Thrad类

创建线程的第一种方式:继承Thread类。

1. 定义类继承Thread

2. 复写Thread类中的run方法。

目的:将线程要执行的代码储存在run方法中,让程序运行。

3. 创建类的实例对象,就相当于创建了一个线程。

4. 调用线程的start方法,该方法有两个作用

a. 启动线程

b. 调用run方法

start:调用到底层让控制单元去执行的动作。

run:封装线程要运行的代码

注意:调用线程的start方法是开启线程并执行该线程的run方法。

而直接调用run方法仅仅是对象调用方法,而线程创建了,并没有运行

为什么要覆盖run方法呢?

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

也就是说Thread类中的run方法,用于存储线程要运行的代码。

下面我们来看一个使用继承方法,创建线程的实例

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

//定义一个ThreadA类继承Thread
class ThreadA extends Thread
{
ThreadA(String name)
{
//使用super方法,访问父类构造函数
super(name);
}
//复写run方法写入要执行的语句
public void run()
{
for(int x=0;x<80;x++)
//通过Thread.currentThread方法获得当前执行线程对象的引用
//再用getName方法获得这个正在运行线程的名称,以示区分
System.out.println(Thread.currentThread().getName()+"..run..."+x);
}
}

class  ThreadTest
{
public static void main(String[] args)
{
//创建ThreadA的匿名对象,并传入name参数,开启自定义的第一个线程
new ThreadA("线程11111111————————————").start();

//创建ThreadA的匿名对象,并传入name参数,开启自定义的第一个线程
new ThreadA("线程22222*******").start();

//编写主线程要执行的代码
for(int x=0;x<170;x++)
System.out.println("Hello World!");
}
}


运行结果



注:Thread.currentThread()== this 这种获取线程对象的方法不建议使用,因为this不一定通用。

直接用Thread.currentThread()就好,因为它是标准通用的。

为什么每次运行的结果都不一样呢?

因为多个线程都获取CPU的执行权,CPU执行到谁谁就运行。

明确点的说就是在某个时刻只能一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果,

我们可以形象的把多线程的运行形容为在互相抢夺CPU的执行权,

这就是多线程的一个特性:随机性。(谁抢到谁执行,至于执行多长时间,CPU说了算)

创建线程的第二种方式,实现方式

使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,就无法通过Thread类来创建线程了。

这样就有了第二种创建线程的方式:实现Runnable接口,并复写run方法

1. 定义类实现Runnable接口

2. 覆盖Runna接口中的run方法,将线程要执行的代码存放在run方法中。

3. 通过Thread类创建线程对象。

4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

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

因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法,

就必须明确该run方法的所属对象。

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

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

实例程序

/*
使用实现Runnable接口的方法,创建线程

现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋
现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。
*/
class Shoes implements Runnable//extends Thread
{
//有100个号
private  int shoesNumber = 100;

//复写run方法
public void run()
{
while(true)
{
if(shoesNumber>0)
{
//输出第几个人发出去的号,并且每卖出去一个号减一
System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--);
}
}
}
}
class  ShoesDemo
{
public static void main(String[] args)
{
//创建Runnable接口的子类对象
Shoes s = new Shoes();

//将Runnable接口的子类对象作为参数传递给Thread类的构造函数,创建线程
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
Thread t4 = new Thread(s);

//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}


运行结果



在这个程序的运行结果中出现了Thread-0这类的名称,通过查看源代码我们知道这是通过获取线程名得来的,

原来线程有自己的默认名称:Thread—编号(编号从0开始)

实现方法和继承方法有什么区别呢?

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

两种方式的区别:线程代码存放的位置不一样。

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

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

线程的状态

线程的几种状态分别为

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

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

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

冻结状态:有两个状态

a. 睡眠:sleep(time)方法

b. 等待:wait()方法。放弃了执行资格。

当sleep方法时间到或者调用到notify()方法时,获得执行资格,变为临时状态。

变为临时状态后,如果CPU空闲再执行。

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

总结:没有执行资格是冻结状态,有执行资格没有执行权是阻塞状态,执行权执行资格都有是运行状态。

线程的状态图示



线程安全问题

刚才那个卖鞋的程序,最后一个号不是1,而是别的数字。当然因为程序运行结果都不一样,不一定是哪个数字,反正不是1。

虽然这不是一个安全性的问题,但是如果真的在做项目的过程中,出现一点小问题都是很可怕的。

而且如果我们在卖鞋程序的输出语句前加入:

try{Thread.sleep(10);}catch(Exception e){}(因为sleep可能会发生异常,所以要try catch处理)代码,让线程睡一会儿,

运行出的结果可能会发生如下图所示的情况

会出现0,-1,-2的号,这就表明多线程出现了很严重的安全问题,我们必须要解决



问题的原因

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,

另一个线程就参与进来执行,导致了共享数据的错误。

解决办法

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

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

同步有两种方式。

1. 同步代码块。

2. 同步函数。

都是利用synchronized关键字实现的

先来介绍同步代码块

格式:

Synchronized(对象){

需要被同步的代码;

}

判断那些代码需要同步,就看那些语句在操作共享数据。

同步可以解决安全问题的原因就在对象上。对象如同锁,持有锁的线程可以在同步中执行,

没有持有锁的线程即使获取了CPU的执行权也进不去,因为没有获取锁。

下面我们就给卖鞋那号的程序加上同步代码块,再来看运行结果

给程序加入同步代码块

/*
使用实现Runnable接口的方法,创建线程

现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋
现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。
*/
class Shoes implements Runnable//extends Thread
{
//有1000个号
private  int shoesNumber = 1000;
//创建一个对象,作为锁
Object obj = new Object();

//复写run方法
public void run()
{
while(true)
{
//创建同步代码块obj对象为锁,把操作共享数据的代码放入同步代码块中
synchronized(obj)
{
if(shoesNumber>0)
{
try{Thread.sleep(10);}catch(Exception e){}
//输出第几个人发出去的号,并且每卖出去一个号减一
System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--);
}
}
}
}
}
class  ShoesDemo
{
public static void main(String[] args)
{
//创建Runnable接口的子类对象
Shoes s = new Shoes();

//将Runnable接口的子类对象作为参数传递给Thread类的构造函数,创建线程
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
Thread t4 = new Thread(s);

//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}


运行结果



现在就可以一直卖到最后一张票了,在截图中只能看到Thread-3线程在运行,是因为我的电脑是双核的,比较不容易看到线程交替。

虽然鞋号数已经调到了1000,还是看不出来,所以只截了卖到最后一张的截图,望老师见谅,嘿嘿。

同步函数

格式

把synchronized加在函数上public后即可。

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

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

再来看刚才卖鞋的程序用同步代码块来使它安全。

/*
使用实现Runnable接口的方法,创建线程

现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋
现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。
*/
class Shoes implements Runnable//extends Thread
{
//有100个号
private  int shoesNumber = 1000;

public void run()//复写run方法
{
//调用同步方法
show();
}
//将sysnchronized关键字加入函数中
public synchronized void show()
{
while(true)
{
if(shoesNumber>0)
{
try{Thread.sleep(10);}catch(Exception e){}
//输出第几个人发出去的号,并且每卖出去一个号减一
System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--);
}
}
}
}
class  ShoesDemo2
{
public static void main(String[] args)
{
//创建Runnable接口的子类对象
Shoes s = new Shoes();

//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread t1 = new Thread(s);//创建线程;
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
Thread t4 = new Thread(s);

t1.start();//启动线程
t2.start();
t3.start();
t4.start();
}
}


运行结果



同步的前提

1. 必须有两个以上的线程。

2. 必须是多个线程使用同一个锁。

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

同步的好处和弊端

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

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

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

1. 明确哪些代码是多线程运行代码。

2. 明确共享数据(一般成员都是,对象也是)

3. 明确多线程运行代码中那些语句是操作共享数据的。

静态函数的同步方式

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

通过验证发现,不是this,因为静态方法中不可以定义this,静态进内存时,内存中没有本类对象。

但是一定有该类对应的字节码文件对象。类名.class。该对象的类型是class,

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

同步的经典懒汉式加上同步

/*
加入了同步的懒汉单例设计模式
*/
class  Single
{
//定义私有,静态的类的引用,类加载时引用就存在,而且不能被外界访问
private static Single s = null;

//定义私有的构造函数,让用户不能创建对象
private Single(){}

//定义获取对象方法,类名可以直接调用,所以是静态的
public static void getInstance()
{
//判断对象的引用是否指向null,这里加了双重判断。
//在多线程并发的情况下,每个线程要创建对象都要判断一下锁,效率比较低,为了提高效率加入双重判断。
//如果第一个线程已经成功的获取了实例对象,Single类的引用就不指向空了,就不用,每次都判断锁了
if(s == null)
{
//使用同步代码块,让线程同步,锁为Single的字节码
synchronized(Single.class)
{
//这里面才是真正需要被同步的代码
//如果Single类的引用指向null
if(s == null)
//创建Single的实例对象,并指向s引用型变量
s = new Single();
}
}
//返回类的引用
return s;
}
}


死锁

在同步中嵌套同步时可能出现死锁现象

用程序来体现死锁:面试题

/*
这是一个死锁程序
这个程序中会出现两个线程。
通过给构造函数传入不同标记的方式,用if else代码块控制线程运行的代码。

一号线程:A锁的同步代码块套着B锁的同步代码块
二号线程:B锁的同步代码块套着A锁的同步代码块
会出现死锁的情况
*/

class Test implements Runnable
{
private boolean flag;

//建立对象时接收boolean类型的数据
Test(boolean flag)
{
this.flag = flag;
}

//复写run方法
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.lockA)//A锁
{
System.out.println(Thread.currentThread().getName()+"...运行if代码块 lockA");
synchronized(MyLock.lockB)//B锁
{
System.out.println(Thread.currentThread().getName()+"..运行if代码块 lockB");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockB)//B锁
{
System.out.println(Thread.currentThread().getName()+"..运行else代码块 lockB");
synchronized(MyLock.lockA)//A锁
{
System.out.println(Thread.currentThread().getName()+".....运行else代码块 lockA");
}
}
}
}
}
}

//创建一个类用于放锁
class MyLock
{
static Object lockA = new Object();
static Object lockB = new Object();
}

class  DeadLockTest
{
public static void main(String[] args)
{
//创建线程,并把标记作为参数传入实现Runnable接口的类的匿名对象中
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));

//开启线程
t1.start();
t2.start();
}
}


结果:程序卡主,不能继续运行



线程间的通信

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

下面我们来看一个线程间通信的例子

/*
这是一个线程之间通信的例子

两个线程
一个往资源里面存东西,一个从资源里面取东西。存一个取一个
*/
//定义一个手机类
class Phone
{
//手机品牌
String brand;
//手机耐久度
String durable;
//标记
boolean flag = false;
}

//创建往资源里存东西的类实现Runnable端口
class Input implements Runnable
{
private Phone p ;

//Input类的构造函数接收Phone对象
Input(Phone p)
{
this.p = p;
}
//复写run方法
public void run()
{
//定义标记,为了交替打印用
int x = 0;
//定义循环
while(true)
{
//同步代码块,手机对象为锁
synchronized(p)
{
//判断手机类中的标记
if(p.flag)
//使线程等待
try{p.wait();}catch(Exception e){}
//给Phone对象赋值
if(x==0)
{
p.brand="苹果";
p.durable="耐久度不好,容易碎屏";
}
else
{
p.brand="诺基亚";
p.durable="耐久度好,能砸核桃";
}
//控制交替打印
x = (x+1)%2;
//改变手机标记
p.flag = true;
//唤醒等待的第一个线程,因为等下我只会创建两个线程,所以使用notify()就好
//自己睡了,唤醒对方。
p.notify();
}
}
}
}

//创建从资源里往外拿东西的类实现Runnable接口
class Output implements Runnable
{
private Phone p ;
//参数接收Phone类对象
Output(Phone p)
{
this.p = p;
}
//复写run方法
public void run()
{
//定义循环
while(true)
{
//同步代码块,Phone对象为锁
synchronized(p)
{
//判断标记,与Input类判断相反
if(!p.flag)
//使线程等待
try{p.wait();}catch(Exception e){}
//获取P对象的值并打印
System.out.println(p.durable+"...."+p.durable);
//改变Phone类的标记
p.flag = false;
//唤醒等待的第一个线程。
p.notify();
}
}
}
}

class  InputOutputDemo
{
public static void main(String[] args)
{
//创建手机对象
Phone p = new Phone();

//创建读取资源和存入资源的对象,并将手机对象作为实际参数传入构造函数中
Input in = new Input(p);
Output out = new Output(p);

//将Runnable的子类对象作为参数传入Thread的构造函数中,创建线程并启动线程。
new Thread(in).start();
new Thread(out).start();
}
}


运行结果



下面有几个小知识

1. wait(); notify() ; notifyAll() 都使用在同步中,因为要对持有监视器(锁)的线程操作。

为什么这些操作线程的方法要定义在Object类中呢?

因为这些方法在操作同步线程时,都必须要标识他们所操作的线程持有的锁。

只有同一个锁上的被等待线程,可以被同一个锁notify()唤醒,不可以对不同锁中的线程进行等待唤醒。

也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。

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

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

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

3. 为什么要定义notifyAll()?

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

因为notify()只通知队列中的第一个线程,而notifAll()通知的是等待队列中的所有线程

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

将同步synchronized替换成显示的Lock操作,将Object中的wait() ; notify() ; notifyAll()替换成condition对象,该对象可以通过Lock锁进行获取。

多线程的升级解决方案示例

import java.util.concurrent.locks.*;
/*
想想一个好玩儿一点儿的例子,想不出来啊,就用毕老师讲课的商品例子吧,我尽力了!

创建四个线程。
两个生产者,生产一次就等待消费一次
两个消费者,等待生产者生产一次就消费一次

*/

//资源类
class Resource
{
private String name;
private int count = 1;
//标记
private boolean flag = false;
//多态,父类引用指向子类对象
private Lock lock = new ReentrantLock();

//创建两个Condition对象,用于控制等待或唤醒本方与对方线程
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();

//生产方法
public  void set(String name)throws InterruptedException
{
lock.lock();	//l锁
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();//释放锁的动作一定要执行。
}
}
//消费方法
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)
{
}
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
//创建资源对象
Resource r = new Resource();

//创建生产者线程和消费者线程对象,并把资源对象作为参数传入
Producer pro = new Producer(r);
Consumer con = new Consumer(r);

//创建两个生产者线程,连个消费者线程,并启用
new Thread(pro).start();
new Thread(pro).start();
new Thread(con).start();
new Thread(con).start();
}
}


运行结果



停止线程

JDK1.5版本之前可以用stop方法来停止线程,但是1.5版本更新以后stop方法过时了。

那么要如何停止线程呢?

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

如下面的代码:

public voidrun() {

while(flag) {

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

}

}

使循环条件为一个变量,等程序运行一段时间后,只要在主函数,或者除了这个线程中的其他线程中

将false赋给标记flag,run方法就会结束,线程也会停止。

但是会出现特殊状况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。

当没有指定的方式让冻结恢复到运行状态时,就需要对冻结进行清除,强制让线程恢复到运行状态中来

这样就可以操作标记,让线程结束。Thread类中提供了该方法 interrupt()

特出情况示例

/*
停止线程的特殊情况
线程处于冻结状态,无法读取到标记,所以我们要这么做!
*/

//创建一个线程,就是等一下要被冻结,唤醒,再结束的线程(怎么那么可怜)
class StopThread implements Runnable
{
//定义标记
private boolean flag =true;
//复写run方法
public  void run()
{
//将标记作为循环条件
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....我运行啦~");
}
}

//改变标记的方法
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)
{
//当主函数线程运行并且num正好到60的时候,将自定义线程从冻结状态唤醒
t1.interrupt();
t2.interrupt();

//调用改变循环标记的方法,让循环停止
st.changeFlag();
//跳出while循环
break;
}
//主函数线程输出的语句
System.out.println(Thread.currentThread().getName()+"......."+num);
}
//结束
System.out.println("over");
}
}


运行结果



让主函数线程代码执行完时,冻结的线程执行结束。

什么时候写多线程?

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

多线程扩展知识

1. join():(抢夺CPU执行权)当A线程执行到了B线程的join()方法时,A就会等待,B执行完,A才会执行,join()可以用来临时加入线程执行。

2. setPriority():用来设置优先级:

MAX_PRIORITY 最高优先级10

MIN_PRIORITY 最低优先级1

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

优先级就是抢资源的频率,越大抢到CPU执行权的机会就越大

3. yield():暂停当前正在执行的线程对象,并执行其他线程。临时释放,稍微减缓线程的运行,而且能达到平均运行效果。

谢谢大家的观看~!

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