线程基础---入门很简单

1.什么是线程

线程(Thread)是操作系统分配处理器时间的基本单位,也是程序执行的最小单元。一个进程中可以包含多个线程,这些线程可以并行或并发地执行,共享同一进程的资源。

线程就是程序执行过程中的一个打工仔,程序执行离不开进程,线程就是进程单位下的打工仔,它的作用就是让打工效率更高。


2.什么是进程

进程(Process)是正在运行的程序的实例,他是操作系统分配资源和调度人物的基本单位。

每个进程都有自己独立的执行上下文和内存空间等,进程之间不可以直接相互访问,需要通过操作系统提供的机制(进程通信)才能进行数据交换和共享。

一个进程由一个或多个线程组成,这些线程可以并发或者并行执行不同的任务,因此,线程的存在大大增加了进程执行任务的效率和程序的响应速度。


3.进程和线程有什么关联

  1. 进程中包含一个或者多个线程,每个进程中都有一个main线程,它可以开辟新的子线程
  2. 线程不会凭空存在,每一个线程都独属于的某个进程,共享这个进程的资源。
  3. 线程是进程的执行单位,线程可以为进程处理并发的任务。

4.在Java中如何创建线程

创建线程有多种方式,这里只叙述了两种。

4.1继承Thread类

一个类继承了Thread类,就可以作为线程使用,一般会重写Thread里面的run()方法,写上2自己的业务逻辑(Thread类中的run()方法实际上是实现了Runnable接口里面的run()方法)。

Code:Cat类可以作为线程使用

class Cat extends Thread {

    @Override
    public void run() {
        int i = 0;
        while (true) {
            //该线程每隔一秒。在控制台输出一段话
            System.out.println("喵喵喵~~~~~,线程名" + Thread.currentThread().getName() + ++i);
            try {
                Thread.sleep(1000);//线程休眠一秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 80) {
                break;
            }
        }
    }
}

/**
 * 如果主线程已经结束了,如果此时程序内还有其他线程在执行,它并不会导致这个进程的结束
 * 只有当所有线程结束,这个进程才会结束
 */

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        //1.当运行这个程序,底层会开启一个进程,进程会执行到main方法,这个进程会开启一个主(main)线程
        //2.主线程里面又会开辟一个新的线程(可以叫做子线程),也就是当执行了start()的时候
        //创建Cat对象,可以当作线程使用
        Cat cat = new Cat();

        //会执行Cat的run()方法
        cat.start();//启动线程--->会自动执行run方法
        //3.当main线程执行了start开启一个新的线程的时候,main线程不会阻塞,会继续执行
        int j = 0;
        System.out.println("主线程开启线程后不会阻塞,会继续执行");
        for (int i = 0; i < 60; i++) {
            System.out.println("主线程 i=" + i);
            Thread.sleep(1000);
        }//4.主线程和子线程会交替执行
    }
}

4.2实现Runnable接口

一个类实现了Runable接口,也可以作为一个线程使用,实现里面的run()方法编写自己的逻辑。

注意点:实现Runnable接口实现里面的run()方法后,发现在main函数里面直接调用start()方法没用,因为不存在该方法;同时也不能直接调用run()方法,因为这样还是调用一个普通函数。

解决办法:在Thread类的构造函数中,传入dog对象作为参数。这意味着该线程对象将执行dog对象中定义的任务或逻辑。

Code:Dog实现了Runnable接口就可以作为线程使用

package com.hua.runnable;

public class TestRunnableCreate {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //当使用Runnable接口实现多线程的方式不能直接调用start方法,采取以下方式实现
        Thread thread = new Thread(dog);
        //底层使用了静态代理设计模式
        thread.start();
    }
}

/**
 * 实现多线程的方式二:实现一个Runnable接口
 */
