多线程基础

1 篇文章 0 订阅
1 篇文章 0 订阅

线程的创建

1. 通过继承Thread类实现多线程

通过继承Thread类来自定义一个线程类, 并重写run()方法. run()方法是一个线程的线程体, 也就是开启这个线程后它要执行的部分. 之后, 直接在main方法中通过new()的方式创建两个线程实例, 再通过start()方法, 让这两个线程运行起来. 运行起来之后就开始执行run()方法中的代码了.

为了方便我们看到结果, 使用getName()方法来获取当前线程的name. 这个方法是父类Thread中的方法, 直接调用即可.

public class ThreadTest extends Thread {

    public ThreadTest() {
        System.out.println(this.getName());
    }

    /**
     * 线程的线程体
     */
    @Override
    public void run() {
        System.out.println(this.getName() + "线程开始");
        for (int i = 0; i < 20; i++) {
            System.out.println(this.getName() + " " + i);
        }
        System.out.println(this.getName() + "线程结束");
    }

    public static void main(String[] args) {
        System.out.println("主线程开始");
        // 创建线程
        ThreadTest t1 = new ThreadTest();
        ThreadTest t2 = new ThreadTest();
        // 启动线程
        t1.start();
        t2.start();
        System.out.println("主线程结束");
    }
}

2. 通过实现Runnable接口实现多线程

通过继承Thread类的方式确实可以帮助我们创建自己的线程, 但是由于java单继承的特点, 我们自定义的线程类无法再继承其他的类, 这样会给我们带来一些限制. 此时我们可以采取实现Runnable接口的方式创建我们自己的线程. 查看源码可知, Thread类本身也是继承了Runnable接口的.

// Thread类源码
public
class Thread implements Runnable {
    ...
}

Runnable接口中只有一个方法, 就是run()方法, 可以看出, 这就是我们多线程的核心方法.

// Runnable接口源码
public interface Runnable {
    public abstract void run();
}

在我们的测试类中, 依然是对run()方法的重写, 不同的是由于我们没有继承Thread类, 于是就没有了getName()方法. 不过也可以使用Thread.currentThread()方法得到当前的线程的对象实体, 再调用getName()方法就可以了.

Thread.currentThread()方法是Thread类中的一个静态方法, 返回的就是当前线程的对象实体.

// Thread类源码
/**
 * Returns a reference to the currently executing thread object.
 *
 * @return  the currently executing thread.
 */         
public static native Thread currentThread();

由于我们的测试类只是实现了Runnable接口, 所以它并没有start()方法, 那么怎么让它启动呢? 这时我们就需要把测试方法包装成Thread对象. 方式就是Thread t1 = new Thread(new RunnableTest());.

这里还有一点需要注意的是虽然我们的测试类的构造方法Runnable()是我们自定义线程的构造方法, 但是在我们new RunnableTest()时, 这个构造方法是在主线程(即main)中执行的, 也就是说line3这段代码的输出结果是main. 其原因就是这段代码是在main线程中执行的, 此时我们的自定义线程还没开启呢.

我们的自定义线程通过start()方法启动之后, 就开始执行run()方法, 在run()方法中(即线程体中)执行的Thread.currentThread().getName()得到的才是我们自定义线程的名字.

