|
第十章 多线程
一、多线程原理
1. 多线程原理
程序运行时,操作系统为其建立一个进程,直到程序运行结束,这个进程才会消失。过去一般使用多个进程执行多个任务。
一个进程中至少包括一个线程,也可以包括多个线程。
不同的线程可以完成不同的工作,且由于在同一个进程中数据交换更方便快捷,维护线程消耗资源更少
PS:多核CPU和多枚CPU的概念是不同的,通常两枚CPU可以提高70%的效率,而双核CPU提高的效率是40%
2.线程的生命周期
线程从启动到终止一般有就绪、运行、和阻塞三个状态。
通常,启动一个线程后,这个线程会先进入就绪状态等待调用,调用先后取决于线程的优先级,优先级决定于规定优先级的标准:比如等待的时间越长优先级越高。
程序被运行一段时间(但是还没有完成任务)可能会转回就绪状态或者因为缺乏资源转入阻塞状态,如果资源获得而又没有被运行则变为就绪状态。
当线程完成自己的任务后便终止。
二、产生线程的方法
1.通过继承Thread产生线程
(1)创建一个线程
序列 | 行为 |
|
1 | 扩展Thread类 |
|
2 | 重写run方法 |
|
3 | 创建主线程 |
|
4 | 执行结果(如果主线程启动后不做任何其他操作就产生线程实例并启动这个线程那么这个步骤就不会产生) |
|
5 | 产生线程类实例并通过start方法启动线程 | 主线程继续完成其他功能代码 |
6 | 执行结果 |
|
7 | 所有线程结束,程序完成运行 |
|
public class TestThread extends Thread{//继承Thread
public void run()//重写run,在Thread类中run不会完成任何操作,一般把线程的主要功能写在这个方法之中
{
int i = 0;
while(true)
{
System.out.println("T:"+i);
i++;
if(i>100)
{
break;
}
}
}
public static void main(String[] args)
{
TestThread tt = new TestThread();
tt.start();//用start启动线程,start是“异步方法”(异步方法是说该方法执行后立即返回,而不是必须在方法执行完后才返回,如果尝试直接调用runf方法,语法没有问题,但是就不会出现交替出现的情况,run执行完后才会执行后面的代码,即,不会出现异步状态)
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
}
}
}
M和T交替输出,说明被重写的run和主程序(主线程)交替执行
class Test extends Thread//这个类在外面也可以
{
public void run()
{
int i = 0;
while(true)
{
System.out.println("T:"+i);
i++;
if(i>100)
{
break;
}
}
}
}
public class TestThread {
public static void main(String[] args)
{
Test tt = new Test();
tt.start();
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
}
}
}
(2)对同一线程类启动多个线程
如果同一线程对象在主函数启动多次,会报错:IllegalThreadStateException。因此如果希望启动多个同一类线程就必须产生多个线程实例。
class Test extends Thread
{
char flag;
public Test(char flag)
{
this.flag = flag;
}
public void run()
{
int i = 0;
while(true)
{
System.out.println(flag+":"+i);
i++;
if(i>100)
{
break;
}
}
}
}
public class TestThread {
public static void main(String[] args)
{
Test tt = new Test('A');//其实对于每一个线程,系统会为它分配一个标识该线程的ID号,可以使用线程实例getId的方法来取得这个长整型的ID号码,所以也可以通过ID来区分而不是像这里用用字母
Test tt1 = new Test('B');
Test tt2 = new Test('C');
tt.start();
tt1.start();
tt2.start();
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
}
}
}
(3)为线程添加休眠功能
休眠方法:
public static void sleep(long millis) throws InterruptedException
//参数含义是线程休眠的毫秒数
public static void sleep(long millis,int nanos)
throws InterruptedException
//在指定毫秒后再指定若干纳秒作为休眠时间
两者都抛出异常
class Test extends Thread
{
char flag;
public Test(char flag)
{
this.flag = flag;
}
public void run()
{
int i = 0;
while(true)
{
System.out.println(flag+":"+i);
try
{
sleep(200);
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
i++;
if(i>100)
{
break;
}
}
}
}
public class TestThread extends Thread{//如果想要这里面的主函数用sleep的话必须继承Thread类
public static void main(String[] args)
{
Test tt = new Test('A');
Test tt1 = new Test('B');
Test tt2 = new Test('C');
tt.start();
tt1.start();
tt2.start();
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
try
{
sleep(200);//需要有try,有了这个速度变慢了好多
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
}
}
}
(4)暂停线程执行
静态方法yield();
class Test extends Thread
{
char flag;
public Test(char flag)
{
this.flag = flag;
}
public void run()
{
int i = 0;
while(true)
{
System.out.println(flag+":"+i);
i++;
yield();
if(i>100)
{
break;
}
}
}
}
public class TestThread extends Thread{
public static void main(String[] args)
{
Test tt = new Test('A');
Test tt1 = new Test('B');
Test tt2 = new Test('C');
tt.start();
tt1.start();
tt2.start();
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
yield();//这里改为Thread.yield的时候执行一次放弃一次比较明显,同时上面直接调用,即yield(),但是直接调用的话就连着执行的多,当然也有可能放了之后又是这个线程接上继续执行,并不能证明没有放弃。
}
}
}
(5)设置线程的优先级
线程对象可以调用setPriority()来设置线程,调用getPriority()来获得线程
class Test extends Thread
{
char flag;
public Test(char flag)
{
this.flag = flag;
}
public void run()
{
int i = 0;
while(true)
{
System.out.println(flag+":"+i);
i++;
yield();
if(i>100)
{
break;
}
}
}
}
public class TestThread extends Thread{
public static void main(String[] args)
{
Test tt = new Test('A');
Test tt1 = new Test('B');
Test tt2 = new Test('C');
tt.setPriority(tt1.getPriority()+2);//+是增加优先级
tt2.setPriority(tt1.getPriority()-2);//-是减少优先级
tt.start();
tt1.start();
tt2.start();
for(int i=0;i<100;i++)
{
System.out.println("M:"+i);
yield();
}
}
}
感觉用处不大,因为在加了优先级后,输出的顺序也有第优先级的在前面输出不知道哪里出问题了,反正能运行
setPriority不一定起作用的,在不同的操作系统不同的jvm上,效果也可能不同。现在很多jvm的线程的实现都使用的操作系统线程,设置优先级也是使用的操作系统优先级,java层面有10个优先级别,
假设操作系统只有3个优先级别,那么jvm可能将1-4级映射到操作系统的1级,5-7级映射到操作系统的2级,剩下的映射到3级,这样的话,在java层面,将优先级设置为5,6,7,其实本质就是一样的了。
另外,操作系统也不能保证设置了优先级的线程就一定会先运行或得到更多的CPU时间。
在实际使用中,不建议使用该方法(网上百的)
2.通过实现接口Runnable产生线程
因为Java只允许但继承,扩展了Thread类后就不能扩展其他的类了,所以有接口Runnable.
Runnable里面只有一个run方法。实现接口Runnable的类就必须实现run,在定义线程时,需要将该类的实例作为构造器参数先产生一个Thread类的实例,然后调用Thread类实例的start方法启动运行线程。
class Test1 implements Runnable
{
public void run()
{
int i = 0;
while(true)
{
System.out.println("T:"+i);
i++;
if(i>100)
{
break;
}
}
}
}
public class TestRunnable {
public static void main(String[] args)
{
Test1 t = new Test1();
Thread tt = new Thread(t);//需要将该类的实例作为构造器参数先产生一个Thread类的实例,和 扩展类Thread后只要实例化子类就可以了 不同。
tt.start();
int i=0;
while(true)
{
System.out.println("M:"+i);
i++;
if(i>100)
{
break;
}
}
}
}
class Test1 implements Runnable
{
int i = 1;//作为类成员,共享资源,但是如果使用Thread类的话通过静态成员的方式也可以实现资源共享
public void run()
{
while(i<=20)//用小于等于20和用大于20break出现的结果不一样而且会重复,使用大于break那个条件输出会有大于20的数字出现是因为在输出和i++在前面,但是具体要搞清楚为什么就要考虑线程执行和i++的执行使用顺序了。
{
System.out.println("T:"+i);
i++;
Thread.yield();//有这个函数就不会出现重复了,还是会出现重复,多次重复执行看输出的情况下
//if(i>20)
{
// break;
}
}
}
}
public class TestRunnable {
public static void main(String[] args)
{
Test1 t = new Test1();
Thread tt = new Thread(t);//将统一实例作为线程的构造参数传入,以便多个线程共享资源
Thread tt1 = new Thread(t);
Thread tt2 = new Thread(t);
tt.start();
tt1.start();
tt2.start();
}
}
三、线程同步
1.多线程带来的问题
实际上上面的资源共享还是不能轻易的无缺陷实现吧。还有yield出现并不会代表一定就会执行,只是暗示执行者这个执行完了就可以放了,还有设置优先级也是不一定有效的,这个取决于运行平台
2.同步线程
下面这个代码是使方法同步,我的理解是,当共享资源在被synchronized修饰的方法里面时候,该方法执行时该资源是被锁定的,只有执行完该方法后才可以对该资源进行使用。书上说该关键字还可以修饰对象,但是举的例子让我无语(下面代码实现的是同一类的线程协同)
class SaleTicket implements Runnable
{
private int i = 1;
private synchronized int getATicket()
{
int res;
if(i<=20)
{
res = i;
i++;
}
else
{
res = 0;
}
return res;
}
public void run()
{
while(true)
{
int ticket = getATicket();
if(ticket==0)
{break;}
System.out.print(ticket+"\t");
Thread.yield();
}
}
}
public class TestSynchronizedMethod {
public static void main(String [] args)
{
SaleTicket st = new SaleTicket();
Thread t = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
t.start();
t2.start();
t3.start();
}
}
3.协同线程
这个代码有点长,我必须细细消化一下:(不同类的线程协同)
class DataInfo
{
private String name;
private String sex;
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public void setSex(String sex)
{
this.sex = sex;
}
public String getSex()
{
return sex;
}
}
class DataProc
{
private DataInfo di;
private Boolean fullFlag = false;
public synchronized void setData(DataInfo di)
{
if(fullFlag)
{
try
{
wait();
}
catch(InterruptedException ie)
{}
}
this.di = di;
fullFlag = true;
notify();
}
public synchronized DataInfo getData()
{
if(!fullFlag)
{
try
{
wait();
}
catch(InterruptedException ie)
{}
}
DataInfo di;
di = this.di;
fullFlag = false;
notify();
return di;
}
}
class Producer implements Runnable
{
private DataProc dp;
public Producer(DataProc dp)
{
this.dp = dp;
}
public void run()
{
for(int i= 0;i<10;i++)
{
int rnd = (int)(Math.random()*100);
String name = "name"+rnd;
String sex;
if(rnd%2 == 0)
{sex="man";}
else
{sex="female";}
DataInfo di = new DataInfo();
di.setName(name);
di.setSex(sex);
dp.setData(di);
}
}
}
class Consumer implements Runnable
{
private DataProc dp;
public Consumer(DataProc dp)
{
this.dp = dp;
}
public void run()
{
DataInfo di;
for(int i = 0; i<10;i++)
{
di = dp.getData();
System.out.println(di.getName()+" "+di.getSex());
}
}
}
public class TestWaitAndNotify {
public static void main(String[] args)
{
DataProc dp = new DataProc();
Producer p = new Producer(dp);
Thread t1 = new Thread(p);
Consumer c = new Consumer(dp);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
这个程序中共享的资源有两个,一个是DataInfo di,还一个是flag..Producer的功能是创建一个DataInfo 对象然后存储到DataProc共享对象di中,因为flag的初始值为false,所以在调用setData的时候不会被阻塞(不会执行wait),调用完后在该方法结尾部分将flag设为了true,如果继续调用则会被阻塞,只有在getData被调用后(因为getData在方法结尾会将flag改为false)才可以继续调用,这样通过flag控制了getData方法的调用顺序。
感觉还是没有讲得很清楚,但是这个程序的最终目的是,首先一定要先执行随机产生编号和性别,并将其存储,然后只有将存储的值读取后才可以继续产生下一个随机编号和性别。实现这两个功能的有序交替进行。(让不属于同一类的线程交替执行)
四、Daemon线程
根据书本,该线程的特征是:和主线程同一进程,只有在该进程结束时,即该进程中的所有普通线程结束时,才结束,也就是说它的结束条件有两个,(以下面这个代码为例)一个是自身的循环中的结束条件,一个是主进程结束条件,满足其中一点就结束,所以不一定会完成所有runz中定义的操作
import java.util.Date;
class TestDaemon1 implements Runnable
{
public void run()
{
for(int i=0; i<10;i++)
{
System.out.println(new Date().getTime());
}
}
}
class TestDaemon2 implements Runnable
{
public void run()
{
for(int i=0; i<5;i++)
{
System.out.println(new Date());
}
}
}
public class TestEaemon {
public static void main(String[] args)
{
TestDaemon1 td1 = new TestDaemon1();
TestDaemon2 td2 = new TestDaemon2();
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.setDaemon(true);//定义t1为守护进程,定义必须在线程启动之前
t2.start();
t1.start();
}
}实质上我这个程序基本每次都是先完全执行完守护进程才执行的普通进行,没有守护进程的感觉。
五、线程组
线程组表示一个线程的集合,而且一个线程组还可以包含其他的线程组。使用线程组管理线程,使得线程的管理更富有条理性和更易于控制。
如果一个线程产生时,没有指明其所属的线程组,那么该线程与产生该线程的线程同属于一个线程组。
例如,主线程属于一个名为main的线程组,所以此前的示例代码中产生的所有线程均属于main线程组。
class DemoThread implements Runnable
{
private String threadFlag;
public DemoThread(String threadFlag)
{
this.threadFlag = threadFlag;
}
public void run()
{
for(int i = 0; i<= 100; i++)
{
// System.out.println(threadFlag+":"+i);
}
}
}
public class TestThreadArray {
public static void main(String[] args)
{
DemoThread dt1 = new DemoThread("A");
DemoThread dt2 = new DemoThread("B");
DemoThread dt3 = new DemoThread("C");
ThreadGroup tg = new ThreadGroup("test the thread array");//创建一个线程组。组名为"test the thread array"
Thread t1 = new Thread(dt1);//这里没有显示加入tg但是貌似也被加进去了,根据tg.list()输出的情况来看
Thread t2 = new Thread(tg,dt2);//这里是将t2加入线程tg,同时这些线程之间的关系和用用一个对象创建出来的线程之间的关系又不一样,尽管是由同一类对象创建的。
Thread t3 = new Thread(tg,dt3);
t1.start();
t2.start();
t3.start();
// t1.getName();//没有这一句不影响输出,如果有这个句子,没有下面这个句子,则不会有输出(前提是我把run里面的输出屏蔽了)
tg.list();//只要有这一句没有线程加入也会输出java.lang.ThreadGroup[name=test the thread array,maxpri=10]
}
}
输出:
java.lang.ThreadGroup[name=test the thread array,maxpri=10]
Thread[Thread-2(表示线程数-1),5(表示优先级),test the thread array(线程名)]//这个句子有时候输出一句有时候输出两句
习题:
1.编制程序,在其中启动两个线程,一个线程输出数字1到26,另一个输出大写字母A到Z。
class OutputNumber implements Runnable
{
public void run()
{
for(int i=1;i<=26;i++)
{
System.out.println(i);
Thread.yield();//为了使效果更佳明显我用了这个
}
}
}
class OutputCharacter implements Runnable
{
public void run()
{
for(char i='A';i<='Z';i++)
{
System.out.println(i);
Thread.yield();
}
}
}
public class Question_1 {
public static void main(String[] args)
{
OutputCharacter oc = new OutputCharacter();
OutputNumber on = new OutputNumber();
Thread c = new Thread(oc);
Thread n = new Thread(on);
c.start();
n.start();
}
}
2.编制程序,使用一个线程产生10个随机数存入一个共享空间中,然后另外一个线程从该共享空间中读取并且在标准输出设备中输出。
a~b 之间的随机数语句是:随机数=(int)(Math.random()*(b-a))+a;
class Info
{
int num;
boolean flag = false;
public synchronized void setData(int num)
{
if(flag)
{
try
{
wait();
}
catch(InterruptedException ie)
{}
}
this.num = num;
flag = true;
notify();
}
public synchronized int getData( )
{
if(!flag)
{
try
{
wait();
}
catch(InterruptedException ie)
{}
}
flag =false;//首先没有这个语句和下面那个语句导致程序一直挂着
notify();
return this.num;
}
}
class Set implements Runnable
{
private Info ifo;
public Set(Info ifo)
{
this.ifo = ifo;
}
public void run()
{
for(int i=0;i<10;i++)
{
int rnd = (int)(Math.random()*9)+1;
System.out.println(rnd);
this.ifo.setData(rnd);
}
}
}
class Get implements Runnable
{
private Info ifo;
public Get(Info ifo)
{
this.ifo = ifo;
}
public void run()
{
for(int i=0;i<10;i++)
{
System.out.println( "this is the " +i+"number:"+this.ifo.getData());
}
}
}
public class Question_2 {
public static void main(String[] args)
{
Info ifo = new Info();
Set s = new Set(ifo);
Get g = new Get(ifo);
Thread s1 = new Thread(s);
Thread g1 = new Thread(g);
s1.start();
g1.start();
}
}
小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)
GMT+8, 2024-5-18 10:00
Powered by Discuz! X3.4
© 2001-2023 Discuz! Team.