class Dog implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (true) {
            System.out.println("小狗汪汪叫。。。hi" + count + "-" + Thread.currentThread().getName());
            count++;
            try {
                Thread.sleep(1000);
                if (count == 10) {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

疑问:为什么不直接调用Cat里面的run()方法呢?而是通过调用start()方法。

1.如果直接在main函数里面用Cat.run()调用方法的话,就是一个普通的调用,会在当前线程中串行执行,并没有创建出一个线程然后调用run()方法,并非并发或并行执行,而且通过Thread.currentThread().getName()得到的线程名是main线程。

2.start()方法负责完成线程的初始化工作并启动线程的执行。在调用 start() 方法后,线程会进入就绪状态,并等待系统调度来执行。

3.通过调用 start() 方法,系统会为线程分配 CPU 时间片,并与其他线程并发执行,实现真正的多线程并发。

总结:使用 start() 方法启动线程能够实现真正的并发执行,而直接调用 run() 方法只是普通的方法调用,并没有创建新的线程。


5.注意点

真正实现多线程的不是run()方法,而是start()底层的start0()方法,它是一个native方法(底层由c/c++实现),由JVM自动调用,我们无法调用它。


6.线程常用方法

  1. setName---------//设置线程名称
  2. getName---------//得到该线程的名称
  3. start---------------//使线程开始执行;JVM底层调用该线程的start0方法
  4. run----------------//调用线程对象run方法
  5. setPriority-------//设置线程的优先级
  6. getPriority-------//得到线程的优先级
  7. sleep-------------//在指定毫秒数内让当前正在执行的线程休眠(暂停执行)
  8. getState---------//获取进程当前的状态(生命周期六大状态)
  9. interrupt---------//中断线程(如果正在休眠会中断休眠)
  10. yield--------------//线程礼让,让出cpu让其他线程执行先,但是不一定会让出cpu,底层调度还要取决于cpu
  11. join---------------//线程插队,直接让出cpu让其他线程执行,如:在t1中执行t2.join()方法后,t1会让t2执行完t2的任务后再重新执行自身的任务,此时t1的状态是阻塞,阻塞不会影响cpu的性能

6.线程的生命周期

线程的生命周期分为六个

  1. New(新建状态),当线程被创建(new),它就处于New状态,此时还没用start开启;
  2. Runnable(可运行状态),当线程调用了start()方法,就处于可运行状态,此时它已经在操作系统底层线程池中被调度,等待cpu分配时间片等资源来执行;
  3. Block(阻塞状态),如果一个运行中的线程被停止或者阻塞,它就处于阻塞状态了,此时该线程不能被执行,只有得到主动唤醒的通知后,它才会进入可运行状态;
  4. Waitting(等待状态),当线程执行需要等待条件时候,它就会处于等待状态,只有当条件满足被唤醒后,才会进入可运行状态,如果该线程此时持有锁,会释放它所持有的锁,以便其他线程继续执行;
  5. TimeWaitting(计时等待状态),与等待状态类似,只是该状态会有一个倒计时条件,在指定时间内线程会等待某个条件的发生,条件满足或时间到了会直接唤醒,进入可运行状态;
  6. Terminated(结束状态),当线程run()方法执行过程中遇到了没有捕获到到的异常时候,它就进入结束状态,此时该线程无法继续执行。

图取自度娘:


7.线程同步机制

7.1 概念(为什么要引入线程同步机制)

多个线程同时访问共享资源,从而可能出现以下问题:

  1. 竞态条件(Race Condition):由于多个线程同时访问共享资源,可能会导致数据不一致、结果错误等问题。比如,在多线程环境下对一个变量进行递增操作时,多个线程可能会读取到相同的值并执行相同的加法操作,导致最终结果不正确。

  2. 死锁:如果多个线程彼此占有对方需要的资源,则可能会出现死锁现象,导致所有线程都无法继续执行。

  3. 饥饿:某个线程可能始终无法获取到需要的资源,导致一直处于等待状态,无法执行任务。

线程同步机制可以保证在任意时刻只有一个线程可以访问共享资源,从而避免竞态条件。同时,线程同步机制还可以协调多个线程之间的执行顺序,避免出现死锁和饥饿等问题。

7.2 eg:模拟售票
public class SellTicket02 implements Runnable {
    private int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("余票不足,停止售票");
                break;
            }
            try {
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

开启多线程模拟多个售票窗口同时售票 

public class Ticket {
    public static void main(String[] args) {

        SellTicket02 sellTicket02 = new SellTicket02();
        Thread thread1 = new Thread(sellTicket02);
        Thread thread2 = new Thread(sellTicket02);
        Thread thread3 = new Thread(sellTicket02);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这样执行会有一个问题就是售票可能会出现负数票的情况,并且会数据不同步,比如:

  1. 线程1读取ticketNum为100,线程2读取ticketNum为100,线程3读取ticketNum为100;
  2. 线程1对ticketNum进行了减一操作,更新后为99;
  3. 线程2对ticketNum进行了减一操作,更新后为99;
  4. 线程3对ticketNum进行了减一操作,更新后为99。

在以上过程中,票数的实际值应该是97,但最终结果却为99,出现了数据不一致的情况。

如果最后只有一张票的时候多个线程进入了,那么就会出现负数了。这里我们就可以使用线程同步机制,使用synchronized关键字。



改进(使用synchronized)

//使用Synchronized线程同步锁,可以实现同一时刻多个线程的操作只有一个才会进入
//其他线程必须等它操作完成后才能执行----线程同步
public class SellTicket03 implements Runnable {
    private int ticketNum = 1000;
    private Boolean loop = true;

    public synchronized void sell() {
        if (ticketNum <= 0) {
            System.out.println("余票不足,停止售票");
            loop = false;
            return;
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));

    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}
public class Ticket {
    public static void main(String[] args) {

        //第三种方式,同步锁加上
        SellTicket03 sellTicket03 = new SellTicket03();
        Thread thread1 = new Thread(sellTicket03);
        Thread thread2 = new Thread(sellTicket03);
        Thread thread3 = new Thread(sellTicket03);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

售票将不会出现负数的情况,,并且数据同步,没有出现异常。

7.3 互斥锁
  • 互斥锁是一种用于保护共享资源的线程同步机制。它可以确保在任意时刻只有一个线程可以访问共享资源,从而避免了多个线程同时访问共享资源导致的竞态条件问题。

7.3.1基本思想

    互斥锁的基本思想是:在访问共享资源之前先尝试获取锁。如果获取到了锁,就进入临界区(即访问共享资源的区域)执行相应操作;如果没有获取到锁,则等待直到获取到锁为止。当一个线程正在访问共享资源时,其他线程都会被阻塞,等待该线程释放锁之后才能继续尝试获取锁。

7.3.2实现方式--使用synchronized关键字定义互斥锁
  • 方式一,直接给方法加锁
//同步方法:在方法声明中使用synchronized关键字,表示该方法为同步方法。
//当一个线程正在访问该方法时,其他线程都会被阻塞,直到该线程退出该方法。
    public synchronized void sell() {

            if (ticketNum <= 0) {
                System.out.println("余票不足,停止售票");
                loop = false;
                return;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));
        }
  • 方式二 ,给代码块添加锁,如上面的sell方法,这里可以不用this,只要是同一个对象即可
//同步块:使用synchronized关键字定义一个同步块,表示在其中的代码需要获取某个对象的锁。
//同步块可以指定锁定的对象,如果不指定则默认为this对象。
    public /*synchronized*/ void sell() {

        //二.给代码块加锁
        synchronized (this) {//这里可以不用this,只要是同一个对象就可以
            if (ticketNum <= 0) {
                System.out.println("余票不足,停止售票");
                loop = false;
                return;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));

        }
    }

不使用this 

package com.hua.sychronized;

//使用Synchronized线程同步锁,可以实现同一时刻多个线程的操作只有一个才会生效
//其他线程必须等它操作完成后才能执行----线程同步
public class SellTicket03 implements Runnable {
    private int ticketNum = 100;
    private Boolean loop = true;
    private Object lock = new Object();

    public  void sell() {

        synchronized (lock) {
            if (ticketNum <= 0) {
                System.out.println("余票不足,停止售票");
                loop = false;
                return;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口出售了一张票,余票:" + (--ticketNum));

        }
    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}
用的比较多的是代码块加锁,因为直接给方法加锁可能会造成性能浪费。
 7.3.3 当synchronized遇到static
  • 当同步锁是加在静态方法上的时候,实际上这个锁是加在当前类本身的
public synchronized static void sell(){

    System.out.println("11111");
}
  • 当同步锁加在静态方法内部的代码块上的时候,这个时候使用this是行不通的,要使用类对象,同时一定要记得对象必须一致。
//如果当前类叫Thread01
public static void sell(){

    synchronized(Thread01.class){
        System.out.println("1111111");
    }
}
7.3.4  互斥锁中注意事项

1、同步方法如果没有被static修饰,默认锁对象为this;

2、如果被static修饰,默认锁对象为类名.class;

3、多个线程的锁对象必须是同一个对象;

7.4 死锁
7.4.1 概念

        死锁是指两个或多个线程互相持有对方需要的资源,并且都在等待对方释放资源,导致所有线程无法继续执行的情况。简单来说,就是一种循环等待的状态。

7.4.2 产生死锁的条件
  1. 互斥条件:共享资源同时只能被一个线程占用。
  2. 请求与保持条件:一个线程已经持有了至少一个资源,并且在等待其他线程释放所需的资源。
  3. 不可剥夺条件:线程已经获得的资源不能被其他线程强制性地抢占。
  4. 循环等待条件:若干个线程形成一种头尾相接的循环等待资源关系。
7.4.3 例子

        假设有两个线程A和B,以及两个资源X和Y。线程A首先获取到资源X,线程B首先获取到资源Y。然后,线程A想要获取资源Y,而线程B想要获取资源X。由于互斥条件,每个资源一次只能被一个线程持有,所以线程A无法获取到资源Y,线程B也无法获取到资源X。这时,线程A和线程B就进入了互相等待对方释放资源的状态,从而形成了死锁。

7.4.4 解决死锁的方式
  1. 避免死锁:通过合理的资源分配和调度策略,避免系统进入死锁状态。
  2. 破环互斥条件:对某些资源进行改造,使其不再是互斥的,从而避免死锁的发生。
  3. 破坏请求与保持条件:一次性申请所有所需资源,或者在申请资源时释放已拥有的资源,从而破坏死锁发生所需要的请求和持有的条件。
  4. 破坏不可剥夺条件:允许系统强制性地抢占线程所持有的资源,从而破坏死锁发生所需要的不可剥夺条件。
  5. 破坏循环等待条件:按照规定的顺序申请资源,避免循环等待的情况发生。

7.5 释放锁
7.5.1以下情况会发生释放锁
  1. 当前线程的同步方法,同步代码块执行完毕;
  2. 当前代码块执行遇到break或return;
  3. 当前线程在同步方法或同步代码块中遇到未处理的Error或Exception,导致异常结束;
  4. 程序中主动停止,如使用了wait()方法;
  5. sleep并不会释放锁,而是会将锁持有进入阻塞状态;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值