文章目录
- 1.PCB
- PID
- 文件描述符表
- 内存指针
- 状态
- 上下文
- 优先级
- 记账信息
- tgid
- 2.线程与进程的区别
- 3.sleep和interrupt方法的关系
- 变量终止线程
- interrupt方法终止线程
- 4.线程状态
- 5.出现线程不安全的原因
- 线程在系统中是随即调度,抢占式执行的。
- 多个线程修改同一个变量
- 线程针对变量的修改操作不是“原子”的
- 内存可见性
- 指令重排序
- 6.死锁发生的三种场景
- 锁是不可重入锁,一个线程针对同一个锁对象连续加锁多次。
- 两个线程两把锁。
- N个线程,M把锁。
- 7.死锁的必要条件(背)
- 锁具有互斥性特性(基本特点)
- 锁不可抢占(不可剥夺)(基本特点)
- 请求和保持(代码结构)
- 循环等待(代码结构)
- 8.单例模式中的饿汉模式与懒汉模式的区别
- 饿汉模式
- 懒汉模式
- 9.编译器优化
- 内存可见性
- 指令重排序
- 10.阻塞队列-生产者消费者模型
- 解耦合
- 削峰填谷
- 11.线程池
- 12.定时器
1.PCB
PID
不同线程的PID是不同的。
文件描述符表
记录使用的文件资源。
内存指针
指向线程要使用数据以及指令。
状态
指明系统状态。
上下文
当线程切换出cpu停止执行,此时上下文会记录中间结果,方便切换回cpu后继续执行,这个过程和程序计数器有关。
优先级
给线程分配在cpu上执行的时间存在倾斜。
记账信息
操作系统也要避免一些线程一直吃不到资源,记录时间,给吃的少的多分配一点资源。
tgid
是进程的id,同一个进程下的不同线程是相同的。
2.线程与进程的区别
参考以下博客
3.sleep和interrupt方法的关系
变量终止线程
package Thread;
public class Demo10 {
private static boolean isRunning=true;
public static void main(String[] args) {
Thread t=new Thread(()->{
while (isRunning) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程已经终止");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("准备终止线程");
isRunning=false;
}
}
通过在主线程中修改变量的值,来跳出t线程中的循环。但是有一个缺点就是即使修改了变量循环可能也不会立刻结束,因为修改变量时可能线程t代码刚好执行到sleep,所以t不会立马终止,至少要等这一次循环执行完成后才能够终止。
interrupt方法终止线程
package Thread;
public class Demo11 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted())
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t.interrupt();
}
}
使用interrupt方法可以修改Thread.currentThread().isInterrupted()这个函数值为true,从而终止上述代码的线程t。如果说使用interrupt方法时线程t代码刚好执行到循环条件,那么t直接终止,如果使用interrupt方法时线程又执行到sleep,interrupt方法会直接唤醒线程t,但是同时会将Thread.currentThread().isInterrupted()这个函数值重新变为false,为了避免循环继续进行,此时就可以在sleep被唤醒的哪个trycatch中加入处理逻辑。
4.线程状态
5.出现线程不安全的原因
线程在系统中是随即调度,抢占式执行的。
多个线程修改同一个变量
线程针对变量的修改操作不是“原子”的
内存可见性
指令重排序
6.死锁发生的三种场景
锁是不可重入锁,一个线程针对同一个锁对象连续加锁多次。
两个线程两把锁。
N个线程,M把锁。
7.死锁的必要条件(背)
锁具有互斥性特性(基本特点)
一个线程拿到锁,如果另一个线程想要申请同一个锁就要阻塞等待。
锁不可抢占(不可剥夺)(基本特点)
一个线程拿到锁,除非自己释放,否则别人拿不走。
请求和保持(代码结构)
一个线程拿到一把锁之后,在不释放锁的前提下,去尝试获取其它锁。
循环等待(代码结构)
多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A。当代码中确实需要多个线程获取多把锁,约定好加锁的顺序,这样就能避免死锁。
8.单例模式中的饿汉模式与懒汉模式的区别
饿汉模式
package Thread;
class Singleton {
public static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
public class Demo31 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
懒汉模式
package Thread;
class Singleton1 {
public static Object locker = new Object();
public static Singleton1 instance = null;
public static Singleton1 getInstance() {
if (instance == null) { //避免已经建立了对象重新上锁浪费性能,直接返回对象即可
synchronized (locker) {
if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题
instance = new Singleton1();
}
}
}
return instance;
}
private Singleton1() {
}
}
public class Demo32 {
public static void main(String[] args) {
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
System.out.println(s1 == s2);
}
}
饿汉模式在多线程的情况下使用getInstance方法是安全的,因为类对象已经创建好了,getInstance方法做的只是读。懒汉模式则是不安全的,因为在其getInstance方法中会创建类对象。通过给代码加锁会解决懒汉模式的线程安全问题,但是懒汉模式只有在创建类对象实例的时候会出现线程安全问题,创建以后也就是读。为了避免每次都要给代码加上一个锁给程序增加负担,在sychroinzed前面加上一个if语句进行判断,如果已经创建实例了就不用加锁了。
9.编译器优化
内存可见性
package Thread;
import java.util.Scanner;
public class Demo27 {
private static int count=0;
public static void main(String[] args) {
Thread t=new Thread(()->{
while(count==0) {
//
}
System.out.println("t1 执行结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入数字:");
count=scanner.nextInt();
});
t.start();
t2.start();
}
}
下面这段代码会出现内存的可见性问题,将从内存中读取count值的操作称为load 判断操作称为cmp,load和cmp的执行速度差了好几个数量级,在线程2开始执行代码提示输入数字时,线程1的while循环已经执行了很多遍。java编译器会自动给代码进行优化,导致load只是第一次时真正从内存中读取count值,其余都是从cpu的寄存器中读取,然而线程2修改count是在内存中进行修改,线程1根本访问不到count的值,可以在变量前加上volatile关键字来提醒编译器不要优化。
指令重排序
指令重排序指的是编译器优化的一种,改变指令在cpu上执行的顺序,但是不影响最终的逻辑结果。对于单线程这样不会出现问题,但是多线程不行。
package Thread;
//单例模式-懒汉模式
//在多线程的情况下是不安全的
class Singleton1 {
public static Object locker = new Object();
public static Singleton1 instance = null;
public static Singleton1 getInstance() {
if (instance == null) { //避免已经建立了对象重新上锁浪费性能,直接返回对象即可
synchronized (locker) {
if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题
instance = new Singleton1();
}
}
}
return instance;
}
private Singleton1() {
}
}
public class Demo32 {
public static void main(String[] args) {
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
System.out.println(s1 == s2);
}
}
举例说这里的懒汉模式的代码,在getInstance方法中建立对象分三步
(1)为对象申请空间。
(2)初始化空间(调用构造函数)。
(3)将地址赋给对象的引用。
现在假设一种情况,t1线程在执行上面的懒汉模式的getInstance方法时,因为编译器优化建立对象的指令顺序变为了1,3,2,那么如果t1线程运行到3时刚好t2线程运行到getInstance方法中的第一个判断语句,发现此时instance引用已经被赋值过了就直接返回,但是实际上这里的instance只是得到了地址,地址指向的空间并未初始化,这种情况就是指令重排序所造成的线程安全问题。
解决这种问题的方式也很简单,在你要处理的变量前面加上volatile即可告诉编译器这里不需要优化。
10.阻塞队列-生产者消费者模型
生产者消费者模型的两个优势:
(1)削峰填谷
(2)解耦合
一般在一个进程内的多线程中使用阻塞队列实现生产者消费者模型,在分布式系统中使用消息队列来实现。消息队列就是根据topic分为不同的阻塞队列,根据topic对不同的阻塞队列上进行操作。
解耦合
如果直接让服务器A和服务器B进行交互,那么它们必定会包含很多与彼此相关的代码。修改A会影响到B,修改B也会影响到A。
如上图引入一个消息队列,这样A只关心与队列的交互,B也只关心与队列的交互,因此A和B之间的互相影响就被减小非常多。
削峰填谷
对于服务器A客户端可能会突然发来大量请求,A的处理比较简单,A将请求发送给B,B接收处理的开销相对较大,一旦请求数目过多,B就会挂掉。使用一个条件队列来接收A发送给B的请求,这样无论A发送的请求数目有多少,B都可以按照自己的节奏来处理请求。
11.线程池
我们引入线程就是因为进程创建销毁的代价比较大,但是随着发展,客户端向服务器发送的请求可能呈指数增长,使用线程也觉得创建销毁的开销大了,所以引入线程池以及协程的概念,协程暂不讨论。
为什么引入线程池能够提高效率?因为创建销毁线程的操作主要是用户态以及内核态代码配合完成的工作,但是线程池先提前将线程创建好,然后建立好数据结构保存这些线程,需要线程直接拿,不需要了就放回去,这样的过程全是用户态的就节省了开销,避免与内核态交互。
另外线程池的使用主要分为Executors.newFixedThreadPool这种包装的线程池以及ThreadPoolExecutor这种标准的线程池的类,前者简单参数少,后者参数多更精细。
12.定时器
基本使用就是Timer类,然后构造对象就是需要两个参数,第一个是要执行的任务,第二个就是时间。构建这样的对象之后,定时器中的线程就会自动在你指定的第二个参数时间后去执行你指定的任务。
package Thread;
import java.util.Timer;
import java.util.TimerTask;
public class Demo39 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(3333);
}
}, 3333);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(2222);
}
}, 2222);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(1111);
}
}, 1111);
}
}