【学习笔记】Java笔记3:线程

295 阅读4分钟

1. 相关概念

  • 程序:指令的集合,是一段静态的代码。

  • 进程:正在运行的程序,是一个动态的过程;有自身的生命周期,是资源分配的单位(系统为每个进程分配不同的内存区域)。

  • 线程:进程的进一步细化,是程序内部的一条执行路径。

2. 线程的创建

2.1 继承Thread

1、声明一个类继承Thread类,并重写run()方法

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

2、创建该类的实例对象并调用start()方法

public class Test {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        // 如果调用run()则只是普通的方法调用,不会创建新线程
        t1.start();
    }
}

2.2 实现Runnable

1、声明一个类实现Runnable接口,并重写run()方法

public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

2、创建该类的实例对象,并把该对象作为参数传入Thread的构造方法中,再调用Thread对象的start()方法

public class Test2 {
    public static void main(String[] args) {
        MyThread2 t2 = new MyThread2();
        new Thread(t2).start();
    }
}

2.3 实现Callable

1、声明一个类实现Callable接口,并重写call()方法

public class MyThread3 implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

2、创建该类的实例对象,把它作为参数传入FutureTask的构造器中,再把FutureTask对象传入Thread的构造器中,调用Thread对象的start()。如果需要获取call()的返回值,则调用FutureTask对象的get()方法

public class Test3 {
    public static void main(String[] args) {
        MyThread3 t = new MyThread3();
        FutureTask ft = new FutureTask(t);
        new Thread(ft).start();
        try {
            // get()的返回值是FutureTask构造器中,参数Callable实例对象的call()的返回值
            Object sum = ft.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.4 使用线程池

1、声明一个类,实现Runnable或Callable接口,如上面的MyThread2或MyThread3

2、创建ExecutorService对象,并调用它的execute()方法或submit()方法

public class Test4 {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

//        设置线程池的各种属性
//        ThreadPoolExecutor newService = (ThreadPoolExecutor) service;
//        newService.setXXX();

        service.execute(new MyThread2()); // 适合实现Runnable的对象
//        service.submit(new MyThread3());  适合实现Callable的对象

        // 关闭线程池
        service.shutdown();
    }
}

3. Thread的常用方法

方法作用
void start()启动线程,调用run()方法
void run()如果该线程是使用独立的Runnable运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
Thread currentThread()获取当前正在执行的线程
String getName()获取当前线程的名字
void setName(String name)设置当前线程的名字
void yield()暂停当前正在执行的线程对象,并执行其他线程
void join()等待当前线程执行完成。比如:在线程a中调用线程b的join()方法,那么a就会进入阻塞状态,直到线程b执行完,a才继续执行
void stop()(已过时)强制结束当前线程
void sleep(long millis)让线程休眠millis毫秒
boolean isAlive()判断线程是否处于活动状态

4. 线程优先级

4.1 三个基本优先级

​ 1.MAX_PRIORITY:10

​ 2.NORM_PRIORITY:5

​ 3.MIN_PRIORITY:1

  高优先级线程会抢占低优先级线程的CPU执行权,但只是概率上的。并不代表只有当高优先级的线程执行完后,低优先级线程才执行。

4.2 相关方法

方法作用
int getPriority()获取线程的优先级
void setPriority(int newPriority)更改线程优先级

5. 线程的生命周期

  线程的生命周期大致如下图所示:

005-01.png

6. 线程同步

6.1 线程安全问题

  当多个线程操作某个共享资源时,就会出现线程不安全问题。例子如下。

6.2 线程不安全例子

  模仿售票站卖票的情况。

1、创建一个售票站,共10张票

public class Station implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

2、创建三个窗口卖票,这10张票就是共享资源

public class Test {
    public static void main(String[] args) {
        Station s = new Station();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

3、分析结果

输出结果如下所示

窗口1 : 10
窗口2 : 10
窗口1 : 9
窗口2 : 8
窗口1 : 7
窗口2 : 6
窗口1 : 5
窗口1 : 3
窗口1 : 2
窗口1 : 1
窗口3 : 4
窗口2 : 4

  虽然每次运行结果不同,但是大概率会出现多个窗口出售同一张票的情况,这显然是不合理的,是线程不安全的。

6.3 解决线程不安全

6.3.1 同步代码块

1、synchronized代码块的编写

synchronized (mutex) {
    // 操作共享资源的代码
}
// mutex:同步监视器(锁),任何对象都可以充当同步监视器,但这多个线程必须共享同一个监视器。

2、修改上述例子的Station类

public class Station implements Runnable{
    private int ticket = 10;
    Object mutex = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (mutex) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " : " + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

3、修改后的运行结果如下

窗口2 : 10
窗口2 : 9
窗口2 : 8
窗口2 : 7
窗口2 : 6
窗口2 : 5
窗口2 : 4
窗口3 : 3
窗口3 : 2
窗口3 : 1

无论运行多少次,都不会出现多个窗口出售同一张票的情况。

6.3.2 同步方法

1、把操作共享资源的代码封装到一个方法中,再用synchronized修饰这个方法

public class Station implements Runnable{
    private int ticket = 10;
    @Override
    public void run() {
        while (true) {
            boolean flag = sell();
            if (!flag) {
                break;
            }
        }
    }

    private synchronized boolean sell() { // 此时同步监视器是this关键字
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + ticket);
            ticket--;
            return true;
        }
        return false;
    }
}

2、修改后的运行结果也是合理安全的

3、注意事项

  • 非静态同步方法的同步监视器是this
  • 静态同步方法的同步监视器是所属类本身,即XXX.class

6.3.3 Lock锁

  使用Lock接口的实现类ReentrantLock也可以解决线程不安全问题。代码如下

public class Station implements Runnable{
    private int ticket = 10;
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                // 上锁
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " : " + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                // 解锁
                lock.unlock();
            }
        }
    }
}

