JAVA多线程和并发编程(四)- 多线程管理

目录​​​​​​​ 

一、线程状态

二、线程管理API

三、生产者消费者模型 - 多线程并发协作实践

1. 使用wait/notifyAll实现

2. 使用LinkedBlockingQueue实现

四、线程暂停和终止 

五、线程死锁

六、守护(后台)线程

七、线程查看工具


一、线程状态

1)NEW 刚创建。即new一个Thread实例,这时线程只是在堆内存中占据了一些空间,还没有开始运行。

2)RUNNABLE 就绪态。调用start方法后,线程便从NEW状态进入RUNNABLE就绪态,等待CPU的调度。

3)RUNNING 运行态。线程被CPU选中执行,为运行态。如果线程CPU时间片用完,或主动让出CPU时间片,则线程会从运行态回到就绪态。

4)BLOCKED 阻塞态。线程某个条件未满足,会进入阻塞态,待条件满足后,进入就绪态。如调用sleep方法会让线程进入阻塞态。

5)TERMINTED 结束。

二、线程管理API

Thread的如下方法已经被废弃,官方文档建议不适用这些有危险的代码:

        - 暂停和恢复:suspend、resume

        - 消亡:stop、destroy

线程阻塞和唤醒:

        - sleep 线程自我休眠,休眠结束后会自己醒来。休眠中是BOLCKED状态,休眠结束后自动变成RUNNABLE状态

        - wait/notify/notifyAll,等待,需要别人唤醒。如果没有别的线程来唤醒该线程,则该线程就会一直阻塞,无法再继续运行。

        - join ,当前线程等待另外一个线程结束。

        - interrupt,给另外一个线程发送中断信号,该线程收到中断信号后会触发InterruptedException(可解除阻塞??),程序员可以针对该异常做进一步的处理。

三、生产者消费者模型 - 多线程并发协作实践

        生产者消费者模式是多线程并发协作的经典案例。生产者的作用是生产一定量的数据放到缓冲区中,并重复此过程;于此同时,消费者也在消费缓冲区中的数据。该模型需要保证生产者不会在缓冲区满时放入数据,消费者不会在缓冲区空时消耗数据,否则将引发缓冲区越界异常等。

        假设有生产者和消费者两类线程,分别用于生产和消费数据,为了解耦生产者和消费者的关系,引入共享数据仓库。该共享仓库需要具有如下功能:

        1)仓库满时,阻塞生产者线程;

        2)仓库空时,阻塞消费者线程。

1. 使用wait/notifyAll实现生产者消费者模型

/**
 * 生产者消费者模型测试
 */
public class ProConTest {
    public static void main(String[] args) throws InterruptedException {
        Storage storage = new Storage();

        Thread p1 = new Thread(new Producer(storage));
        p1.setName("生产者1");
        Thread p2 = new Thread(new Producer(storage));
        p2.setName("生产者2");

        Thread c1 = new Thread(new Consumer(storage));
        c1.setName("消费者1");
        Thread c2 = new Thread(new Consumer(storage));
        c2.setName("消费者2");
        Thread c3 = new Thread(new Consumer(storage));
        c3.setName("消费者3");

        p1.start();
        p2.start();
        Thread.sleep(1000);
        c1.start();
        c2.start();
        c3.start();
    }
}

import java.util.ArrayList;
import java.util.List;

/**
 * 共享数据区域
 */
public class Storage {
    private final List<Integer> data = new ArrayList<>(4);

