一、概述
程序(program)计算机指令的集合,以文件形式存储在磁盘上,指一段静态的代码,静态对象。
进程(process) :
-
是一个程序在其自身的地址空间中的一次执行活动,它是有生命周期的,经历创建、运行和消亡的过程。
-
是系统进行资源分配、调度和独立运行的基本单位(它使用系统资源)。
-
一个应用程序可以同时运行多个进程(windows系统可以运行多个软件)。
-
程序是静态的,进程是动态的。
-
程序都存在硬盘里,运行时会加载到内存里,占用内存,进入到内存的程序可以称为进程。
线程(thread):
- 进程可进一步细化为线程,是进程的一个执行单元,负责当前进程中的程序执行。
- 线程是系统独立调度和分配CPU(独立运行)的基本单位。
- 一个进程可以拥有多个线程。
多线程:在单个程序中同时运行多个线程完成不同的问题。
(1)用户向服务器发送了一个请求,服务器获取请求并且根据请求响应结果,这就是一个线程。
(2)客户端N个请求同时请求服务器,这就是多线程。
(3)**线程两种调度方式:**抢占式调度和分时调度,平均分配每个线程占用CPU的时间。
- 分时调度:所有线程轮流使用CPU。
- 抢占式调度:优先级高的线程先使用CPU,java使用的时抢占式调度。
为了更深一步理解,下面说一下CPU个数、内核数和线程数三者之间的关系。
cpu个数是指物理上安装了几个cpu,一般的个人电脑是安装了1个cpu 。
cpu内核数是指物理上,一个cpu芯片继承了几个内存单元,现代cpu都是多核的。
cpu线程数是指逻辑上处理单元,这个技术是Intel的超线程技术,它让操作系统识别到有多个处理单元。
在这里说一下内核数和线程数的关系。一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。
如下图所示:插槽指cpu个数,内核数量是2个,线程数是4个。I5 7200 cpu支持超线程技术,一个内核就是两个线程。
并发和并行。
并发:多个任务在同一时间段内发生,各个任务实际上是依次执行,一个处理器通过不断切换任务同时处理多个任务。
并行:多个任务在一时刻同时发生,各个任务实际上是同时进行,多个处理器同时处理多个不同的任务。
为什么使用多线程?
提高CPU的计算能力,避免资源浪费,提高系统的响应速度。
二、多线程的实现
先来看一个单线程的例子👇。
package com.hpe.java;
//main是主线程
public class TestMain {
//单线程
//怎么确定程序是不是多线程呢?
//如果程序在运行时能通过一条线串起来,那就是单线程。
public static void main(String[] args) {
System.out.println("main");
method1();
}
public static void method(){
System.out.println("method");
}
public static void method1(){
System.out.println("method1");
method();
}
}
看一下多线程👇。
例子:创建一个子线程,完成1-100之间的自然数的输出,同样主线程执行同样的操作。
创建线程的第一种方式:
1.继承Thread类。
2.重写run()。
3.创建子类对象。
4.调用start()。
package com.hpe.java;
//1.创建一个类继承Thread接口
class SubThread extends Thread {
//2.重写run():线程中实际要实现的业务逻辑
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
//3.创建一个子类对象
SubThread sub = new SubThread();
SubThread sub1 = new SubThread();
//4.调用start():启动子线程,调用run()
sub.start();
sub1.start();
//sub.run();//直接调用run()不是多线程
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
}
多线程常用方法:
1.void start():启动线程,并执行对象的run()方法
2.run():线程在被调度时执行的操作
3.String getName():返回线程的名称
4.void setName(String name):设置该线程名称
5.static currentThread():返回当前线程
6.static void yield():使线程立马释放CPU资源执行权,当一个线程使用了此方法后,就会把自己的CPU执行权释放,让给自己或者其他线程(自己和其它线程再去争夺CPU的执行权)。
7.join():在子线程1中调用子线程2的join方法后,线程1会进入阻塞状态,直到线程2执行完成之后,线程1才会继续执行
8.sleep(long millis):让该线程休眠,单位是毫秒
9.boolean isAlive():返回boolean,判断线程是否还活着
10.设置线程优先级:默认是5,最小值是1,最大值是10
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级
MAX_PRIORITY(10);
MIN _PRIORITY(1);
NORM_PRIORITY(5);
package com.hpe.java;
class SubThread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
try {
//使线程休眠1s再运行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//currentThread():返回当前线程
//getName():返回线程的名称
//getPriority() :返回线程优先值
System.out.println(Thread.currentThread().getName() + ":"
+ Thread.currentThread().getPriority() + ":" + i);
}
}
}
public class TestThread1 {
public static void main(String[] args) {
SubThread1 sub = new SubThread1();
//给线程设置名字
sub.setName("子线程1");
//设置子线程的优先级为10
sub.setPriority(Thread.MAX_PRIORITY);
sub.start();
//设置主线程的名字
Thread.currentThread().setName("主线程");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
//if(i % 10 == 0){
// Thread.currentThread().yield();
// //使主线程立马释放CPU资源
// //但有可能会出现这种情况:主线程释放CPU资源后,又立即抢占CPU,另一个线程却没有抢到CPU
//}
//主线程执行到20时停止,子线程开始执行直到结束,主线程继续执行
if(i == 20){
try{
sub.join();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(sub.isAlive());//false
}
}
}
}
创建线程的第二种方式:
1.实现Runnable接口
2.重写Runnable接口的run()
3.创建一个Thread对象
4.将实现类对象最为参数传给Thread类的构造方法
5.调用Thread对象的start()
package com.hpe.java;
class PrintNum implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class TestThread2 {
public static void main(String[] args) {
//创建一个是实现类
PrintNum p = new PrintNum();
//怎么启动一个线程?必须调用start()
Thread t = new Thread(p);
t.setName("子线程");
t.start();//执行p的run()
//主线程
Thread.currentThread().setName("主线程");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
例1:模拟火车站窗口售票,开启三个窗口同时售票,共100张票。
package com.hpe.ex;
class Window extends Thread {
//定义一个变量,标识票数
static int ticket = 100;
//抢票
@Override
public void run() {
while (true) {
if (ticket > 0) {
//打印抢到的票号
System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
} else {
break;//票已经售完
}
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("window1");
w2.setName("window2");
w3.setName("window3");
w1.start();
w2.start();
w3.start();
}
}
例2:创建两个子线程,让其中一个输出1-100之间的偶数,另一个输出1-100之间的奇数。
package com.hpe.ex;
//创建两个子线程,让其中一个输出1-100之间的偶数,另一个输出1-100之间的奇数。
class SubThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class SubThread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class TestE {
public static void main(String[] args) {
SubThread sub = new SubThread();
SubThread1 sub1 = new SubThread1();
sub.setName("子线程1");
sub1.setName("子线程2");
sub.start();
sub1.start();
}
}
对比两种方式:
哪种方法好:实现的方式由于继承的方式。
1.实现的方式避免了java单继承的问题。
2.如果多个线程要操作同一份资源(共享资源),实现的方式更适合。
**共享资源:**允许多个不同的线程访问同一个资源。
线程的分类:
Java中的线程分为两类:一种是守护线程,一种是用户线程。
1.用户线程:java创建的线程默认都是用户线程。
2.守护线程:后台运行的线程,用来提供后台的服务(gc)。
3.守护线程是用来服务用户线程的。
4.通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
区分:
如果进程中还有用户线程,进程是不会终止的;如果进程中只有守护线程,进程会终止。
三、线程的生命周期
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止
四、同步机制
我们再来看一下模拟火车站窗口售票的例子。
在run()中加入sleep()。
package com.hpe.ex;
class Window1 implements Runnable {
int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印抢到的票号
System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
} else {
break;//票已经售完
}
}
}
}
public class TestWindow1 {
public static void main(String[] args) {
//因为我只创建了一个线程对象
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();;
t2.start();
t3.start();
}
}
尝试运行几次会发现出现这种情况👇。
...
窗口3售票,票号是:5
窗口2售票,票号是:5
...
窗口2售票,票号是:1
窗口1售票,票号是:0
窗口3售票,票号是:-1
我们发现有的打印了相同的数字,有的打印出了-1,这明显与实际情况不相符。
为什么会出现这种问题呢?
假设当票只剩下1张的时候,这时线程1抢到了CPU资源,ticket > 0为true,然后线程1休眠,随后 线程2抢到CPU资源,ticket > 0为true,然后线程2休眠,随后线程3抢到CPU资源,ticket > 0为true,然后线程3休眠, 这时线程1休眠时间到,抢到CPU资源,打印1,ticket做自减变成0,然后线程2抢到CPU资源,打印0,做自减变成-1,然后线程3抢到了CPU资源,打印-1。
总结一下原因:
由于一个线程在操作共享数据的过程中,未执行完毕,其他的线程也参与进来,导致共享的数据出现线程安全问题。
理想状态是下面这样的。
那怎么解决这个问题呢?(如何答到这种理想状态呢?)
解决这个问题的原理就是当一个线程访问某一代码块(或某一方法)的时候,给这个代码块(或者这个方法)加一个锁🔒,从而其它线程不能参与进来。
1.同步代码块
synchronized (同步监视器){
//需要被同步的代码(操作共享数据的代码块)
}
2.同步方法
public synchronized void show (String name){
...
}
解释一下同步监视器。
同步监视器:由一个对象(任何对象)来充当的,哪一个线程获得了此监视器,这个线程就执行同步代码,可以理解为锁🔒。
要求:多个线程使用一把锁。
先用同步代码块来接解决这个问题。
修改后的例子。
package com.hpe.ex;
class Window2 implements Runnable {
int ticket = 100;
Object obj = new Object();
//Student stu = new Student();
@Override
public void run() {
while (true) {
//obj作为同步监视器,任何对象都可以,比如stu
//this也可以,原因:只创建了一个Window2对象,this始终表示这个对象,一般用this
synchronized (this) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印抢到的票号
System.out.println(Thread.currentThread().getName()
+ "售票,票号是:" + (ticket--));
}else{
break;
}
}
}
}
}
public class TestWindow2 {
public static void main(String[] args) {
//因为我只创建了一个线程对象
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();//实际运行的是w的run()
t2.start();
t3.start();
}
}
关于this能否作为锁的问题,这里还要解释一下。
对于接口实现的线程(上面修改的例子),可以用this作为锁,因为只创建了一个实现类对象,this始终指向这个对象,锁每次只能让一个线程使用;而对于继承实现的线程(下面修改的例子),不能用this作为锁,因为创建了许多子类对象,this指向w1,w2,w3...即this的指向不唯一,每个对象都能使用自己的锁,从而代码块不能被真正的锁住,如果要锁住,只能用静态对象作为锁。
package com.hpe.ex;
/**
* 模拟火车站窗口售票,开启三个窗口同时售票,共100张票
*/
class Window extends Thread {
//定义一个变量,标识票数
static int ticket = 100;
static Object obj = new Object();
//抢票
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印抢到的票号
System.out.println(Thread.currentThread().getName(
+ "售票,票号是:" + (ticket--));
} else {
break;//票已经售完
}
}
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("window1");
w2.setName("window2");
w3.setName("window3");
w1.start();
w2.start();
w3.start();
}
}
下面说一下同步方法。
实现原理:将操作的共享代码的方法声明为synchronized,该方法就是一个同步方法,保证其中一个线程执行该方法时,其他线程等待,直到此线程执行完方法。
同步方法也有锁🔒,谁调用同步方法,谁就是锁🔒,其实也是this。
修改后的例子。
package com.hpe.ex;
class Window3 implements Runnable {
int ticket = 100;
@Override
public void run() {
while (true) {
//this.print();
print();
if(ticket == 0){
break;
}
}
}
//同步方法
private synchronized void print() {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印抢到的票号
System.out.println(Thread.currentThread().getName() + "售票,票号是:" + (ticket--));
}
}
}
public class TestWindow3 {
public static void main(String[] args) {
//因为我只创建了一个线程对象
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
下面说一下死锁。
什么是死锁?
死锁是线程执行过程中由于争夺资源或者彼此通信发生的线程阻塞,若无外力作用他们永远无法推进下去。
死锁发生的原因。
(1)互斥使用:当一个线程占用一个资源时,其他线程不能使用。
(2)不可抢占:资源请求者不能强行从资源占用者处抢夺资源,资源只能由资源占用者主动释放。
(3)请求和保持:当一个线程在请求一个线程时同时也保留着对原资源的占有。
(4)循环等待。
代码举例。
package com.hpe.java;
public class TestDeadLock {
static StringBuffer sb = new StringBuffer();
static StringBuffer sb1 = new StringBuffer();
public static void main(String[] args) {
new Thread() {
public void run() {
synchronized (sb) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb.append("a");
synchronized (sb1) {
sb.append("b");
System.out.println(sb);
System.out.println(sb1);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (sb1) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb1.append("c");
synchronized (sb) {
sb1.append("d");
System.out.println(sb);
System.out.println(sb1);
}
}
}
}.start();
}
}
五、线程通信
线程通信的三个方法:必须使用在同步代码块或者同步方法中
wait:Object类的方法。作用是挂起当前线程,释放获取到的锁,直到别的线程调用了这个对象的notify或notifyAll方法。
notify:Object类的方法。作用是唤醒因调用wait挂起的线程,如果有多个线程,**随机唤醒一个**。
notifyAll:Object类的方法。作用是唤醒全部因调用wait挂起的线程。
对象有两个池:
锁池:请求锁的线程放在这里。
等待池:被wait挂起的线程丢在这里,当线程被notify或者notifyAll唤醒后,进入锁池,继续抢锁。
看一个栗子:使用两个线程打印 1-100. 线程1和线程2交替打印。
package com.hpe.ex;
//使用两个线程打印 1-100. 线程1和线程2交替打印
class Print1 implements Runnable {
int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (num <= 100) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class TestPrint {
public static void main(String[] args) {
Print1 p = new Print1();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.setName("thread1");
t2.setName("thread2");
t1.start();
t2.start();
}
}
六、生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20)。
1.如果生产者试图生产更多的产品,店员会叫生产者停一下;
2.如果店中有空位放产品了再通知生产者继续生产;
3.如果店中没有产品了,店员会告诉消费者等一下;
4.如果店中有产品了再通知消费者来取走产品。
我们来分析一下。
1.是否涉及到多线程?是,消费者和生产者。
2.是否会涉及到共享数据?是,产品的数量。
3.涉及到数据共享就要考虑线程安全的问题。
4.是否涉及到线程通信?是,店员、生产者和消费者的通信。
package com.hpe.ex;
class Clerk {
int pnum;
//生产产品的方法
public synchronized void addProduct() {
if (pnum >= 20) {
//数量大于20,不能生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
pnum++;
System.out.println(Thread.currentThread().getName() + "生产了第 " + pnum + " 个产品");
notifyAll();
}
}
//消费的方法
public synchronized void consume() {
//数量小于等于0,不能消费
if (pnum <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "消费了第 " + pnum + " 个产品");
pnum--;
notifyAll();
}
}
}
class Productor implements Runnable {
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("生产者开始生产产品了");
while (true) {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Customer implements Runnable {
Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("消费者开始消费了");
while (true) {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consume();
}
}
}
public class TestProductorCustomer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor p = new Productor(clerk);
Customer c = new Customer(clerk);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.setName("生产者");
t2.setName("消费者");
t1.start();
t2.start();
}
}