线程与进程及多线程实现

线程与进程概述

进程:一个进程指一个应用程序,每个进程都有一个独立的内存空间(独立的堆、栈空间等),互不干扰。

线程:

  • 是进程的一个执行路径,共享一个内存空间。线程之间可以自由切换,并发执行。
  • 一个进程最少有一个线程,若线程为0则进程会被关闭。
  • 一个进程启动后,里面的若干执行路径被划分为若干线程。
  • 每个线程都有自己的栈空间,共用一份堆内存。

线程调度

我们的电脑一般会有上千个线程,但是cup一般是8线程或者16线程,如何使用这为数不多的cup运行上千个线程,就涉及到了线程调度问题:

线程调度分为:

  • 分时调度:

所有线程轮流使用CPU的使用权,平均分配每个线程所占CPU的时长。例如一个CPU把1秒拆成1万分平均分给每个线程轮流使用,就造成多个线程同时运行假象。

  • 抢占式调度:

我们可以调整线程的优先级,优先让优先级高的线程抢到CPU分出的时间。如果线程优先级相同,则会随机选择一个(线程随机性)。

对于CPU的一个核新而言,一个时刻只能执行一个事情,而CPU在多个线程之间来回切换的速度很快使得看上去象是同一个时刻运行。其实多线程程序并不能提高程序的运行速度,但能够提高程序的运行效率,让CPU的使用率更高。

同步与异步

**同步:**排队执行,效率低但是安全。

**异步:**同时执行,效率高但是不安全。

同步异步指一个程序内的多个线程使用同一块内存

并发与并行

并发:指的是多个事件在同一个时间段内发生。

并行:指的是多个事件在同一时刻发生(同时发生)。

在JAVA中实现多线程程序

线程创建方式一:继承Thread:

Thread是Java中提供用于开启一个线程的类,我们想要通过一个类开启一个线程,这个类就需要先继承Thread。

下面演示一个在Java中实现多线程最简单的代码:

注意的是:由一个线程调用的方法,这个方法也会执行在该线程里面。

public class demo1 {
    
    
    /**
     * 多线程技术
     * @param args
     */
    public static void main(String[] args) {
    
    
        //下面两个线程抢占式调度
        MyThread m = new MyThread();
        m.start();                     //分支线程
        for(int i=0; i<10; i++){
    
           //主线程
            System.out.println("B"+i);
        }
    }
}
public class MyThread extends Thread{
    
    
    /**
     * run方法就是线程要执行的任务方法
     */
    @Override
    public void run() {
    
    
        //这里的代码,就是一条新的执行路径
        //这里执行路径的触发方式,不是通过调用run方法,而是通过调用thread对象的start()来启动任务。
        for(int i=0; i<10; i++){
    
    
            System.out.println("A"+i);
        }
    }
}

两个线程之间进行抢占式调度,最后的输出结果每次是不一样的:下面展示其中的一次运行结果:

A0 B0 A1 A2 B1 A3 B2 A4 A5 B3 A6 B4 A7 B5 A8 B6 A9 B7 B8 B9

运行流程图如下:

image-20211122151548364

线程创建方式二:实现Runnable

Runnable是一个接口,通过实现创建线程任务,但需要Thread创建一个线程执行该任务。

举例实现Runnable接口如下:

public class demo1 {
    
    
    
    public static void main(String[] args) {
    
    
        
        for(int i=0; i<10; i++){
    
           //主线程
            System.out.println("B"+i);
        }
        
        //实现Runnable
        //1. 创建一个任务对象
        MyRunnbale r = new MyRunnbale();
        //2. 创建一个线程,并为其分配任务
        Thread t = new Thread(r);  //分支线程
    }
}
public class MyRunnbale implements Runnable{
    
    
    @Override
    public void run() {
    
    
        //线程的任务
        //要把任务执行起来还得借助Thread
        for(int i=0;i<10;i++){
    
    
            System.out.println("A"+i);
        }
    }
}

最后的结果和继承Thread的结果一样。

实现Runnable与继承Thread相比有如下优势:

  1. 通过创建任务,然后给线程分配任务的方式来实现的多线程,更合适多个线程同时执行相同任务的情况。
  2. 可以避免单继承带来的局限性。(Java中只有单继承extends)
  3. 通过创建任务,然后给线程分配任务的方式,任务与线程是分离的,提高了程序的健壮性。
  4. 后续学习的线程池技术,可以接收Runnable类型的任务,而不接收Thread类型的线程。

Thread的优势在于简单,对于简单的线程任务可以如下写:

public class demo2 {
    
    
    public static void main(String[] args) {
    
    
        //线程一
        new Thread(){
    
    //匿名内部类
            @Override
            public void run() {
    
    
                for(int i=0; i<10; i++){
    
    
                    System.out.println("A"+i);
                }
            }
        }.start();
        
        //线程二
        for(int i=0; i<10; i++){
    
    
            System.out.println("B"+i);
        }
    }
}

Thread类

上面介绍的两种线程的创建方式,都需要用到Thread这个类。

