Java线程基础

大家好呀!我是小笙!我学习了韩顺平老师的线程基础的知识,收获颇丰!现在来和大家分享笔记!

线程

进程和线程的概念

概念:进程是指运行中的程序,是程序的一次执行过程或是正在运行的一个程序。动态过程:产生,存在,消亡的过程

image-20220306182410060

那么线程是什么?

线程由进程创建,是进程的一个实体,一个进程可以拥有多个线程

  • 单线程:同一个时刻只允许一个线程
  • 多线程:同一个时刻,可以执行多个线程

并发:同一个时刻,多个任务交替执行,单核cpu实现多任务

并行:同一个时刻,多个任务同时执行,多核cpu可以实现并行执行多任务

那我们为什么要用多线程而不是多进程呢?

线程间的切换和调度成本远小于进程

线程的生命周期

public enum State {
    // 创建进程,但是资源条件未满足
    NEW,
    // 运行进程
    RUNNABLE,
    // 阻塞进程
    BLOCKED,
    // 无时间限制等待notify()方法唤醒
    WAITING,
    // 有时间限制等待notify()方法唤醒
    TIMED_WAITING,
    // 结束进程
    TERMINATED;
}

image-20220309092226604

image-20220309205604317

线程基本使用

创建线程的两种方式
  1. 继承Thread类,重写run方法(本质:Thread类也实现了Runable接口)

  2. 实现Runable接口,重写run方法

    // 使用Thread构造接受实现了Runnable的类,可以调用start()方法
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
    

image-20220306191925689

源码解析多线程机制

多线程机制说明

image-20220307111422720

用例代码

// 疑问:为什么调用start()方法而不是直接调用run()方法,不都是实现run()方法吗?
// 本质区别有没有创建新的线程,直接调用run方法就是和使用普通方法一样没什么区别,并没有创建线程
public class Thread01 extends Thread{
    int times = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread01 thread01 = new Thread01();
        thread01.start();

        for (int i = 0; i < 60; i++) {
            System.out.println(Thread.currentThread().getName()+i);
            Thread.sleep(1000);
        }
    }

    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
            System.out.println("喵喵,我是小猫咪"+ ++times );
            if(times == 80){
                break;
            }
        }
    }
}

使用Terminal – jconsole工具观察进程

注意要main方法和其他进程要持续较长时间。这样子才好观测

image-20220307112032227

源码分析

image-20220307194551556

// 调用线程start方法:thread01.start();
// 源码分析
public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
        if (!started) {
            group.threadStartFailed(this);
        }
        } catch (Throwable ignore) {

        }
    }
}

// 本地方法,开辟线程
private native void start0();

进程终止

stop()方法(不推荐)

为什么stop()方法被废弃而不推荐使用呢?

因为stop()方法太过于暴力,强行把执行到一半的程序强行退出,会导致数据不一致的问腿

自制设置标志位退出
public class StopThread {
    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1();
        thread1.start();
        Thread.sleep(10000);
        thread1.setFlag(false);
    }
}

class Thread1 extends Thread{
    private int count = 0;
    // 设置标志位来判断线程终止时间
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(++count);
        }
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
中断方式退出程序

中断方式类似于之前通过标志位方式退出线程的方法,但是中断更加强劲一些,它可以让等待在sleep或者wait的线程引发异常退出

public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Interrupt thread1 = new Interrupt();
        thread1.start();
        Thread.sleep(10000);
        thread1.interrupt();
    }
}

class Interrupt extends Thread{
    private int count = 0;
    private boolean flag = true;
    @Override
    public void run() {
        while (true){
            if(this.isInterrupted()){
                System.out.println("Interrupted");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 为什么这里还需要中断一次
                // 因为sleep()方法中断抛出的异常会清除中断标志位,因此还需要再中断一次
                this.interrupt();
            }
            System.out.println(++count);
        }
    }
}

线程的常用方法

等待wait()和通知notify()

有些人会好奇,wait和notify方法不是Object类的方法吗,为什么放在线程这里特别拿出来讲?

