进程的概念
进程是操作系统对一个正在运行的程序的一种抽象,可以把进程看作程序的一次运行过程,同时在操作系统内部,进程又是系统进行资源分配的基本单位
进程控制块PCB
进程是由PCB描述的,它可以将进程的各种属性都表述出来,每一个PCB对象都代表着一个实实在在运行的程序,也就是进程,操作系统再通过线性表/搜索树将PCB对象组织起来,方便管理
PCB的核心属性
1.进程标识符pid
2.内存指针,指明该进程依赖的指令和数据
3.文件描述符,表明该进程打开了哪些文件
4.状态,优先级,上下文,记账信息
线程的引入
线程的引入相当于把进程的任务拆分。一个进程可以有多个线程,一个线程就是一个执行流,每个线程之间都可以按照顺序执行自己的代码,多个线程之间可以“同时”执行多份代码,线程的引入就是为了使一个程序更高效地执行。
实际上一个PCB是描述一个线程的,若干个PCB联合在一起,是描述一个进程的
进程与线程的区别
1.进程是包含线程的,每个进程至少有一个线程的存在,即主线程
2.进程和进程间不共享内存空间,同一个进程的线程之间共享一个内存空间
3.进程是系统分配资源的最小单位,线程是系统调度的最小单位
4.一个进程的奔溃不会影响其他进程,但是一个线程挂了,可能把同进程内其他线程搞奔溃
一个 多线程代码的四种写法
1.继承Thread类,重写run方法
package thread;
class MyThread extends Thread{
@Override//注解,描述当前的方法重写了父类的方法
public void run(){
//这里写的代码,就是该线程要完成的工作
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class deom1 {
public static void main(String[] args){
//thread线程
Thread thread = new MyThread();
thread.start();
//主线程
while (true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
run方法里面的是支线程要执行的具体代码,而要通过Thread对象调用start方法才能真正开始调用,像run方法这种将调用权交给别的函数叫做“回调函数”,它使得执行与所要执行的操作分离开来,降低了代码的耦合性
2.实现Runnable接口,重写run
package thread;
class MyRunnable implements Runnable{//实现Runnable接口,run方法
@Override
public void run() {
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class demo2 {
public static void main(String[] args){
Thread thread = new Thread(new MyRunnable());
//MyRunnable对象作为参数传入,记录了支线程要干什么,Thread负责执行
thread.start();
while (true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
3.使用匿名内部类,继承Thread类,重写run
package thread;
public class demo3 {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(){
@Override
public void run(){
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
};
thread.start();
while (true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
注意:
1.创建了一个Thread的子类
2.同时创建了该子类的实例
3.此处的子类重写了父类的run方法
4.使用匿名内部类,实现Runnable接口,重写run方法
Runnable作为参数传入
package thread;
public class demo4 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
});
t1.start();
while (true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
5.lambda表达式
package thread;
public class demo5 {
public static void main(String[] args) {
Thread t = new Thread(() ->{//此处为lambda
while (true){
System.out.println("hello world!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
throw new RuntimeException(e);
}
}
});
t.start();
while (true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
6.Thread中的一些常用的属性和方法
线程的六种状态
1. NEW
Thread 对象有了,还没调用start 系统内部的线程还未创建
2.TERMINATED
线程已经终止了,内核中的线程已经销毁了
3.RUNNABLE
就绪状态,指的是这个线程随叫随到
a) 这个线程正在cpu上执行
b)这个线程虽然没有在cpu上执行,但是随时可以调度到cpu上执行
4.WAITING
死等 进入阻塞join的无参函数
5.TIMED_WAITING
带有时间的阻塞join带一个参数的函数
6.BLOCKED
阻塞状态
解释线程安全问题
要讨论线程安全问题,首先我们来看如下例子
public class demo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() ->{
for (int i=0;i<50000;i++){
count++;
}
});
Thread t2 = new Thread(() ->{
for (int i=0;i<50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count: "+count);
}
}
上述代码由于join与start的位置的不同而造成运行结果的差异,实际上就是线程安全问题
count++操作,不是原子指令,站在cpu指令的角度上看,其实是3个指令
load :把内存中的数据加载到寄存器中
add : 把寄存器中的指令加一
save :把寄存器中的值写回内存
当线程t1执行count++时,随时可能被t2抢占,若此时t1还没有执行save操作,t2便执行了load操作,就会使得t2得到的操作数和t1得到的操作数是一样的,++操作实际执行就是一次,而一个线程什么时候会被另一个线程所打断是未可知的,所以第一种的执行结果是随机的
出现线程安全问题的原因有如下几个:
(1)是抢占式执行,随机调度
(2)多个线程修改同一个变量
(3)修改操作不是原子的
(4)内存可见性
(5)指令重排序
加锁操作
要解决这种问题,就要进行加锁,加锁首先需要一个锁对象,(锁对象存在的意义就是起到一个“身份标识”的作用)用Object 类定义一个对象locker,当成synchronized 的参数传入 ,synchronized(locker)。synchronized{ } 进入代码块自动加锁,出代码块自动解锁,两个线程针对同一个对象进行加锁操作就可能产生阻塞/锁竞争/锁冲突。修饰普通方法相当于针对this加锁,修饰静态方法相当于针对类对象加锁
加锁操作如下
public class demo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException{
Object locker = new Object();
Thread t1 = new Thread(() ->{
for (int i=0;i<50000;i++){
//加锁操作
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(() ->{
for (int i=0;i<50000;i++){
//加锁操作
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count: "+count);
}
}
死锁问题
public class demo22 {
public static void main(String[] args) throws InterruptedException{
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (locker1){
try{
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1获取了两把锁");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker1){
System.out.println("t2获取了两把锁");
}
}
});
t1.start();
t2.start();
t2.join();
t1.join();
}
}
如上代码中t1 t2 会因为竞争同一把锁而发生死锁,因此要避免锁的嵌套使用
死锁的四个必要条件
1.锁的互斥性。同一时刻,一把锁只能供一个节点使用
2.锁的不可抢占性。当某个节点获得锁后,其他节点不能再获取该锁,只能等释放所之后才能使用
3.请求和保持。当某个进程在请求新资源时,不放弃原有的资源
4.循环等待。进程所需资源都被别的进程占领,同时也占领了其他进程所需资源
引起线程不安全的几种可能
内存可见性
如下代码
package thread;
import java.util.Scanner;
public class demo23 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
while (count == 0){
;
// System.out.println("t1");
}
System.out.println("t1结束");
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数字");
count = scanner.nextInt();
});
t1.start();
t1.start();
}
}
我们期望它的运行过程是t1 t2 线程交替执行时,输入一个数使得t1线程结束,但是实际上t1并没有退出,上述问题的产生原因,就是内存可见性
上述t1线程看似什么也没做,但是站在指令的角度来看,有以下几步
1.load 从内存读取数据到cpu
2.cmp 比较,同时产生跳转,若条件成立继续顺序执行,条件不成立,就跳转到另外一个地址来执行
while循环是个空循环,循环旋转速度很快,由于load操作要比cmp操作快几个数量级,并且在t2线程未执行前,读取到的内存中的值是没有变化的,于是JVM为了提高程序的运行速度,就把上述load操做优化掉了,实际只执行第一次load操作,后续的load操作都是直接读取第一次load后的寄存器中的值,但是这就引入了bug,导致t2执行时也结束不了t1线程
但是,如果循环体不空,那么循环体内就可能存在IO / 阻塞(sleep)操作,这就会使得循环旋转速度大幅降低,IO操作的速度比load操作更慢,JVM也就没有必要优化load
上述情况本质上也就是编译器优化引起的。当t1执行的时候,要从工作内存中读取count的值,而不是从主内存中,后续t2修改count,也是会先修改工作内存,同步拷贝到主内存。但是由于t1没有重新读取主内存,导致t1没有感知到t2的修改,这种就叫“内存可见性”问题(主内存就是我们平时所说的内存,工作内存不是内存,而是CPU寄存器+缓存 )
volatile关键字 ,给上述代码加上volatile修饰后,相当于告诉编译器不要触发优化(具体在Java中就是让javac生成字节码产生“内存屏障”相关的指令,此关键字就是为了解决内存可见性而生的)
指令重排序
编译器按照实际情况将生成的二进制指令执行顺序进行调整,以逻辑不变为前提提高效率,指令重排序也属于编译器优化的范畴。
线程的等待通知机制
某个线程的执行条件由于条件不满足,主动进行等待,防止其他线程饿死,当条件满足时再由锁对象调用notify方法由其他线程将其唤醒
线程饿死:由于某个线程频繁获取释放锁,以至于其他线程无法获取到CPU资源
package thread;
import java.util.Scanner;
public class demo25 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker){
System.out.println("t1等待之前");
try {
locker.wait();
}catch (InterruptedException e){
throw new RuntimeException(e);
}
System.out.println("t1等待之后");
}
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
synchronized (locker){
System.out.println("t2等待之前");
scanner.next();//借助scanner控制阻塞
locker.notify();
System.out.println("t2等待之后");
}
});
t1.start();
t2.start();
}
}
由于t1 t2 执行的顺序是不确定的,有可能是t2先执行了notify,t1还没执行到wait,此时不会抛出异常,但是后续t1 进入wait后,就没有其他线程能将其唤醒了,并且调用一次notify只能唤醒一个wait线程,唤醒的线程是随机的,若想要指定唤醒,就要有多个锁对象,针对不同的线程搭配使用不同的锁对象进行wait,唤醒的时候拿着对应的锁对象进行notify
notifyAll 启动所有正在等待的线程
package thread;
import java.util.Scanner;
public class demo25 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker){
System.out.println("t1等待之前");
try {
locker.wait();
}catch (InterruptedException e){
throw new RuntimeException(e);
}
System.out.println("t1等待之后");
}
});
Thread t2 = new Thread(() ->{
synchronized (locker){
System.out.println("t2等待之前");
try {
locker.wait();
}catch (InterruptedException e){
throw new RuntimeException(e);
}
System.out.println("t2等待之后");
}
});
Thread t3 = new Thread(() ->{
synchronized (locker){
System.out.println("t3通知之前");
locker.notifyAll();
System.out.println("t3通知之后");
}
});
t1.start();
t2.start();
Thread.sleep(500);
t3.start();
}
}
多线程的几个案例
1.单例模式
整个进程中的某个对象,有且只有一个对象,这样的对象就称为单例
1.1饿汉模式
程序运行时就立即创建实例
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
1.2懒汉模式
在第一次使用的时候才去创建实例,如果不使用了,就把实例销毁
有三点要注意:
1.使用加锁操作将if和new包裹起来,使得创建实例不会因多线程的而创建多个实例
2.双重if判断。第一个if是判断是否需要加锁,因为加锁也是一种开销,若instance为空,即表示没有创建了实例,后续操作需要加锁,第二个if是判断是否需要创建对象
3.给变量加上关键字volatile进行修饰,是因为有可能涉及到内存可见性问题(t1线程修改了Instance引用,t2可能读不到,另一方面,加了volatile也能解决指令重排序引起的线程安全问题)
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static Object locker = new Object();
public static SingletonLazy getInstance() {
if (instance == null){//由于首次实例化之后就都是读操作,读操作线程本身安全,所以第一次if判定是否要加锁
synchronized (locker){
if (instance == null) {//判断是否要创建对象
instance = new SingletonLazy();
}
}
}
return instance;
}
}
2.阻塞队列
阻塞队列先进先出,线程安全,并且带有阻塞功能,若队列为空尝试出队,出队操作就会阻塞到队列不空为止,若队列为满尝试入队,入队操作就会遭到阻塞,直到队列不满为止
BlockingQueue是标准库提供的阻塞队列,<E>中E代表要放的元素类型,put和take是带有阻塞功能的方法
package thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class demo30 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue(1000);
//消费者进程
Thread t1 = new Thread(() ->{
try {
while (true) {
Integer value = queue.take();
System.out.println("消费"+ value);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
//生产者进程
Thread t2 = new Thread(() ->{
try {
int count = 1;
while (true){
queue.put(count);
System.out.println("生产"+count);
count++;
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
t2.start();
}
}
3.线程池
ThreadPoolExecute,
线程池里面的线程分为两类线程,核心线程和非核心线程,核心线程数就像一个公司的正式员工,可以长久留在线程池,而非核心线程就像实习生,当任务很少时就被销毁
3.1线程池的几个参数的含义
1.核心线程数/最大线程数
创建一个线程池时,池中的线程数
2.存活时间/时间单位
非核心线程允许空闲的最大时间
3.任务队列 BlockingQueue
保存要执行的任务,后续线程池内部的工作线程,就会消费这个队列
4.线程工厂 ThreadFactory threadFactory
由标准库提供,帮助创建线程的工厂类。
工厂模式主要用于解决 某些对象需要不同的构造方式,构造方法的不同需要方法重载来实现,而方法重载要求 方法名与类名相同,并且参数的 顺序 类型 个数至少有一种出现不一致,但是某些时候的客观需求恰恰需要参数一致,着就导致了无法识别为 方法重载,此时我们就需要用到工厂模式,它会提供一些静态方法把已有的构造方法再去包装一层,使用包装后的方法来构造对象
5.拒绝策略
1.直接拒绝,抛出异常
2.由调用者负责执行
3.丢弃任务队列中队首最早的任务
4.丢弃掉任务队列的新出现的任务
3.2创建一个线程池
public static void main(String[] args) {
//创建一个普通的线程池
//能根据任务数目自动进行线程扩容
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
int j= i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println(j+"hello word!");
}
});
}
// //固定线程数的线程池
// Executors.newFixedThreadPool(4);
// //创建只有一个线程的线程池
// Executors.newSingleThreadExecutor();
// //创建一个固定线程个数,但是任务延迟执行的线程池
// Executors.newScheduledThreadPool(4);
}
3.3自己实现一个线程池
一个线程池至少要考虑以下几点
1.若干个线程
3.任务队列
3.submit方法
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPool{
private int maxPoolSize = 0;
private List<Thread> threadList = new ArrayList<>();
//任务队列,供线程消耗
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
//初始化线程池
public MyThreadPool(int corePoolSize,int maxPoolSize){//固定线程数量的线程池
this.maxPoolSize = maxPoolSize;
//创建线程
for (int i = 0; i < corePoolSize; i++) {
Thread thread = new Thread(() ->{
try {
while (true){
Runnable runnable = queue.take();
runnable.run();
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
thread.start();
threadList.add(thread);
}
}
//把任务添加到线程池中
void submit(Runnable runnable) throws InterruptedException{
//
queue.put(runnable);
if (queue.size() >= 500 && threadList.size() < maxPoolSize){
//创建新的线程
Thread thread = new Thread(() ->{
try {
while (true){
Runnable task = queue.take();
task.run();
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
}
}
}
public class demo35 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(100,20);
for (int i = 0; i < 1000; i++) {
int id = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + id);
}
});
}
}
}
4.定时器
定时器就相当于一个闹钟
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Timer;
class MyTimerTask implements Comparable<MyTimerTask>{
private Runnable runnable;
private long time; //time 是一个毫秒级别的时间戳 绝对时间
public MyTimerTask(Runnable runnable,long delay){
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
//按照时间比较
return (int)( this.time - o.time);
}
}
class Mytimer{
private Object locker = new Object();
private PriorityQueue<MyTimerTask> tasksQueue = new PriorityQueue<>();
public Mytimer(){
//这个线程用于负责不停的扫描上述队列的首元素,来确定是否需要执行任务
Thread t = new Thread(() -> {
try {
while (true){
synchronized (locker){
if (tasksQueue.size() == 0){
locker.wait();
}
MyTimerTask task = tasksQueue.peek();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()){
//时间到了,执行器任务
task.run();
tasksQueue.poll();
}else {
//时间没到,等待
locker.wait(task.getTime() - curTime);
}
}
}
}catch (RuntimeException e){
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
public void schedul(Runnable runnable,long delay){
synchronized (locker){
MyTimerTask task = new MyTimerTask(runnable,delay);
tasksQueue.offer(task);
locker.notifyAll();
}
}
}
public class demo37 {
public static void main(String[] args) {
Mytimer mytimer = new Mytimer();
mytimer.schedul(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
mytimer.schedul(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
mytimer.schedul(new Runnable() {
@Override
public void run() {
System.out.println(" hello 1000");
}
},1000);
}
}