线程基础

一、线程的创建方式

我会用一个模拟隋唐演义的方式来介绍线程的使用

1.实现Runnable方法
class Actress implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"是一个演员");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning){
            System.out.println(Thread.currentThread().getName()+"登台演出:"+(++count));
            if (10 == count ){
                keepRunning = false;
            }
            if (count%3 == 0){
                try {
                    Thread.sleep(2900);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName()+"演出完毕");
    }
}

实现了Runnable方法之后,需要重写里面run方法去实现我们的一些具体业务。
然后,在main函数中,我们可以这样来创建线程

 public static void main(String[] args) {
        System.out.println("演出开始!!");
        Thread actressThread = new Thread(new Actress(),"Ms.Runnable");
        actressThread.start();
    }

用start方法,可以使线程处于运行状态。

2.继承Thread类
public class Actor extends Thread {
    @Override
    public void run() {
        System.out.println(getName()+"是一个演员");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning){
            System.out.println(getName()+"登台演出:"+(++count));
            if (10 == count ){
                keepRunning = false;
            }
            if (count%3 == 0){
                try {
                    Thread.sleep(2900);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println(getName()+"演出完毕");
    }
}

然后在main方法中我们可以这样去创建

public static void main(String[] args) {
        Thread actor = new Actor();
        System.out.println("演出开始!!");
        actor.setName("Mr.Thread");
        actor.start();
    }
3.实现callable接口
class Asst implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"是一个演员");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning){
            System.out.println(Thread.currentThread().getName()+"登台演出:"+(++count));
            if (10 == count ){
                keepRunning = false;
            }
            if (count%3 == 0){
                try {
                    Thread.sleep(2900);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName()+"演出完毕");
        return Thread.currentThread().getName();
    }
}

然后在main方法中我们可以使用线程池的方式去创建这样的线程

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);
        // 可以执行 Runnable 或者 Callable 对象代表的线程
        Future<String> future1 = pool.submit(new Asst());
        Future<String> future2 = pool.submit(new Asst());
        // TODO: 16/7/19
        String s1 = future1.get();
        String s2 = future2.get();

        System.out.println(s1);
        System.out.println(s2);
        // 结束
        pool.shutdown();
    }

其中,ExecutorService 和Executors类都位于Java的util.concurrent包下,首先我去创建了两个线程池,然后在线程池对象中有submit方法,去返回一个带有泛型约束的Future对象,我们来看一下submit的底层实现

/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

 protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

这是抽象类AbstractExecutorService的submit方法,如果传进来的参数不为空则新建一个RunnableFuture的对象,紧接着去执行这个对象并将这个对象返回回去RunnableFuture对象的源代码如下:

package java.util.concurrent;

/**
 * A {@link Future} that is {@link Runnable}. Successful execution of
 * the {@code run} method causes completion of the {@code Future}
 * and allows access to its results.
 * @see FutureTask
 * @see Executor
 * @since 1.6
 * @author Doug Lea
 * @param <V> The result type returned by this Future's {@code get} method
 */
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

它首先继承了Runnable以及Future接口,然后里面只有一个run方法,之后返回给我们的主程序是一个Future对象,里面的String类型是我返回的线程的名称,然后我分别去执行这两个线程,最终打印出两个线程的名称,程序结束。

注意

在创建创建线程的时候,继承Thread类的话,如果要创建线程必须new Thread(线程类),然后手动去setName()去对线程命名的,不然的话JVM会默认起一个名字,但是如果实现了Runnable方法,可以直接通过 Thread actressThread = new Thread(new Actress(),"Ms.Runnable")去给一个线程命名。

二、线程的基本操作

我们来看看常见的线程基本操作


15200008-9ae237bed1b9285c.png
线程的基本操作
1.停止线程的方法

在jdk中 我们发现Thread提供了一个stop()方法,但是stop方法却被废弃了.原因是这个方法太过去暴力了.强行把执行到一半的线程终止.可能会引起一些数据不一致的问题.
因为他释放了这个线程所持有的所有锁,而这些锁恰恰是用来维持一个对象一致性的.例如写数据写到一半,并强行终止.那么对象就会被写坏.同时 由于锁已经被释放了,另一个等待的读线程顺理成章的读到了这个不一致的对象,悲剧也就发生了!
那如果需要停止一个线程时,其实方法很简单,.只需要由我们自行决定线程何时退出就可以了.

public class CreateThread implements Runnable {

    volatile boolean stopme = false;

    public void stopMe() {
        stopme = true;
    }
    @Override
    public void run() {
        while (true) {
            if (stopme) {
                System.out.println("exit by stop me");
                break;
            }
            synchronized (u) {
                int v = (int) (System.currentTimeMillis() / 1000);
                u.setId(v);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                u.setName(String.valueOf(v));
            }
            Thread.yield();
        }
    }
}

也就是说,必须要设置一个标志,或者说是一个指令,去显式地告诉线程,应该去终止运行,这种方式才是正确终止线程的方式

2.争用条件

当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏,这种现象就是“争用条件”。

3.线程的互斥与同步
  • 所谓互斥就是相互排斥,就是说同一时间只能有一个线程对临界区进行数据的操作。
  • 所谓同步就是线程之间的一种通信机制,即一个线程在处理完所要做的事情之后会去告诉其他线程说我做完了。

