多线程


一、多线程的创建

1、继承Thread类,重写run()方法:

①、定义类继承Thread;

②、复写T=hread类中的run方法;

  目的:将自定义代码存储在run方法,让线程运行

③、调用线程的start方法:

  该方法有两步:启动线程,调用run方法。

2、实现Runnable接口,实现接口run()方法:

接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。

①、定义类实现Runnable接口

②、覆盖Runnable接口中的run方法

  将线程要运行的代码放在该run方法中。

③、通过Thread类建立线程对象。

④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

  自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象

⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

3、实现Callable接口,实现call()方法:

①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。

②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值

③、使用FutureTask对象作为Thread对象启动新线程。

④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

三种方式区别

主要是单继承局限性、有无返回值、可否抛出异常3点上

(1)继承Thread:线程代码存放在Thread子类run方法中。

优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。

劣势:已经继承了Thread类,无法再继承其他类。

(2)实现Runnable:线程代码存放在接口的子类的run方法中。

优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

(3)实现Callable

优势:有返回值(用来判断线程是否已经执行完毕或者取消线程执行)、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

劣势:比较复杂、访问线程必须使用Thread.currentThread()方法

(4)异常

Callable()的call()方法可以抛出异常(在调用类中用try/catch捕获),而Runnable()的run()方法不可以

  建议使用实现接口的方式创建多线程。

二、线程的生命周期(状态)

  • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
  • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
  • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡( stop() )三种状态。
  • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
  • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

线程状态的变更

(1)新建New状态

  • 通过start()进入Runnable状态

(2)就绪Runnable状态

  • 获取CPU资源后进入Running

Runnable状态的线程无法直接进入Blocked状态和Terminated状态的。只有处在Running状态的线程,换句话说,只有获得CPU调度执行权的线程才有资格进入Blocked状态和Terminated状态,Runnable状态的线程要么能被转换成Running状态,要么被意外终止。

(3)运行Running状态

  • 被转换成Terminated状态,比如调用 stop() 方法;
  • 被转换成Blocked状态,比如调用了sleep, wait 方法被加入 waitSet 中;
  • 被转换成Blocked状态,如进行 IO 阻塞操作,如查询数据库进入阻塞状态;
  • 被转换成Blocked状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;
  • 该线程的时间片用完,CPU 再次调度,进入Runnable状态;
  • 线程主动调用 yield 方法,让出 CPU 资源,进入Runnable状态

(4)阻塞Blocked状态

  • 被转换成Terminated状态,比如调用 stop() 方法,或者是 JVM 意外 Crash;
  • 被转换成Runnable状态,阻塞时间结束,比如读取到了数据库的数据后;
  • 完成了指定时间的休眠,进入到Runnable状态;
  • 正在wait中的线程,被其他线程调用notify/notifyAll方法唤醒,进入到Runnable状态;
  • 线程获取到了想要的锁资源,进入Runnable状态;
  • 线程在阻塞状态下被打断,如其他线程调用了interrupt方法,进入到Runnable状态;

(5)终止Terminated状态

一旦线程进入了Terminated状态,就意味着这个线程生命的终结,哪些情况下,线程会进入到Terminated状态呢?

  • 线程正常运行结束,生命周期结束;
  • 线程运行过程中出现意外错误;
  • JVM 异常结束,所有的线程生命周期均被结束。

三、多线程同步的实现方法

1、synchronized方法

在方法声明前加入synchronized关键字

public synchronized void Test1(){};        //锁对象是类的实例对象
public synchronized static void Test2(){};        //锁对象是类本身(.class文件)

2、同步代码块

灵活性高

synchronized(锁对象){...}        //锁对象一般是当前类的实例化对象

3、Lock锁

创建Lock接口的实现类ReentrantLock(重入锁)对象,将会出现线程安全的代码用lock()unlock()包裹

ReentrantLock lock = new ReentrantLock();
lock.lock();        //获取锁
//lock.tryLock();    以非阻塞方式尝试获取锁,获取到返回true,否则返回false
...
lock.unlock();        //释放锁

区别

(1)性能不一样

在资源竞争不激烈的情况下,synchronized优于ReentrantLock

在资源竞争激烈的情况下,synchronized性能下降的非常快,而ReentrantLock基本保持不变

(2)锁的机制不一样

synchronized:自动解锁,不会因为异常,导致锁没有释放,而出现死锁

ReentrantLock:需要开发人员手动释放锁,并且必须在finally块中释放,否则会引起死锁。还具有tryLock()方法,能以非阻塞方式尝试获取锁。

四、结束线程的方法

(1)程序正常运行结束

(2)在一些需要长时间运行的线程中,可用一个变量来控制循环,当变量满足某个条件,退出循环。变量定义时使用了一个 Java 关键字 volatile,这个关键字的目的是使 变量同步,也就是说在同一时刻只 能由一个线程来修改变量的值。

(3)stop()方法,线程不安全,调用 thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈 现不一致性

(4)interrupt()方法:

线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实 际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正 常结束run方法。

线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

五、获取所有线程、中断指定线程

Thread thread = new Thread(...);
thread.setName("heartBeat");
thread.start();

