Java 多线程

进程和线程

进程
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

线程
是进程中的单个顺序控制流,是一条执行路径。

线程的生命周期

线程生命周期

线程调度

两种调度模型

  • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

Java使用的是抢占式调度模型。

Thread 类中设置和获取线程优先级的方法

  • public final int getPriority():返回此线程的优先级
  • public final void setPriority(int newPriority):更改此线程的优先级
    线程默认优先级是 5;线程优先级是一个整数,其取值范围是:1 - 10
    线程优先级高仅仅表示线程获取的 CPU 时间片的几率高,并不是一定先执行。

创建线程的方式

继承 Thread 类

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

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

注意事项:run() 方法和 start() 方法的区别

  • run():封装线程执行的代码;直接调用则相当于普通方法的调用
  • start():启动线程;然后由 JVM 调用此线程的 run() 方法

实现 Runnable 接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ", " + i);
        }
    }
}

public class RunnableMain {
	public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

相比继承 Thread 类,实现 Runnable 接口的优点

  • 避免了 Java 单继承的局限性
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较子的体现了面向对象的设计思想

通过 Callable 接口和 FutureTask 类

// 创建 Callable 接口的实现类
public class MyCallable implements Callable<Integer> {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用 FutureTask 类来包装 Callable 对象
        // 该 FutureTask 对象封装了该 Callable 对象 call() 方法的返回值
        FutureTask<Integer> ft = new FutureTask<>(new MyCallable());
        new Thread(ft).start();
        // 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
        System.out.println("子线程结束后返回的值:" + ft.get());
    }

    // 实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }
}

线程同步

问题场景示例

多个窗口同时卖票:

public class SellTicket implements Runnable {
    private int tickets = 100;
    @Override
    public void run() {
        for (; ; ) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出 " + tickets + " 号票");
                tickets--;
            }
        }
    }
}

	public static void main(String[] args) {
        SellTicket st = new SellTicket();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        t1.start();
        t2.start();
        t3.start();
    }

/* 部分输出:
窗口二卖出 2 号票
窗口三卖出 2 号票
窗口一卖出 2 号票
窗口二卖出 -1 号票 */

出现的问题:

  • 相同票被卖多次
  • 超卖

synchronized 关键字

Java 以提供关键字 synchronized 的形式,为防止资源冲突提供了内置支持。当任务希望执行被 synchronized 关键字保护的代码片段的时候,Java 编译器会生成代码以查看锁是否可用。如果可用,该任务获取锁,执行代码,然后释放锁。

三大特性
有序性、可见性、原子性

同步代码块

给代码块加锁,任何对象都可以看成是一把锁。格式:

synchronized (对象) {
	// 对共享数据操作的代码
}

对卖票线程的改进:

public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        for (; ; ) {
        	// 此处如果这样写 代表每一个线程都有独立的线程锁,而我们需要的是三个线程共用一个线程锁
            // synchronized (new Object()) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出 " + tickets + " 号票");
                    tickets--;
                }
            }
        }
    }
}

好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,每个线程都会去判断同步上的锁,这样很耗费资源,会降低程序的运行效率

同步方法

即把 synchronized 关键字加到方法上

  • 同步普通方法格式
    修饰符 synchronized 返回值 类型方法名(方法参数){ }
    同步普通方法的锁对象是 this
  • 同步静态方法格式
    修饰符 static synchronized 返回值 类型方法名(方法参数){ }
    同步静态方法的锁对象是 类名.class

Lock 锁

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
相关方法:

方法名说明
void lock()获得锁
void unlock()释放锁

对卖票线程的改进:

public class SellTicket implements Runnable {
    private int tickets = 100;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        for (; ; ) {
            lock.lock();
            try {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出 " + tickets + " 号票");
                    tickets--;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

线程间通信

生产者和消费者场景示例

模拟生产者生产商品放入仓库,消费者从仓库取出商品消费,要求当仓库中有商品时不生产,无商品时不消费。

仓库类

public class Warehouse {
    /** 商品 */
    private int goods;
    /** 仓库中是否有商品标识 */
    private boolean flag = false;
    
    /** 没加 synchronized 将抛出 java.lang.IllegalMonitorStateException 异常 */
    public synchronized void put(int goods) {
        // 有商品时 生产者等待生产
        if (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.goods = goods;
        System.out.println("生产者将商品 " + goods + " 号放入仓库");
        // 标记仓库中有商品
        flag = true;
        // 唤醒其它等待线程
        notifyAll();
    }

    /** 没加 synchronized 将抛出 java.lang.IllegalMonitorStateException 异常 */
    public synchronized void get() {
        // 无商品时 消费者等待消费
        if (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者将商品 " + goods + " 号取出仓库");
        // 标记仓库中无商品
        flag = false;
        // 唤醒其它等待线程
        notifyAll();
    }
}

生产者类

public class Producer implements Runnable {
    private Warehouse warehouse;
    public Producer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            this.warehouse.put(i);
        }
    }
}

消费者类

public class Consumer implements Runnable {
    private Warehouse warehouse;
    public Consumer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }
    @Override
    public void run() {
        for (; ; ) {
            this.warehouse.get();
        }
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();
        Producer producer = new Producer(warehouse);
        Consumer consumer = new Consumer(warehouse);
        Thread p1 = new Thread(producer);
        Thread c1 = new Thread(consumer);
        p1.start();
        c1.start();
    }
}

/** 输出
生产者将商品 1 号放入仓库
消费者将商品 1 号取出仓库
生产者将商品 2 号放入仓库
消费者将商品 2 号取出仓库
...
*/

相关代码地址

相关代码地址


版权声明:本文为Amazing20原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。