1 线程的创建与启动
进程:进程是进程实体的运行过程,是系统进行资源分配的和调度的一个独立单位。
线程:线程是系统调度和分派的基本单位
区别:1.调度的基本单位:传统OS中,进程是作为独立调度和分派的基本单位,因而进程是能独立运行基本单位;在引入线程的OS中,线程作为调度和分派的基本单位,因而线程是能独立运行基本单位。2.并发性:在引入线程的OS中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,甚至还允许在一个进程中的所有线程都能并发执行。同样,不同进程中的线程也能并发执行。3.拥有资源:进程可以拥有资源,并作为系统中拥有资源的一个基本单位。线程本身并不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源。4.独立性:每个进程都拥有一个独立的地址空间和其他资源,而同一进程中的不同线程是共享进程的内存地址空间和资源。5.系统开销:在创建或撤销进程时,系统都要为之分配和回收进程控制块、其他资源等。OS为此所付出的开销,明显大于线程创建或撤销时所付出的开销。6.支持多处理机系统:对于单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,这将加速进程的完成。
Thread使用方法:
Thread类是线程类,其每一个实例表示一个可以并发运行的线程。我们可以通过继承该类并重写run方法来定义一个具体的线程。
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
Runnable使用方法:
步骤一:创建一个实现Runnable接口的类;
步骤二:创建一个类对象;
步骤三:由Runnable创建一个Thread对象;
步骤四:启动线程。
1. 第一种方法(定义一个”MyR”类,创建一个Runnable接口的对象并使用Thread对象启动它)
packageorg.hhh;
class MyRimplements Runnable{
privateString msg;
publicMyR(String msg){
this.msg=msg;
}
@Override
publicvoidrun() {
while(true){
try {
Thread.sleep(1000);
System.out.println(msg);
}catch(InterruptedException e){
e.printStackTrace();
break;
}
}
}
}
publicclass whh {
publicstaticvoid main(String[] args) {
Threadthread1 = new Thread(new MyR("hello"));
thread1.start();
Threadthread2 = new Thread(new MyR("hi"));
thread2.start();
}
}
2. 第二种方法(定义一个实现Runnable接口的匿名类)
packageorg.hhh;
publicclass jjj {
publicstaticvoid main(String[] args) {
Runnable runnable= new Runnable() {
@Override
publicvoidrun() {
while(true){
try {
Thread.sleep(1000);
System.out.println("haha");
}catch(InterruptedException e){
e.printStackTrace();
break;
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
3. 第三种方法(Lambda表达式)
packageorg.hhh;
publicclass hh3 {
publicstaticvoid main(String[] args) {
new Thread(()-> {
System.out.println("haha");
}).start();
}
}
2.1 同步的概念和必要性
线程同步:即当有一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。而其他线程又处于等待状态,目前实现线程同步的方法有很多,临界区对象就是其中一种。
必要性:当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到
Synchronized:synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
同步块:被Synchronized修饰的代码块称为同步块。
package org.hhh;
importjavax.sql.rowset.Joinable;
import com.sun.media.jfxmedia.events.NewFrameEvent;
public class www {
static int c=0;
static Object Lock = new Object();//(1)建一个变量,作为锁变量
public static void main(String[] args) {
Thread[] threads = new Thread[1000];
for(int i=0;i<1000;i++) {
finalint index=i;//(4)建立一个final变量,放在lamba中使用
threads[i]= new Thread(()->{
synchronized(Lock) {//(2)创建一个同步块,需要一个锁
System.out.println("thread"+index+"leave");//(5)输出
inta=c;//获取c的值
a++;
try{//模拟复杂化处理过程
Thread.sleep((long)(Math.random()*1000));
}catch (InterruptedException e) {
e.printStackTrace();
}
c=a;//将值存回去
System.out.println("thread"+index+"leave");//(6)输出
}//(3)这是块的终结
});
threads[i].start();//线程开始
}
for(int i=0;i<1000;i++) {
try {
threads[i].join();//等待thread i 完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}//循环后。所有线程都完成
System.out.print("c="+c);//输出c的结果
}
}
生产者生产一段数据,只有缓冲区没有满时,生产者才能把信息放到缓冲区中,否则等待;同时,只要缓冲区不空时,消费者才能从中取出消息,否则必须等待。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。
3.3 Java实现该问题的代码
package org.hhh;
importjava.util.LinkedList;
importjava.util.concurrent.locks.Condition;
importjava.util.concurrent.locks.Lock;
importjava.util.concurrent.locks.ReentrantLock;
publicclass hh5 {//队列
//(1)建立一个锁,两个信号量
private Lock lock=newReentrantLock();//锁
private Condition fullc;//信号量
private Condition emptyc;//信号量
privateintsize;
public hh5(intsize){
this.size=size;
//(2)为信号量赋初值
fullc=lock.newCondition();
emptyc=lock.newCondition();
}
LinkedList<Integer>list = new LinkedList<Integer>();
/**
* 入队
* return
*/
publicboolean EnQueue(intdata){
lock.lock();//上锁
while(list.size()>=size) {
try {
fullc.await();
}catch(InterruptedException e){
returnfalse;
}
}
list.addLast(data);
emptyc.signalAll();
lock.unlock();
returntrue;
}
/**
* 出队
*return
* @throws
*/
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;
}
/**
* 队空
* @return
*/
publicboolean isFull() {
returnlist.size()>=size;
}
/**
* 队满
* @return
*/
publicboolean isEmpty() {
returnlist.size()==0;
}
}
packageorg.hhh;
publicclass TestP {
static hh5 queue=newhh5(5);
publicstaticvoid main(String[] args) {
//创建三个生产者
for(inti=0;i<3;i++) {
finalintindex=i;
newThread(()-> {
while(true) {
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;
newThread(()-> {
while(true) {
System.out.printf("customer %d want to DeQueue \n",index);
intdata=queue.DeQueue();
System.out.printf("customer thread %d DeQueue %d success\n",index,data);
sleep2();//随机休息一段时间
}
}).start();
}
}
//sleep随机时间
publicstaticvoid sleep() {
intt=(int)(Math.random()*100);
try {
Thread.sleep(t);
}catch(InterruptedException e){
e.printStackTrace();
}
}
publicstaticvoid sleep2() {
intt=(int)(Math.random()*1000);
try {
Thread.sleep(t);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
当生产能力超出消费者能力时,生产速度快,生产者经常等待消费者。
当生产能力弱于消费者能力时,生产速度慢,消费者经常等待生产者。
此次的实验有三个,线程的创建与启动、线程的简单同步以及生产者消费者问题。第一个实验中,重点是用.Thread类和Runnable类来创建并运行线程。第二个实验中重点是使用Synchronized关键字。第三个实验,首先要明确生产者消费者的问题,其次是明白用信号量去解决生产者消费者问题的思路。