并发编程 - 创建多线程的方法以及线程的启动与停止

1. 官方回答

Oracle官方文档的描述是这样的

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started

也就是说,有两种方法区创建线程
方法一:实现Runnable接口

class PrimeThread extends Thread {
	long minPrime;
	PrimeThread(long minPrime) {
		this.minPrime = minPrime;
	}
	public void run() {
		// compute primes larger than minPrime
		. . .
	}
}
PrimeThread p = new PrimeThread(143);
p.start();

方法二:继承Thread类

class PrimeRun implements Runnable {
	long minPrime;
	PrimeRun(long minPrime) {
		this.minPrime = minPrime;
	}
	public void run() {
		// compute primes larger than minPrime
		. . .
	}
}
 
PrimeRun p = new PrimeRun(143);
new Thread(p).start();

start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行(Runnable),什么时候运行是由操作系统决定的。

但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。

在上面两种方法中使用Runnable更好

  1. 从代码的架构考虑:具体执行的任务应该和Thread类解耦,Runnable是具体的任务,而Thread类主要用于管理线程的整个生命周期,比如创建线程,暂停线程,销毁线程,他们的任务不一样所以在代码架构的角度上应该解耦
  2. 当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入执行单元run方法体去执行具体的任务
  3. 从新建线程的损耗:使用第一种方法可以使用线程池,对应资源的损耗更小
  4. java不支持多继承,所以使用第二种方法就不能再继承

就像上面说的,创建多线程的方法通常我们可以分为两类,Oracle也是这么说的,但是准确的讲,创建线程只有一种方法那就是构建Thread类,而实现线程的执行单元(run)有两种方法

  • 方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
  • 方法二:重写Thread的run方法(继承Thead类)

这是什么意思呢?当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入执行单元run方法体去执行具体的任务,看一下Thread类中run方法的源码

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

在使用继承Thead类的方法的时候会重写Thread的run方法来实现线程的执行单元
如果实现Runnable接口的run方法,并把Runnable实例传给Thread类呢?看一下这个构造函数

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

target就是传入的Runnable实例

2. 网上的回答

对于网上的使用lambda表达式的方法,使用线程池的方法等方法实际上都是从代码写法层面上来回答这个问题的,本质依旧离不开上面的两种方法

① 继承 Thread 类

② 实现java.lang.Runnable接口(更推荐)

③ 使用lambda简化
Runnable接口之间一个run方法,带有@FunctionalInterface注解,可以使用lambada表达式简化

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
Runnable r = () -> {System.out.println("running")};
Thread t = new Thread(r, "t2");
//启动线程
t.start();

更简单

Thread t = new Thread(() -> {
	System.out.println("running");
	System.out.println();
}, "t2");
//启动线程
t.start();

④ 使用FutureTask
可以在线程执行把结果返回给其他对象,里面的Callable对象可以有返回值

FutureTask<Integer> take = new FutureTask<>(new Callable<Integer>() {
	@Override
	public Integer call() throws Exception {
		return 1;
	}
});
Thread thread = new Thread(take);
thread.start();
int i = task.get();
//Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞

⑤ 使用 Executors 工具类创建线程池
Executors提供了一系列工厂方法用于创造线程池,返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutornewScheduledThreadPool

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}

public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }

}

3. 线程的启动

1.1 start()方法原理解读

start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行(Runnable),什么时候运行是由操作系统决定的。

start方法不能重复调用,会出现java.lang.IllegalThreadStateException异常,这是因为调用了start,线程已经变为了就绪状态,而start方法只能在new状态调用,在start方法中就会对这个状态进行检查

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

执行start方法做了三件事
① 启动新线程检查线程状态,如果不是new的状态就合法,也就会抛出上面的问题
② 加入线程组
③ 调用start0()
当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入执行单元run方法体去执行具体的任务

4. 线程的停止

4.1 线程停止的原理

原理
A线程使用interrupt来通知B线程停止而不是强制停止B线程,真正停止B线程与否的决定权依旧是B线程本身

为什么要这么设计
因为发送停止信号的A线程可能并不了解B线程的内部业务逻辑,我们实际想做的是等B线程做完内部的保存工作,内部的业务处理再去停止而不能让他处于一种混乱的状态;在这种情况下最了解B的还是B自己本身,所以最好的方法就是把停止的权力交给B线程自己

4.2 正确的停止方法

4.2.1 普通情况
public class RightWayStopThreadWithoutSleep implements Runnable {

    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num++;
        }
        System.out.println("任务运行结束了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
    }
}

对于普通线程,打断线程,会对线程设置一个打断标记,之后判断打断标记就能实现时候应该停止线程

4.2.2 对于阻塞线程的打断

如果在线程被阻塞的时候对他进行中断(wait/sleep/join),这种情况时如何响应的?

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    thread.start();
    thread.interrupt();
}

打断线程,如果被打断的线程处于wait/sleep/join的状态,则会抛出InterruptedException,并且会清除打断标记

4.2.3 关于ReentrantLock的打断

我在等待锁的过程中,其他线程可以打断我的等待,被动的避免死等
可打断的ReentrantLock要使用lockInterruptibly方法获得,被打断或获得InterruptedException

	public static ReentrantLock lock = new ReentrantLock();
	public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
            	//可打断的ReentrantLock要使用lockInterruptibly方法获得
                lock.lockInterruptibly();
                //执行逻辑
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
                //被打断了就不再向下执行,直接返回
                log.debug("没有获得锁");
                return;
            }
            try {
                log.debug("获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
		//以下是主线程的代码
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("执行打断");
        } finally {
            lock.unlock();
        }
    }

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

4.2.4 关于park的打断

parkLockSupport的方法,打断 park 线程, 不会清空打断状态,如果打断标记已经是 true, 则 park 会失效,关于park的用法具体看park

public class ParkTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始");
                System.out.println("打断");
                LockSupport.park();
                System.out.println("恢复");
                System.out.println("打断标记" + Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt(); 
    }
}
开始
打断
恢复
打断标记true

如果打断标记已经是 true, 则 park 会失效,可以使用 Thread.interrupted() 清除打断状态

4.2.5 打断标记

对于普通线程,打断线程,会对线程设置一个打断标记,之后判断打断标记就能实现时候应该停止线程
但是如果被打断的线程处于sleep的状态,则会抛出InterruptedException,被唤醒,虽然打断的线程会有一个打断标记,但是处于阻塞线程的线程被打断后会把打断标记清理

可以看一下下面的代码实际上并不会停止,原因就是因为打断标记被清理

public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
情况打断标记
处于park的线程被打断不清除
处于wait/sleep/join状态的线程被打断清除
正常运行的线程不清除
4.2.4 传递中断

对于低层次方法的中断的异常应该在低层次的方法签名中抛出,让高层次的方法能够感知,以达到线程终止的目的
比如我有下面的低层次方法

private void throwInMethod() {
	try{
        Thread.sleep(2000);
    }catch(Exception e){
    	...
    }
}

这里的方法没有对低层次方法throwInMethod的异常处理选择在throwInMethod本身进行处理,高层次方法无法感知,如下所示

public class StopThreadInProd implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("go");
            throwInMethod();
        }
    }
}

显然上层的run方法是无法感知低层次方法throwInMethod的中断的,throwInMethod自己将中断吞掉了
正确的写法是对于低层次方法的中断的异常应该在低层次的方法签名中抛出,让高层次的方法能够感知,以达到线程终止的目的

/**
 * 描述:     最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制try/catch
 */
public class RightWayStopThreadInProd implements Runnable {

    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
            Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

4.2.5 恢复中断

如果对于低层次的方法无法传递中断,比如方法签名无法抛出异常,这种情况下如果想要正确的响应中断应该设置好中断标记位,比如在sleep的catch中再次执行中断设置标记位让上层能够感知

/**
 * 描述:最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断
 * 回到刚才RightWayStopThreadInProd补上中断,让它跳出
 */
public class RightWayStopThreadInProd2 implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序运行结束");
                break;
            }
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

4.3 错误的停止方法

4.3.1 被弃用的stop,suspend和resume方法
  • stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作,会造成脏数据,也就是说直接停止了线程,无论线程是不是应该停止(注意,和网上有些说的不同,stop是会释放锁的)
  • resume和suspend不会释放锁,会造成死锁
4.3.2 用volatile设置boolean标记位

当陷入阻塞的时候,volatile是无法停止线程的

/**
 * 描述:     演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
 */
public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");

        //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况
        producer.canceled=true;
        System.out.println(producer.canceled);
    }
}

class Producer implements Runnable {

    public volatile boolean canceled = false;

    BlockingQueue storage;

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


    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

class Consumer {

    BlockingQueue storage;

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

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}

正确的方法就是使用interrupted来改进

public class WrongWayVolatileFixed {

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");


        producerThread.interrupt();
    }


    class Producer implements Runnable {

        BlockingQueue storage;

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


        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者结束运行");
            }
        }
    }

    class Consumer {

        BlockingQueue storage;

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

        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}

4.4 关于线程中断的相关重要函数

static boolean interrupted()获取中断标志并重置,注意这是Thread的方法,方法的目标对象是“当前线程”,而不管本方法来自于哪个对象

可以看一下他的源码

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

所以说方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
interrupted方法在取出打断标记之后会将打断标记设置为false

boolean isInterrupted()获取中断标志

public boolean isInterrupted() {
        return isInterrupted(false);
    }
/**
 * 描述:     注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
 */
public class RightWayInterrupted {

    public static void main(String[] args) throws InterruptedException {

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
                }
            }
        });

        // 启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        //获取中断标志 true
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        //获取中断标志并重置 false
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //获取中断标志并重置 false 
        System.out.println("isInterrupted: " + Thread.interrupted());
        //获取中断标志 false
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

4.5 总结

想要停止线程,要请求方(发送请求信号),被请求方(判断中断信号),子方法被调用方(抛出中断信号)相互配合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值