1 线程的创建与启动
1.1 进程与线程
进程:是指系统中能够独立运行并作为资源分配的基本单位,由一组机器指令、数据和堆栈组成的。是一个能够独立运行的活动实体。
线程:是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。
区别:①因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
②体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。。
③属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同过的进程相互独立。
④线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
⑤线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
⑥体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。
1.2 Java中的Thread和Runnable类
Thread类
Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。
下面是Thread类中常用的方法:
以下是关系到线程运行状态的几个方法:
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
Runnable接口
通过继承Thread类实现多线程,但是这种方式有一定的局限性。因为在java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了person类,就无法再继承Thread类创建的线程。为了克服这种弊端,Thread类提供了另外一种构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建一个线程对象时,只需该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,二不需要调用Thread类中的run()方法。
1. package test;
2.
3. public class example {
4. public static void main(String[] args){
5. MyThread myThread=new MyThread();
6. Threadthread=new Thread(myThread);
7. thread.start();
8. while(true)
9. {
10. System.out.println("Main方法在运行");
11. }
12. }
13. }
14.
15. class MyThread implements Runnable{
16. public void run(){
17. while(true){
18. System.out.println("MyThread类的run()方法在运行");
19. }
20. }
21. }
区别:java不允许多继承,因此实现了Runnable接口的类可以再继承其他类。
Thread也可以资源共享啊,为什么呢,因为Thread本来就是实现了Runnable,包含Runnable的功能是很正常的啊!!至于两者的真正区别最主要的就是一个是继承,一个是实现;其他还有一些面向对象的思想,Runnable就相当于一个作业,而Thread才是真正的处理线程,我们需要的只是定义这个作业,然后将作业交给线程去处理,这样就达到了松耦合,也符合面向对象里面组合的使用,另外也节省了函数开销,继承Thread的同时,不仅拥有了作业的方法run(),还继承了其他所有的方法。综合来看,用Runnable比Thread好的多。
1.3 三种创建线程的办法
继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
1. package com.thread;
2.
3. public classFirstThreadTest extends Thread{
4. int i = 0;
5. //重写run方法,run方法的方法体就是现场执行体
6. public void run()
7. {
8. for(;i<100;i++){
9. System.out.println(getName()+" "+i);
10.
11. }
12. }
13. public static void main(String[] args)
14. {
15. for(int i = 0;i< 100;i++)
16. {
17. System.out.println(Thread.currentThread().getName()+" : "+i);
18. if(i==20)
19. {
20. new FirstThreadTest().start();
21. new FirstThreadTest().start();
22. }
23. }
24. }
25.
26. }
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
示例代码为:
1. package com.thread;
2.
3. public classRunnableThreadTest implements Runnable
4. {
5.
6. private int i;
7. public void run()
8. {
9. for(i = 0;i <100;i++)
10. {
11. System.out.println(Thread.currentThread().getName()+""+i);
12. }
13. }
14. public static void main(String[] args)
15. {
16. for(int i = 0;i < 100;i++)
17. {
18. System.out.println(Thread.currentThread().getName()+""+i);
19. if(i==20)
20. {
21. RunnableThreadTest rtt = newRunnableThreadTest();
22. new Thread(rtt,"新线程1").start();
23. new Thread(rtt,"新线程2").start();
24. }
25. }
26.
27. }
28.
29. }
三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实例代码:
1. package com.thread;
2.
3. importjava.util.concurrent.Callable;
4. import java.util.concurrent.ExecutionException;
5. importjava.util.concurrent.FutureTask;
6.
7. public classCallableThreadTest implements Callable<Integer>
8. {
9.
10. public static void main(String[] args)
11. {
12. CallableThreadTest ctt = new CallableThreadTest();
13. FutureTask<Integer> ft = newFutureTask<>(ctt);
14. for(int i = 0;i < 100;i++)
15. {
16. System.out.println(Thread.currentThread().getName()+"的循环变量i的值"+i);
17. if(i==20)
18. {
19. new Thread(ft,"有返回值的线程").start();
20. }
21. }
22. try
23. {
24. System.out.println("子线程的返回值:"+ft.get());
25. } catch (InterruptedException e)
26. {
27. e.printStackTrace();
28. } catch (ExecutionException e)
29. {
30. e.printStackTrace();
31. }
32.
33. }
34.
35. @Override
36. public Integer call() throws Exception
37. {
38. int i = 0;
39. for(;i<100;i++)
40. {
41. System.out.println(Thread.currentThread().getName()+""+i);
42. }
43. return i;
44. }
45.
46. }
二、创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创见多线程时,优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
2 线程简单同步(同步块)
2.1 同步的概念和必要性
概念:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程"同步"执行。
必要性:不管是多线程还是多进程,涉及到共享相同的内存时,需要确保好同步问题。对线程来说,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题,同样的,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是如果其中的某个线程去改变该变量,其他线程也能读取或者修改的时候,我们就需要对这些线程进行同步,确保他们访问变量的存储内容时不会访问到无效的值。当线程修改变量的时候,其他线程在读取这个变量时可能会看到一个不一致的值,在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与写这两个周期交叉,不一致就会出现。
2.2 synchronize关键字和同步块
在进行多线程的开发中经常会遇到线程安全问题,那么我们应该如何避免呢.
使用synchronize关键字就可以很好的实现线程安全的程序.
在什么情况下会遇到非线程安全问题呢?比如:
线程A 线程B
1.线程A在数据库中查询存票,发现票C可以卖出
2.线程A接受用户订票请求,准备出票.
3. 这时切换到了线程B执行
4. 线程B在数据库中查询存票,发现票C可以卖出
5. 线程B将票卖了出去
6.切换到线程A执行,线程A卖了一张已经卖出的票
这时就会出现非线程安全问题,原因是两个线程同时访问了一个共同的对象并修改了他.所以我们应该在某个线程正在执行一个不可分割的部分时,其它线程不能同时执行这一部分.
synchronize到底锁住了什么?
对于同步块,synchornized获取的是参数中的对象的锁:
synchornized(obj){
//...............
}
线程执行到这里时,首先要获取obj这个实例的锁,如果没有获取到,线程只能等待.如果多个线程执行到这里,只能有一个线程获取obj的锁,然后执行{}中的语句,所以,obj对象的作用范围不同,控制程序不同.
对于对象和方法,调用synchronize的方法一定是排队运行的.
2.3 实例
package zheng;
importcom.sun.media.jfxmedia.events.NewFrameEvent;
public class testhread {
static int c=0;
staticObject lock = new Object();
publicstatic void main(String[] args) {
Thread[]thread =new Thread[1000];
for(inti=0;i<1000;i++) {
finalint index = i;
thread[i]= new Thread(()->{
synchronized(lock){
System.out.println("thread"+index+"enter");
inta = c;//获取c的值
a++;//将值加一
try{//模拟复杂处理过程
Thread.sleep((long)(Math.random()*1000));
}
catch(InterruptedExceptione) {
e.printStackTrace();
}
c=a;//存回去
System.out.println("thread"+index+"leave");
}
});
thread[i].start();//线程开始
}
for(inti=0;i<1000;i++){
try{
thread[i].join();//等待thread i完成
}catch(InterruptedExceptione) {
e.printStackTrace();
}
}//循环后所有的线程都完成了
System.out.println("c="+c);//输出c的结果
}
}
问题描述:一群生产者进程在生产产品,并将这些产品提供给消费者去消费。为了使生产者进程与消费者进程能够并发进行,在两者之间设置一个具有n个缓冲区的缓冲池,生产者进程将产品放入一个缓冲区中;消费者可以从一个缓冲区取走产品去消费。尽管所有的生产者进程和消费者进程是以异方式运行,但它们必须保持同步:当一个缓冲区为空时不允许消费者去取走产品,当一个缓冲区满时也不允许生产者去存入产品。
我们这里利用一个一个数组buffer来表示这个n个缓冲区的缓冲池,用输入指针和输出指针+1来表示在缓冲池中存入或取出一个产品。由于这里的缓冲池是循环缓冲的,故应把in和out表示成:in = ( in +1 ) % n (或把out表示为 out = ( out +1 ) % n )当( in +1) % n= out的时候说明缓冲池满,in = out 则说明缓冲池空。在这里还要引入一个整型的变量counter(初始值0),每当在缓冲区存入或取走一个产品时,counter +1或-1。那么问题的关键就是,把这个counter作为临界资源处理,即令生产者进程和消费者进程互斥的访问它。
3.3实现代码
packageorg.zheng;
import java.util.LinkedList;
importjava.util.concurrent.locks.Condition;
importjava.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Queue { //队列
//(1)建立一个锁,俩信号量
private Lock lock =new ReentrantLock(); //锁
private Condition fullC; //信号量
private Condition emptyC; //信号量
private int size;
public Queue(int size) {
this.size = size;
//(2)为信号量赋初值
fullC = lock.newCondition();
emptyC = lock.newCondition();
}
LinkedList<Integer> list = new LinkedList<Integer>();
/**
* 入队
* @return
*/
public boolean EnQueue(int data) {
lock.lock(); //上锁
while(list.size()>=size) {
try {
fullC.await();
} catch (InterruptedException e) {
lock.unlock();
return false;
}
}
list.addLast(data);
emptyC.signalAll(); lock.unlock();
return true;
}
/**
* 出队
* @return
*/
public int DeQueue() {
lock.lock(); //先上锁
while(list.size() == 0) { //如果队列为空,则等待生产者 唤醒我
try {
emptyC.await();
} catch (InterruptedException e) {
lock.unlock();
return -1; //失败返回
}
}
int r = list.removeFirst(); //获取队列头部
fullC.signalAll(); //唤醒所有的生产者
lock.unlock(); //解锁
return r;
}
public boolean isFull() {
return list.size()>=size;
}
public boolean isEmpty() {
return list.size()==0;
}
}
当生产能力超出消费能力时,生产者线程生产物品时没有空缓冲区可用,生产者线程必须等待消费者线程释放出一个空缓冲区。
生产能力弱于消费能力时,消费者线程消费物品,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产者线程生产出来。
这次课程设计中,不仅培养了独立思考、动手操作的能力,在各种其它能 力上也都有了提高。更重要的是,在实验课上,我们学会了很多学习的方法。而这是以后 最实用的,真的是受益匪浅。要面对社会的挑战,只有不断的学习、实践,再学习、再实 践。这对于我们的将来也有很大的帮助。回顾起此课程设计,至今我仍感慨颇多,从理论 到实践,在这段日子里,可以说得是苦多于甜,但是可以学到很多很多的东西,同时不仅 可以巩固了以前所学过的知识,而且学到了很多在书本上所没有学到过的知识。通过这次 课程设计使我懂得了理论与实际相结合是很重要的,只有理论知识是远远不够的,只有把 所学的理论知识与实践相结合起来,从理论中得出结论,才能真正为社会服务,从而提高 自己的实际动手能力和独立思考的能力。在设计的过程中遇到问题,可以说得是困难重 重,但可喜的是最终都得到了解决。