java 线程基础知识讲解

0. 简介

这个系列开始来讲解 Java 多线程的知识,这节就先讲解多线程的基本知识。

1. 进程与线程

1.1 什么是进程?

进程就是在运行过程中的程序,就好像手机运行中的微信,QQ,这些就叫做进程。

1.2 什么是线程?

线程就是进程的执行单元,就好像一个音乐软件可以听音乐,下载音乐,这些任务都是由线程来完成的。

1.3 进程与线程的关系

  • 一个进程可以拥有多个线程,一个线程必须要有一个父进程
  • 线程之间共享父进程的共享资源,相互之间协同完成进程所要完成的任务
  • 一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行

2. 如何创建线程

Java 中创建线程的方法有三种,以下来逐一详细讲解。

2.1 继承 Thread 类创建线程

使用继承 Thread 类创建线程的步骤如下:

  1. 新建一个类继承 Thread 类,并重写 Thread 类的 run() 方法。
  2. 创建 Thread 子类的实例。
  3. 调用该子类实例的 start() 方法启动该线程。
    class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
           Log.i(TAG,"线程运行了")
        }
    }
  MyThread myThread = new MyThread();
        myThread.start();

最后运行打印出来的结果是:

线程运行了

 

2.2 继承 Thread 类创建线程

使用继承 Thread 类创建线程的步骤如下:

  1. 创建一个类实现 Runnable 接口,并重写该接口的 run() 方法。
  2. 创建该实现类的实例。
  3. 将该实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
  4. 调用该 Thread 线程对象的 start() 方法。

代码举例如下:

   class MyRunnable implements Runnable{
        @Override
        public void run() {
            Log.i(TAG,"线程运行了")
        }
    }

 Thread thread = new Thread(new MyRunnable());
        thread.start();

最后运行打印出来的结果是:

线程运行了

2.3 使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

》call()方法可以有返回值

》call()方法可以声明抛出异常

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码实现::

   FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 0;
            }
        });
        new Thread(task,"有返回值的线程").start();

        try{
            System.out.println("子线程的返回值:"+task.get());//get()方法会阻塞,直到子线程执行结束才返回
        }catch (Exception e){

        }

最后运行打印出来的结果是:

子线程的返回值:0

2.4 三种创建方式的区别

实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

  1. 线程只是实现Runnable或实现Callable接口,还可以继承其他类。
  2. 这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
  3. 但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
  4. 继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

注:一般推荐采用实现接口的方式来创建多线程

 

3. 线程的生命周期

当一个线程开启之后,它会遵循一定的生命周期,它要经过新建,就绪,运行,阻塞和死亡这五种状态,理解线程的生命周期有助于理解后面的相关的线程知识。

首先通过一张图片来加强记忆 图片来源百度百科

 

生命周期的五种状态

 3.1、 新建(new Thread)

当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread  t1=new Thread();

3.2、就绪(Runnable)

线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

3.3、运行(Running)

线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

3.4、死亡(Dead)

当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止

异常终止:调用stop()方法让一个线程终止运行

3.5、堵塞(Blocked)

由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。

正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

正在等待:调用wait()方法。(调用notify()方法回到就绪状态)

被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

4.常用方法

void run()   创建该类的子类时必须实现的方法

void start() 开启线程的方法

static void sleep(long t) 释放CPU的执行权,不释放锁

static void sleep(long millis,int nanos)

final void wait()释放CPU的执行权,释放锁

final void notify()唤醒线程

final void notifyAll()唤醒线程

static void yied()可以对当前线程进行临时暂停(让线程将资源释放出来)

5.容易混淆的一些方法区别

sleep/join/yield/join方法介绍区别

5.1 sleep()方法

需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。 

public class Main2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyThread thread2 = new MyThread("t1");
        thread2.start();// 启动线程
        for (int i = 0; i <= 5; i++) {
            System.out.println("I am main Thread");
        }
    }


    class MyThread extends Thread {
        MyThread(String s) {
            super(s);
        }

        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(getName() + ":" + i);
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.2 wait()方法

 wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。注意,它们都是Object类的方法,而不是Thread类的方法。 
  wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。 
  除了使用notify()和notifyAll()方法,还可以使用带毫秒参数的wait(long timeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。 
  此外,wait(),notify()及notifyAll()只能在synchronized语句中使用,但是如果使用的是ReenTrantLock实现同步,该如何达到这三个方法的效果呢?解决方法是使用ReenTrantLock.newCondition()获取一个Condition类对象,然后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。

5.3 yield方法

暂停当前正在执行的线程对象。

yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

yield()只能使同优先级或更高优先级的线程有执行的机会。 

public class Main2Activity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        MyThread3 t1 = new MyThread3("t1");
        /* 同时开辟了两条子线程t1和t2,t1和t2执行的都是run()方法 */
        /* 这个程序的执行过程中总共有3个线程在并行执行,分别为子线程t1和t2以及主线程 */
        MyThread3 t2 = new MyThread3("t2");
        t1.start();// 启动子线程t1
        t2.start();// 启动子线程t2
        for (int i = 0; i <= 5; i++) {
            System.out.println("I am main Thread");
        }

    }
    class MyThread3 extends Thread {
        MyThread3(String s) {
            super(s);
        }

        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(getName() + ":" + i);
                if (i % 2 == 0) {
                    yield();// 当执行到i能被2整除时当前执行的线程就让出来让另一个在执行run()方法的线程来优先执行
                /*
                 * 在程序的运行的过程中可以看到,
                 * 线程t1执行到(i%2==0)次时就会让出线程让t2线程来优先执行
                 * 而线程t2执行到(i%2==0)次时也会让出线程给t1线程优先执行
                 */
                }
            }
        }
    }
}

4.join方法

等待该线程终止。

等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

package concurrent;

public class TestJoin {

    public static void main(String[] args) {
        Thread thread = new Thread(new JoinDemo());
        thread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主线程第" + i + "次执行!");
            if (i >= 2)
                try {
                    // t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
    }
}

class JoinDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程1第" + i + "次执行!");
        }
    }
}

 

 

大家都有了以上相同的教程,但是人和人之间的区别在于:别人有颗更强大的内心,可怕的是比你聪明的人比你还要更努力!!

当你下定决心,准备前行的时候,剩下的只有坚持了。。。

 

如果大家觉得我写的还可以的话,请关注我的微信公众号:

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值