09 - JavaSE之线程

线程

线程的基本概念

线程是一个程序里面不同的执行路径。

  • 进程与线程的区别
  1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换开销大。
  2. 线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程的切换开销小。
  3. 多进程:在操作系统中能同时运行多个程序。
  4. 多线程:在同一应用程序中有多个顺序流同时执行。

线程的创建与启动

  1. Java 的线程是通过 java.lang.Thread 类来实现的。
  2. VM 启动时,会有一个由主方法 main 所定义的线程。
  3. 可以通过创建 Thread 的实例来创建新的线程。
  4. 每个线程都是通过某个特定的 Thread 对象所对应的 run() 方法来完成这个线程要做的任务,方法 run() 成为线程体。
  5. 通过调用 Thread 类的 start() 方法来启动一个线程。
  • 有两种方法创建新的线程:
  1. 第一种(推荐使用):定义线程类实现 Runnable 接口,然后重写 run 方法, 然后以这个线程类创建 Thread 类,然后调用这个 Thread 类的 start() 方法,就可以开始执行这个线程,这个线程具体要执行的内容在 run 方法里面。

    public class Test {
    public static void main(String[] args) {
    MyThread mt = new MyThread();
    Thread th = new Thread(mt);
    th.start();

        for(int i=0; i<100; i++) {
            System.out.println("Main: " + i);
        }
    }

    }

    class MyThread implements Runnable {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println("MyThread:" + i);
    }
    }
    }

PS: 如果我们没有 new一个 Thread 对象出来,而是直接使用 MyThread 的 run 方法(mt.run()),这就是方法调用,而不是启动线程了,结果就是先后打印语句,而不是并行打印语句了。

  1. 第二种:可以定义一个 Thread 的子类 myThread,并且重写 Thread 的 run 方法(Thread 也实现了Runnable 接口),然后生成子类 myThread 的对象,最后调用子类 myThread 对象的 start() 方法即可。

    public class Test {
    public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();

        for(int i=0; i<100; i++) {
            System.out.println("------ " + i);
        }
    }

    }

    class MyThread extends Thread {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println("MyThread:" + i);
    }
    }
    }


线程的状态转换

09 - JavaSE之线程


线程控制基本方法

isAlive() // 判断线程是否还活着,即线程是否还未终止
getPriority() // 获得线程的优先级数值
setPriority() // 设置线程的优先级数值
Thread.sleep(...) // 将当前线程指定睡眠时间
join() // 将一个线程合并到某个线程上,成为一个线程执行
yield() // 让出CPU,当前线程进入就绪队列等待调度
wait() // 当前线程进入对象的 wait pool
notify()/notifyAll() // 唤醒对象 wait pool中的一个/所有等待线程
  • sleep 方法

可以调用 Thread 的静态方法 Thread.sleep(long ms) 使得当前线程休眠。(哪个线程调用了Thread.sleep 方法,哪个线程就 sleep)

import java.util.*;
public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread th = new Thread(mt);
        th.start();

        try {
            Thread.sleep(10000);
        } catch(InterruptedException e) {

        }

        th.interrupt();
    }
}

class MyThread implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            System.out.println("=== " + new Date() + " ===");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                flag = false;
            }
        }
    }
}
  • join 方法

将一个线程合并到某个线程上,成为一个线程执行。(如下程序就先执行线程 th ,之后才会执行主线程 Main ,而不是并行执行。)

import java.util.*;
public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread th = new Thread(mt);
        th.start();

        try {
            th.join();
        } catch(InterruptedException e) {

        }

        for(int i=0; i<10; i++) {
            System.out.println("Main Thread......");
        }

        th.interrupt();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            System.out.println("=== " + new Date() + " ===");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}
  • yield 方法

让出CPU,当前线程进入就绪队列等待调度。

public class Test {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("mt1");
        MyThread mt2 = new MyThread("mt2");
        mt1.start();
        mt2.start();
    }
}

