多线程——线程与进程,线程的实现方式

例子见Github-JavaSE-Day07

线程的概念

程序:Program,是一个指令的集合

进程:Process,正在执行中的程序。

  • 进程是程序的一次静态执行过程,占用特定地址空间
  • 每个进程是独立的,包括三部分:cpu,data,code

线程:是进程中一个执行路径。

  • 又称为轻量级线程
  • 一个进程可拥有多个并行的线程
  • 一个进程中的线程共享相同的内存单元–>可访问相同变量和对象,而且它们从同一个堆中分配对象–>通信、数据交换、同步操作
  • 由于共享资源,所以不需要额外通信机制,线程间切换时候不需要保护、恢复现场,效率就比较高

web服务器:nginx http
它们最大的区别在于底层用的I/O模型不同。nginx用的NIO,http用的是BIO
e.g 四核电脑,同时间只能有四个进程运行。此时同时刻进来了1000个请求。全进来后,四个进程进行抢占。
如果是BIO模型:第1个CPU拿第1个请求,第2个拿第2个……第5个只能等待
如果是NIO模型:第1个CPU拿第1个请求,第2个拿第2个……第5个进来后会直接进入第1个CPU中,切换成n多个线程来进行一个并行交替执行
因此NIO效率是比BIO效率高

  • 程序是进程的一部分,而线程包含于进程。
  • 进程是申请资源的最小单位
  • 程是进程的执行单元,是调度和执行的单位

进程与线程

一个进程至少有一个线程。
各个应用程序之间会去抢占cpu资源(一个时间片上一个cpu只能处理一个程序),进程抢占到资源后,每个线程再去抢占cpu资源(所以每次执行顺序不同)
自己设置当前线程的优先级,可以提高抢到资源的概率。

JVM启动的时候会有一个进程java.exe,在该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称为主线程

  • 进程有独立代码和数据空间(进程上下文),切换开销大
  • 线程间共享代码和数据空间,但有独立的运行栈和程序计数器(PC),切换开销小

Java中实现多线程

创建线程方式一

在JAVA中负责线程的是Java.lang.Thread这个类

  • 可以通过创建Thread的实例来创建新线程。
  • 线程代码都要写到run()里面,run()方法称为线程体。
  • 可以通过调用Thread类中start()启动一个线程。

实现多线程的时候:

  1. 必须继承Thread类
  2. 必须重写run方法,指核心执行逻辑
  3. 线程启动时,不要直接调用run方法,而是用start()调用
  4. 每次运行相同代码,出来结果可能不一样
创建线程方式二

第二种实现方式:使用了代理设计模式

  1. 实现Runnable接口
  2. 重写run方法
  3. 创建Thread对象,将刚刚创建好的runnable的子类实现作为Thread的构造参数
  4. 通过thread.start()启动

备注

  • Thread类是实现了Runnable接口的。由于Runnable接口中没有start方法,所以只能通过Thread来进行启动了。
  • 推荐使用第二种方式:
    1.java是单继承,将继承关系留给最需要的类。
    2.使用Runnable接口不需要给共享变量添加static每次创建一个对象作为共享对象即可。
案例

卖票

方式一创建线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TicketThread extends Thread{
private int ticket = 5; // 一共5张票
@Override
public void run() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread();
TicketThread t3 = new TicketThread();
TicketThread t4 = new TicketThread();

t1.start();
t2.start();
t3.start();
t4.start();
}
}

结果:

1
2
3
4
Thread-1正在出售第5张票
Thread-0正在出售第5张票
Thread-2正在出售第5张票
Thread-3正在出售第5张票

原因:每次调用run方法时候进行判断,判断完整个程序就结束了

如果将if语句包含在一个不小于5次的循环中

结果:

Thread-1正在出售第5张票
Thread-3正在出售第5张票
Thread-3正在出售第4张票
Thread-3正在出售第3张票
Thread-3正在出售第2张票
Thread-3正在出售第1张票
Thread-2正在出售第5张票
Thread-2正在出售第4张票
Thread-2正在出售第3张票
Thread-2正在出售第2张票
Thread-2正在出售第1张票
Thread-0正在出售第5张票
Thread-1正在出售第4张票
Thread-0正在出售第4张票
Thread-1正在出售第3张票
Thread-1正在出售第2张票
Thread-0正在出售第3张票
Thread-0正在出售第2张票
Thread-0正在出售第1张票
Thread-1正在出售第1张票

原因(同时也是我们不推荐使用Thread的第二个原因):
创建多对象并不是共享值5。
每次进行new TicketThread();时候,都是创建了一个TicketThread对象。这意味着每个对象在进行创建的时候,都要实例化(拥有ticket = 5这样的属性值),即意味着每个堆空间中都有一个5。
这意味着,每个对象只是操作我当前堆空间中的值。所以打印结果都是54321这种情况。

