进程和线程
并发和并行
- 并发:同一时刻,多个任务交替执行,造成一种“貌似同时”的错觉,单核CPU执行多任务的过程叫做并发
- 并行:同一时刻,多个任务同时执行,多核CPU同时执行,实现并行
CPU时间片轮转
CPU把时间分成片,我们叫做时间片,一个片为20毫秒,也就是说一个程序的运算值给20毫秒的时间,时间一到,则马上运算另一个程序的数据。这样做虽然没有使得CPU性能得到充分发挥,但是做到了用户友好性。
进程
- 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。
- 程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列
- 进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
线程
- 线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。
- 一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。
**线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。**同样多线程也可以实现并发操作,每个请求分配一个线程来处理。 - 线程的五个状态:新建、就绪、运行、阻塞、死亡
- 其中,程序从运行态到阻塞态有三种可能,1、程序调用了sleep、wait方法暂停了线程 2、线程需要的I/O资源被占用 3、
线程的五种状态
1、** 新建状态:**在程序中新建一个线程( new Thread() )时,该线程进入新建态,在内存中拥有独立的工作内存,但是还没有开始运行。
2、 **就绪状态:**当线程的start()方法被调用,那么该线程进入就绪态,也就是说该线程已经具备了运行的条件,由于还没有分配到CPU资源,因此线程还需要在线程队列中排队。
3、** 运行状态:**当就绪状态的线程被调用并获得CPU资源时,线程进入运行状态,自动调用该线程的run()方法
4、** 阻塞状态:**在运行状态的线程如果碰到一些特殊原因
1、人为将该线程挂起:比如调用线程的sleep()、suspend()、wait()方法
2、 线程中需要的I/O资源被其它线程调用等
那么就需要暂时中止自己的执行,进入阻塞状态,只有当造成阻塞的原因解除之后才能重新回到就绪态,等待CPU资源调度
5、 死亡状态:当我们的线程调用stop()、destory()或者是run()方法执行结束之后,线程结束,进入死亡状态
创建线程的三种方法
使用线程:
- 定义线程
- 创建线程对象
- 启动线程
- 终止线程:线程进入死亡状态,所有资源被释放
1、继承Thread类,重写run()方法:
/**
* @Author: 李文波
* @Date: 2023/1/6 9:36
* Description: 使用继承Thread并重写run()方法的办法来使用线程
* 需要线程去完成的功能代码写在run方法中
* 当run方法中的代码执行完,线程就死亡,资源会被释放
* @return: void
*/
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程--" + i);
}
}
}
2、实现你Runnable接口,重写run()方法:
注意:
//使用实现接口的方式来创建线程
//实现接口的方式是没有start方法启动的
//用Thread变成宿主,把这个线程对象寄生到Thread类中
Mythread_ofImplementInterface mtii = new Mythread_ofImplementInterface();
Thread thread1 = new Thread(mtii);
thread1.setName("子线程二 ");
thread1.start();
3、匿名内部类
使用lambda表达式直接在new后边重写run方法即可,适用于已经确定只会使用一次,不必专门为其创建新类的线程;
Thread t1 = new Thread(() -> {
System.out.println("我就是那个匿名内部类线程");
});
线程阻塞原因
1、线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行。
2、线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁,才能恢复运行。
3、线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒。
4、线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态。例如,当线程执行System.in.read()方法时,如果用户没有向控制台输入数据,则该线程会一直等读到了用户的输入数据才从read()方法返回。进行远程通信时,在客户程序中,线程在以下情况可能进入阻塞状态。
5、请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回。
6、线程从Socket的输入流读取数据时,如果没有足够的数据,就会进入阻塞状态,直到读到了足够的数据,或者到达输入流的末尾,或者出现了异常,才从输入流的read()方法返回或异常中断。输入流中有多少数据才算足够呢?这要看线程执行的read()方法的类型。
为何启动进程的时候用 start调用 而不是直接调用run()方法:
因为start()方法会调用start0()方法,该方法调用之后欧,线程并不会马上执行,而是会变成可运行状态,具体什么时候执行,还需要看CPU调度
线程的常用调度方法:
方法名称 | 参数列表 | 返回值 | 方法效果 |
---|---|---|---|
setPriority | int | void | 更改线程的优先级,优先级是1-10,1最低,10最高 |
static sleep | long millis | void | 设置线程休眠millis毫秒 |
join | void | 等待该线程终止,比如t1.join();那么t1就抢占CPU资源,其他线程要等t1死亡之后才能运行 | |
join | long millis | void | 等待该线程终止,不过最多只能等待millis毫秒 |
static yield | void | 在当前线程不重要时,将CPU资源让位给其他优先级相同或者更高的线程,当前线程进入就绪状态,不转为阻塞状态,也就是说,可能暂停之后马上又被CPU执行了 | |
interrupt | void | 调用此方法后,会给此线程添加一个中断标记以告知线程需要立即停止或者进行其他操作,比如t.interrupt()中断t线程,这样做的好处是,相比于stop强制停止线程可能导致资源释放不充分,中断标记可以让线程自行处理后续操作 | |
Thread.currentThread.isInterrupted() | boolean | 可以判断当前线程是否被打了中断标记 | |
interrupted() | 调用此方法可以让该线程从中断状态复原 | ||
wait | void | 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。而当前线程排队,等候其他线程调用notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行 | |
notify | void | 一旦执行此方法,那么会唤醒被wait的线程,如果有多个线程被wait,那么唤醒优先级最高的那个 | |
notifyAll | void | 一旦执行此方法,那么会唤醒所有被wait的线程 | |
suspend | 和resume方法一样,容易导致死锁 | 暂停当前线程,等待resume方法调用恢复 不推荐使用 | |
resume | 恢复当前暂停的线程 不推荐使用 |
interrupt中断标记的使用技巧
首先我们创建一个Thread t,然后在主线程中
睡眠三秒,然后调用t.interrupt(),这样就可以中断t
如果在t的run方法中添加一个判断 if( Thread.currentThread.isInterrupted() )判断当前线程已经被中断
那么可以调用t.interrupted()将该线程从中断状态复原
这样也就实现了主线程通国中断线程t的方式来通知线程t要做什么事情
join()方法
如其名字一样,假设在线程一中使用了 线程二.join() 那么就相当于,在线程一的这个位置添加(插入)了线程二,这个时候我们需要等线程二执行完了之后才能接着跑线程一。
线程的暂停和恢复
Thread t = new Thread(){
@Override
public void run() {
System.out.println("线程开始运行");
Thread.currentThread().suspend();//暂停当前线程,等待resume方法唤醒
System.out.println("线程继续运行");
}
};
t.start();
try{
Thread.sleep(2000);
t.resume();//恢复线程
} catch (Exception e){
e.printStackTrace();
}
yield让位操作
Thread t1 = new Thread(() -> {
System.out.println("开始");
for (int i = 0; i < 50; i++) {
if(i % 5 == 0){
System.out.println("让位");
Thread.yield();//进行让位操作
}
System.out.println("一打印:" + i);
}
System.out.println("结束");
});
Thread t2 = new Thread(() -> {
System.out.println("开始");
for (int i = 0; i < 50; i++) {
System.out.println("二打印:" + i);
}
System.out.println("结束");
});
t1.start();
t2.start();
sleep() 与 wait() 的异同
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
(1) 两个方法声明的位置不同:Thread类中声明sleep() ,
Object类中声明wait()
(2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
(4)当调用某一对象的 wait() 方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的 notifyAll() 方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
线程锁和线程同步
synchronized关键字
在我们多线程中,Java程序的每一个线程在主内存中都有一块独立的空间,也叫工作内存,
其中存储了该线程读写共享变量的副本,也就是说,多个线程同时修改一个变量i,实际上修改的是自己工作内存中的副本,然后再将副本写入主内存修改i
这也就带来了问题,如果多个线程高频访问i的话,在线程一将对i操作后的结果写入主内存之前,线程二可能就对i的值读取完毕了,这就造成了同步问题,因此我们需要对i进行加锁操作,也就是变量i在同一时间内只允许一个线程对其进行操作:
这里就需要提到**synchronized关键字
**
synchronized关键字意为同步,当一个方法或者代码块被synchronized修饰时,意为着这个方法或者这一段代码块是线程安全的
也就是说,如果当前有一个线程访问到了加锁内容,那么其他线程需要等该线程结束才能对加锁内容进行操作,保证在任意时刻只能有一个线程对加锁内容进行操作。
但是我们一般不推荐锁方法,因为方法并不是所有代码都需要上锁,如果只锁需要上锁的代码块可以提高CPU利用率
- 如果锁的是方法,那么只需要在权限修饰符后添加synchronized关键字即可
- 如果锁的是代码块,那么需要使用对象锁
synchronized (obj){
//代码块内容
}
如果要锁方法的话
首先要确定方法的类型:
- 如果是给静态方法上锁,那么就是当前的类作为锁,也就是Main.class
- 如果是给成员方法上锁,那么就是当前的对象作为锁
死锁(DeadLock)
概念和实例
两个线程同时持有对方的锁,但又迟迟不释放,那么就会造成无限期的阻塞
我们之所以不推荐使用suspend和resume方法对线程进行暂停和恢复,是因为
- suspend在暂停线程的同时不会释放任何锁资源,其他线程都无法访问被他占用的锁
- 只有当其他线程调用其resume方法之后,被挂起的线程才能继续,从而使得其他被阻塞在这个锁的线程才能继续执行
- 如果说,resume在suspend之前就执行了,那么suspend的线程一直处于挂起状态,容易造成死锁
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (o1){
System.out.println("线程一获得o1锁");
try{
Thread.sleep(1000);
synchronized (o2){
System.out.println("线程一获得o2锁");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o2){
System.out.println("线程二获得o2锁");
try{
Thread.sleep(1000);
synchronized (o1){
System.out.println("线程二获得o1锁");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
t1.start();
t2.start();
IDEA检查代码死锁的方法
在控制台使用jstack + 代码的线程id号
其中线程id号可以在任务管理器或者控制台输入jps查看
那么控制台会输出一大串Java程序的子线程信息。。。
其中,在最后显示死锁检测的结果如下:
IDEA中监控整个程序资源使用和检测死锁的图形化界面
控制台输入jconsole
,进入上方线程栏菜单,最下面检测死锁
Wait和Notify方法
首先,wait和notify、notifyAll这些方法都是配合synchronized来使用的,依赖于锁,因此,
只有在同步代码块中才能使用这些方法,而且要通过锁对象来调用此方法
wait方法
效果:会使得当前线程进入等待状态,并且释放当前代码块所持有的锁,线程进入就绪态
当其他线程(通过锁对象)调用该线程的notify方法之后,会唤醒刚才变成等待状态的线程
注意!!!使用notify方法并不代表着立刻执行被wait的线程,而是要等当前锁被释放之后才去执行被wait的线程
wait( long timeout )
表示最多等多少毫秒之后,如果没人唤醒它,那么自动醒来
notify方法
看运气随机唤醒一个相同锁下wait中的线程
notifyAll方法
和notify一样,不过是唤醒所有相同锁下wait中的线程