1 线程的创建与启动
进程:是进程实体的运行过程中,是系统进行资源分配和调度的一个独立单位。
线程:是调度和分派的基本单位。
进程、线程概念和差别:
(一)调度的基本单位。
在传统的os中,进程是作为独立调度和分派的基本单位,因而进程是能独立运行的基本单位。
在引入线程的os中,把线程进程作为独立调度和分派的基本单位,因而线程是能独立运行的基本单位。
(二)并发性。
在引入线程的os中,进程之间可以并发执行,而且在一个进程的多个线程之间亦可以并发执行。同样,不同进程中的线程也能并发执行。
(三)拥有资源。
进程可以拥有资源,并作为系统中拥有资源的一个基本单位。
线程拥有很少系统资源。
(四)独立性。
在统一进程中的不同线程之间的独立性比不同进程之间低得多。
每个进程都拥有一个独立的地址空间和其它资源,不允许其它进程的访问。而同一进程中的不同线程共享进程的内存空间地址和资源。
(五)系统开销。
创建或撤销进程时,系统都要为之分配和回收内存、I/O设备等资源。
线程只保存少量寄存器,不涉及存储器管理;线程的通信与同步更容易,无需内核的干预。
(六)支持多处理机系统
在多处理机系统中,对于传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。但对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行。
Thread类:
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
Runnable创建线程步骤:
(1)创建一个实现Runnable接口的类,并且在这个类中重写run方法
classMyThreadimplementsRunnable{
publicvoid run(){
..........
}
(2)使用关键字new创建一个MyThread的实例
MyThread myThread=new MyThread();
(3)通过Runnable的实例创建一个线程对象,在创建线程对象时,调用的构造函数是
new Thread(myThread),它用myThread中实现的run()方法作为新线程对象的run()方法。Threadthread=newThread(myThread);
(4)通过调用ThreadType对象的start()方法启动线程运行
thread.start();
方法一、定义实现Runnable接口的一般方法
package org;
/**
* Runnable的实现类,是线程执行的主体。
* run函数是入口
*
*/
class MyR implements Runnable{
private String msg;
public MyR(String msg) {
this.msg = msg;
}
//线程的入口
@Override
publicvoid run() {
while(true) {
try {
System.out.println(msg);
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
publicclass TestThread{
publicstaticvoid main(String[] args) {
//创建了线程
Threadthread1 = new Thread(new MyR("hello"));
thread1.start(); //启动了线程
Threadthread2 = new Thread(new MyR("wuwu"));
thread2.start(); //启动了线程
}
}
实现:
结果:
方法二、定义实现Runnable接口的匿名类
package org;
publicclass TestTheard2 {
publicstaticvoid main(String[] args) {
TestTheard2testThread2 = new TestTheard2();
//匿名信 匿名类 引用就是指针
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();
}
}
实现:
结果:
方法三、用lamda表达式创建线程
package org;
publicclass TestTheard3 {
publicstaticvoid main(String[] args) {
//new Thread(new Runnable() {
// @Override
// publicvoid run() {
// System.out.println("haha");
// }
//}).start();
// lamda 表达式 java 1.8+ (第5-12行相当于)
new Thread(()->{
System.out.println("haha");
}).start();
}
}
运行:
结果:
2 线程简单同步(同步块)
线程同步解决的是在一个程序中多个线程之间的关系的协调,对竞争资源的访问的一种处理方式,避免一个线程长期占用一个资源的目的。
必要性:当线程共享数据时,为了保证数据的准确性和安全性,需要线程同步。
如:在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
2.2 synchronize关键字和同步块
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
2.3 实例
package org;
publicclass TestSync {
staticintc=0;
publicstaticvoid main(String[] args) {
Thread[]threads = new Thread[1000];
for(inti=0;i<1000;i++) {
threads[i]=new Thread(()-> {
inta=c; //获取c的值
a++; //将值加一
try { //模拟复杂处理过程
Thread.sleep((long)(Math.random()*1000));
}catch (InterruptedException e) {
e.printStackTrace();
}
c=a; //存回去
});
threads[i].start(); //线程开始
}
for(inti=0;i<1000;i++) {
try {
threads[i].join(); //等待 thread i 完成
}catch (InterruptedException e) {
e.printStackTrace();
}
}//循环后,所有的线程都完成了
System.out.println("c="+c); //输出c的结果
}
}
运行:
结果:
3 生产者消费者问题
有一个队列:
生产者负责将元素加入队列中,谓之生产;
消费者负责将元素从队列中移出,谓之消费。
当队列满的时候,让生产者等待,直到有人唤醒他。
唤醒的时机:当有消费者消费了队列中的元素,队列不再满,就可以唤醒生产者让其生产。
当队列为空时,让消费者等待,当生产者生产了元素,队列不再为空,则唤醒消费者。
3.2 实现思路
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
3.3 Java实现该问题的代码
package org;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
publicclass Queue { //队列
//(1)建立一个锁,俩信号量
private Lock lock =new ReentrantLock(); //锁
private Condition fullC;
private Condition emptyC;
privateintsize;
public Queue(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) {
lock.unlock();
returnfalse;
}
}
list.addLast(data);
emptyC.signalAll();
lock.unlock();
returntrue;
}
/**
* 出队
* @return
*/
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;
}
}
package org;
publicclass TestPC {
static Queue queue=new Queue(5);
publicstaticvoid main(String[] args) {
//创建三个生产者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
intdata=(int)(Math.random()*1000);
System.out.printf("producer thread %d want to EnQueue%d\n",index,data);
queue.EnQueue(data);
System.out.printf("producer thread %d EnQueue %dSuccess\n",index,data);
sleep();//随机休息一段时间
}).start();
}
//创建消费者
for(inti=0;i<3;i++) {
finalintindex=i;
new Thread(()->{
while(true) {
System.out.printf("customer thread %d want toDeQueue\n",index);
intdata=queue.DeQueue();
System.out.printf("customer thread %d DeQueue %dSuccess\n",index,data);
sleep2(); //随机休息一段时间
}
}).start();
}
}
//sleep随机时间
privatestaticvoid sleep() {
intt=(int)(Math.random()*100);
try {
Thread.sleep(t);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
privatestaticvoid sleep2() {
intt=(int)(Math.random()*1000);
try {
Thread.sleep(t);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.4 测试
结果:
3.4.1 当生产能力超出消费能力时的表现
当生产能力超出消费能力时,即当生产者个数多于消费者个数时,生产速度快,生产者经常等待消费者
3.4.2 当生产能力弱于消费能力时的表现
生产能力弱于消费能力时,当生产者个数少于消费者个数时,生产速度低于消费速度,则消费者经常等待生产者。
4 总结
此次实验主要是围绕线程和同步的问题,有线程的创建和同步问题,还有进程同步的经典问题——生产者-消费者问题。Java程序是建立在线程之上的。线程的创建一般用继承Thread类或是实现Runnable接口。(java 1.8+的版本还能用lamda 表达式,大大缩减了代码,更加简单)。线程的同步主要是用synchronize关键字来实现。利用信号量和锁来解决生产者-消费者问题的同步,有较高的效率,并且易于实现。