0
点赞
收藏
分享

微信扫一扫

软件设计概述

紫荆峰 1天前 阅读 1
java

1、进程与线程

1.1、进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程。

1.2、线程

        一个进程之内可以分为一到多个线程。 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器。

1.3、二者对比

  1. 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

  2. 进程拥有共享的资源,如内存空间等,供其内部的线程共享

  3. 进程间通信较为复杂

    • 同一台计算机的进程通信称为 IPC(Inter-process communication)

    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP

  4. 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

  5. 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

1.4、并行与并发

        单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行 的 。总结为一句话就是: 微观串行,宏观并行 。一般会将这种 线程轮流使用 CPU 的做法称为并发concurrent,多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

  1. 并发(concurrent)是同一时间应对(dealing with)多件事情的能力

  2. 并行(parallel)是同一时间动手做(doing)多件事情的能力

        ok说到这里其实已经完成了,但是突然想到一个概念:同步异步,同步与异步是指访问数据的机制,同步一般指主动请求并等待IO操作完成的方式。异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知。同步和异步最大的区别就在于:同步需要等待,异步不需要等待。

        说到这里来不得不说一下阻塞非阻塞,阻塞与非阻塞是描述线程在访问某个资源时,数据是否准备就绪的一种处理方式。当数据没有准备就绪时:阻塞:线程持续等待资源中数据准备完成,直到返回响应结果。非阻塞:线程直接返回结果,不会持续等待资源准备数据结束后才响应结果。

总结:

  1. 阻塞与非阻塞是针对线程来说的,阻塞可能发生在IO期间也可能发生在IO之前。

  2. 同步与异步是针对IO操作来说的,同步是用户线程一直盯着IO直到完成,异步是用户线程在IO完成时会收到通知。

2、java线程

2.1、创建线程对象

方式1:直接使用 Thread

@Slf4j(topic = "c.Ltest1")//日志 topic是日志的标签
public class Ltest1 {
    public static void main(String[] args) {
​
        /*new Thread(() -> {
            log.info("创建的 runing");
        }).start();*/
        Thread t = new Thread() {
            public void run() {
                log.debug("runing");
            }
        };
        t.setName("小李");
        t.start();
        log.debug("main running");
    }
}

方式2:使用Runable配合Thread

@Slf4j(topic = "c.Ltest2")
public class Ltest2 {
    public static void main(String[] args) {
​
        /*Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.debug("运行代码");
            }
        };*/
        Runnable runnable = () -> log.debug("运行代码");
​
        Thread t=new Thread(runnable);
        t.start();
    }
}

方式3:使用FutureTask配合Thread

@Slf4j(topic = "c.Ltest3")
public class Ltest3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("任务。。。");
                Thread.sleep(2000);
                return 111;
            }
        });
        Thread xiali = new Thread(task, "xiali");
        xiali.start();
        log.info("{}",task.get());
    }
}

2.2、栈与栈帧

        我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

2.3、线程上下文切换

以下一些原因会导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

        当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能

3、线程中的方法

方法名static说明注意事项
start启动一个新线 程,在新的线程 运行 run 方法 中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
run新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
join等待线程运行结束
join(long n)等待线程运行结 束,最多等待 n 毫秒
getId获取线程长整型 的 id唯一
getName获取线程名
setName修改线程名
getPriority获取线程优先级
setPriority修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率
getState获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInteerupted判断是否被打断不会清除 打断标记
isAlive线程是否存活 (还没有运行完 毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断 的线程抛出 InterruptedException,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标 记 ;park 的线程被打断,也会设置 打断标记
interrupted()static判断当前线程是 否被打断会清除 打断标记
currentThread()static获取当前正在执 行的线程
sleep(long n)static让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程
yield()static提示线程调度器 让出当前线程对 CPU的使用主要是为了测试和调试

3.1、start 与 run

public static void main(String[] args) {
   Thread t1 = new Thread("t1") {
 @Override
 public void run() {
      log.debug(Thread.currentThread().getName());
      FileReader.read(Constants.MP4_FULL_PATH);
 }
 };
      t1.run();
      log.debug("do other things ...");
}

执行结果:

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

执行方法的还是我们的主线程而不是我们创建的t1线程!

#将 t1.run()更换为t1.start()

程序在 t1 线程运行, FileReader.read() 方法调用是异步的。

总结:

        直接调用 run 是在主线程中执行了 run,没有启动新的线程 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。注意start只能调用一次!

3.2、sleep和yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

@Slf4j(topic = "c.Test7")
public class Test7 {
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();
​
        Thread.sleep(1000);
        log.debug("interrupt...");
        t1.interrupt();
    }
}

yield:

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

  2. 具体的实现依赖于操作系统的任务调度器

