java编程之进程线程

线程与进程

线程:操作系统能够运行调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
进程:程序的基本执行实体。一般而言,一个正在运行的软件就是一个进程。
例如:在一个运行中的杀毒软件中,该软件可以看成一个进程,软件内的不同功能可以视作不同的线程。

多线程程序:cpu可以在多个程序之间来回切换,把等待的时间充分利用起来,提高运行的效率。
应用场景:拷贝、迁移大文件,加载大量资源文件等等

并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行。
并行:在同一时刻,有多个指令在多个CPU上同时执行。
我的理解:笔记本内只含有一个CPU,但是不同的CPU有不同的核心数,例如2核4线程、8核16线程等,CPU线程的数量表示最多同时并行的线程数量。当程序的线程多于CPU所支持的最大线程数时,CPU的核心会在所有线程中不断切换,即并发。

多线程的实现方式

①继承Thread类的方式进行实现: 

a.自己定义一个类继承Thread,重写run方法

public class MyThread extends Thread{
    @Override
    public void run(){
        //书写线程要执行代码
        for (int i = 0; i < 100; i++) {
            //因为MyThread是继承Thread的,所以可以直接使用getName方法获取线程名
            System.out.println(getName() + "Hello");
        }
    }
}

b.创建子类的对象,并启动线程 

public class 多线程的实现_thread {
    public static void main(String[] args) {
        /*第一种启动方法
        *   1.自己定义一个类继承Thread
        *   2.重写run方法
        *   3.创建子类的对象,并启动线程
        * */

        MyThread t1 = new MyThread();
        //开启线程,注意要使用start方法,会自动执行类里面的run方法。
        t1.setName("线程1");
        t1.start();
        //当使用多线程的时候,要给个线程起名避免出现认不清的情况
        MyThread t2 = new MyThread();
        t2.setName("线程2");
        t2.start();

    }
}
②实现Runnable类的方式进行实现:

a.自己定义一个类实现Runnable接口,然后重写runn方法,要注意getName的使用方法

public class MyRun implements Runnable{
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            //获取到当前线程的对象
            //当前方法线程的对象是谁,就以谁作为改对象作为返回值
            Thread t = Thread.currentThread();
            //不能直接使用getName方法的原因是:Runnable接口里面没有getName方法
            System.out.println(t.getName()+"Hello2");
        }
    }
}

b.创建MyRun对象,表示多线程要执行的任务,同时创建一个Thread对象,然后start线程

public class 多线程的实现_Runnable {
    public static void main(String[] args) {
        /*第二种启动方法
         *   1.自己定义一个类实现Runnable接口
         *   2.重写run方法
         *   3.创建自己的类的对象,并启动线程
         *   4.创建一个Thread类的对象,并开启线程
         * */

        //创建MyRun对象,表示多线程要执行的任务
        MyRun mr = new MyRun();
        //创建线程对象,需要将创建好的对象作为参数传入Thread
        //否则会使用Thread默认的run方法实例化对象
        Thread t1 = new Thread(mr);
        //给线程设置名字
        t1.setName("线程1");
        //开启线程
        t1.start();

        //设置线程2
        Thread t2 = new Thread(mr);
        t2.setName("线程2");
        t2.start();
    }
}
③利用Callable接口和Future接口方式实现:

a.创建一个类MyCallable实现Callable接口,然后重写call方法

public class MyCallable implements Callable<Integer> {
    //这里的泛型表示结果的类型
    @Override
    public Integer call() throws Exception {
        //求1~100之间的和
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}

b.创建MyCallable的对象,用来表示多线程要执行的任务,再创建一个FutureTask对象来管理多线程运行的结果,最后创建Thread类的对象来启动线程

public class 多线程的实现_callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*第三种启动方法
         *   特点:可以获取到多线程运行的结果
         *
         *  1.创建一个类MyCallable实现Callable接口
         *  2.重写call方法(存在返回值,表示多线程的运行结果)
         *  3.创建MyCallable的对象(表示多线程要执行的任务)
         *  4.创建FutureTask的对象(作用:管理多线程运行的结果)
         *  5.创建Thread类的对象,并启动线程
         * */

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        //创建一个FutureTask的对象
        FutureTask<Integer> ft = new FutureTask<>(mc);
        //创建线程的对象
        Thread t1 =new Thread(ft);

        //启动线程
        t1.start();
        //获取多线程运行的结果
        Integer result = ft.get();
        System.out.println(result);
    }
}
总结

①继承Thread类
优点:编程简单,可以直接使用Thread类中的方法
缺点:扩展性较差,不能再继承其他的类
②实现Runnable接口和Callable接口
优点:扩展性强,实现接口的同时还可以继承其他的类
缺点:编程相对复杂,不能直接使用Thread类中的方法
注:三种方法中只有Callable接口可以获取线程运行结果,其他都不能

创建一个线程的步骤:1.写一个循环         2.同步代码块        3.判断共享数据是否到了末尾        4.到了,结束,未到,执行核心代码

线程的常见成员方法: 