因为这两个方法平时并不能随便调用,它必须包含在对应的同步块中

public final void wait() throws InterruptedException {
    wait(0L);
}
// 当多个线程在等待,则随机通知其中一个等待线程
public final native void notify();
// 通知所有等待线程
public final native void notifyAll();

Object.wait()方法和Thread.sleep()方法的区别

  • wait()方法可以被唤醒,使用wait方法之后会释放目标对象的锁

  • sleep()方法不会释放任何的资源

等待线程结束join()

join()方法:线程的插队,如果插队的线程一旦插入成功,则肯定先执行完插入的线程的所有任务、

// 无线等待,直到目标线程的任务执行完成
public final void join() throws InterruptedException
// 给出一个最大的等待时间
public final synchronized void join(long millis, int nanos) throws InterruptedException

示例代码

public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread3 t = new Thread3();
        t.start();
        for (int i = 1; i <= 20; i++) {
            if(i == 5){
                System.out.println("让Thread3先完成");
                t.join();
                System.out.println("main继续执行");
            }
            System.out.println("main: " + i);
            Thread.sleep(1000);
        }
    }
}

class Thread3 extends Thread{
    private int count;
    @Override
    public void run() {
        while(true){
            if(count++ == 20){
                System.out.println("Thread3结束了");
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread3: "+count);
        }
    }
}

join()方法的底层源码

// join方法的本质就是调用wait方法在当前对象实例进行等待
// 被等待的线程会在执行完成后调用notifyAll()方法唤醒等待的进程进程
public final synchronized void join(final long millis)
    throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                   TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) 
        // 测试此线程是否存在。如果线程已启动且尚未死亡,则该线程处于活动状态 RUNNABLE状态
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}
谦让yeild()

yeild():线程的礼让,让出cpu让其他进程执行,但是礼让的时间不确定,也不一定礼让成功,还有就是当前处理器是否忙碌,如果处理器完成处理的过来,就不会进行礼让

使用场景:当你觉得这个线程不重要或者优先级很低,那适当让出cpu给那些更重要的线程是否是一个明智之举

用户线程和守护线程

用户线程:又称工作线程,当执行的任务执行完或通知方式结束

守护线程:一般是为工作线程服务,当所有线程结束,守护线程自动结束(比如:垃圾回收机制)

public class ThreadMethod {
    public static void main(String[] args) {
        MyDaemonThread md  = new MyDaemonThread();
        // 设置为守护线程
        md.setDaemon(true);
        md.start();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
            System.out.println("用户线程在此");
        }
    }
}

class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
            System.out.println("守护线程在此");
        }
    }
}

线程同步机制

同步概念:当有多个线程同时在对内存进行操作,在某一个时刻只允许一个线程对该内存进行操作(比如:写操作)

关键字synchronized的作用是实现线程的同步,它的工作是对同步代码枷锁,使得每一次只能有一个线程进入同步块,从而保证了线程的安全

关键字synchronized的用法

// 指定锁对象  默认锁对象就是this
synchronized(对象){} // 需要得到对象的锁,才能操作同步代码
// 直接作用于实例方法  默认锁对象就是this
public synchronized void method(){}
// 直接作用于静态方法  默认锁对象就是当前类.class
public static synchronized void method(){}
public class increase01 implements Runnable{
    static int count = 0;
    static int count2 = 0;

    public static synchronized  void increase(){
        count++;
    }
    public  synchronized  void increase2(){
        count2++;
    }
    public static void main(String[] args) throws InterruptedException {
        // 如果同一类传入的对象不同,对象锁就无法启到作用了,必须使用类的锁才可以锁住
        Thread t1 = new Thread(new increase01());
        Thread t2 = new Thread(new increase01());
        t1.start();t2.start();
        t1.join(); t2.join();
        System.out.println(count); // 20000000
        System.out.println(count2); // 小于20000000
        
        i
        System.out.println("-------");
        increase01.count2 = 0;
        increase01.count = 0;
        // 传入了相同对象,就不需要使用静态锁,对象锁就可以实现
        increase01 inc = new increase01();
        Thread thread = new Thread(inc);
        Thread thread1 = new Thread(inc);
        thread.start(); thread1.start();
        thread.join(); thread1.join();
        System.out.println(count);  // 20000000
        System.out.println(count2); // 20000000
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000000; i++) {
            increase();
        }
    }
}

