线程

进程和线程

Process和Thread

区别 进程 线程
根本区别 作为资源分配的单位 调度和执行的单位
开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行
分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源
包含关系 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线共同完成的 线程是进程的一部分,所以线程有的时候被称为轻权进程或轻量级进程

注意:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

核心概念

线程就是独立的执行路径

在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如GC线程、主线程

main()称之为主线程,为系统的入口点,用于执行整个程序

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的

对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制

线程会带来额外的开销,如CPU调度时间,并发控制开销

每个线程在自己的工作内存交互、加载和存储主内存控制不当会造成数据不一致

线程创建

继承Thread类

将一个类声明为Thread的子类,重写run类的方法Thread,创建线程对象,调用start()方法来运行

public class Test2 extends Thread{

    long minPrime;
    Test2(long minPrime){
        this.minPrime = minPrime;
    }

    @Override
    public void run() {
        // ...
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2(143);
        test2.start();
    }
}

start()和run()

start() 调用为开启线程,当方法执行时,调用run()代码将被执行

run() 线程体的入口点,线程执行的代码

执行线程必须调用start() 加入到调度器中,不一定立即执行,系统安排调度分配执行,直接调用run() 不是开启多线程,是普通方法调用

启动多个线程的话,start()没有先后顺序,run()有

实现Runnable接口

声明一个实现Runnable接口的类,实现run方法,然后分配类的实例,在创建Thread时作为参数传递,并启动

public class Test2 implements Runnable{

    long minPrime;
    Test2(long minPrime){
        this.minPrime = minPrime;
    }

    @Override
    public void run() {
        // ...
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2(143);
        new Thread(test2).start();
    }
}

Thread和Runnable

Thread Runnable
子类继承Thread具备了多线程能力,启动线程:子类对象.start() ,不建议使用:避免OOP单继承局限 实现接口Runnable具有多线程能力,启动线程:传入目标对象+Thread对象.start(),推荐使用:OOP多实现,灵活方便,方便同一份对象的代理

实现Callable接口

JUC并发包下实现Callable接口(不怎么用)

线程状态

image-20220228191830428

image-20220228193424160

生命周期

image-20220302184735425

线程方法

sleep()

使线程停止运行一段时间,将处于阻塞状态;如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行

sleep(时间)指定当前线程阻塞的毫秒数;sleep存在异常InterruptedException;sleep时间达到后线程进入就绪状态;sleep可以模拟网络延时、倒计时等;每一个对象都有一个锁,sleep不会释放锁

public class Web12306 implements Runnable{
    private int num = 50;
    @Override
    public void run() {
        while (true){
            if (num <= 0)
                break;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了" + num--);
        }
    }
}

join()

等待此线程执行完成后再执行其他线程,其他线程阻塞

yield()

让当前正在执行的线程暂停,不是阻塞线程,而是将线程转入就绪状态;调用了yield方法后,如果没有其他等待的线程,此时当前线程就会马上恢复执行

setDaemon()

可以将指定的线程设置成后台线程,守护线程;创建用户线程的线程结束时,后台线程也随之消亡;只能在线程启动之前把它设为后台线程

线程分为用户线程和守护线程,虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕,如后台记录操作日志、监控内存使用等

守护线程:是为用户线程服务的,JVM停止不用等待守护线程执行完毕

setPriority(int newPriority) getPriority()

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照线程的优先级决定应该调度哪个线程来执行

线程的优先级代表的是概率,范围从1到0,默认为5

Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5

使用下述方法获得或设置线程对象的优先级

int getPriority();
void setPriority(int newPriority);

优先级的设定建议在start()调用前

注意:优先级低只是意味着获得调度的概率低,并不是绝对先调用优先级高调用优先级低的进程

stop()

停止线程,不推荐使用

isAlive()

判断线程是否还活着,即线程是否还未终止

setName()

给线程取一个名字

getName()

获取线程的名字

currentThread()

取得当前正在运行的线程对象,也就是获取自己本身

并发

并发:同一个对象多个线程同时操作

线程同步

多个线程访问一个对象,某些线程还想这个对象,这个时候需要用到线程同步,线程同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可

存在以下问题:

一个线程持有锁会导致其他所有需要此锁的线程挂起

在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块

同步方法

public synchronized void method(int args){}

synchronized方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属的线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态

缺陷:若将一个大的方法声明为synchronized将会大大影响效率

同步块

synchronized(obj){} ,obj称之为同步监视器

obj可以是任何对象,但是推荐使用共享资源作为同步监视器;同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子

同步监视器的执行过程:

第一个线程访问,锁定同步监视器,执行其中代码

第二个线程访问,发现同步监视器被锁定,无法访问

第一个线程访问完毕,解锁同步监视器

第二个线程访问,发现同步监视器未锁,锁定并访问

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有两个以上对象的锁时,就可能发生死锁

线程通信

线程同步问题

生产者和消费者共享一个资源,生产者和消费者之间相互依赖,互为条件;对于生产者,没有生产产品之前,要通知消费者等待;而生产了产品之后,又需要马上通知消费者消费;对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费;在生产者消费者问题中,仅有synchronized是不够的;synchronized可阻止并发更新同一个共享资源,实现了同步;synchronized不能用来实现不同线程之间的消息传递(通信)

解决方式1

并发协作模型“生产者/消费者模式”:管程法

生产者:负责生产数据的模块(方法、对象、线程、进程)

消费者:负责处理数据的模块(方法、对象、线程、进程)

缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”;生产者将生产好的数据放入”缓冲区“,消费者从”缓冲区“拿需要处理的数据

方法名 作用
final void wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
final void wait(long timeout) 指定等待的毫秒数
final void notifiy() 唤醒一个处于等待状态的线程
final void notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度