// 获取当前线程所在的线程组
ThreadGroup group = Thread.currentThread().getThreadGroup();
// 获取所有线程
while(group != null) {
    Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
    int count = group.enumerate(threads, true);
    for(int i = 0; i < count; i++) {
        // 查找指定线程
        if("heartBeat".equals(threads[i].getName()) && threads[i].isAlive()) {
            // 中断线程
            threads[i].interrupt();
            return "断开成功";
        }
    }
    // 获取该线程组的父线程组
    group = group.getParent();
}

六、sleep()和wait()区别

//线程休眠1秒
Thread.sleep(1000);
TimeUnit.SECONDS.sleep(1);  //企业常用

(1)来自不同的类

sleep()方法属于Thread类中的。而wait()方法,则是属于 Object类中的。

(2)wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!

在调用sleep()方法的过程中,线程不会释放对象锁。

sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然 保持着,当指定的时间到了又会自动恢复运行状态。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(进入阻塞状态),只有针对此 对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态(就绪状态)。

(3)使用范围

wait:在同步代码块中

sleep:可以在任意地方

当你在调用notify(), notifyAll(),wait(), wait(long), wait(long, int)等线程控制操作方法时,必须要有两个前提。第一:必须要在被synchronized关键字控制的同步代码块中,才能调用这些方法。第二,调用者必须为你当前的锁对象。

七、start()和run()区别

(1)start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。

(2)通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运 行。

(3) 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运 行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

(4)如果直接调用run()方法,则不会开启多线程,而是以单线程的方式执行run()方法

八、同步代码块对象锁和类锁区别

synchronized (this):对象锁

synchronized (System.class):类锁

当存在多个对象调用同步代码块时,对象锁只针对单个对象的多线程。而类锁可以锁住所有线程、所有对象。

对象锁

public class SynLockTest {
    @Test
    public void test1() throws IOException {
        //创建连个对象
        User user1 = new User("张三");
        User user2 = new User("李四");
        UserThread userThread1 = new UserThread(user1);
        UserThread userThread2 = new UserThread(user2);
        //2、启动线程
        for (int i = 0; i < 10; i++) {
            new Thread(userThread1).start();
            new Thread(userThread2).start();
        }
        System.in.read();
    }

}
//自定义一个线程类
class UserThread implements Runnable{
    private User user;
    public UserThread(User user){
        this.user = user;
    }
    @Override
    public void run() {
        try {
            user.info();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class User{

    public String username;

    public User(String username){
        this.username = username;
    }

    public void info() throws InterruptedException {
        synchronized (this){
//            System.out.println("this="+this);
            Thread.sleep(1000);
            System.out.println("当前线程的名字为:"+Thread.currentThread().getName()+"____"+username);
        }
    }
}

发现结果是两条数据一起打印的,说明当前的锁并没有起作用

类锁

public void info() throws InterruptedException {
    synchronized (System.class){
        //            System.out.println("this="+this);
        Thread.sleep(1000);
        System.out.println("当前线程的名字为:"+Thread.currentThread().getName()+"____"+username);
    }
}

发现数据是一行一行输出的,说明同步代码块中的锁起到了作用

九、suspend()和 resume()方法

两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume()使其恢复。

注意区别:

初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。

final Object lock = new Object();


@Test
public void test1() throws InterruptedException {

    new Thread(()->{
        synchronized (lock){
            try {
                System.out.println("1-1");
                lock.wait();
                System.out.println("1-2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    new Thread(()->{
        synchronized (lock){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("2-1");
            lock.notifyAll();
            System.out.println("2-2");
        }
    }).start();

    TimeUnit.SECONDS.sleep(5);
}

suspend()resume()

final Object lock = new Object();
final Object lock2 = new Object();

@Test
public void test2() throws InterruptedException {

    Thread thread1 = new Thread(() -> {
        synchronized (lock) {
            try {
                System.out.println("1-1");
                Thread.currentThread().suspend();
                System.out.println("1-2");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    thread1.start();

    new Thread(()->{

        // 如果这里采用 lock, 则会死锁, 因为 suspend() 不会释放锁
        synchronized (lock2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("2-1");
            thread1.resume();
            System.out.println("2-2");
        }
    }).start();

    TimeUnit.SECONDS.sleep(5);
}

两个 @Test 方法的输出

1-1
2-1
2-2
1-2

十、sleep() 和 yield()方法

这两个方法都定义在Thread.java中

**sleep()**的作用是让当前线程休眠(正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务),即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时候会大于或者等于该休眠时间,当时间过后该线程重新被会形式,他会由“阻塞状态”编程“就绪状态”,从而等待cpu的调度执行,注意:sleep方法只是让出了cpu的执行权,并不会释放同步资源锁。

**yield()**的作用是让步,它能够让当前线程从“运行状态”进入到“就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权,也有可能是当前线程又回到“运行状态”继续运行,注意:这里我将上面的“具有相同优先级”的线程直接改为了线程,很多资料都写的是让具有相同优先级的线程开始竞争,但其实不是这样的,优先级低的线程在拿到cpu执行权后也是可以执行,只不过优先级高的线程拿到cpu执行权的概率比较大而已,并不是一定能拿到。

注意:无法使用 yield() 唤醒调用 wait() 的线程

举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,

有可能是其他人先上车了,也有可能是Yield先上车了。

但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,

最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。


  目录