Java多线程技术~线程的定义与同步

Java多线程技术

线程和进程

程序Program

       程序是一段静态的代码,它是应用程序执行的蓝本

进程Process

       进程是指一种正在运行的程序,有自己的地址空间。

在这里插入图片描述

进程的特点

       动态性

       并发性

       独立性

       并发和并行的区别

              并行:多个CPU执行多个CPU任务

              并发:一个CPU(采用时间片)同时执行多个任务

线程Thread

       进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。

       线程又被称为轻量级进程(lightweight process)

       如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程

线程特点

       轻量级进程

       独立调度的基本单位

       共享进程资源

       可并发执行

线程和进程的区别

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

线程定义和创建

使用多线程实现赛跑

public class Test{
    public static void main(String[] args) {
        //创建一个线程
        Student student = new Student();
        //启动一个线程
        //thread.run();这不是在启动线程,是在调用方法run()
        //启动线程,不见得立刻执行,而是进入就绪队列,等待获得CPU
        student.start();
        //小刚也在跑
        for(int i = 0; i < 100; i++){
            System.out.println("小刚现在领先了,加油!    当前线程的名称: " + Thread.currentThread().getName());
        }
    }
}

class Student extends Thread{
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("小明现在领先了,加油!   当前线程的名称: " + this.getName());
        }
    }
}

       run() 线程体,线程要完成的任务

       start() 启动线程,线程进入就绪队列,等待获取CPU并执行

实现Runnable接口

public class Test{
    public static void main(String[] args) {
        //创建线程对象
        Runnable runnable = new Student();
        Thread thread = new Thread(runnable);
        //启动线程
        thread.start();

        for(int i = 0; i < 100; i++){
            System.out.println("小刚领先了,加油   当前线程的名称:" + Thread.currentThread().getName());
        }
    }
}

class Student implements Runnable{

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("小明领先了,加油!   当前线程的名称:" + Thread.currentThread().getName());
        }
    }
}

两种方式的优缺点

       方式1:继承Thread类

       缺点:Java单继承,无法继承其他类

       优点:代码稍微简单

       方式2:实现Runnable接口

       优点 还可以去继承其他类 便于多个线程共享同一个资源

       缺点:代码略有繁琐

       实际开发中,Runnable使用更多一些

方法介绍

       currentThread()

              返回对当前正在执行的线程对象的引用。

       getName()

              返回该线程的名称

       getPriority()

              返回线程的优先级。

       run()

              果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

       setName(String name)

              改变线程名称,使之与参数 name 相同。

       setPriority(int newPriority)

              更改线程的优先级。

       start()

              使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

Callable接口

       JDK1.5后推出了第三种定义线程的方式,实现Callable接口

使用多线程获取随机数
public class Test implements Callable<Integer> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程对象
        Callable<Integer> callable = new Test();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        //获取返回值
        System.out.println(futureTask.isDone());
        //必须等线程执行完毕后,才能得到返回值,线程在此会阻塞
        Integer num = futureTask.get();
        System.out.println(num);
        System.out.println(futureTask.isDone());
    }

    @Override
    public Integer call() throws Exception {
        Thread.sleep(500);
        return new Random().nextInt(10);
    }
}

在这里插入图片描述

与Runnable相比, Callable功能更强大些

       方法名不同

       可以有返回值,支持泛型的返回值

       可以抛出检查异常

       需要借助FutureTask,比如获取返回结果

Future接口

       可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等

       FutrueTask是Futrue接口的唯一的实现类

       FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

在这里插入图片描述

新生状态

       用new关键字建立一个线程对象后,该线程对象就处于新生状态。

       处于新生状态的线程有自己的内存空间,通过调用start进入就绪状态

就绪状态

       处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU

       当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”

运行状态

       在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。

       如果在给定时间片内没执行结束,就会被系统给换下来回到等待执行状态。

阻塞状态

       处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。

       在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