public class RunnableTest implements Runnable {
    public RunnableTest() {
        System.out.println(Thread.currentThread().getName());
    }
    /**
     * 当前线程的线程体
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始");
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        System.out.println(Thread.currentThread().getName() + "线程结束");
    }
    public static void main(String[] args) {
        System.out.println("主线程开始");
        Thread t1 = new Thread(new RunnableTest());
        Thread t2 = new Thread(new RunnableTest());

        t1.start();
        t2.start();

        System.out.println("主线程结束");
    }
}

另外, 如果我们自定义的线程类只需要使用一次, 那么可以使用匿名内部类的方式书写如下:

Thread t1 = new Thread(new Runnable(){
    @Override
    public void run() {
        ...
    }
});

Runnable接口中只有一个方法, 这种接口我们称之为函数式接口, 那么就可以使用lambda表达式来书写. 如果方法体只有一行, {}都可以省略.

Thread t3 = new Thread(() -> {
    ...
});

线程的声明周期

在这里插入图片描述

新生状态(New)

用 new 关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用 start 方法进入就绪状态。

就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到 CPU,处于“线程就绪队列”,等待系统为其分配 CPU。就绪状态并不是执行状态,当系统选定一个等待执行的 Thread 对象后,它就会进入执行状态。一旦获得 CPU,线程就进入运行状态并自动调用自己的 run 方法。有 4 中原因会导致线程进入就绪状态:

  1. 新建线程:调用 start()方法,进入就绪状态;

  2. 阻塞线程:阻塞解除,进入就绪状态;

  3. 运行线程:调用 yield()方法,直接进入就绪状态;

  4. 运行线程:JVM 将 CPU 资源从本线程切换到其他线程。

运行状态(Running)

在运行状态的线程执行自己 run 方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有 4 种原因会导致阻塞:

  1. 执行 sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

  2. 执行 wait()方法,使当前线程进入阻塞状态。当使用 nofity()方法唤醒这个线程后,它进入就绪状态。

  3. 线程运行时,某个操作进入阻塞状态,比如执行 IO 流操作read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

  4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它 run()方法内的全部工作; 另一个是线程被强制终止,如通过执行 stop()destroy()方法来终止一个线程(注:stop()/destroy()方法已经被 JDK 废弃,不推荐使用)。

当一个线程进入死亡状态以后,就不能再回到其它状态了。

线程的使用

1. 终止线程

如果我们想在一个线程中终止另一个线程我们一般不使用 JDK 提供的 stop()/destroy()方法(它们本身也被 JDK 废弃了)这种方式过于粗暴, 线程会直接挂掉, 就算有后续的收尾工作也不会执行。

通常的做法是提供一个 boolean 型的终止变量,当这个变量值为 false 时,则终止线程的运行。

public class StopThreadTest implements Runnable{

    private boolean flag = true;

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始");
        int i = 0;
        while(flag) {
            System.out.println(Thread.currentThread().getName() + " " + i++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //模拟线程的收尾工作
        System.out.println(Thread.currentThread().getName() + "线程结束");
    }

    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) throws IOException {
        System.out.println("主线程开始");
        StopThreadTest st = new StopThreadTest();
        Thread t1 = new Thread(st);
        t1.start();
        System.in.read();
        st.stop();
        System.out.println("主线程结束");
    }
}

2. 暂停线程

sleep()方法

**作用: **让正在运行的线程休眠一段时间并进入阻塞状态, 等待休眠结束后再进入就绪队列.

public class SleepThreadTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始");
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            try {
                // 让线程暂停, 并设置时间为1000, 单位是毫秒(millis)
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "线程结束");
    }

    public static void main(String[] args) {
        System.out.println("主线程开始");
        Thread t0 = new Thread(new SleepThreadTest());
        t0.start();
        System.out.println("主线程结束");
    }
}

yield()方法

**作用: **让正在运行的线程停止运行, 让出cpu, 进入就绪队列排队.

注意: yield()方法是Thread类的一个静态方法. 使用yield()方法使线程重新排队后, 该线程有一定几率会被调度程序再次选中, 所以该方法让出cpu是有可能失败的.

public class YieldThreadTest implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i == 0 && "Thread-1".equals(Thread.currentThread().getName())) {
                // 暂停线程让出cpu, 但是可能会失败
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        Thread t0 = new Thread(new YieldThreadTest());
        Thread t1 = new Thread(new YieldThreadTest());
        t0.start();
        t1.start();
    }
}

3. 联合线程

线程A邀请线程B优先执行, 在线程B执行完之后线程A才能继续执行, 类似于方法的嵌套调用. "邀请"的方式是调用线程Bjoin()方法.

join() 方法的使用

join()方法就是指调用该方法的线程在执行完 run()方法后,再执行 join ()方法后面的代码, 即将两个线程合并,用于实现同步控制.

在下面的实例代码中, 主线程(main)和在执行的过程中执行了t0.join(), 联合了Thread-0线程, 于是main线程停止执行, 并等待Thread-0执行完再接着执行. 于此同时Thread-1线程不受任何影响. 依然是与其他线程并发执行.

public class JoinTreadTest {
    public static void main(String[] args) throws InterruptedException{
        Thread t0 = new Thread(new ThreadA());
        Thread t1 = new Thread(new ThreadB());
        t0.start();
        t1.start();
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " "+ i);
            if (i == 2) {
                t0.join();
            }
            Thread.sleep(1000);
        }
    }
}

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

联合线程实例

使用线程联合的方式实现同步.

public class JoinDemo {
    public static void main(String[] args) {
        System.out.println("爸爸和儿子买烟的故事");
        Thread t = new Thread(new FatherTread());
        t.start();
    }
}

class FatherTread implements Runnable {
    @Override
    public void run() {
        System.out.println("爸爸想抽烟, 发现烟抽完了");
        System.out.println("爸爸让儿子去买一包红塔山");
        Thread t = new Thread(new SonThread());
        System.out.println("等待儿子买烟回来");
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("儿子丢了, 爸爸出门找儿子");
            System.exit(1);
        }
        System.out.println("爸爸抽上了烟, 并把零钱给了儿子");
    }
}
class SonThread implements Runnable {
    @Override
    public void run() {
        System.out.println("儿子出门买烟");
        System.out.println("儿子买烟需要十分钟");
        for (int i = 0; i < 10; i++) {
            System.out.println("第" + i + "分钟");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4. Thread类其他常用方法

4.1 获取线程名称

  • this.getName()获取当前线程名称, 适用于继承Thread类实现的多线程方式.
  • Thread.currentThread.getName()先获取当前线程的实例, 再获取当前线程名称.

4.2 设置线程名称

  • 通过构造方法设置线程名称

    Thread类有一个public Thread(String name)的构造方法, 子类继承Thread类之后, 可以在子类的构造方法的第一行调用父类的这个构造方法super(name)

  • 通过Thread类的setName()方法设置线程名称

    无论是通过继承Thread类, 还是通过实现Runnable接口实现的多线程, 都可以在线程对象创建完成后, 直接调用Thread类的普通方法setName()来为线程设置名称.

4.3 判断当前线程是否存活

thread.isAlive()方法: 判断当前线程是否处于活动状态.

线程创建并启动之后(执行thread.start()方法), 到执行完自己的run()(线程体)之前, 都属于活动状态.

线程的优先级

1. 什么是线程优先级

每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保 证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从 1 到 10, 一个线程的缺省优先级是 5。 Java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关, 如非特别需要,一般无需设置线程优先级。

注意:线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就 是有可能被执行的概率高。并不是优先执行。

2. 线程优先级的使用

  • thread.getPriority()方法可以获取线程的优先级
  • thread.setPriority()方法可以设置线程的优先级

**注意: **线程优先级必须在线程启动之前(start())设置, 启动之后设置是无效的.

守护线程

1. 什么是守护线程

在 Java 中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。

  • Daemon Thread(守护线程):服务于用户线程的线程, 比如垃圾回收线程,就是最典型的守护线程。

    守护线程特点: 守护线程会随着用户线程死亡而死亡

2. 守护线程与用户线程的区别

用户线程,不随着主线程的死亡而死亡。

用户线程只有两种情况会死掉,

  1. run()方法执行过程中异常终止。
  2. 正常把run()方法执行完毕,线程死亡。

守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡.

3. 守护线程的使用

使用setDaemon()方法设置守护线程

public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new UserThread());
        t.start();
    }
}


/**
 * 守护线程
 */
class DaemonThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 用户线程
 */
class UserThread implements Runnable {
    @Override
    public void run() {
        Thread daemonThread = new Thread(new DaemonThread());
        // 将该线程设为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程同步

1. 什么是线程同步

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

1.1 线程冲突

当多个线程同时操作某个资源时很容易就会出现冲突. 如果多个线程同时执行写操作就容易出现覆盖的情况.

1.2 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时 访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用.

2. 实现线程同步

2.1 synchronized关键字

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突 的问题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线 程同时访问造成的这种问题。这套机制就是 synchronized 关键字。

// synchronized 语法结构: 
public synchronized void accessVal(int newVal); 

synchronized(锁对象){ 
    同步代码
} 

synchronized 关键字使用时需要考虑的问题:

  • 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。
  • 需要对哪些线程中的代码具有互斥能力(通过 synchronized 锁对象来决定)。 它包括两种用法:synchronized 方法和 synchronized 块。

2.2 synchronized关键字的使用

  • synchronized 方法

    通过在方法声明中加入 synchronized 关键字来声明,语法如下:

    public synchronized void method() {
        
    }
    

    synchronized 在方法声明时使用:放在访问权限修饰符(public)之后,返回类型声明(void) 之前, 也可以放在最前面, 但是不太规范。这时同一个对象下synchronized 方法在多线程中执行时,该方法是同步的,即一次 只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

  • synchronized 块

    synchronized 方法的缺陷:如果直接将一整个方法声明为 synchronized 将会大大影响效率。

    Java 为我们提供了更好的解决办法,那就是synchronized块, 可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率.

    synchronized(锁对象){ 
        ...
    } 
    

synchronized关键字不能被继承

按对象锁的类型分类
使用this作为锁

在不同线程中, 调用相同对象的synchronized(){} 会发生互斥.

本质是对同步代码所在的对象加锁.

  • 语法结构
synchronized(this) {
    // 同步代码
}
// 等同于对普通方法加synchronized
public synchronized void method() {
    
}
使用字符串作为锁

所有使用统一字符串为对象锁的synchronized(){}代码块都会同步.

本质是对字符串常量对象加锁

  • 语法结构
synchronized("String") {
    // 同步代码
}
使用Class作为锁

在不同的线程中, 使用统一Class对象的synchronized(){} 代码块会同步执行.

本质是对类的class对象加锁,

  • 语法结构
synchronized(XX.class) {
    // 同步代码
}
// 等同于对类的静态方法加synchronized修饰
public synchronized static void staticMethod() {
    // 同步代码
}

总结

无论是使用何种类型的对象锁, 本质都是对对象加锁, 字符串和类对象的本质也是对象, 在对象的内存结构中, 对象头的位置, 存储量该对象的锁信息, 加锁的实质就是修改这个锁信息, 当某个线程需要获取锁时, 会先判断对象是否已经加锁, 如果没有就自己加锁, 然后执行同步代码, 如果已被加锁, 就需要等待锁被释放, 再去尝试加锁.

使用自定义对象作为锁
synchronized关键字的使用方式分类
  • 修饰普通方法: 锁当前对象
  • 修饰静态方法: 锁当前对象所属类的类对象
  • 包裹代码块: 小括号里写什么就对什么加锁

3. wait 和 notify

方法名所用
final void wait()表示线程一直等待, 直到得到其他线程通知
void wait(long timeout)线程等待指定的毫秒数
final void wait(long timeout, int nanos)线程等待指定的毫秒, 微秒时间
final void notify()唤醒一个处于等待状态的线程
final void notifyAll()唤醒同一个对象上, 所有调用wait()方法的线程, 优先级高的先运行

以上方法斗是java.lang.Object类的方法;

都只能在同步方法或者同步代码块中使用, 否则会抛出异常.

死锁

1. 死锁的概念

1.1 死锁的定义

多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.

1.2 死锁产生的原因

  • 系统资源的竞争
  • 线程推进顺序不合理

1.3 死锁产生的必要条件

  • 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
  • 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
  • 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
  • 循环等待–这些陷入死锁的线程中存在一个循环等待链

| final void wait(long timeout, int nanos) | 线程等待指定的毫秒, 微秒时间 |
| final void notify() | 唤醒一个处于等待状态的线程 |
| final void notifyAll() | 唤醒同一个对象上, 所有调用wait()方法的线程, 优先级高的先运行 |

以上方法斗是java.lang.Object类的方法;

都只能在同步方法或者同步代码块中使用, 否则会抛出异常.

死锁

1. 死锁的概念

1.1 死锁的定义

多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.

1.2 死锁产生的原因

  • 系统资源的竞争
  • 线程推进顺序不合理

1.3 死锁产生的必要条件

  • 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
  • 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
  • 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
  • 循环等待–这些陷入死锁的线程中存在一个循环等待链
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值