总结:

        使用Sleep的线程时从Runing状态到Timed Waiting(阻塞状态)任务调度器不会将时间分片分给它,但是对于使用yield来说,不一样!它时从Runing状态到Runable(就绪状态),在这个期间是有可能会被分配时间片的。

3.3、线程优先级

        线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。

@Slf4j(topic = "c.Test9")
public class Test9 {
​
    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
                Thread.yield();
                System.out.println("              ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

3.4、join方法

@Slf4j(topic = "c.Test10")
public class Test10 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        //t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

        现在想知道r的结果,啧啧啧,按照我们的想法就是想要打印的是10,但是我们由于是两个线程,异步操作,在执行t1中的run方法的时候,主线程已经执行了,此时r的是0,那么如何让打印的结果是10,让主线程睡一会嘛!虽然没毛病但是不太好,有更好的方式使用join()

        上面是一个线程,如果时两个、三个。。。想要等待线程执行结果,那么只用想要等待的线程执行join方法。

        同时,可以设置最大的等待时间join(毫秒),你小子等了那么就还没好!不等了拜拜了您嘞!

3.5、interrupt 方法

        打断 sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态打断 sleep 的线程, 会清空打断状态,以 sleep 为例。

打断阻塞状态

@Slf4j(topic = "c.Test11")
public class Test11 {
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");
​
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
}

执行结果:

16:53:38.253 c.Test11 [t1] - sleep...
16:53:40.264 c.Test11 [main] - interrupt
16:53:40.264 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at cn.itcast.test.Test11.lambda$main$0(Test11.java:12)
    at java.lang.Thread.run(Thread.java:750)

打断正常运行

@Slf4j(topic = "c.Test12")
public class Test12 {
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("被打断了, 退出循环{}",interrupted);
                    break;
                }
            }
        }, "t1");
        t1.start();
​
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}

两阶段终止模式

        在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

4、线程状态

4.1、守护线程和主线程

        默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

@Slf4j(topic = "c.Test15")
public class Test15 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
            log.debug("结束");
        }, "t1");
        //设置为守护线程
        t1.setDaemon(true);
        t1.start();
​
        Thread.sleep(1000);
        log.debug("结束");
    }
}
  • 垃圾回收器线程就是一种守护线程

  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

4.2、线程的状态

4.2.1、操作系统
  1. 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

  2. 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  3. 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

  4. 【阻塞状态】

    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】

    • BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】

    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们

  5. 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

4.2.2、API
  1. NEW 线程刚被创建,但是还没有调用 start() 方法

  2. RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)

  3. BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分

  4. TERMINATED 当线程代码运行结束

5、共享模型

        是这样的,在高并发的情况下,不可避免的会出现多个线程同时对公共资源进行数据操作业务,这种情况下会出现指令交错的情况,这就导致可能出现和“超卖”问题。

临界区

        一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题,一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

竞态条件

        多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

5.1、synchronized

5.1.1、使用synchronized

        synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意 :虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

static int counter = 0;
    static final Object room = new Object();
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", counter);
    }

        synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

        注意:使用synchronized想要保护的资源,多个线程要使用同一把锁,也就是保证锁的唯一性。

5.1.2、synchronized
class Test{
     public synchronized void test() {
 
     }
}
等价于
class Test{
    public void test() {
        synchronized(this) {
 
        }
    }
}
​
class Test{
     public synchronized static void test() {
     
     }
}
等价于
class Test{
    public static void test() {
        synchronized(Test.class) {
 
        }
    }
}

5.3、Monitor

        Monitor 被翻译为监视器管程每个Java对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

 

  1. 刚开始 Monitor 中 Owner 为 null

  2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner

  3. 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED

  4. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  5. 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

5.4、 轻量级锁

        轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 synchronized 假设有两个方法同步块,利用同一个对象加锁。

static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
         // 同步块 A
         method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}

        当然不是每一次都错开,存在出现同时加锁的时刻,那么毕然会导致一个加锁成功一个加锁失败,这时候就出现锁膨胀的,然后为锁对象申请Monitor锁,此时锁升级为重量级锁。

        但是这种情况下,如果获取锁一次不成功,直接让当前线程到EntryList中等待,这也不太好叭,万一下一次就成功呐,所以在重量级锁竞争的时候,可以用自旋进行优化,自旋也太高大上了,其实就是让其多重复几次去获取锁(多核cpu下有意义!)。当然重试了几次后依然获取锁失败,那没有办法只能进入EntryList中等待叭!

  1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  2. 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  3. Java 7 之后不能控制是否开启自旋功能

5.5、偏向锁

        轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

5.6、死锁

        不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

诱发死锁的原因:

  • 互斥条件

  • 占用且等待

  • 不可抢夺(或不可抢占)

  • 循环等待

以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

  1. 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

  2. 针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题,对共享的资源要么全申请完毕要么全不申请。

  3. 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

  4. 针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

举报

相关推荐

0 条评论