死亡状态

       死亡状态是线程生命周期中最后一个阶段。线程死亡原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过执行stop方法来终止一个线程[不推荐使用】,三是线程抛出未捕获的异常

线程控制

       理解了线程生命周期的基础上,可以使用Java提供的线程控制命令对线程的生命周期进行干预。

       实际开发中经常使用Thread.sleep()来模拟线程切换,暴露线程安全问题。

join ()

       阻塞指定线程等到另一个线程完成以后再继续执行

public class Test {
    public static void main(String[] args){
        int j = 1;
        for(int i = 0; i < 100; i++){
            if(i == 50){
                Thread thread = new Student();
                thread.setName("关云长");
                thread.start();
                try {
                    // 线程A正在执行 线程B进来,线程B执行完,A才会执行。
                    //A此间处于阻塞状态
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("小刚领先了,加油!   当前线程名: " + Thread.currentThread().getName() + j++);
        }
    }
}

class Student extends Thread{
    int j = 1;
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("小明领先了,加油!   当前线程名: " + this.getName() + j++);
        }
    }
}

sleep()

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

public class Test {
    public static void main(String[] args){
        Thread thread = new Student();
        thread.start();
        for(int i = 0; i < 100; i++){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("小刚领先了,加油!   当前线程名: " + Thread.currentThread().getName());
        }
    }
}

class Student extends Thread{
    public void run(){
        for(int i = 0; i < 100; i++){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("小明领先了,加油!   当前线程名: " + this.getName());
        }
    }
}

setDaemon()

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

public static void main(String[] args){
        Thread thread = new Student();
        thread.setDaemon(true); //后台线程 
        thread.start();
        for(int i = 0; i < 100; i++){
            System.out.println("小刚领先了,加油!   当前线程名: " + Thread.currentThread().getName());
        }
    }

interrupt()

       并没有直接中断线程,而是需要被中断线程自己处理

stop()

       结束线程,不推荐使用。

yield

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


线程同步

线程同步的实现方案

       同步代码块

              synchronized(obj){}

       同步方式

              private synchronized void makeWithdrawal(int amt) {}

       Lock锁

售票案例(Synchronized)

public class Test {
    public static void main(String[] args){
        Ticket ticket = new Ticket();
        //创建Thread类
        Thread A = new Thread(ticket,"A窗口");
        Thread B = new Thread(ticket,"B窗口");
        Thread C = new Thread(ticket,"C窗口");
        //启动线程
        A.start();
        B.start();
        C.start();
    }
}
class Ticket implements Runnable{
    private int ticket = 5;

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){   //每个窗口排了100个人
            synchronized (this){
                if(ticket > 0){ //有票
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket-- + " 张票");
                }
            }
        }
    }
}

在这里插入图片描述

       这个时候我们会看到票是一张一张的来卖的

       假如没有锁呢?

在这里插入图片描述

       那么就会出现这种情况

认识同步监视器(锁子)

       synchronized(同步监视器){ }

              必须是引用数据类型,不能是基本数据类型

              在同步代码块中可以改变同步监视器对象的值,不能改变其引用

              尽量不要String和包装类Integer做同步监视器.如果使用了,只要保证代码块中不对其进行任何操作也没有关系

              一般使用共享资源做同步监视器即可

              也可以创建一个专门的同步监视器,没有任何业务含义

              建议使用final修饰同步监视器

       同步代码块的执行过程

              第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码

              第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open

              第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态

              第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open

              第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,重复第一个线程的处理过程(加锁)

       线程同步优点是比较安全,缺点就是效率低下,可能出现死锁。

       多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块,多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块

Lock锁

       JDK1.5中推出了新一代的线程同步方式:Lock锁

class Ticket implements Runnable{
    private int ticket = 5;
    private Lock lock = new ReentrantLock();    //可重入锁
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){   //每个窗口排了100个人
            try{
                //上锁
                lock.lock();
                if(ticket > 0){ //有票
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket-- + " 张票");
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

Lock锁

       JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活

       java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

       ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。

       注意:如果同步代码有异常,要将unlock()写入finally语句块

Lock和synchronized的区别

       Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,遇到异常自动解锁

       Lock只有代码块锁,synchronized有代码块锁和方法锁

       Lock锁可以对读不加锁,对写加锁,synchronized不可以

       Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以

       使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

       Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

猜你喜欢

转载自blog.csdn.net/qq_41424688/article/details/106779154