JavaEE初阶进程与线程

进程的概念

每个应⽤程序运⾏于现代操作系统之上时,操作系统会提供⼀种抽象,好像系统上只有这个程序在运 行,所有的硬件资源都被这个程序在使⽤。这种假象是通过抽象了⼀个进程的概念来完成的

进程是操作系统对一个正在运行的程序的一种抽象,可以把进程看作程序的一次运行过程,同时在操作系统内部,进程又是系统进行资源分配的基本单位

进程控制块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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值