第一看常用构造方法,用来创建一个线程:

构造器 描述
Thread() 分配新的 Thread对象。
Thread(Runnable target) 分配新的 Thread对象。
Thread(Runnable target, String name) 分配新的 Thread对象。
Thread(String name) 分配新的 Thread对象。

第二看常用的普通中常用的方法:

long getId() 返回此Thread的标识符。
String getName() 返回此线程的名称。
int getPriority() 返回此线程的优先级。
void setPriority(int newPriority) 更改此线程的优先级。
void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法。
void stop() 已过时。 这种方法本质上是不安全的。
static void sleep(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
static void sleep(long millis, int nanos) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。
void setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。

关于setPriority( ),优先级有如下三种:

变量和类型 字段 描述
static int MAX_PRIORITY 线程可以拥有的最大优先级。
static int MIN_PRIORITY 线程可以拥有的最低优先级。
static int NORM_PRIORITY 分配给线程的默认优先级。

关于setDaemo( ):

  • 用户线程:主线程和子线程统称为用户线程,可以自己决定自己的死亡,当用户线程没了程序也就结束了。

  • daemon线程:守护线程,用来保护用户线程,不能自己决定自己死亡,当用户线程全死亡时,它会自动死亡。

设置和获取线程名称

设置线程名称:setName( ).

获取线程名称:getName().

获取当前正在执行的线程对象:currentThread().得到的结果类型是Thread对象。

在正在执行代码的地方放入currentThread()就能获取当前执行该代码的线程是谁。

线程休眠sleep

static void sleep(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
static void sleep(long millis, int nanos) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。

使用举例:

for(int i=0; i<10; i++){
    
    
            System.out.println("A"+i);
            Thread.sleep(1000);
        }

结果是每隔一秒钟打印一次结果。

线程阻塞

线程阻塞的意思不单单指线程休眠,它的意思是指所有会消耗时间的操作

我们知道线程是一条执行路径,假设执行路径有1000行代码,前100行代码为读取文件,花费时间0.5s,那么这0.5秒的时间就称之为线程阻塞。

或者我们常见的等待用户输入代码:String number = input.nextLine(),线程执行会停在这里等待用户输入,此时也称为线程阻塞。

线程中断

一个线程是一个独立的执行路径,是否中断应该由其自身决定

**外部中断的后果:**我们为线程启动以后,它有自己的任务,会涉及和使用到很多的资源,如有由外部中断的话,很有可能会导致资源来不及释放而产生内存垃圾,这垃圾可能还不能由GC回收。

**那如何中断:**线程对象调用一个属性给自己打上一个标记,在线程运行在某些特殊的时候查看这个标记,当看到标记有让它中断的提示,则会抛出一个异常,使用try-catch接收后在catch代码块里面编写程序使之中断。

看下面一个例子:

public static void main(String[] args)  {
    
    
      
       //线程一
       Thread t1 = new Thread(new MyRunnable());
       t1.start();

       //线程二
        for(int i=0; i<5; i++){
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    
    static class MyRunnable implements Runnable{
    
    
        @Override
        public void run() {
    
    
            for(int i=0; i<5; i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

上面的打印结果是线程一和线程二分别每隔一秒钟打印一次两者不干扰,而不是线程一打印隔1s线程二打印

打印结果:(一共耗时5S)

main:0
Thread-0:0
main:1
Thread-0:1
main:2
Thread-0:2
main:3
Thread-0:3
Thread-0:4
main:4

下面在上面代码的基础上修改,使满足一定条件线程中断:

我们的目的:当线程二运行完之后,我们打算让线程一中断;

 public static void main(String[] args)  {
    
    
       //线程一
       Thread t1 = new Thread(new MyRunnable());
       t1.start();

        //线程二
        for(int i=0; i<2; i++){
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        t1.interrupt();//给线程打标记。
    }

    static class MyRunnable implements Runnable{
    
    
        @Override
        public void run() {
    
    
            for(int i=0; i<5; i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                   // e.printStackTrace();
                    System.out.println("发现中断标记,线程死亡");
                    return;//return即可表示线程死亡。
                }
            }
        }
    }

运行结果:

main:0
Thread-0:0
main:1
Thread-0:1
Thread-0:2
发现中断标记,线程死亡

解释:

  • InterruptedException e:线程中断异常;
  • 在写sleep代码的时候,系统自动抛出了这个异常,以下情况会触发这个异常并且进入catch代码块:
image-20211122224455278
  • 在catch中处理中断的意义:线程突然中断会导致很多资源无法释放,所以在catch中可以设置要中断时,把该释放的资源全部释放等等操作,然后再人为中断(如上加入return)。

线程守护

之前提到过,线程分为守护线程和用户线程。

如何设置守护线程:通过set.Daemon(),守护线程的设置必须要在线程运行前设置。

看例子:

public static void main(String[] args)  {
    
    
       //线程一
       Thread t1 = new Thread(new MyRunnable());
       t1.set.Daemon(true); //把t1设置为守护线程
       t1.start();

        //线程二
        for(int i=0; i<2; i++){
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        
    }

    static class MyRunnable implements Runnable{
    
    
        @Override
        public void run() {
    
    
            for(int i=0; i<5; i++){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
                }
            }
        }
    }

t1成为守护线程后,按照逻辑,线程二运行完之后,t1就会自动中断。

线程安全问题

多个线程在同时运行时,会很容易发生线程安全问题。一般发生在于操作了同一个数据。

为避免出现安全问题,可以让多个线程在操作数据的时候排队执行。

线程安全1—同步代码块

让线程有三种不同的排队方式,都是通过加锁的方式使得线程排队。

同步代码块的锁是:对象名

线程同步synchronized:

  • 格式

synchronized(锁对象){ }

Java中任何对象都能往里面传。

  • 举例子:
 public static void main(String[] args) {
    
    
        //线程不安全
        Runnable run = new Ticket();//此处使用了多态
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

    }

    static class Ticket implements Runnable{
    
    
        private int count = 10;//总票数
        private Object o;

        @Override
        public void run() {
    
    
            while(true){
    
    
                synchronized (o){
    
    
                    if(count>0){
    
    
                        count--;//卖票
                        try {
    
    
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        System.out.println("出票成功,余票还剩:"+count);
                    }else {
    
    
                        break;
                    }
                }
            }
        }
    }

通过打上锁的标记的意思:表示当线程A在操作数据时会打上锁标记,其他线程看到该标记则会在外面等,当线程A操作完之后出来,所有线程又一起抢这个锁,谁抢到了谁就进去操作数据,其他线程则在外面等。

这样可以防止一个数据被同时操作。

线程安全2—同步方法

同步方法逻辑也是通过加锁的方式使之排队,只是写的方法不一样。

同步方法的锁是:

  • 如果方法没用static修饰,就是这个方法的this
  • 如果方法用static修饰,类名.class(字节码文件),如下面例子中(方法用static修饰的话)就是:Ticket.class

用法:对方法修饰synchronized关键字。

举例:

public static void main(String[] args) {
    
    
        //线程不安全
        Runnable run = new Ticket();//此处使用了多态
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

    }

static class Ticket implements Runnable{
    
    
        private int count = 10;//总票数

        @Override
        public void run() {
    
    
            while(true){
    
    
                boolean flag = sale();
                if(!flag)
                    break;
            }
        }

        public synchronized boolean sale(){
    
     //添加关键字synchronized
            if(count>0){
    
    
                count--;//卖票
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("出票成功,余票还剩:"+count);
                return true;
            }else{
    
    
                return false;
            }
        }
    }

线程安全3—显示锁Lock

上面介绍的同代码块和同步方法都属于隐式锁,意思是具体怎么锁不需要管,只需把格式写好就可以。

显示锁:是一个Lock类,我们指需要使用它的子类ReentrantLock。

操作就是自己创建锁对象,自己锁自己解锁。对于锁的操作更加直观。

举例:

public static void main(String[] args) {
    
    
        //线程不安全
        Runnable run = new Ticket();//此处使用了多态
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

   }

static class Ticket3 implements Runnable{
    
    
        private int count = 10;//总票数
        //显示锁 l
        private Lock l = new ReentrantLock();

        @Override
        public void run() {
    
    
            while(true){
    
    
                l.lock();  //代码执行到这里上锁
                if(count>0){
    
    
                    count--;//卖票
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println("出票成功,余票还剩:"+count);
                }else {
    
    
                    break;
                }
                l.unlock();//代码执行到这里解锁。
            }
        }
    }

公平锁和非公平锁

公平锁:指哪个线程先过来,就先拿到锁。

非公平锁:指不管哪个线程先过来,谁先抢到锁就归谁。

上面提到的三种线程安全用到的机制都是非公平锁。

如何使用公平锁:

在显示锁Lock中,private Lock l = new ReentrantLock( )括号里面加上true即就是公平锁。

线程死锁

public class demo4 {
    
    
    /**
     * 线程死锁问题
     */
    public static void main(String[] args) {
    
    
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);

    }

    static class MyThread extends Thread{
    
    
        private Culprit c;
        private Police p;

        public MyThread(Culprit c,Police p){
    
    
            this.c=c;
            this.p=p;
        }

        @Override
        public void run() {
    
    
            p.say(c);
        }
    }

    static class Culprit{
    
    
        public synchronized void say(Police p){
    
    
            System.out.println("罪犯:你放了我,我放人质");
            p.fun();
        }
        public synchronized void fun(){
    
    
            System.out.println("罪犯被放了,罪犯也放了人质");
        }

    }
    static class Police{
    
    
        public synchronized void say(Culprit c){
    
    
            System.out.println("警察:你放了人质,我放了你");
            c.fun();
        }
        public synchronized void fun(){
    
    
            System.out.println("警察放了罪犯,解救了人质");
        }

    }

猜你喜欢

转载自blog.csdn.net/m0_58702068/article/details/121483129
今日推荐