目录
一、什么是进程
进程就是运行中的程序。比如打开QQ,就是启动了一个进程,操作系统会为该进程分配内存空间。再打开迅雷,就又启动了另一个进程,操作系统为迅雷也分配一个内存空间。
进程是动态过程,有产生、存在和销毁的过程。
二、什么是线程
迅雷中同时下载多个任务,每一个下载任务就是一个线程。
- 线程是由进程创造的。
- 一个进程可以拥有多个线程。
1、单线程
同一个时刻,只允许执行一个线程。
2、多线程
同一个时刻,可以执行多个线程。例如:迅雷同时下载多个文件;QQ同时打开多个聊天窗口。
3、并发
同一个时刻,多个任务交替执行,但是由于切换的速度很快,给人一种"貌似同时进行"的错觉。单核cpu实现多任务就是并发。(因为cpu只有一个,一次只能干一件事情)
4、并行
同一时刻,多个任务同时执行。多核cpu实现的就是并行。
三、创建线程的两种方式
1、继承Thread类,重写run方法。
注意:该线程的业务逻辑(即要做的事情),就写在run方法中。
Thread类的体系图:
下面举一个例子,需求是:1、让程序每隔1s,输出一次"我是猫"。2、输出80次,结束该线程。
package com.hspedu.System_;
public class Thread_01 {
public static void main(String[] args) {
//创建Cat对象,可以当作线程使用
Cat cat = new Cat();
cat.start(); //启动线程
}
}
//1.当一个类继承了Thread类,它就可以当作线程使用
//2.重写run方法,写上自己的业务代码
//3.run方法实际上是Runnable接口中的方法
class Cat extends Thread{
@Override
public void run() { //重写run方法
while (true) {
//该线程每隔1s,输出一次"我是猫"
System.out.println("我是猫");
//让该线程休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
(1)为什么是start方法开启线程?
问题解答:
还记得上面"我是猫"的例子吗?cat.start() 启动了线程,实际上是调用了Cat类里面的run方法,为什么要这样用start多此一举呢?
答案:如果用cat.run() 程序就会把run方法执行完毕再往下走,因为run方法就只是一个普通的方法,并没有真正启动线程。
其实,真正实现多线程效果的是start0方法。
2、实现Runnable接口,重写run方法。
为什么要用这种方式实现多线程?
答案:java是单继承的,如果已经继承了别的类,就无法再继承Thread类创建多线程。
下面举一个例子:需求是每隔1s,输出一次”小狗汪汪叫“
package com.hspedu.System_;
public class Thread_02 {
public static void main(String[] args) {
Dog dog = new Dog();
//创建Thread对象,把dog对象放入Thread
Thread thread = new Thread(dog); //通过实现Runnable接口创建线程,不能直接start
thread.start();
//上面这种用法很神奇!成为 "代理模式"
}
}
class Dog implements Runnable{ //通过实现Runnable接口,创建线程
int count = 0;
@Override
public void run() {
while (true){
//Thread.currentThread().getName() 是获取当前线程名字
System.out.println("小狗汪汪叫" + (++count) + Thread.currentThread().getName());
//休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3、继承Thread类 VS 实现Runnable接口
- 它俩的本质一样,都是通过start0方法开启线程。
- Thread类实现了Runnable接口。
- 实现Runnable接口的方式更适合多个线程共享一个资源的情况,并且避免了单继承的限制。
因此,建议使用Runnable接口。
四、多线程执行
上面启动的都是单线程,下面写一个同时启动两个线程。
需求:一个线程每隔1s输出"hello world",输出10次退出,另一个每隔1s输出"hi",输出5次退出。
package com.hspedu.System_;
import java.util.concurrent.TransferQueue;
public class Thread_03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start(); //启动第一个线程
thread2.start(); //启动第二个线程
}
}
class T1 implements Runnable{
int count = 0;
@Override
public void run() {
while (true) {
//每隔1s输出"hello world",输出10次
System.out.println("hello, world" + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}if (count == 10){
break;
}
}
}
}
class T2 implements Runnable{
int count = 0;
@Override
public void run() {
while (true) {
//每隔1s输出"hi",输出5次
System.out.println("hi" + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}if (count == 5){
break;
}
}
}
}
五、使用多线程,模拟三个窗口同时售票100张
六、线程退出
退出逻辑:让t1退出run方法,从而中止t1线程。
package com.hspedu.ticket;
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
t1.start();
//如果希望main线程去控制t1线程的终止,必须可以修改loop
//通知方式:让t1退出run方法,从而中止t1线程
//让主线程休眠10s,再通知t1线程退出
Thread.sleep(10*1000);
t1.setLoop(false);
}
}
class T extends Thread{
private int count = 0;
//设置一个控制变量
private boolean loop = true;
@Override
public void run() {
while (loop){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T 运行中..." + (++count));
}
}
public void setLoop(boolean loop){
this.loop = loop;
}
}
七、线程的常用方法
1、第一组(线程中断)
注意细节:
- start方法的底层会创建线程,而调用run,run方法就是一个普通的方法,不会创建线程。
- interrupt方法是中断线程,但并没有真正的结束线程。一般用于中断正在休眠的线程。
- sleep方法是线程的static方法,使当前线程休眠。
- Thread.currentThread().getName();获取当前线程名字。
2、第二组(线程插队)
3、第三组(守护线程)
八、线程的生命周期
九、线程同步机制(Synchronized)
1、为什么要有同步机制?
- 在多线程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多只有一个线程访问,以保证数据的完整性。
- 即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,一直到该线程完成操作,其他线程才能对该内存地址进行操作。
2、同步的实现方式
(1)同步代码块
synchronized(对象){ //得到对象的锁,才能操作同步代码
//需要被同步的代码
}
(2)同步方法
synchronized还可以放在方法声明中,表示整个方法为同步方法。
public synchronized void m(String name){
//需要被同步的代码
}
通俗理解:大黄去上厕所,把门关上(上锁),完事开门出来(解锁),然后其他的人才可以上厕所。
十、互斥锁
1、分析同步原理
t1、t2、t3分别代表一个线程,哪个线程先拿到锁(锁是在对象上的,所以叫做对象锁),哪个线程就去执行里面的内容,执行完后回来,锁放回去,下一个线程再重复该步骤。
2、互斥锁
上面提到的那把锁,就叫做互斥锁。
互斥锁介绍:
- Java中引入互斥锁的概念,保证共享数据操作的完整性。
- 每个对象都对应一个可称为"互斥锁"的标记,该标记用于保证在任一时刻,只能有一个线程访问该对象。
- 关键字synchronized用于与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻时,只能由一个线程访问。
注意细节:
- 同步:会降低程序的执行效率。
- 同步方法(非static):默认锁对象是this,也可以是其他对象(要求是同一个对象)。
- 同步方法(静态):默认锁对象是当前类。
实现互斥锁的步骤:
- 先分析要上锁的代码。
- 选择同步代码块or同步方法。
- 要求多个线程的锁对象为同一个即可。
十一、线程死锁
1、什么是线程死锁
多个线程都占用了对方的锁资源,且不肯相让,就导致了死锁,编程过程中一定要避免这种情况。
举个小例子:(这就是死锁)
妈妈:你先完成作业,才让你玩手机。
大黄:你先让我玩手机,我才去写完作业。
十二、释放锁
1、什么时候释放锁?
- 当前线程的同步方法、同步代码块执行结束。eg:上厕所,完事出来。
- 当前线程在同步代码块、同步方法中遇到break、return。eg:厕所没上完,但是有急事,不得已也出去。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。eg:没有正常的完事,但是发现忘带纸了,不得不出来。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。eg:没有正常完事,觉得需要再酝酿一下,所以出来,等会再进去。
2、下面的操作,不会释放锁
线程执行同步代码块or同步方法时,程序调用Thread.sleep()、Thread.yield()方法,暂停了当前线程的执行,此时不会释放锁。eg:上厕所太困了,在坑上眯了一会。
线程执行同步代码块时,其他线程调用了该线程的suspend()方法,将该线程挂起,该线程不会释放锁。提示:应该尽量避免suspend()和resume()方法来控制线程,因为这些方法不再推荐使用。