String getName():返回此线程的名称(若没setName,则返回默认名称Thread-num)
Void setName(String name):设置线程的名字(构造方法也可以设置名字)
static Thread currentThread():获取当前线程的对象(在哪个线程调用这个方法,就返回对应线程的对象)
static void sleep(long time):让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority):设置线程的优先级
final int getPriority():获取线程的优先级
final void setDaemon(boolean on):设置为守护线程
public static void yield():出让/礼让线程
public static void jion():插入线程

细节:
①main也是一个线程,称为主线程
②线程的优先级不是绝对的,高优先级的线程不一定每次都能首先执行结束
③当其他非守护线程执行完毕之后,守护线程会陆续结束。如:聊天窗口为线程1,传输文件为线程2,且线程2为守护线程,当聊天窗口关闭时,即线程1结束时,传输文件就没有必要了,因此线程2也随之结束
④当前线程执行到出让线程方法时,会让出当前CPU的执行权,重新与其他线程进行CPU执行权的争抢。(该方法影响的时线程占用CPU执行权的时间)
⑤线程1在线程2的代码中执行了插入线程,即线程1作为插入线程,线程2作为被插入线程,此时线程2会暂停,直到线程1全部执行完毕后,才会继续执行。

线程的生命周期

①线程调用了start方法时,该线程具有执行资格(抢CPU执行权的资格),但是还没有执行权,只有抢到CPU的执行权,该线程才会运行。(调用sleep方法或者被其他阻塞式方法时会暂时剥夺该线程的执行资格,无法抢夺CPU执行权)
②当线程执行完run方法时,线程死亡(结束),变成垃圾。

线程的安全问题

小知识:类里面定义变量时,前面加个static,表示所有类共享这个变量。

同步代码块

格式:

static Object obj = new Object();

public void run(){
    synchronized(boj){
        //代码体
    }
}


特点:
①锁默认打开,有一个线程进去了,锁自动关闭
②里面的代码全部执行完毕,线程出来,锁自动打开
③synchronized写在循环里面,否则容易出现一个线程
④尽可能使用同一把锁

同步方法:把修饰符synchronized关键字加到方法上

    修饰符 synchronized 返回值类型 方法名(方法参数){ …… }

特点:①同步方法是锁住方法里面所有的代码
②锁对象不能自己指定(由java规定,非静态方法的锁对象为this,静态方法的锁对象为当前类的字节码文件对象)

也可采用Lock锁的方法

线程等待唤醒机制(生产者和消费者)

特点:①生产者:生产数据        ②消费者:消费数据
核心思想:通过某一个中介平台控制线程的执行

具体方法:

void wait():当前线程等待,直到被其他线程唤醒
void notify():随机唤醒单个线程
void notifyAll():唤醒所有线程

代码实现(厨师吃货为例):

①创建“厨师”线程

public class Cook extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.Lock) {
                if (Desk.count == 0) {
                    break;
                }
                else {
                    if (Desk.foodFlag == 1) {
                        //如果食物满了,则等待
                        try {
                            Desk.Lock.wait();
                        }
                        catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    else {
                        //食物还没有满,则继续生产
                        System.out.println("厨师在制作面条,还需制作" + Desk.count + "碗,吃货才能吃饱");
                        //生产完之后唤醒吃货来吃
                        Desk.Lock.notify();
                        //修改桌子的状态
                        Desk.foodFlag = 1;
                    }
                }
            }
        }

    }
}

②创建“吃货”线程