释放锁

四种情况释放锁

  • 当前线程的同步方法和同步代码块执行结束
  • 当前的线程在同步代码块和同步方法中遇到break,return
  • 当前线程在同步代码块中出现了未处理的Error或者Exception导致被迫退出
  • 当前的线程在同步代码块或者同步方法中执行了wait房啊,暂停当前的线程同时释放资源

二种情况不释放锁

  • 线程执行同步代码块或者同步方法时,程序调用Thread.sleep()和Thread.yield()方法不会释放锁
  • 线程执行同步代码块或者同步方法时,其他线程调用suspend方法将它挂起,此时它并不会释放该锁(不推荐使用挂起方法)

编程题

1.在main方法中启动两个线程,在第一个线程循环随机打印100 以内的整数,直到第二个线程从键盘中读取了‘Q’命令就终止了第一个线程

public class Homework01 {
    public static void main(String[] args) throws InterruptedException {
        RandomNum randomNum = new RandomNum();
        Thread thread = new Thread(randomNum);
        Thread thread2 = new Thread(new Input(randomNum));
        thread.start();
        thread2.start();
    }
}

// 线程1
class RandomNum implements Runnable{
    private boolean loop = true;

    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {
        while(loop){
            System.out.println((int)(Math.random()*100));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.getMessage();
            }
        }
        System.out.println("RandomNum退出程序");
    }
}

// 线程2
class Input implements Runnable{

    private  RandomNum r;

    public Input(RandomNum r) {
        this.r = r;
    }

    @Override
    public void run() {
        Scanner sc = new Scanner(System.in);
        while(true){
            char c = sc.nextLine().charAt(0);
            if(c == 'Q'){
                r.setLoop(false);
            }
            System.out.println(c);
            if()
        }
    }
}

2.有两个用户分别从同一个卡上取钱(总金额10000).每次只能取1000元,当金额不足够时,就不能取款了,不能出现超额取款

public class Homework02 {
    public static void main(String[] args) {
        // 同一个对象 指代的是从同一个卡上取款
        withdrawals withdrawals = new withdrawals();
        // 不用线程指代的是不用用户取款
        Thread user1 = new Thread(withdrawals);
        Thread user2 = new Thread(withdrawals);
        user1.start();
        user2.start();
    }
}

class withdrawals implements Runnable{
    static int moneySum = 10000;
    @Override
    public void run() {
        while(true){synchronized(this){
            if(moneySum>= 1000){
                moneySum -= 1000;
                System.out.println(Thread.currentThread().getName() +": 取款1000元 ");
                System.out.println("余额剩余: "+moneySum);
            }else{
                System.out.println("余额不足...");
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }}
    }
}

相关面试题

1.创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。

  • 通过继承Thread类来创建并启动线程的步骤如下:
    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象。
    3. 调用线程对象的start()方法来启动该线程。
  • 通过实现Runnable接口来创建并启动线程的步骤如下:
    1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
    2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
    3. 调用线程对象的start()方法来启动该线程。
  • 通过实现Callable接口来创建并启动线程的步骤如下:
    1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
    2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

2.线程是否可以重复启动,会有什么后果?

只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。

扩展阅读

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

3. 如何实现线程同步?

  1. 同步方法

    即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

  2. 同步代码块

    即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

  3. ReentrantLock

    Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。

  4. volatile

    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

  5. 原子变量

    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

4.说一说Java多线程之间的通信方式

  1. wait()、notify()、notifyAll()

    如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。

    wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

  2. await()、signal()、signalAll()

    如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

  3. BlockingQueue

    Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

    程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

5.说一说sleep()和wait()的区别

  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗念笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值