    public String toString() {
        return data.toString();
    }
    public synchronized void produce(Integer i) {
        while (data.size() >= 10) {
            try {
                System.out.println("队列已满," + Thread.currentThread().getName() + "生产数据被阻塞");
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // 生产数据
        data.add(i);
        System.out.println(Thread.currentThread().getName() + "生产数据" + i);
        // 唤醒所有在等待的线程
        this.notifyAll();
    }

    public synchronized Integer consume() {
        while (data.size() == 0) {
            try {
                System.out.println("队列为空," + Thread.currentThread().getName() + "消费数据被阻塞");
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        Integer result = data.remove(data.size()-1 );
        System.out.println(Thread.currentThread().getName() + "消费数据" + result);
        this.notifyAll();
        return result;
    }
}

        注意Storage的produce和consume方法中,判断队列为空/满的语句都是用的while,而不是if。如果使用if会出现数组越界访问异常,原因是:

        假设有三个线程:consumer1、consumer2、producer。

        当consumer1调用wait方法后,线程处于阻塞状态,将释放Storage 对象锁;

        此时consumer2获取到锁,进入同步代码块执行,同样走到wait方法,进入阻塞状态,释放Storage 对象锁;

        producer获取到锁,进入同步代码块执行,生产一个数据到仓库中,唤醒在等待的consumer1、consumer2;

        consumer1获取到对象锁后,从wait方法退出,继续往下执行,从仓库中消费一个数据后,退出同步代码块,释放锁;

        consumer2获取到对象锁后,从wait方法退出,继续往下执行,这时候consumer2再删除一个数据就会报错,因此consumer1删除一个元素后仓库已经为空了。

        总结上述重复消费的原因是,线程从wait退出后,没有再次判断当前仓库的状态是否需要wait,因此,使用while可以使得线程从wait退出后,再次进行条件判断。

import java.util.Random;

/**
 * 生产者线程
 */
public class Producer implements Runnable{
    private Storage storage;

    public Producer(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for(int i=0; i<4; i++) {
            Integer data = new Random().nextInt(10);
            storage.produce(data);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
/**
 * 消费者线程
 */
public class Consumer implements Runnable{

    private Storage storage;

    public Consumer(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i=0; i<4; i++) {
            storage.consume();
        }
    }
}

2. 使用LinkedBlockingQueue实现生产者消费者模型

        将 LinkedBlockingQueue 用作生产者和消费者的共享数据仓库。

import java.util.concurrent.LinkedBlockingQueue;

/**
 * 生产者消费者模型测试
 */
public class ProConTest {
    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingQueue<Integer> storage = new LinkedBlockingQueue<>(4);

        Thread p1 = new Thread(new Producer(storage));
        p1.setName("生产者1");
        Thread p2 = new Thread(new Producer(storage));
        p2.setName("生产者2");

        Thread c1 = new Thread(new Consumer(storage));
        c1.setName("消费者1");
        Thread c2 = new Thread(new Consumer(storage));
        c2.setName("消费者2");
        Thread c3 = new Thread(new Consumer(storage));
        c3.setName("消费者3");

        p1.start();
        p2.start();
        Thread.sleep(1000);
        c1.start();
        c2.start();
        c3.start();
    }
}
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 生产者线程
 */
public class Producer implements Runnable{
    private LinkedBlockingQueue<Integer> storage;

    public Producer(LinkedBlockingQueue<Integer> storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for(int i=0; i<4; i++) {
            Integer data = new Random().nextInt(10);
            System.out.println(Thread.currentThread().getName() + "生产数据" + data);
            try {
                storage.put(data);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 消费者线程
 */
public class Consumer implements Runnable{

    private LinkedBlockingQueue<Integer> storage;

    public Consumer(LinkedBlockingQueue<Integer> storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i=0; i<4; i++) {
            Integer result = null;
            try {
                result = storage.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "消费数据" + result);
        }
    }
}

四、线程暂停和终止 

        线程被动终止:

        可以通过调用线程的interrupt方法是线程被动终止,但不建议这种方式,因为被终止的线程毫无准备,如果它持有一些数据库连接、文件、锁等资源,被动终止后就来不及释放这些资源。

        线程主动终止:        

        建议使用线程主动终止的方式。可以在线程的执行体中监听一个共享变量,线程内部通过判断该共享变量的值,决定是否主动终止线程,终止之前可以进行资源的释放操作。外部如果要终止该线程,可以通过修改该共享变量的值实现。

        示例:

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread_1 thread1 = new Thread_1("thread-1");
        Thread_2 thread2 = new Thread_2("thread-2");

        thread1.start();
        thread2.start();

        // 让线程运行一会后终止
        Thread.sleep(1000);

        thread1.interrupt();
        thread2.flag = true;

        System.out.println("Main thread is existing");
    }
}

class Thread_1 extends Thread {

    public Thread_1(String name) {
        super(name);
    }

    @Override
    public void run() {
        while(!interrupted()) {
            System.out.println(Thread.currentThread().getName() + " is running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + " is existing");
    }
}

class Thread_2 extends Thread {
    public Thread_2(String name) {
        super(name);
    }

    // 内存可见的共享变量。别的线程一点修改flag,所有线程都可以看到该修改,然后从内存中获取最新的值
    public volatile boolean flag = false;

    @Override
    public void run() {
        while(!flag) {
            System.out.println(Thread.currentThread().getName() + " is running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + " is existing");
    }
}

        运行结果:

thread-1 is running
thread-2 is running
thread-1 is running
Main thread is existing
thread-2 is existing
Exception in thread "thread-1" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
    at org.example.thread.interrupt.Thread_1.run(InterruptTest.java:33)
Caused by: java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at org.example.thread.interrupt.Thread_1.run(InterruptTest.java:31)

        可以看到thread-1被main线程调用interrupt后被动终止,触发interrupt异常。thread-2通过监听共享变量flag,可以主动优雅终止线程。

五、线程死锁

        每个线程互相持有别人需要的锁(哲学家吃面问题)。

        预防死锁,需要对加锁的资源进行等级排序,严格按顺序依次加锁。

        死锁示例代码:

package org.example.thread.deadlock;

public class DeadLockTest {

    static final Integer r1 = 1;
    static final Integer r2 = 2;
    public static void main(String[] args) {
        new Thread3("thread-3").start();
        new Thread4("thread-4").start();
    }
}

class Thread3 extends Thread {
    public Thread3 (String name) {
        super(name);
    }
    public void run() {
        synchronized (DeadLockTest.r2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (DeadLockTest.r1) {
                System.out.println("thread-3 is running");
            }
        }
    }
}

class Thread4 extends Thread {
    public Thread4 (String name) {
        super(name);
    }

    public void run() {
        synchronized (DeadLockTest.r1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (DeadLockTest.r2) {
                System.out.println("thread-4 is running");
            }
        }
    }
}

        执行上述main方法,可以看到控制台程序一直在运行,但没有任何输出: 

        按顺序加锁来预防死锁:将上述代码的加锁顺序都控制为先对r1加锁,再对r2加锁,则程序可以正常运行,结果如下:

六、守护(后台)线程

        普通线程的结束,是run方法运行结束

        守护线程的结束,是run方法运行结束,或main函数执行结束。

        守护线程永远不要访问资源,如文件或数据库等。因为main函数执行结束后,守护线程将被动结束,来不及释放它持有的资源。

package org.example.thread.daemon;

public class DaemonTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable3());
        // 增加这句后,如果main线程退出,则子线程thread也随之退出
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(1000);
        System.out.println("main thread is existing");
    }
}

class Runnable3 implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

七、线程查看工具

-- 待补充

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值