尝试解决以上问题

  1. 共享数据,把ticket用static修饰,static修饰的变量归属于类而不归属于对象,所以不管main中创建多少个对象都无所谓。
    但是仍会出问题。因为
    可能会出现的情况
    Thread-1正在出售第5张票
    Thread-1正在出售第2张票
    Thread-3正在出售第3张票
    Thread-0正在出售第5张票
    Thread-2正在出售第4张票
    Thread-1正在出售第1张票

方式二创建线程:

代码:

public class TicketRunnable implements Runnable{
    private static int ticket = 5; // 一共5张票
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
            }
        }
    }
    public static void main(String[] args) {
        TicketRunnable ticket = new TicketRunnable();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        Thread t4 = new Thread(ticket);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

ticket不加static修饰,也不会出现方式一中那种情况(因为在创建主函数的时候,每次只new了一个对象,所以只有一个5)。虽然还是错误的。

可能会出现的结果
Thread-0正在出售第3张票
Thread-3正在出售第4张票
Thread-3正在出售第1张票
Thread-1正在出售第3张票
Thread-2正在出售第5张票
Thread-0正在出售第2张票
真正解决方法–数据同步

多线程——线程同步与死锁

线程的代理设计模式

例如上面Thread t1 = new Thread(ticket);,代理是t1,被代理(实际干活的)是传入的TicketRunnable对象——ticket。t1.start();看上去是执行t1的方法,实际上是在执行ticket中的东西。这就是一个代理模式的实现。
具体可参考Day07中proxy(王婆潘金莲西门庆案例)。

代理模式主要使用了 Java 的多态,干活的是被代理类,代理类主要是
接活,你让我干活,好,我交给幕后的类去干,你满意就成,那怎么知道被代理类能不能干呢?同根就成,
大家知根知底,你能做啥,我能做啥都清楚的很,同一个接口呗
————《设计模式之禅》

线程的状态

五个状态: 创建 就绪 运行 终止 阻塞
线程的生命周期

  1. 新生状态:当创建好线程对象后,没有启动之前(调用start方法之前)
    ThreadDemo = new ThreadDemo();
    RunnableDemo = new RunnableDemo();
    有自己的内存空间,但是没有资源
  2. 就绪状态:准备开始执行,并没有执行。表示调用start方法之后
    当对应的线程创建完成,且调用start方法后,所有的线程会添加到一个就绪队列中,所有线程同时去抢占cpu资源
  3. 运行状态:当前进程获取到cpu资源后,就绪队列中所有的线程会去抢占cpu资源,谁先抢占到谁先执行。在执行的过程就称为运行状态
    抢占cpu资源,执行代码逻辑(run中代码)开始,直到等待某资源而阻塞,或完成而死亡。
    如果给定时间片内未完成,系统换下回到等待状态
  4. 死亡状态:当运行的进程正常执行完所有的代码逻辑或者因为异常终止程序叫做死亡状态
    进入方式:
    • 正常运行完成且结束
    • 人为中断执行,如使用stop()
    • 程序抛出未捕获的异常
  5. 阻塞状态:在程序运行过程中,发生某些异常情况导致当前线程无法再顺利进行下去,此时会进入阻塞状态。进入阻塞状态的原因消除之后,所有的阻塞队列会再次进入到就绪状态中,随机抢占cpu资源,等待执行
    进入方式:
    • 执行了sleep()
    • 等待I/O设备等资源
    • join方法(代码中执行的逻辑)
      在这里插入图片描述

Thread的API方法

见Day07_thread中api包

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
 // 获取当前线程对象 静态方法
Thread thread = Thread.currentThread();
// 获取当前线程的名称
System.out.println(thread.getName());
// 获取线程的ID
System.out.println(thread.getId());
// 获取线程的优先级,在一般系统中,范围是0~10的值,默认值5.有些系统是0~100
System.out.println(thread.getPriority());
// 设置线程池的优先级
thread.setPriority(1);
System.out.println(thread.getPriority());
// 判断线程是否存活
System.out.println(thread.isAlive());

结果:

1
2
3
4
5
main
1
5
1
true

优先级越高,一定越先执行吗?
不。优先级只能反映线程执行紧急程度 , 不能决定是否一定先执行

运行态 –> 阻塞态:

  1. public final void join()
    描述:调用join方法的线程被强制执行,其他线程处于阻塞态,该线程执行完后其他线程再执行

  2. public static void sleep(long millis)
    描述:使用该方法的当前线程被阻塞,睡眠millis秒

运行态 –> 就绪态:
public static void yield()
描述:当前执行的线程暂停一次,允许其他线程执行,不阻塞,线程进入就绪态。如果没有其他等待线程,这时当前线程会马上恢复执行
 只是这次不执行,也可能出现暂停一次后下一次依旧是该线程执行。因为暂停一次后又到了就绪队列抢占资源。

对象的等待唤醒过程线程的等待唤醒过程的区别
Object类包括了等待唤醒方法(notified()wait()),这是对象的等待唤醒。
虽然线程也属于对象,但一般情况下,线程这个类或者线程这个对象,不会被当作一个共享空间。
在多线程的时候,可以实现唤醒和等待的过程,但是唤醒和等待操作的对应不是Thread类,而是我们设置的共享对象或者共享变量。