• Java 多线程与并发【知识点笔记】


    Java 多线程与并发【知识点笔记】

    Java多线程与并发

    先说一下线程与进程的由来

    在初期的计算机,计算机只能串行执行任务,并且需要长时间的等待用户的输入才行

    到了后来,出现了批处理,可以预先将用户的指令集中成清单,然后批量串行处理用户指令,但是这仍然无法并发执行

    然后进程就出现了,进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,为并发处理任务提供了可能,这就解决了无法并发的情况

    但是因为一个进程一段时间只做一个事情,如果一个进程有多个任务,只能一个一个的处理,甚是麻烦,因为就出现了线程,一个进程就包括了多个线程,线程可以共享进程的内存资源,相互之间切换更加的快速,支持更细粒度的任务控制,是进程内的子任务得到并发执行

    进程和线程的区别

    首先我们清楚,进程是资源分配的最小单位,线程是CPU调度的最小单位,而所有的与进程相关的资源都被记录在PCB中

    而且,进程是抢占处理机的调度单位,线程属于某个进程,共享其资源,而线程只由堆栈寄存器,程序计数器和TCB组成

    那么总的来说,这两个的区别有下面几点:

    1.线程不能看做是一个独立的应用,而进程可以看做独立应用

    2.进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径,线程没有独立的地址空间,多进程的程序比多线程程序健壮

    3.进程的切换比线程的切换开销更大

    在Java中进程和线程的关系

    1.Java作为与平台无关的编程语言,就会对操作系统提供的功能进行封装,包括进程和线程

    2.每运行一个Java程序就会产生一个进程,而进程包含至少一个线程

    3.每个进程对应一个JVM实例,多个线程共享JVM里的堆

    4.进程类似于投资者,其手中掌握着资源,拿着资源去干活打工的就是线程,而Java采用的是单线程编程模型,程序会自动创建主线程

    5.主线程可以创建子线程,原则上要后于子线程完成执行

    Thread中的start和run方法的区别

    调用start()方法会创建一个新的子线程启动,而run()方法只是thread的一个普通方法的调用

    Thread和runnable的关系

    Thread是一个类,而runnable是一个接口,Thread是一个可以实现runnable接口的类,可以让runnable支持多线程

    因为Java类的单一继承原则,为了提升系统的可扩展性,推荐使业务类实现runnable接口,将业务逻辑分装在run方法里,便于给普通的类附上多线程的特性

    怎么给run()方法传参?

    实现的方法主要有三种:构造函数传参,成员变量传参以及回调函数传参

    如何实现处理线程的返回值?

    实现的方式主要有三种:

    第一种方法,主线程等待法,实现起来比较简单,缺点就是需要自己实现循环等待的逻辑,当需要等待的变量多起来,代码就会看起来很臃肿

    代码如下:

    public class CycleWait implements Runnable{
        private String value;
        public void run() {
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            value = "we have data now";
        }
    
        public static void main(String[] args) throws InterruptedException {
            CycleWait cw = new CycleWait();
            Thread t = new Thread(cw);
            t.start();
            while (cw.value == null){
                Thread.currentThread().sleep(100);
            }
            System.out.println("value : " + cw.value);
        }
    }
    

    第二种方法,使用Thread类的join()阻塞当前线程以等待子线程处理完毕,这种能够比主线程等待法的控制更加精准,实现起来也更简单,但是缺点是粒度不够细

    代码不同之处就是

        public static void main(String[] args) throws InterruptedException {
            CycleWait cw = new CycleWait();
            Thread t = new Thread(cw);
            t.start();
            t.join();
            System.out.println("value : " + cw.value);
        }
    }
    

    第三种方法,通过callable接口实现:通过futuretask 或者是通过线程池获取,可以更加精准的进行控制

    其中的一种代码如下:

    public class ThreadPoolDemo {
        public static void main(String[] args) {
            ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
            Future<String> future = newCachedThreadPool.submit(new MyCallable());
            if(!future.isDone()){
                System.out.println("task has not finished, please wait!");
            }
            try {
                System.out.println(future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } finally {
                newCachedThreadPool.shutdown();
            }
        }
    }
    

    线程的状态

    线程主要有六个状态:

    新建(new):创建以后尚未启动的线程的状态

    运行(runnable):包含running和ready,有可能正在执行,也可能在等待

    无限期等待(waiting):不会被分配CPU执行时间,需要显式被唤醒,通过三个方法可以进入无限期等待:调用了没有设置timeout参数的object.wait()方法或者是没有设置timeout参数的thread.join()方法以及locksupport.park()方法

    限期等待(timed waiting):在一定时间后会被系统自动唤醒,主要有五个方法可以进入限期等待:第一个是thread.sleep()方法,第二个是设置了设置timeout参数的object.wait()方法,第三个是设置timeout参数的thread.join()方法,第四个是locksupport.parknanos()方法,第五个是locksupport.parkuntil()方法

    阻塞(blocked):等待获取排它锁

    结束(terminated):已终止线程的状态,线程已经结束执行

    线程状态以及状态之间的转换

    sleep和wait的区别

    基本的差别

    sleep是thread类的方法,wait是object类中定义的方法

    sleep方法可以在任何地方使用,而wait方法只能在synchronized方法或者是synchronized块中使用

    最主要的本质上的区别

    Thread.sleep只会让出CPU,不会导致锁行为的改变,而object.wait不仅让出CPU,还会释放已经占有的同步资源锁

    notify和notify all的区别

    要想先了解这两个的区别,就得先知道两个概念:锁池entrylist以及等待池waitset

    锁池的本质可以这样说,假设线程a已经拥有了某个对象的锁,而其他的线程b和c想要调用这个对象的某个synchronized方法或者是synchronized块,由于b和c线程在进入对象的synchronized方法(或者是块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程a占用,此时线程b和c就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池

    等待池的本质可以这样说,假设线程a调用了某个对象的wait方法,线程a就会释放该对象的锁,同时线程a就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁

    然后就可以说一下notify和notifyall的区别

    notifyall会让所有处于等待池中的线程全部进入锁池去竞争获取锁的机会,没有获得锁的只能在所持中等待下个机会,不能再回到等待池中

    而notify只会随机取一个处于等待池中的线程进入锁池中去竞争获取锁的机会

    yield函数

    概念部分:当调用thread.yield()函数的时候,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示

    示例代码如下:

    public class YieldDemo {
        public static void main(String[] args) {
            Runnable yieldTask = new Runnable() {
                @Override
                public void run() {
                    for (int i = 1; i <= 10; i++) {
                        System.out.println(Thread.currentThread().getName() + i);
                        if (i == 5) {
                            Thread.yield();
                        }
                    }
                }
            };
            Thread t1 = new Thread(yieldTask, "A");
            Thread t2 = new Thread(yieldTask, "B");
            t1.start();
            t2.start();
        }
    

    如何中断线程?

    已经被抛弃的方法:通过调用stop()方法停止线程(因为太过暴力等问题被停止使用)以及通过调用suspend()和resume()方法

    目前在使用的方法:调用interrupt()方法,作用不是中断线程,而是通知线程应该中断了,具体来说,就是如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个interruptedexception异常,那么如果线程处于正常的活动状态,那么会将该线程的中断标志设置为true,被设置中断标志的线程将继续正常运行,不受影响

    因此interrupt并不能真正的中断线程,而是需要被中断的线程来配合中断才可以,也就是说,一个线程有了被中断的需求才能这样做,在正常运行任务的时候,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,被设置中断标志的线程将继续正常运行,不受影响

    示例代码如下:

    public class InterruptDemo {
        public static void main(String[] args) throws InterruptedException {
            Runnable interruptTask = new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    try {
                        //在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
                        while (!Thread.currentThread().isInterrupted()) {
                            Thread.sleep(100); // 休眠100ms
                            i++;
                            System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") loop " + i);
                        }
                    } catch (InterruptedException e) {
                        //在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程)
                        System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") catch InterruptedException.");
                    }
                }
            };
            Thread t1 = new Thread(interruptTask, "t1");
            System.out.println(t1.getName() +" ("+t1.getState()+") is new.");
    
            t1.start();                      // 启动“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is started.");
    
            // 主线程休眠300ms,然后主线程给t1发“中断”指令
            Thread.sleep(300);
            t1.interrupt();
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
    
            // 主线程休眠300ms,然后查看t1的状态
            Thread.sleep(300);
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
        }
    }
    

    感谢观看,文笔有限,博客不出彩,还请多多见谅
  • 相关阅读:
    json转换字符串
    windows下Xshell远程访问虚拟机
    win7去箭头指令
    n核CPU为什么计算速度达不到单核n倍
    vim字符串的替换
    转发的别人的vim编码和终端编码的设置
    音频操作
    scanf函数
    文字常量区和栈区区别
    Linux 进程
  • 原文地址:https://www.cnblogs.com/jokingremarks/p/14508153.html
Copyright © 2020-2023  润新知