class MyThread extends Thread {
    MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        try {
            for (int i = 0; i < 50; i++) {
                System.out.println(getName() + " --- " + i);
                Thread.sleep(100);

                if (i % 10 == 0) {
                    Thread.yield();
                }
            }
        } catch (InterruptedException e) {

        }
    }
}

线程的优先级

  • Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。
  • 线程的优先级用数字表示,范围从 1 到 10,一个线程的缺省优先级是 5。

    Thread.MIN_PRIORITY = 1
    Thread.MAX_PRIORITY = 10
    Thread.NORM_PRIORITY = 5

  • 使用下面方法获得或设置线程对象的优先级:

    int getPriority();
    void setPriority(int newPriority);


Java 中的线程临界区

  • 系统中每次只允许一个线程访问的资源叫做临界资源。
  • 对临界资源进行访问的程序代码区域叫做临界区。
  • Java 中通过 synchronized 关键字和对象锁机制对临界区进行管理。
  • Java 中的每个对象都可以作为对象锁使用。

线程同步

我们先看这样一个程序:

public class Test implements Runnable {
    Timer time = new Timer();
    public static void main(String args[]){
        Test test = new Test();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        time.add(Thread.currentThread().getName());
    }
}

class Timer {
    private static int num = 0;
    public void add(String name) {
        num ++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(name + " 是第 " + num + " 个使用timer的线程");
    }
}

我们两个线程同时执行,而且调用同一个方法,相当于访问同一个共享资源 num,执行的结果为:

t2 是第 2 个使用timer的线程
t1 是第 2 个使用timer的线程

这是怎么回事呢?按照猜想,虽然 t1 t2 线程并行执行,但是先开启的 t1 进程,num = 1;在开启的 t2 线程,num = 2,所以,应该是:t1 是第 1 个使用timer的线程;t2 是第 2 个使用timer的线程 才对啊?

其实,这就涉及到线程同步的问题,如果在一个线程访问一个共享对象的时候没有给这个共享资源上锁的话,那么这个线程操作的共享资源可能就是错误的,因为可能别的进程也在访问这个共享资源。

那么,我们就需要在进程访问这个共享资源的时候,将其上锁,上锁的方式有两种:(还是以上面的程序为例:)

// 方式一
class Timer {
    private static int num = 0;
    public void add(String name) {
        synchronized (this) {} {  // 资源上锁
            num ++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {

            }
            System.out.println(name + " 是第 " + num + " 个使用timer的线程");
        }
    }
}

synchronized (this) {

// 需要锁定的内容

}

synchronized(this)表示: 锁定当前对象。括号中的语句在一个线程执行的过程中,不会被另一个线程打断。

// 方式二
class Timer {
    private static int num = 0;
    synchronized public void add(String name) { // 资源上锁
        num ++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(name + " 是第 " + num + " 个使用timer的线程");
    }
}

在 add 函数加上 synchronized 关键字,就表示哪个进程调用了add 函数,那么这个进程在执行这个方法的时候,锁定当前对象。

  • 在 Java 语言中引入了对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任意时刻,智能有一个线程访问该对象。
  • 关键字 synchronized 来与对象的互斥锁联系。当某个对象 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。

线程死锁

什么是线程死锁?

如果两个或多个线程分别拥有不同的资源, 而同时又需要对方释放资源才能继续运行时,就会发生死锁。简单来说:死锁就是当一个或多个进程都在等待系统资源,而资源本身又被占用时,所产生的一种状态。

public class Test implements Runnable {
    public int flag = 0;

    static Object o1 = new Object();
    static Object o2 = new Object();
    
    public static void main(String args[]){
        Test test1 = new Test();
        Test test2 = new Test();

        test1.flag = 1;
        test2.flag = 2;

        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test2);

        t1.start();
        t2.start();
    }

    public void run() {
        System.out.println("flag=" + flag);
        if(1 == flag) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }

        if(2 == flag) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o1) {
                    System.out.println("2");
                }

            }
        }
    }
}

注意:以上代码中 Object 对象 O1 O2 一定是 static 的,否则不能得到进程死锁。还有一定要有 Thread.sleep 语句。