在代码中,如何进行同步与互斥呢?(synchronized 关键字)
首先,我需要声明一个锁对象,类型为object的。


15200008-003f2cc2eddfb233.png
锁对象

构造完锁对象之后,我们对其进行操作,从而实现互斥行为。在这我们使用synchronized块的方式实现互斥行为。对处理共享数据的代码片段去进行加锁


15200008-97293bb34295f3ce.png
加锁处理

在执行完毕之后,我们还需要进行通知其他线程的操作(Notify)


15200008-b622359d8bb2c19f.png
notify

当一个线程进入wait状态之后,会放入一个锁对象的wait set之中,当使用notify或者notifyAll之后,会去通知wait set中的所有线程,去进行时间片的调度。

三、关键字

1.认识Thread的 start() 和 run()
start()
  • 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。.
  • 结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
  • 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
  • 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
run():
  • 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
  • Thread 的子类应该重写该方法。
  • run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
2.等待(wait)和通知(notify)
  • 为了支持多线程间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法,这两个方法并不是在Thread类中的 而是在Object类中,这意味着任何对象都可以调用这两个方法.
  • 当在一个对象实例上调用wait()方法后,当前先吃就会在这个对象上等待,比如线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,而转为等待状态,等待何时结束呢?线程A会一直等到其他线程调用了obj.notify()方法为止,这时,obj对象就俨然成为了多线程之间通信的有效手段.
  • 那wait()和notify()究竟是如何工作的呢?如果一个线程调用了object.wwait() 那么他就会进入object对象的等待队列,这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待一个对象.当object.notify()被调用时,他就会从这个等待队列中,随机选择一个线程,并将其唤醒,这个选择是不公平的,并不是先等待的线程就会优先选择,这个选择是完全随机的.
  • 除了notify()方法外,Object对象还有一个类似的notifyAll()方法,.他和notify()的功能基本一样,区别在于他会唤醒等待队列中所有等待的线程,而不是随机的一个,
  • 这里需要注意,Object.wait()方法并不是可以随便调用的.他必须包含在对应的synchronized语句中.无论是wait()和notify()都需要首先获得目标对象的一个监听器,
    注意

Object.wait()和Thread.sleep()两个方法都可以让线程等待若干时间.除了wait()可以被唤醒外,wait()执行完会释放目标对象的锁,而sleep()不会释放任何资源!

public final synchronized void join(long millis)throws InterruptedException
public final void join() throws InterruptedException

第一个方法给出了一个最大等待时间,如果超出给定时间目标线程还在执行,当前线程也会因为等待不及了,而继续执行下去 第二方法表示无线等待.它会阻塞当前线程,知道目标线程执行完毕.

public class JoinMain {
    public volatile static int i = 0;

    public static class AddThread extends Thread {
        public void run() {
            for (i = 0; i < 10000000; i++) ;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AddThread at = new AddThread();
        at.start();
        at.join();
        System.out.println(i);
    }
}

主函数中,如果不是用join()等待AddThread, 那么得到的i 很可能是0或者一个非常小的数字,但在join()后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,故在join()返回时.AddThread已经执行完了.故i 永远是10000000.
注意:不要在应用程序中 在Thread对象实例上使用类似wait()或者notify等方法, 因为折痕有可能会影响系统API的运行,或者被系统API所影响!
另外一个比较有趣的方法是Thread.yield();

public static native void yield();
  • 这是一个静态方法,一旦执行,它会使当前线程让出CPU.当时注意,让出CPU并不代表当前线程不执行了.当前线程让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了,因此 对Thread.yield()的调用就好像在说:我已经完成一些最重要的工作 了.我应该休息一下了.可以给其他线程一些工作机会了!
  • 如果你觉得一个线程不那么重要,或者优先级很低,而且害怕他会占用太多CPU资源,那么可以适当的时候调用这个方法,.给予其他重要的线程更多的工作机会!
3.等待线程结束(join)和谦让(yield)

很多时候 一个线程的输入可能非常依然于另一个或者多个线程的输出.此时 这个线程就需要等待依赖线程执行完毕,才能继续执行,JDK提供了join()操作来实现这个功能,

4.volatile
  • Java使用了一些特殊的操作或者关键字来申明,告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令. 易变得 不稳定的.
  • 当你用volatile声明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或者线程修改.为了确保可见性,虚拟机做了特殊的手段,保证这个变量的可见性!

四线程的状态

线程状态从大的方面来说,可归结为:初始状态、可运行状态、不可运行状态和消亡状态,说明如下:

1)线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样,当我们new了thread实例后,线程就进入了初始状态;

2)当该对象调用了start()方法,就进入可运行状态;

3)进入可运行状态后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;

4)进入运行状态后case就比较多,大致有如下情形: ﹒run()方法或main()方法结束后,线程就进入终止状态;

  • 当线程调用了自身的sleep()方法或其他线程的join()方法,就会进入阻塞状态(该状态既停 止当前线程,但并不释放所占有的资源)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配时间片;
  • 当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被锁牢(synchroniza,lock),将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得,一旦线程获得锁标记后,就转入可运行状态,等待OS分配 CPU时间片;
  • 当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒所有所线程,线程被唤醒后会进入锁池,等待获取锁标记。 当线程调用stop方法,即可使线程进入消亡状态,但是由于stop方法是不安全的,不鼓励使用,大家可以通过run方法里的条件变通实现线程的 stop。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值