(注:使用Lock锁需要手动上锁和解锁)

7. 线程通信

  本文的线程通信可以理解为线程之间有规律地访问共享资源。在上述的线程安全例子中,各线程总是随机获取CPU执行权,导致每次运行结果不一。那么我们可以通过几个方法来让线程之间形成某种规律。比如,有这样的需求:让两个线程轮流输出1至100,于是可以这么做:

7.1 编写Number类

public class Number implements Runnable {
    private int num = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                obj.notify(); // 唤醒进入了wait()的线程,若有多个,则按优先级来
                if (num <= 100) {
                    System.out.println(Thread.currentThread().getName() + " : " + num);
                    num++;
                    try {
                        obj.wait(); // 当前线程进入阻塞状态,直到其他线程调用监视器的notify()方法
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

7.2 编写测试类

public class Test02 {
    public static void main(String[] args) {
        Number n = new Number();
        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

7.3 输出结果

线程1 : 1
线程2 : 2
线程1 : 3
线程2 : 4
线程1 : 5
线程2 : 6
线程1 : 7
线程2 : 8
线程1 : 9
线程2 : 10
......

7.4 相关方法说明

方法作用
void wait()当前线程进入阻塞状态,直到其他线程调用监视器的notify()方法
void notify()唤醒进入wait()的线程,若有多个,则按优先级来
void notifyAll()唤醒全部进入wait()的线程

注意事项

  • 这三个方法要使用在同步代码块或同步方法中
  • 这三个方法的调用者必须是同步监视器。在上述例子中,obj对象是监视器,所以由它来调用
  • 由于任何对象都可以是同步监视器,且这三个方法的调用者得是监视器,所以这三个方法其实是在Object类中声明的

7.5 sleep()和wait()的比较

相同点:

  • 都可以使当前线程进入阻塞状态

不同点:

  • 声明的位置不同,sleep()是声明在Thread类中,而wait()是声明在Object类中
  • 调用的场景不同,sleep()可以在任何场景下用,而wait()要用在同步代码块或同步方法中
  • sleep()不会释放同步监视器(锁),而wait()会释放同步监视器(锁)

8. 生产者/消费者问题

  需求如下:某产品最多为20个,当产品个数大于0时,消费者才可以消费;当产品个数小于20时,生产者才可以生产。

1、编写产品类

public class Product {
    private int num = 0;
    public synchronized void addProduct() {
        if (num < 20) {
            System.out.println(Thread.currentThread().getName() + "生产产品");
            num++;
            System.out.println("现有产品" + num + "个");
            System.out.println("-----------------------------------");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void delProduct() {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "消费产品");
            num--;
            System.out.println("现有产品" + num + "个");
            System.out.println("-----------------------------------");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、编写生产者类

public class Producer extends Thread {
    private Product product;
    @Override
    public void run() {
        while (true) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.addProduct();
        }
    }
    public Producer(Product product) {
        super();
        this.product = product;
    }
}

3、编写消费者类

public class Consumer extends Thread {
    private Product product;
    @Override
    public void run() {
        while (true) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.delProduct();
        }
    }
    public Consumer(Product product) {
        super();
        this.product = product;
    }
}

4、编写测试类

public class Test {
    public static void main(String[] args) {
        Product p = new Product();
        Producer producer = new Producer(p);
        Consumer consumer = new Consumer(p);

        producer.setName("生产者");
        consumer.setName("消费者");

        producer.start();
        consumer.start();
    }
}

5、运行结果如下

生产者生产产品
现有产品1个
-----------------------------------
消费者消费产品
现有产品0个
-----------------------------------
生产者生产产品
现有产品1个
-----------------------------------
消费者消费产品
现有产品0个
-----------------------------------
......

9. 相关链接