8.3 线程的生命周期
1. 线程的状态
JDK中用Thread.State类定义了线程的几种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
2. 线程五种状态之间的转换
8.4 线程的同步
1. 问题引入
多个线程执行的不确定性引起执行结果的不稳定,例如下面的问题,就可能出现多个窗口售卖同一张票或出售错票的情况(出现了线程的安全问题)。
例子:在火车站有三个窗口正在售票
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "号票");
ticket--;
} else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window, "窗口一");
Thread thread2 = new Thread(window, "窗口二");
Thread thread3 = new Thread(window, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
问题原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
在上述例子中,假设票数只剩一张,窗口一线程判断ticket大于0进入if语句,还未进行ticket的减1操作,这时窗口一线程被阻塞了,窗口二线程判断ticket也大于0则也进入了if语句,随后也被阻塞了,这时窗口一线程重新运行,输出了票号为1的票,并且进行了减票操作,随后窗口二线程也醒来了,那么这时ticket为0,因此输出的票号为0,出现了错票的情况;重票情况类似。
解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式:同步机制
同步锁机制:
对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
2. 解决线程安全问题的方式一:使用synchronized关键字
2.1 同步代码块:
使用synchronized关键字将操作共享数据的代码包裹起来
格式:
synchronized (同步监视器(或对象)){
// 需要被同步的代码
}
例子:
class Window implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "号票");
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window, "窗口一");
Thread thread2 = new Thread(window, "窗口二");
Thread thread3 = new Thread(window, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
2.2 同步方法
将synchronized关键字放在方法的声明中,表示整个方法为同步方法
格式
public synchronized void show (String name){
// 处理逻辑
}
例子:
class Window implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
show();
}
}
}
public synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "号票");
ticket--;
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window, "窗口一");
Thread thread2 = new Thread(window, "窗口二");
Thread thread3 = new Thread(window, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
2.3 说明:
-
操作共享数据的代码,即为需要被同步的代码。—> 不能包含代码多了,也不能包含代码少了,原因:
- 范围太小:没锁住所有有安全问题的代码
- 范围太大:没发挥多线程的功能
-
共享数据:多个线程共同操作的变量。比如:上述例子中的ticket就是共享数据。
-
同步监视器,即为锁。任何一个类的对象,都可以充当锁;但是要求多个线程必须要共用同一把锁。
-
在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器;在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用当前类(Xxx.class)充当同步监视器。
-
同步方法仍然涉及到同步监视器,只是不需要显式的声明。
-
非静态的同步方法,同步监视器是:this
-
静态的同步方法,同步监视器是:当前类本身
-
-
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
-
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)(应尽量避免使用suspend()和resume()来控制线程,已过时)
-
同步的方式,解决了线程的安全问题;但是,操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
3. 单例模式之懒汉式——线程安全
3.1 同步方法
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
3.2 同步代码块
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}
3.3 双重校验锁(推荐)
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4. 线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 使用同步时,要避免出现死锁。
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
例子
public class DealLock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
}
}
}
}).start();
}
}
5. 解决线程安全问题的方式二:使用Lock锁(JDK5新增)
Lock锁
-
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
-
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁;其构造方法的参数是指是否公平,即锁的使用是否按照线程到来的顺序进行,默认为false
-
一般与try-catch-finally配合使用
格式
class A {
private ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock(); // 加锁
try{
//保证线程安全的代码;
}
finally{
lock.unlock(); // 释放锁
}
}
}
例子
class Window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "号票");
ticket--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Thread thread1 = new Thread(window, "窗口一");
Thread thread2 = new Thread(window, "窗口二");
Thread thread3 = new Thread(window, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
synchronized 与 Lock 的对比
-
二者都可以解决线程安全问题
-
Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
-
优先使用顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源) —> 同步方法(在方法体之外)
8.5 线程的通信
1. 问题引入
使用两个线程打印 1-100。线程1, 线程2 交替打印
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 调用该方法会唤醒一个线程
notify();
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + "正在打印" + number);
number++;
} else {
break;
}
try {
// 调用该方法的线程会进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread thread1 = new Thread(number, "线程一");
Thread thread2 = new Thread(number, "线程二");
thread1.start();
thread2.start();
}
}
2. 线程通信的相关方法
wait() 与 notify() 和 notifyAll()
- wait():执行此方法,当前线程就进入阻塞状态,释放同步监视器等资源,并等候其他线程调用notify()或notifyAll()方法唤醒
- notify():执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个
- notifyAll ():执行此方法,就会唤醒所有被wait的线程
其他说明
- wait(),notify(),notifyAll()这三个方法必须使用在同步代码块或同步方法中,否则会报java.lang.IllegalMonitorStateException异常
- wait(),notify(),notifyAll()这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会报java.lang.IllegalMonitorStateException异常
- wait(),notify(),notifyAll()这三个方法是定义在java.lang.Object类中
3. sleep() 和 wait()的异同
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
- 两个方法声明的位置不同:Thread类中声明sleep();Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用;wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁;wait()会释放锁。
4. 线程通信经典例题:生产者/消费者问题
问题描述
生产者(producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
代码实现
class Clerk {
private int productNum = 0;
public synchronized void produceProduct() {
if (productNum < 20) {
productNum++;
System.out.println(Thread.currentThread().getName() + "正在生产第" + productNum + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumeProduct() {
if (productNum > 0) {
System.out.println(Thread.currentThread().getName() + "正在消费第" + productNum + "个产品");
productNum--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(this.getName() + "开始生产产品");
while (true) {
clerk.produceProduct();
}
}
}
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(this.getName() + "开始消费产品");
while (true) {
clerk.consumeProduct();
}
}
}
public class ProducerAndConsumerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
producer.setName("生产者");
consumer.setName("消费者");
producer.start();
consumer.start();
}
}
8.6 JDK5.0新增线程创建方式
1. 新增方式一:实现Callable接口
步骤
- 创建一个实现Callable接口的实现类
- 实现call()方法,将线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 使用带参构造器,将Callable接口实现类的对象传递到带参构造器中创建FutrueTask对象
- 使用带参构造器,将FutrueTask对象传递到带参构造器中创建Thread对象
- 调用Thread对象的start()方法
- 如果需要返回值调用FutrueTask对象的get()方法获取call()方法的返回值
例子
使用实现Callable接口的方式遍历100以内的整数,并获取总和
// 1.创建一个实现Callable接口的实现类
class Number implements Callable { // 可以在这里指定泛型,也可以不要,一旦泛型,返回值类型必须是泛型本身或其子类
// 2.实现call()方法
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
System.out.println("i = " + i);
sum += i;
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
// 3.创建Callable接口实现类的对象
Number number = new Number();
// 4.使用带参构造器,将Callable接口实现类的对象传递到带参构造器中创建FutrueTask对象
FutureTask futureTask = new FutureTask(number);
// 5.使用带参构造器,将FutrueTask对象传递到带参构造器中创建Thread对象
Thread thread = new Thread(futureTask);
// 6.调用Thread对象的start()方法
thread.start();
try {
// 7.如果需要返回值调用FutrueTask对象的get()方法获取call()方法的返回值(不需要这一步可以不要)
Object sum = futureTask.get();
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
说明
与使用Runnable相比, Callable功能更强大些
- call()可以有返回值的;而run()没有返回值
- call()可以抛出异常,被外面的操作捕获,获取异常的信息;而run()不能抛出异常,只能在内部处理
- Callable支持泛型;而Runnable不支持泛型
- Callable需要借助FutureTask类,比如获取返回结果
2. 新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:提供了很多线程池相关的参数
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- …
线程池相关API
- JDK 5.0起提供了线程池相关API:ExecutorService和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
- void shutdown() :关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
步骤
- 创建线程池(使用Executors(不推荐)或手动创建)
- 根据需要设置线程池的属性(需要转换为ExecutorService的实现类对象)
- 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
- 关闭连接池
例子
遍历100以内的整数
class Number implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ":i = " + i);
}
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
// 1.创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 2.根据需要设置线程池的属性
// 如果需要设置相关参数,就需要将service转为子类对象进行设置
// ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// service1.setCorePoolSize(1);
// 3.执行指定的线程的操作
service.execute(new Number());
// 4.关闭连接池
service.shutdown();
}
}