P1 关于多线程编程的几个知识点
1 进程
(1) 进程是动态的
进程与程序之间存在密切的关系,程序是静态的,存放在磁盘中,一旦写好就不会再发生变化,而进程指的是程序从磁盘加载到内存中,并享受CPU服务的动态过程
操作系统需要“创建”进程,为其分配一定的资源才能存在,进程存在期间,需要操作系统对其进行管理,进程结束后,需要操作系统对其占用的资源进行回收,并“销毁”这个进程,对进程的所有操作,都是要消耗计算机资源的,至少是时间资源
(2) 每个进程有一套独立的数据
如果在一台机器上同时运行两个qq账户,这时就有了两个不同的进程,这两个进程各自运行各自的,而且每一个进程随时会进入运行状态,又会从运行状态中退出,下一次有进入运行状态,这需要进程能够在暂时退出运行状态时,记住当前运行的所有详细状况,以便下一次能成功运行
每个进程都需要记载大量的数据,进程的管理不但要消耗计算机时间资源,还要消耗计算机内存资源
(3) 进程是计算机资源的竞争者
某次进程可能需要计算机的各种资源,如:输入,输出,文件资源,,磁盘资源等等
进程向操作系统申请这些资源,对于计算机资源的多样性和进程对资源申请的多样性,如果不加以严格的管理,可能会导致严重的后果
(4) 进程的管理与调度
对于上图,强调以下几点:
1,进程(线程)被创建后,不是立刻进入运行态,先进入就绪态
2,阻塞态的进程(线程)在唤醒前,没有资格竞争CPU
3,阻塞态的进程(线程)必须由其它进程(线程)唤醒
4,被唤醒的进程(线程)不是立刻进入运行态,而是进入就绪态
2 保护临界资源
(1) 原语
由高级程序设计语言编写的源程序的一条语句,编译成机器语言后可能对应多条语句,而在多道程序并行环境中,这些多条语句的执行可能随时被中断
但是在实际编程需求中,有些语句的执行是不希望被打断的,必须完全执行,为了满足这样的要求,计算机系统提出了“原语”
原语:一条或多条语句,对于它们的执行不会被中断
对于进程(线程)的创建,调度,阻塞,唤醒的操作实质上都是原语,作为原语,应该做到:代码尽可能少,尽可能不要出现长循环,尤其不能出现递归调用和I/O操作
(2) 临界资源
有些情况下,需要两个或多个进程之间的互相配合,共同完成某一编程任务,它们之间通过“共享数据”的方式建立联系,对于这种存在着关联关系的多个进程,如果不仔细处理其中的逻辑关系,就可能造成程序的失败
临界资源指的是进程中的某些代码段,在进程的代码中,与“共享数据”操作有关的代码往往被称为临界资源
(3) 锁
对于临界资源,通过加锁,阻止不该进入的进程
对于锁的的几点声明:
1,锁必须是所有相关进程共享的,即大家都知道这个锁的存在
2,多个相关进程检查/打开/关闭的应该是同一把锁
3,进程(线程)若是遇到锁,一定先查看锁的状态
若锁的状态是打开的,先关闭锁,再进入临界资源
若锁的状态是关闭的,则阻塞自己,将自己放入该锁的阻塞态队列
查看锁和关闭是一个原语,不会被打断,在Java中,对锁的查看,开锁和关锁操作,都是由JVM实现的
(4) 进程与线程
线程是轻量级的进程
线程是由进程创建的,但线程不再申请另外的计算机资源,即线程不需要像进程那样有庞大的资源表,也不需要像进程那样对资源进行严格的管理
线程所使用的资源都是进程申请的,多个线程的状态切换比进程更简单,更省时,线程也可以生成新的线程(子线程)
P2 创建多线程
1 继承Thread类
线程类:
package com.mec.thread;
public class MyThread extends java.lang.Thread {
private String threadName;
public MyThread(String threadName) {
this.threadName = threadName;
}
//Thread类中必须覆盖run()方法
//run()方法体中的内容是线程将执行的代码
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(this.threadName + ":" + i);
}
}
}
测试类:
package com.mec.thread.test;
import com.mec.thread.MyThread;
public class Test {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程1");
MyThread thread2 = new MyThread("线程2");
MyThread thread3 = new MyThread("线程3");
//start()用来创建一个新线程,将该创建的线程放入就绪态
//这个线程等待JVM的线程调度器,被JVM调度到运行态方可执行
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果1:
运行结果2:
对比这两次运行结果发现各个线程运行的状况是不确定的,这与当前时刻操作系统的状态有关,每一个线程都是独立运行的
在向测试类中加一行输出语句:
运行结果1:
运行结果2:
对于输出“"main()函数所在主线程运行结束”这句话,绝大多数都是输出在第一行
上述的结果再一次说明了start()方法的作用是创建线程,而不是执行线程,start()创建一个线程后,将其放入就绪态,等待操作系统调度
2 实现Runnable接口
线程类:
package com.mec.thread;
public class MyThread2 implements Runnable {
//count被static修饰,只有一份
//在这个程序中,用两个线程来更改同一个count的值
private static int count;
private String threadName;
public MyThread2(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
for(int i = 0; i < 100; i++) {
MyThread2.count += 5;
for(int j = 0; j < 10000; j++) {
}
MyThread2.count -= 4;
System.out.println(this.threadName + ":" + MyThread2.count);
}
}
}
测试类:
package com.mec.thread.test;
import com.mec.thread.MyThread2;
public class Test2 {
public static void main(String[] args) {
//通过Runnable接口创建的线程类没有start()方法,
//必须先生成一个对象
//再使用Thread类的方法,将先前的对象作为一个参数传入
//才可以使用start()方法
MyThread2 myThread1 = new MyThread2("线程1");
MyThread2 myThread2 = new MyThread2("线程2");
Thread thread1 = new Thread(myThread1);
thread1.start();
new Thread(myThread2).start();;
}
}
运行结果1:
运行结果2:
上述线程类中run()方法中的线程体目的是让n个线程独立各自运行100次,其中对static修饰的count进行操作,理想情况下线程交替运行,那么count最后的结果是100n,且输出是有顺序的,从1到100n
但是这仅仅是理想情况下,真实运行的多线程不可能这么简单,考虑假设线程1开始第一次运行,对count+5后,此时count的值是5,进入循环时,循环一段时间后,线程1的时间片用尽,进入就绪态,线程2被调度到运行态,开始执行代码,对于此时的count,它直接+5,此时count就成了10,接下来如此往复,线程2也可能在时间片用完后被调入就绪态,这样对于共享的数据count就彻底乱套了
3 使用extends继承Thread类和实现Runnable接口的区别
虽然extends Thread简明扼要,但是更推荐使用实现Runnable接口的方式来创建线程类,其原因有:
1,extends只能单继承,而接口可以多实现
2,Runnable接口很干净,里面只有一个run()方法
P3 锁
对于上面count的输出混乱的结果不是我们想要的,想要count有序的输出数据,这时就需要给临界资源(涉及到count的代码段)加锁
根据锁的定义:
1,锁必须是所有相关进程共享的,即大家都知道这个锁的存在
2,多个相关进程检查/打开/关闭的应该是同一把锁
3,进程(线程)若是遇到锁,一定先查看锁的状态
1 synchronized (lock) {…}
推荐使用对象锁,给临界资源上锁:
package com.mec.thread;
public class MyThread2 implements Runnable {
//count被static修饰,只有一份
//在这个程序中,用两个线程来更改同一个count的值
private static int count;
private String threadName;
//定义单独一份的对象锁
private static Object lock;
//初始化一个对象锁
static {
lock = new Object();
}
public MyThread2(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
for(int i = 0; i < 100; i++) {
//将与共享数据有关的临界资源上锁
synchronized (lock) {
MyThread2.count += 5;
for(int j = 0; j < 10000; j++) {
}
MyThread2.count -= 4;
System.out.println(this.threadName + ":" + MyThread2.count);
}
}
}
}
运行结果:
在对共享数据的临界资源加锁后,假设一开始线程1运行,线程1遇到lock,先检查lock此刻的状态,此时lock是打开的,线程1先对lock加锁,接着运行临界资源中的代码,如果此时线程1被调度到了就绪态,线程2开始运行,线程2遇到lock,检查到现在lock的状态是关闭的,线程2自己阻塞自己,操作系统将线程2放入到lock的阻塞队列中,此时线程2只能等待被唤醒,它无法竞争CPU,接着线程1继续运行临界资源中的代码,直到遇到synchronized (lock) { 的右花括号,此时线程1顺利的执行完了临界资源中的代码,且不会被其它的线程打断,线程1将lock打开,并唤醒该锁上阻塞队列的所有线程,将它们调度到就绪态,等待运行,自此count的输出就会变得有顺序
2 Thread.sleep(x)
对于上述的测试结果,可以看到一个线程可能执行很多次才轮到另一个线程,因为在整个线程体中,只有for(int i = 0; i < 100; i++)中进行i < 100和i++的操作时,才可能会被其它的线程打断,其它的所有程序都被lock包住了,如果想更有顺序的轮换线程运行,可以:
@Override
public void run() {
for(int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
//将与共享数据有关的临界资源上锁
synchronized (lock) {
MyThread2.count += 5;
for(int j = 0; j < 10000; j++) {
}
MyThread2.count -= 4;
System.out.println(this.threadName + ":" + MyThread2.count);
}
}
}
sleep(10),会让该线程空等10ms,这段时间,如果该线程的时间片用尽,就会有别的线程从就绪态被调度到运行态执行程序:
P4 volatile关键字
1 计算机存储体系简介
计算机存储体系从外到内分为5层:
1,海量外存,存储空间最大,速度最慢
2,外存,存储空间大,速度较慢
3,内存,存储空间不是很大,速度较快
4,高速缓存,存储空间很小,速度很快
5,寄存器,存储空间最小,速度最快
我们所编写的程序,变量,数组的本质就是内存空间,对变量,数组元素的访问本质上就是对内存的访问
for(int i = 0; i < 10000; i++) {
...
}
对于循环的条件和步长i,会在程序中频繁被访问,如果每次都要从内存中访问i进行判断,从内存中取出i,对i+1后,再将其写入内存,程序的执行效率会因为内存访问速度较慢,而变得低效
2 变量的寄存器优化
很多编译软件都会对“要频繁访问内存的变量”,进行寄存器优化
即对于这个变量,编译系统第一次从内存中访问它时,用一个寄存器保存它的值,之后对它的读和写都在寄存器中完成,由于寄存器的高速,整个程序的速度就会提升
但这意味着,如果存在两个线程,它们对同一变量进行操作,但该变量被编译系统寄存器优化后,两个线程表面上是对同一线程进行访问,但是却是对各自的寄存器中的值进行访问,即就算某个线程更改了“同一个变量”的值,其本质只是操作自己的寄存器的值,并没有影响到内存,从而使得两个线程并没有真正联系在一起
3 private volatile static int count
对于先前的count,虽然程序测试的结果表明他没有被寄存器优化,但如果将循环增大,可能会出现上述的问题,所以对与共享数据,一般都加上volatile,避免寄存器优化有实用意义的共享数据导致多线程没有联系在一起
但使用volatile也是有代价的,volatile拒绝寄存器优化,坚持从内存中读写,也必然会降低速度,对volatile的使用不能泛滥
P5 使用Java多线程完成生产者,消费者问题
1 生产者,消费者问题
考虑这样一个场景:两个线程,一个线程负责生产数据,一个线程负责消耗数据,如果生产者没有生产出数据,消费者自然没法消费,但如果生产者已经生产出一个数据了,而消费者尚未消耗该数据,那么生产者应该等待消费者消耗完该数据,再生产数据
其本质就是两个线程的同步问题,做到两个线程交替执行即可
2 wait()和notify()
wait()方法的本质是让执行这个方法的线程进入阻塞态
notify()方法用于唤醒处在阻塞态的相关线程
3 实现过程
对于生产者,需要设置一个表示“之前生产的数据是否已经被消耗”的标志
对于消费者,需要设置一个表示“是否有数据可供消费”的标志
对于生产者和消费者,需要设置一个共同的对象锁
需要设置一个共享的数据,即生产者生产的数据消费者消耗的数据,这两个是同一个
通过创建一个父类,由两个子类继承来完成:
package com.mec.thread;
public class ProducerCustomer {
//是否有数据以供消费
public static boolean hasValue = false;
//是否以消费
public static boolean isConsume = true;
//对象锁
public static Object lock = new Object();
//共享数据
public volatile static int data;
}
生产者类:
package com.mec.thread;
import java.util.Random;
public class Producer extends ProducerCustomer implements Runnable {
private Random random;
private String threadName;
private Thread thisThread;
public Producer() {
random = new Random();
threadName = "生产者";
thisThread = new Thread(this,threadName);
}
public void startProducer(){
thisThread.start();
System.out.println("线程" + threadName + "建立");
}
@Override
public void run() {
while(true) {
synchronized (lock) {
//如果已消耗,生产一个数据,将已消耗标志改为false
//将是否有数据标志改为true,唤醒阻塞的消费者
if(isConsume) {
data = random.nextInt(1000);
System.out.println(threadName + "生产了一个数据" + data);
isConsume = false;
hasValue = true;
lock.notify();
} else {
//如果未消耗,阻塞自己,等待被唤醒
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
消费者类:
package com.mec.thread;
public class Customer extends ProducerCustomer implements Runnable {
private String threadName;
private Thread thisThread;
public Customer() {
threadName = "消费者";
thisThread = new Thread(this,threadName);
}
public void startCustomer(){
thisThread.start();
System.out.println("线程" + threadName + "建立");
}
@Override
public void run() {
while(true) {
synchronized (lock) {
//如果有数据未消耗,消耗该数据,将已消耗标志改为true
//将是否有数据改为false,唤醒阻塞的生产者
if(hasValue) {
System.out.println(threadName + "消耗了一个数据" + data);
isConsume = true;
hasValue = false;
lock.notify();
} else {
//如果已经消耗,则阻塞自己,等待被唤醒
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
测试类:
package com.mec.thread.test;
import com.mec.thread.Customer;
import com.mec.thread.Producer;
public class TestPC {
public static void main(String[] args) {
Producer producer = new Producer();
Customer customer = new Customer();
producer.startProducer();
customer.startCustomer();
}
}
运行结果: