多线程核心技术(1)-线程的基本方法

进程和线程

    了解多线程首先要了解进程和线程的概念,在操作系统里,进程是资源分配最小单位,一般情况下一个应用就会在计算机系统内开启一个进程,线程可以理解为进程中多个独立运行的子任务,是操作系统能够进行调度运算的最小单位,但是线程不拥有资源,只能共享进程中的数据,所以多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。

    由于一个进程中允许存在多个线程,所以在多线程中,如何处理线程并发和线程之间通信的问题,是学习多线程编程的重点。 了解多线程首先要了解进程和线程的概念,在操作系统里,进程是资源分配最小单位,一般情况下一个应用就会在计算机系统内开启一个进程,线程可以理解为进程中多个独立运行的子任务,是操作系统能够进行调度运算的最小单位,但是线程不拥有资源,只能共享进程中的数据,所以多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。由于一个进程中允许存在多个线程,所以在多线程中,如何处理线程并发和线程之间通信的问题,是学习多线程编程的重点。

多线程的使用

    在java中,创建一个线程一般有两种方式,继承Thread类或者实现Runable接口,重写run方法即可,然后调用start()方法即可以开启一个线程并执行。如果想要获取当前线程执行返回值,在jdk1.5以后,可以通过实现Callable接口,然后借助FutureTask或者线程池得到返回值。由于线程的执行具有随机性,所以线程的开启顺序并不意味线程的执行顺序。

继承Thread创建一个线程

/**
 * 继承Thread 重写Run方法
 * 类是单继承的,生产环境中如果此类无需实现其他接口 可使用这种方法创建线程
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        log.info("Hi,I am a thread extends Thread,My name is:{}", this.getName());
    }
}

复制代码

实现Runable接口创建一个线程


/**
 * 实现Runnable接口
 * 类允许有多个接口实现 生产中一般使用这种方式创建线程
 * 线程的开启还需要借助于Thread实现
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
@Getter
public class ThreadRunable implements Runnable {

    private String name;

    public ThreadRunable(String name) {
        this.name = name;
    }

    public void run() {
        log.info("Hi,I am a thread implements Runnable,My name is:{}", this.getName());
    }
}
复制代码

实现Callable 接口 获取线程执行结果

/**
 * 实现Callable接口创建获取具有返回值的线程
 * 线程使用需要借助FutureTask和Thread,或者使用线程池
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class CallableThread implements Callable<Integer> {

    private AtomicInteger seed;
    @Getter
    private String name;

    public CallableThread(String name, AtomicInteger seed) {
        this.name = name;
        this.seed = seed;
    }

    public Integer call() throws Exception {
        //使用并发安全的原子类生成一个整数
        Integer value = seed.getAndIncrement();
        log.info("I am thread implements Callable,my name is:{} my value is:{}", this.name, value);
        return value;
    }
}
复制代码

验证三种线程的启动


/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     threadTest();
     runableTest();
     callableTest();
    }

    public static void threadTest() {
        MyThread threadA = new MyThread("threadA");
        threadA.start();
    }

    public static void runableTest() {
        ThreadRunable runable = new ThreadRunable("threadB");
        //需要借助Thread来开启一个新的线程
        Thread threadB = new Thread(runable);
        threadB.start();
    }

    public static void callableTest() throws ExecutionException, InterruptedException {
        AtomicInteger atomic = new AtomicInteger();
        CallableThread threadC1 = new CallableThread("threadC1", atomic);
        CallableThread threadC2 = new CallableThread("threadC2", atomic);
        CallableThread threadC3 = new CallableThread("threadC3", atomic);
        FutureTask<Integer> task1 = new FutureTask<Integer>(threadC1);
        FutureTask<Integer> task2 = new FutureTask<Integer>(threadC2);
        FutureTask<Integer> task3 = new FutureTask<Integer>(threadC3);
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        thread1.start();
        thread2.start();
        thread3.start();
        while (task1.isDone()&&task2.isDone()&&task3.isDone())
        {
        }
        log.info(threadC1.getName()+"执行结果:"+String.valueOf(task1.get()));
        log.info(threadC2.getName()+"执行结果:"+String.valueOf(task2.get()));
        log.info(threadC2.getName()+"执行结果:"+String.valueOf(task3.get()));
    }
}
复制代码

以下是程序执行结果:

结论:

  1. 这三种方式都可以开启一个线程,实现具体开启线程的任务还是交给Thread类实现,因此对于RunableCallable来说,最后都是要借助于Thread开启线程,类是单继承的,接口是多实现的,由于生产环境业务复杂性,一个类可能会有其他功能,因此一般使用接口实现的方式。
  2. 从上面的线程声明顺序和执行顺序结果来看,线程的执行是无序的,CPU执行任务是采用轮询机制来提高CPU使用率,在线程获取执行资源进行就绪队列后 才会再次被CPU调用,而这个过程跟程序无关。

线程的生命周期

     一个线程的运行通常伴随着线程的启动、阻塞、停止等过程,线程启动可以通过Thread类的start()方法执行,由于多线程可能会共享进程数据,阻塞一般发生在等待其他线程释放进程某块资源的过程,当线程执行完毕,可以自动停止,也可以通过调用stop()强制终止线程,或者在线程执行过程中由于异常导致线程终止,了解线程的生命周期是学习多线程最重要的理论基础。

    下图为线程的生命周期以及状态转换过程

新建状态

    当通过Thread thead=new Thread()创建一个线程的时候,该线程就处于 new 状态,也叫新建状态。

就绪状态

    当调用thread.start()时,线程就进入了就绪状态,在该状态下线程并不会运行,只是表示线程进入可供CPU调用的就绪队列,具备运行条件。

运行状态

    当线程获得了JVM中线程调度器的调度时候,线程就进入运行状态,会执行重写的 run方法。

阻塞状态

    此时的线程仍处于活动状态,但是由于某种原因失去了CPU对其调度权利,具体原因可分为以下几种

  1. 同步阻塞

    此时由于线程A需要获取进程的资源1,但是资源1被线程B所持有,必须等待线程B释放资源1之后,该线程才会进入资源1的就绪线程池里,获取到资源1后,等待被CPU调度器调度再次运行。同步阻塞一般出现在线程等待某项资源的使用权利,在程序中使用锁机制会产生同步阻塞。

  1. 等待阻塞

    当执行Thread类的wait()join()方法时,会造成当前线程的同步阻塞,wait()会使当前线程暂停运行,并且释放所拥有的锁,可以通该线程要等待的某个类(Object)的notify()或notifyall()方法唤醒当前线程。join()方法会阻塞当前线程,直到线程执行完毕,可以通过join(time)指定等待的时间,然后唤醒线程。

  1. 其他阻塞

    调用sleep()方法主动放弃所占用的CPU资源,这种方式不会释放该线程所拥有的锁,或者调用一个阻塞式IO方法、发出了I/O请求,进入这种阻塞状态。被阻塞的线程会在合适的时候(阻塞解除后)重新进入就绪状态,重新等待线程调度器再次调度它。

死亡状态

     当线程执行完run方法时,就会自动终止或者处于死亡状态,这是线程的正常死亡过程。或者通过显示调用stop()终止线程,但不安全。还可以通过抛异常法终止线程。

实例变量与线程安全

多线程访问进程资源

    在多线程任务应用中如果多个线程执行之间使用了进程的不同资源,即运行中不共享任何进程资源,各线程运行不受影响,且不会产生数据安全问题。如果多个线程共享了进程的某块资源,会同时修改该块资源数据,产生最终结果与预期结果不一致的情况,导致线程安全问题。如图:

主内存与工作内存

    Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:

    线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write操作

  • read-load阶段从主内存复制变量到当前工作内存

  • useassign阶段执行代码改变共享变量值

  • storewrite阶段用工作内存数据刷新主存对应变量的值。

  • store and write执行时机

    1. ava内存模型规定不允许readloadstorewrite操作之一单独出现,以上两个操作必须按顺序执行,没必要 连续执行,也就是说readload之间、storewrite之间是可插入其他指令的。
    2. 允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。变量在当前线程中改变一次其实就是一次assign,而且不允许丢弃最近的assign,所以必定有一次store and write,又根据第一条read and load 和store and write不能单一出现,所以有一次store and write必定有一次read and load,因此推断出,变量在当前线程中每一次变化都会执行 read 、 load 、use 、assign、store、write
    3. volatile修饰变量,是在useassign阶段保证获取到的变量永远是跟主内存变量保持同步

    非线程安全问题

    在多线程环境下useassign是多次出现的,但此操作并不是原子性的,也就是说在线程A执行了readload从主内存 加载过变量C后,此时如果B线程修改了主内存中变量C的值,由于线程A已经加载过变量C,无法感知数据已经发生变化,即从线程A的角度来看,工作内存和主内存的变量A已经不再同步,当线程A使用useassign时,就会出现非线程安全的问题。解决此问题可以通过使用volatile关键字修饰,volatile可以保证线程每次使用useassign时,都从主内存中拿到最新的数据,而且可以防止指令重排,但volatile仅仅是保证变量的可见性,无法使数据加载的几个步骤是原子操作,所以volatile并不能保证线程安全。

    如下代码所示:

    多个业务线程访问用户余额balance,最终导致扣款总金额超过了用户余额,由线程不安全导致的资损情景.而且每个业务线程都扣款了两次,也说明了线程启动时需要将balance加载到工作内存中,之后该线程基于加载到的balance操作,其他线程如何改变balance值,对当前业务线程来说都是不可见的。

/**
 * 业务订单代扣线程 持续扣费
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class WithHoldPayThread extends Thread {
    //缴费金额
    private Integer amt;
    //业务类型
    private String busiType;

    public WithHoldPayThread(Integer amt, String busiType) {
        this.amt = amt;
        this.busiType = busiType;
    }

    @Override
    public void run() {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result);
            }
        }
        log.info("业务:{} 共缴费:{} 次", busiType, payTime);
    }
}
复制代码

     测试函数

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
public class WithHodeTest {
    //用户余额 单位 分
    public static volatile Integer balance=100;

    public static void main(String[] args) {
        WithHoldPayThread phoneFare = new WithHoldPayThread(50, "缴存话费");
        WithHoldPayThread waterFare = new WithHoldPayThread(50, "缴存水费");
        WithHoldPayThread electricFare = new WithHoldPayThread(50, "缴存电费");
        phoneFare.start();
        waterFare.start();
        electricFare.start();
    }
}
复制代码

     执行结果:

    实验结果证明,每个线程的扣款都成功了,这就导致了线程安全问题,解决这个问题最简单的做法是在run方法里面加synchronized修饰,并且对balance使用volatile修饰就可以了。

   //用户余额 单位 分
    public static  volatile Integer balance=100;
复制代码
 @Override
    public void run() {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result);
            }
        }
        log.info("业务:{} 共缴费:{} 次", busiType, payTime);
    }
复制代码

执行结果:

线程的基本API

     java线程类Thread提供了线程操作的基本方法,比如判断线程是否存活的isAlive(),阻塞线程的wait() join(),让线程休眠的sleep(),停止线程的stop(), 暂停和唤醒线程 的suspendresume等等,有些方法由于会导致线程不安全或者独占资源已被废弃,所以我们应该谨慎使用。

getId()

获取线程的唯一ID,此方法在实际生产中可以根据线程号跟踪一个业务的调用具体过程

isAlive()

判断线程是否处于活动状态,活动状态就是线程已启动,处于运行或者准备运行,尚未 结束,可以通过调用 isAlive()方法判断当前线程任务是否执行完毕。

sleep()

    使正在执行任务的线程在指定的毫秒时间内暂停执行,正在执行的线程指this.currentThread()返回的线程。该状态下的线程不会释放锁资源。

suspend()与resume()

suspend()可以暂停线程,resume可以恢复线程继续执行。

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/15.
 */
@Slf4j
public class SuspendAndResumeThread extends Thread {
    @Getter
    private  int number = 0;

    @Override
    public void run() {
        while (true) {
            number++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SuspendAndResumeThread thread=new SuspendAndResumeThread();
        thread.start();
        Thread.sleep(200);
        thread.suspend();
        //此时线程已经暂停执行
        log.info("A time:{}  number={}",System.currentTimeMillis(),thread.getNumber());
        Thread.sleep(200);
        //B time的执行结果应该与A time 一致
        log.info("B time:{}  number={}",System.currentTimeMillis(),thread.getNumber());
        //唤醒继续执行
        thread.resume();
        Thread.sleep(200);
        log.info("C time:{}  number={}",System.currentTimeMillis(),thread.getNumber());
    }
}
复制代码

    执行结果和预期一致

suspend()resume()可以暂停和恢复线程,用法也很简单,可是jdk却废弃了这种方法,因为这种用法可能会造成"资源独 占"的情况和数据不一致的情况。

    资源独占:以下代码展示了多个支付业务,在同步代码中,支付业务withdraw在当前线程中暂停了线程执行,后面的支付业务无法在进入付款方法的情况,造成了资源独占

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class PaymentServiceImpl {
    private int balance;

    public PaymentServiceImpl(int balance) {
        this.balance = balance;
    }

    public synchronized void payService(int amt) {
        String buziType = Thread.currentThread().getName();
        log.info("开始做支付业务.....业务类型:{}", buziType);
        if (balance >= amt) {
            balance -= amt;
            //如果是提现业务 先去余额扣款 再做支付业务
            if ("withdraw".equals(buziType)) {
                log.info("转账业务将当前线程 暂停!");
                //暂停线程 模拟去转账 如果转账失败 该线程会一直处于在暂停 而且不会释放同步锁
                Thread.currentThread().suspend();
                log.info("转账成功!");
            }

            log.info("业务:{} 扣款成功,当前用户余额:{}", buziType, balance);
        } else {
            log.info("业务:{} 扣款失败,当前用户余额不足", buziType);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final PaymentServiceImpl paymentService = new PaymentServiceImpl(2000);
        Thread fastpay = new Thread(new Runnable() {
            public void run() {
                paymentService.payService(150);
            }
        }, "fastpay");
        Thread withdraw = new Thread(new Runnable() {
            public void run() {
                paymentService.payService(50);
            }
        }, "withdraw");
        Thread payfor = new Thread(new Runnable() {
            public void run() {
                paymentService.payService(50);
            }
        }, "payfor");
        fastpay.start();
        Thread.sleep(1000);
        withdraw.start();
        Thread.sleep(1000);
        payfor.start();
    }
}
复制代码

    执行结果:

    从执行结果上来看,fastpay付款业务首先执行,不会受withdraw业务暂停线程的影响,当执行withdraw后,暂停了线程,如果在当前线程中不适用resume唤醒线程 会导致withdraw线程一直占用payService()方法资源。payfor业务无法执行,产生资源独占的情况。

    数据不一致:一下代码验证了通过ThreadExcuterUtils获取当前线程名和执行任务,在两个参数赋值过程中发生了线程暂停,导致线程名和执行任务不一致的情况。

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Getter
@Slf4j
public class ThreadExcuterUtils {
    //当前线程名
    private String localThreadName;
    //当前线程id
    @Setter
    private String localTask;

    public void setThreadInfo(String threadName) {
        this.localThreadName = threadName;
        if ("gc_thread".equals(threadName)) {
            Thread.currentThread().suspend();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final ThreadExcuterUtils utils = new ThreadExcuterUtils();
        new Thread(new Runnable() {
            public void run() {
                utils.setThreadInfo("log_thread");
                utils.setLocalTask("记录日志");
            }
        }).start();
        Thread.sleep(1000);
        new Thread(new Runnable() {
            public void run() {
                utils.setThreadInfo("gc_thread");
                utils.setLocalTask("GC垃圾回收");
            }
        }).start();
        Thread.sleep(1000);
        log.info("当前线程:{} 当前任务:{}",utils.getLocalThreadName(),utils.getLocalTask());
    }
}
复制代码

    执行结果:

​ 从执行结果看,当前线程是gc_thread ,对应的任务应该是 “GC垃圾回收”,但是结果却是 “记录日志”,出现了数据不一致的情况。

​ 以上两种情况可能由于业务场景设计的不合适或者可以用其他线程同步解决该问题,但是对于suspendresume的使用,我们应该谨慎小心,suspendresume一定要成对使用,避免造成资源独占和数据不一致的情况。

setPriority()

    在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU的资源越多,也就是CPU优先执行优先级较高的线程对象中的任务,设置线程优先级有助于帮助"线程规划器"确定在下一次选择哪一个线程来优先执行,设置线程优先级可以通过setPriority()设置。在java中,线程优先级分为1-10 这个10个等级,数字越大优先级越高,如果小于1 或者大于10 会抛出throw new IlleageArgumentException()。

    JDK使用3个常量预先设置线程优先级:

   public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
复制代码

1、线程优先级具有继承性

​ 假如ThreadB extend ThreadA,ThreadA线程优先级为5,则ThreadB的优先级也为5

2、线程优先级具有随机性

    设置线程的优先级,只能确保优先级高的尽可能先获得cpu执行资源,但是并不代表着一定比优先级低的线程先获得执行权,具体还是由cpu决定。

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class LowPriorityThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        log.info("☆☆☆☆☆");
    }
}

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class HighPriorityThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        log.info("★★★★★");
    }

}

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            HighPriorityThread thread = new HighPriorityThread();
            LowPriorityThread thread1 = new LowPriorityThread();
            thread.setPriority(10);
            thread1.setPriority(1);
            thread.start();
            thread1.start();
        }
    }
复制代码

    执行结果:

​ 从执行结果看,优先级高的大部分线程总是会先于优先级的线程执行完。

yield()

    yield()表示当前线程可以让出cpu资源,cpu资源是否让出取决于cpu调度,当cpu资源紧张时候可能会收回该线程执行资源,如果cpu资源充足,有可能就不会回收。使用了yield()该线程任务时间可能会延长。

    以下代码验证了不适用yeild和使用yeild线程执行时间对比:

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class YieldThread extends Thread {


    @Override
    public void run() {
//        Thread.yield();
        long beginTime = System.currentTimeMillis();
        int result = 0;
        for (int i = 0; i < 500000000; i++) {
            result += i;
        }
        long endTime = System.currentTimeMillis();
        log.info("当前线程执行时间:{} 毫秒",endTime-beginTime);
    }

    public static void main(String[] args) {
        YieldThread thread=new YieldThread();
        thread.start();
    }
}
复制代码

    以下是两次执行结果对比,会发现使用了yiled()执行方法有所延长,当在CPU资源紧张时,两个执行耗时差别会更明显。

    不使用yeild

    使用了yeild

线程的停止

    停止一个线程意味着停止线程正在做的任务,放弃当前操作,但是停止线程操作并不像java种的for循环使用break退出循环这么简单,因为线程执行任务过程可能会使用到主内存中的共享数据,一旦放弃该线程任务,其所操作的共享数据处理不当就会产生非线程安全问题。停止一个正在执行中的线程可以使用Thread.stop()方法,直接暴力,不过stop方法和suspendresume一样,都是过期方法,会产生不可预料的结果,目前比较安全的方法是使用Thread.interrupt(),在线程中使用该方法会给线程打一个停止的标志,并不会真正的停止,但是我们可以通过判断是否存在这个标志点在run()进行任务终止。总结一下,java中有三种停止线程的方法:

  1. 线程正常执行完毕 即终止。
  2. 使用stop强制终止线程,如果使用共享资源,会产生不可预料的结果,已废弃。
  3. 使用interrupt()终端线程。

判断线程状态

 //会清除线程状态
 public static boolean interrupted()
 //不会清楚线程状态
 public boolean isInterrupted()
复制代码

    第一个方法interrupted()是静态的,用来判断执行这段代码的线程是否处于终止状态,调用该方法会清除当前线程状态,比如第一次调用时获取线程状态时true,之后清除了该线程状态,第二次调用就会变成false。(用的很少)

    第二个方法属于该线程对象,用于判断当前线程是否处于终止状态,该方法不会清除线程状态,所以如果线程状态不变,多次调用该方法获取的线程状态一定时一致的。(非常常用)

异常法停止线程

    我们可以通过thread.isInterrupted()来获取线程状态,在线程执行过程中,可以通过判断线程状态来决定线程是否要继续执行,如果不需要执行可以直接抛出异常 或者使用return直接返回 终止异常 就可以达到终止线程的目的。

    如下代码所示:

    线程类每间隔1s打印一下当前时间,每次打印前先判断线程状态,如果线程未终止继续执行,如果终止就抛出异常!

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class LogRecordThread extends Thread {
    @Override
    public void run() {
        try {
            while (true) {

                if (!this.isInterrupted()) {
                    Thread.sleep(1000);
                    log.info("日志记录工作执行中!当前时间:{}", System.currentTimeMillis());
                } else {
                    throw new InterruptedException();
                }
            }
        } catch (InterruptedException ex) {
            log.error("当前线程已终止,任务终止!");
           // ex.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LogRecordThread thread = new LogRecordThread();
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
复制代码

    运行结果:

    如上所示线程跟预期一样结束了。此外还可以使用return直接返回即可结束线程任务。

sleep中停止线程

    如果线程处于sleep状态中,调用thread.interrupt()方法会抛出InterruptedException异常,此外还会清除停止状态值,使之变成false,可以简单理解为,如果在线程sleep的时候调用thread.interrupt()时候,线程状态值与之前保持一致。

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/17.
 */
@Slf4j
public class SleepInterruptThread extends Thread {
    @Override
    public void run() {
        try {
            log.info("开始执行线程任务....");
            Thread.sleep(3000);
            log.info("线程任务执行完毕!");
        } catch (InterruptedException ex) {
          log.error("线程被终止了,此时线程是否中断标志:{}",this.isInterrupted());
          ex.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SleepInterruptThread thread=new SleepInterruptThread();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
复制代码

    运行结果:

stop暴力停止线程

    直接调用thread.stop就可以停止一个线程,但是由于停止线程会导致一些处理工作无法完成,导致数据不完整,还会和suspend和resume一样产生数据不一致的情况,所以jdk已经不建议使用。如果想要停止线程直接通过上面的异常法即可。

小结

    本文主要介绍了线程的三种创建方式,线程的几种状态以及状态转换、线程安全问题 、线程基本API以及如何停止一个线程,学习掌握了这些其实也就掌握了多线程的核心,虽然线程并发安全问题是我们关注的重点,但不了解上面几点就无法真正理解产生以及解决线程安全问题的方法,虽然简单,还是要记录一下。

猜你喜欢

转载自juejin.im/post/5d010a39f265da1b7e1030b8