public class Foodie extends Thread {
    @Override
    public void run() {
        /*1.循环
         * 2.同步代码块
         * 3.判断共享数据是否到了末尾
         * 4.到了,结束,未到,执行核心代码
         * */
        while (true) {
            synchronized (Desk.Lock) {
                //判断是否还能吃
                if (Desk.count == 0) {
                    break;
                }
                else {
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        //如果没有,就等待
                        try {
                            Desk.Lock.wait();
                        }
                        catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    else{
                        //如果有,则开吃
                        //食物个数-1
                        Desk.count--;
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!");
                        //吃完之后,唤醒厨师继续做
                        Desk.Lock.notify();
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

③创建中间平台

public class Desk {
    /*作用:用来控制生产者和消费者*/
    //判断标志,0表示没有面条,1表示有面条
    public static int foodFlag = 0;
    //消费者能吃的食物个数
    public static int count = 10;
    //定义一把锁
    public final static Object Lock = new Object();
}

④创建测试类

public class Demotext {
    public static void main(String[] args) {
        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();
        //设置名字
        c.setName("厨师");
        f.setName("吃货");
        //开启线程
        c.start();
        f.start();
    }
}

运行结果:

厨师在制作面条,还需制作10碗,吃货才能吃饱
吃货在吃面条,还能再吃9碗!
厨师在制作面条,还需制作9碗,吃货才能吃饱
吃货在吃面条,还能再吃8碗!
厨师在制作面条,还需制作8碗,吃货才能吃饱
吃货在吃面条,还能再吃7碗!
厨师在制作面条,还需制作7碗,吃货才能吃饱
吃货在吃面条,还能再吃6碗!
厨师在制作面条,还需制作6碗,吃货才能吃饱
吃货在吃面条,还能再吃5碗!
厨师在制作面条,还需制作5碗,吃货才能吃饱
吃货在吃面条,还能再吃4碗!
厨师在制作面条,还需制作4碗,吃货才能吃饱
吃货在吃面条,还能再吃3碗!
厨师在制作面条,还需制作3碗,吃货才能吃饱
吃货在吃面条,还能再吃2碗!
厨师在制作面条,还需制作2碗,吃货才能吃饱
吃货在吃面条,还能再吃1碗!
厨师在制作面条,还需制作1碗,吃货才能吃饱
吃货在吃面条,还能再吃0碗!

阻塞队列

继承结构:
①接口:Iterable,Collection,Queue,Blocking,Queue
②实现类:ArrayBlockingQueue,LinkedBlockingQueue

ArrayBlockingQueue:底层是数组,有界
LinkedBlockingQueue:底层是链表,无界(最大值为int的最大值,非常大)

利用阻塞队列实现等待唤醒机制:
①生产者和消费者必须使用同一个阻塞队列
②阻塞队列一般在中介平台上生成

代码实现:

①创建Cook类

public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run(){
        while (true) {
            try{
                //put方法里面已经包含了锁对象了,所以无需使用synchronized
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            }
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

②创建Foodie类

public class Foodie extends Thread {
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                String food = queue.take();
                System.out.println(food);
            }
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

③创建测试类 

public class Demotext {
    public static void main(String[] args) {
        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();
        //设置名字
        c.setName("厨师");
        f.setName("吃货");
        //开启线程
        c.start();
        f.start();
    }

④运行结果:

厨师放了一碗面条
面条
厨师放了一碗面条
面条
面条
厨师放了一碗面条
厨师放了一碗面条
面条
厨师放了一碗面条
面条
厨师放了一碗面条
厨师放了一碗面条
面条
面条
…………

小结:其实阻塞队列就相当于中介平台,通过类的构造方法,在实例化类的过程中传入相同的阻塞队列,从而达到利用阻塞队列实现等待唤醒机制

缺点:由运行结果可以看出,会出现连续打印同一条信息的情况,出现这种情况的原因是:阻塞队列中的put和take方法里面都内含锁,而且是在方法里面完成上锁解锁的操作,print语句是在锁外执行的,加上线程的随机性,就会出现连续打印同一条信息的情况。

线程的状态

线程池 

核心原理:
①创建一个空池子
②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还池子,下次再次提交任务时不需要创建新的线程,直接复用已有的线程即可
③如果提交任务过程中,池子没有空闲线程,也无法创建新的线程,任务就会排队等待

代码实现:
①创建线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

//创建一个没有上限的线程池
public static ExecutorService newCachedThreadPool();

//创建一个上限为nThreads的线程池
pubilc static ExecutorService newFixedThreadPool(int nThreads);

②线程池其他方法

//提交任务
public viod submit(new Runnable{代码体});

//销毁线程池
public viod submit();
自定义线程池 

1.核心元素:
①核心线程数量
②线程池中最大线程的数量
③临时线程空闲时间(数值)
④临时线程空闲时间(单位)
⑤阻塞队列
⑥创建线程的方式
⑦任务拒绝服务策略(有四种,常用的策略是直接舍弃)     

注意:
①核心线程在线程池中会一直存在,不会被销毁
②临时线程创建之后,若系统没有分配任务,则空闲一定时间后自动销毁
③最大线程数量 = 核心线程数 + 临时线程数 
④临时线程只有在阻塞队列满了之后才会创建
⑤当提交的任务数量 >(最大线程数量 + 阻塞队列长度)时,会处罚任务拒绝策略,一般是直接舍弃后提交的任务

代码实现:

        /*
            ThreadPoolExcutor threadPoolExecutor =new ThreadPoolExcutor
            (核心线程数量,最大线程数,空闲线程最大存活时间,时间单位,任务队列,创建线程工厂,任务的拒绝策略);
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,  //核心线程数量,能小于0
                6,  //最大线程数,不能小于0,最大数量 >= 核心线程数量
                60, //空闲线程最大时间(数值)
                TimeUnit.SECONDS,   //时间单位
                new ArrayBlockingQueue<>(3),    //任务队列
                Executors.defaultThreadFactory(),   //创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()    //任务的拒绝策略
        );
 线程池大小的选择

1.CPU密集型运算:最大并行数+1
注:在四核八线程的CPU中,最大并行数为8
小知识:可以通过java获取处理器数目

//获取java虚拟机可使用的处理器数目
public int availableProcessors();

2. I / O密集型运算:最大并行数 * 期望CPU利用率 * (总时间 / CPU计算时间)
注:总时间 = CPU计算时间 + 等待时间

3.理解
①CPU密集型运算:项目中计算操作比较多,读取本地文件或者读取数据库的操作比较少的运算
②I / O密集型运算:读取操作比较多,计算操作比较少的运算
③正在运行的线程数量不能大于CPU最大并行数,当线程数量比较多的时候,其他未运行的线程则会处于等待系统调度的状态,容易与正在运行的线程争夺CPU使用权,即线程竞争

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值