课程总结
课程总结
1 线程的创建与启动
一、定义线程
1、扩展java.lang.Thread类。
此类中有个run()方法,应该注意其用法:
public void run()
如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。
Thread的子类应该重写该方法。
2、实现java.lang.Runnable接口。
void run()
使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run方法。
方法run的常规协定是,它可能执行任何所需的操作。
二、实例化线程
1、如果是扩展java.lang.Thread类的线程,则直接new即可。
2、如果是实现了java.lang.Runnable接口的类,则用Thread的构造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
三、启动线程
在线程的Thread对象上调用start()方法,而不是run()或者别的方法。
在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。
在调用start()方法之后:发生了一系列复杂的事情
启动新的执行线程(具有新的调用栈);
该线程从新状态转移到可运行状态;
1.1 进程与线程
1.进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
2.线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
3.区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
4.优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
1.2 Java中的Thread和Runnable类
2 (1)Thread类
3 Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。
4 下面是Thread类中常用的方法:
5 以下是关系到线程运行状态的几个方法:
6 1)start方法
7 start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
8 2)run方法
9 run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
10 3)sleep方法
11 (2)Runnable类
12 实现Runnable接口相比继承Thread类有如下好处:
1、避免继承的局限,一个类可以继承多个接口。
2、适合于资源的共享。
2.1三种创建线程的办法
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
(1)
package dsdsdasa; //包名
/**
*
* @author Administrator
*
*/
class MyR implements Runnable{
private String msg;
public MyR(String msg) {
// TODOAuto-generated constructor stub
this.msg=msg;
}
@Override
publicvoid run() {
while(true)
{try{
System.out.println(msg);
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
public class TextThread {
public static void main(String[] args) {
Threadthread1=new Thread(new MyR("hello"));
thread1.start();
Thread thread2=new Thread(newMyR("wuwu"));
thread2.start();
}
结果:
}
(2)
package dsdsdasa; //包名
public class xcvxcvx {
publicstatic void main(String[] args) {
xcvxcvxtestxcvxcvx=new xcvxcvx();
Runnablerunnable=new Runnable() {
@Override
publicvoid run() {
while(true)
{try{
System.out.println("haha");
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
Threadthread=new Thread(runnable);
thread.start();
}
}
结果:
(3)
package dsdsdasa;
public class TextThread3 {
publicstatic void main(String[] args) {
new Thread(new Runnable() {
@Override
publicvoid run() {
while(true)
{try{
System.out.println("haha");
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}).start();
/*new Thread(()->{
System.out.println("haha");
}).start();*///与6-22行内容一样 new-----.start();
}
}
结果:
(1)
2 线程简单同步(同步块)
2.1 同步的概念和必要性
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作。其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
例如:
在Java里面,通过synchronized进行同步的保证。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class MyTest{
private static final Object lock=new Object();
public static synchronized void test(){ //同步的方法 }
public void test2(){ synchronized(lock){ //方法级同步,也可以使用this实现对象级同步 } }
} |
2.2 synchronize关键字和同步块
程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题。同步机制可以使synchronized关键字实现。当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。
Java 同步块用来标记方法或者代码块是同步的。Java 同步块用来避免竞争。
2.3 实例
package dsdsdasa;
publicclass TextSync {
staticintc=0;
static Object lock = new Object();//随便建立一个变量,作为锁变量
publicstaticvoid main(String[] args) {
Thread[] threads=new Thread[1000];
for(inti=0;i<1000;i++) {
finalintindex=i;//建立一个final变量,放在lamba中使用
threads[i]= new Thread(()->{
synchronized (lock) {//创建一个同步块,需要一个锁
System.out.println("thread"+index+"enter");//输出
inta=c;//获取c的值
a ++;//将值+1
try {//模拟复杂处理过程
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
// TODO Auto-generatedcatch block
e.printStackTrace();
}
c=a;//存回去
System.out.println("thread"+index+"leave");//输出
}//块的终结
});
threads[i].start();//线程开始
}
for(inti=0;i<1000;i++) {
try {
threads[i].join();}//等待thread i完成
catch(InterruptedException e) {
e.printStackTrace();
}
}//循环后,所有线程完成
System.out.print("c="+c);//输出C的结果
}
3 生产者消费者问题
3.1 问题表述
生产者消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
3.2 实现思路
对于生产者,如果缓存是满的就去睡觉。消费者从缓存中取走数据后就叫醒生产者,让它再次将缓存填满。若消费者发现缓存是空的,就去睡觉了。下一轮中生产者将数据写入后就叫醒消费者。不完善的解决方案会造成“死锁”,即两个进程都在“睡觉”等着对方来“唤醒”。只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥和同步PV操作的位置。使用“进程间通信”,“信号标”semaphore就可以解决唤醒的问题:我们使用了两个信号标:full 和 empty 。信号量mutex作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为 1;信号量 full 用于记录当前缓冲池中“满”缓冲区数,初值为0。信号量 empty 用于记录当前缓冲池中“空”缓冲区数,初值为n。新的数据添加到缓存中后,full 在增加,而 empty 则减少。如果生产者试图在 empty 为0时减少其值,生产者就会被“催眠”。下一轮中有数据被消费掉时,empty就会增加,生产者就会被“唤醒”。
3.3 Java实现该问题的代码
(1)Queue
package asdasda;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
publicclass Queue {
private Condition fullC;//信号量,如果满了则等待
private Condition emptyC;//信号量,如果空了则等待
private Lock lock=new ReentrantLock();
privateintsize;//生产者编号
public Queue(intsize) {
this.size=size;
fullC=lock.newCondition();
emptyC=lock.newCondition();
}
LinkedList<Integer>list = new LinkedList<Integer>();
/**
* 入队
* @param data
* @return
*/
publicboolean EnQueue(intdata) {
lock.lock();//上锁
while(list.size()>=size)
{try {fullC.await();
}catch (InterruptedException e) {
lock.unlock();
returnfalse;
}
}
list.addLast(data);
emptyC.signalAll();
returntrue;
}
/**
出队**/
publicint Dequeue() {
lock.lock();//先上锁
while(list.size()==0) {//如果队列为空,则等待生产者唤醒
try {emptyC.await();}
catch(InterruptedException e) {
lock.unlock();
return -1;//失败返回
}
}
intr=list.removeFirst();//获取队列头部
fullC.signalAll();//唤醒所有生产者
lock.unlock();//解锁
returnr;
}
publicboolean isFull() {
returnlist.size()>=size;
}
publicboolean isEmpty() {
returnlist.size()==0;
}
}
(2)TestPC
package asdasda;
publicclassTestPC {
static Queue queue=new Queue(5);
publicstaticvoidmain(String[] args){
//创建三个生产者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
intdata=(int)(Math.random()*1000);
System.out.printf("thread %d want to EnQueue %d\n",index,data);
queue.EnQueue(data);
System.out.printf("thread %d EnQueue %d Success\n",index,data);
sleep();//随机休息一段时间
}).start();
}
//创建消费者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
System.out.printf("customer thread %d want to EnQueue\n",index);
intdata=queue.Dequeue();
System.out.printf("customer thread %d EnQueue %d Success\n",index,data);
sleep();//随机休息一段时间
}).start();
}
}
publicstaticvoid sleep() {
intt=(int)(Math.random()*1000);
try {
Thread.sleep(t);
}catch (InterruptedException e) {
e.printStackTrace();
}
}}
3.4 测试
3.4.1当生产能力超出消费能力时的表现
3.4.2当生产能力弱于消费能力时的表现
4 总结
此次实验完成了消费者与生产者这两个进程之间的同步协调问题。值得注意的是解决进程同步需要做哪些工作,如何利用信号量机制来解决生程同步问题等等,这些问题其实我们在学习理论知识时都是很少思考的,因为惑触不深,所以学了一遍就过去了,但是在自己做实验时才会发现哪些地方是我们需要考虑的,哪些地方是需要注意的,实验给了我们实践的机会,给了我们理论结合实际的机会, 从实验中可以学到很多东西,不仅仅是书本上的东西这么简单,更重要的是对待事情严谨的态度,对待任何事情都要一丝不苟,细节决定成败!