举例:

public class Test implements Runnable {
    public int  b = 100;

    public synchronized void m1() throws Exception{
        b = 1000;
        Thread.sleep(3000);
        System.out.println("m1:b = " + b);
    }

    public void m2 () {
        System.out.println("m2:b = " + b);
        b = 2;
        System.out.println("m2:b = " + b);
    }

    public void run() {
        try {
            m1();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        Test tt = new Test();
        Thread t = new Thread(tt);
        t.start();

        Thread.sleep(1000);

        tt.m2();

        System.out.println("b = " + tt.b);
    }
}

提问:各个打印 b 的值为多少?

m2:b = 1000

m2:b = 2

b = 2

m1:b = 2

为什么 b 的值可以被修改呢?

因为,如果一个方法加了 synchronized ,而且有一个进程正在访问这个方法,那么只能说明别的进程不可以同时访问这个方法,但是并不妨碍别的进程访问其他的方法,如果其他的方法中有对你需要保护的对象(这里是 b)进行操作的话,也是允许的。

所以,如果要保护一个需要同步的对象的话,对访问这个对象的所有方法考虑加不加 synchronized 。因为一个方法加了锁,只是另一个线程不能访问加了锁的方法,但是可以访问其他的方法,其他的方法可能修改了你需要同步的对象。synchronized 修饰的方法可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。(所以,上面的 m2 方法也需要加 synchronized 修饰。)


生产者和消费者问题:

/**
 * 这里模拟的是生产者和消费者的问题:这里生产者生产樱花膏,消费者吃樱花膏。然后
 * 还有一个装樱花膏的篮子 basket. 我们需要做的生产者生产的樱花膏按照顺序放入篮子,
 * 吃货消费者按照顺序从顶部依次拿出来吃,所以 basket 的数据结构是栈。
 *
 * 注意:生产者生产的樱花膏要和吃货需要吃的樱花膏数量一致,否则程序会wait
 */

public class Test {
    public final int NUM = 10;
    public static void main(String[] args) {
        BasketStack bs = new BasketStack();
        Producer p = new Producer(bs);
        Consumer c = new Consumer(bs);

        new Thread(p).start();
        new Thread(p).start();
        new Thread(p).start();

        new Thread(c).start();
    }
}

// 樱花膏
class Sakura {
    private int id = 0;

    Sakura (int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return ((Integer)id).toString();
    }
}

// 篮子
class BasketStack {
    int index = 0;
    Sakura[] sakuras = new Sakura[6]; // 一个篮子只能装6个樱花膏

    public synchronized void push(Sakura sa) {
        while(index == (sakuras.length - 1)) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        sakuras[index] = sa;
        index ++;
    }

    public synchronized Sakura pop() {
        while(index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        index--;
        return sakuras[index];
    }
}

// 生产者
class Producer implements Runnable {
    BasketStack bs = null;

    Producer(BasketStack bs) {
        this.bs = bs;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            Sakura sa = new Sakura(i);
            bs.push(sa);
            System.out.println("生产者:" + sa.toString());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 吃货
class Consumer implements Runnable {
    BasketStack bs = null;

    Consumer(BasketStack bs) {
        this.bs = bs;
    }

    @Override
    public void run() {
        for(int i=0; i<30; i++) {
            System.out.println("吃货:" + bs.pop().toString());

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

wait和sleep的区别:

1.wait之后 锁就不归我所有,别的线程可以访问锁定对象;sleep后 锁还是我的,别的线程不可以访问锁定对象。

2.调用 wait方法的时候必须先锁定对象,否则谈何wait。

2.wait是Object的方法,sleep是Thread的方法。


总结(关键字联想)

  • 线程/进程的概念
  • 创建和启动线程的方式
  • sleep
  • join
  • yield
  • synchronized
  • wait
  • notify/notifyAll
上一篇:亚洲最大AI超算,商汤智算中心AIDC今天启动运营


下一篇:7 Papers & Radios | Meta AI首个多模态自监督算法;牛津、谷歌等撰文综述AutoRL