第八章 多线程
8.3 线程的生命周期
8.3.1 JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建:当一个Thread类或子类的对象被声明并创建时,新的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地终止或出现异常导致结束
8.4 线程的同步
在Java中我们通过同步机制,来解决线程的安全问题
8.4.1 同步代码块处理线程安全问题
说明:①.操作共享数据的代码,即为需要被同步的代码
②.共享数据:多个线程共同操作的变量
③.同步监视器(锁):任何一个类的对象,都可以充当锁。多个线程必须共用一把锁。
//同步代码块
synchronized (同步监视器/锁) {
//需要被同步的代码
}
//动物园卖票
//同步代码块处理Runnable的线程安全问题
class MyThread1 implements Runnable {
private int ticket = 100;
Object object = new Object();
@Override
public void run() {
//同步代码块
while (true) {
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread1);
thread1.setName("动物园窗口1");
thread2.setName("动物园窗口2");
thread1.start();
thread2.start();
}
}
//动物园卖票
//同步代码块处理继承Thread类的线程安全问题
class MyThread1 extends Thread {
private static int ticket = 100;
private static Object object = new Object();
@Override
public void run() {
//同步代码块
while (true) {
synchronized (object) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread1 myThread2 = new MyThread1();
myThread1.setName("动物园窗口1");
myThread2.setName("动物园窗口2");
myThread1.start();
myThread2.start();
}
}
8.4.2 同步方法处理线程安全问题
说明:同步方法仍然涉及到同步监视器,只是不需要我们显式的声明;非静态的同步方法,同步监视器是this,静态的同步方法,同步监视器是:当前类本身
//同步方法
@Override
public void run() {
show();
}
private synchronized void show() {
//需要同步的数据
}
//动物园卖票
//同步方法处理Runnable的线程安全问题
class MyThread1 implements Runnable {
private int ticket = 100;
@Override
public void run() {
show();
}
private synchronized void show() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread1);
thread1.setName("动物园窗口1");
thread2.setName("动物园窗口2");
thread1.start();
thread2.start();
}
}
//动物园卖票
//同步方法处理Thread类的线程安全问题
class MyThread1 extends Thread {
private static int ticket = 15;
@Override
public void run() {
while (true){
show();
}
}
private static 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 ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread1 myThread2 = new MyThread1();
myThread1.setName("动物园窗口1");
myThread2.setName("动物园窗口2");
myThread1.start();
myThread2.start();
}
}
8.4.3 线程安全的单例模式之懒汉式
public class BankTest {
}
//线程安全的单例模式之懒汉式
class Bank {
public Bank() {
}
private static Bank instance = null;
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
8.4.4 死锁的问题及死锁演示实例
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;出现死锁后,不会发生异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法:专门的算法,原则;尽量减少同步资源的定义;尽量避免嵌套同步。
/**
* 描述:必定死锁的情况
*/
class MustDeadLock implements Runnable {
public int flag;
static Object o1 = new Object();
static Object o2 = new Object();
public void run() {
System.out.println("线程"+Thread.currentThread().getName() + "的flag为" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1获得了两把锁");
}
}
}
if (flag == 2) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2获得了两把锁");
}
}
}
}
public static void main(String[] argv) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 2;
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
t1.start();
t2.start();
}
}
8.4.5 Lock锁方式解决线程安全问题
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java,util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,他拥有与synchronized相同的并发性和内存语义,在实现线程安全控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
import java.util.concurrent.locks.ReentrantLock;
//Lock锁方式解决线程安全
class MyThread1 implements Runnable {
private static int ticket = 15;
//1.实例化ReentrantLock
private ReentrantLock reentrantLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法lock()
reentrantLock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
//3.调用解锁方法
reentrantLock.unlock();
}
}
}
}
public class ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread1);
thread1.setName("动物园窗口1");
thread2.setName("动物园窗口2");
thread1.start();
thread2.start();
}
}
8.4.6 synchronized与Lock的对比
①.二者都可以解决线程安全问题
②.synchronized机制在执行完相应的代码以后,自动的释放同步监视器;Lock需要手动的启动同步(lock( )),同时结束同步也需要手动的实现(unlock( ))
③.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
④.Lock只有代码块锁,synchronized有代码块锁和方法锁
⑤.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供了更多的子类)
优先使用顺序:
Lock——同步代码块(已经进入方法体,分配了相应资源)——同步方法(在方法体外)
8.5 线程的通信
8.5.1 线程通信的三个方法
wait( ):一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify( ):一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的
notifyAll( ):一旦执行此方法,就会唤醒所有被wait的线程
说明:
①.wait() ,notify( ),notifyAll( )三个方法必须使用在同步代码块或同步方法中
②.wait() ,notify( ),notifyAll( )三个方法的调用者必须是同步代码块或同步方法的同步监视器,否则,会抛出异常
③.wait() ,notify( ),notifyAll( )三个方法是定义在java.lang.Object类中
8.5.2 线程通信的练习和示例
面试题:sleep( )和wait( )的异同?
相同点 一旦执行方法,都可以使得当前的线程进入阻塞状态
不同点
两个方法声明的位置不同:Thread类中声明sleep( ),Object类中声明wait();
调用的要求不同:sleep( )可以在任何需要的场景下调用。wait( )必须使用在同步代码块或同步方法中
关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep( )不会释放锁,wait( )会释放锁
//线程通信的示例
class MyThread1 implements Runnable {
private static int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (num <= 20) {
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
//调用wait()方法使得当前线程进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ThreadText1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread1);
thread1.setName("线程一:");
thread2.setName("线程二:");
thread1.start();
thread2.start();
}
}
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
/**
* 生产者消费者模式:使用Object.wait() / notify()方法实现
*/
class ProducerConsumer {
private static final int CAPACITY = 5;
public static void main(String args[]) {
Queue<Integer> queue = new LinkedList<Integer>();
Thread producer1 = new Producer("P-1", queue, CAPACITY);
Thread producer2 = new Producer("P-2", queue, CAPACITY);
Thread consumer1 = new Consumer("C1", queue, CAPACITY);
Thread consumer2 = new Consumer("C2", queue, CAPACITY);
Thread consumer3 = new Consumer("C3", queue, CAPACITY);
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
consumer3.start();
}
/**
* 生产者
*/
public static class Producer extends Thread {
private Queue<Integer> queue;
String name;
int maxSize;
int i = 0;
public Producer(String name, Queue<Integer> queue, int maxSize) {
super(name);
this.name = name;
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
System.out.println("Queue is full, Producer[" + name + "] thread waiting for " + "consumer to take something from queue.");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("[" + name + "] Producing value : +" + i);
queue.offer(i++);
queue.notifyAll();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 消费者
*/
public static class Consumer extends Thread {
private Queue<Integer> queue;
String name;
int maxSize;
public Consumer(String name, Queue<Integer> queue, int maxSize) {
super(name);
this.name = name;
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("Queue is empty, Consumer[" + name + "] thread is waiting for Producer");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
int x = queue.poll();
System.out.println("[" + name + "] Consuming value : " + x);
queue.notifyAll();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
8.6 JDK新增线程创建方式(两种)
8.6.1 JDK新增线程创建方式一:实现Callable接口
与使用Runnable相比,Callable功能更强大些
相比run( )方法,可以有返回值;方法可以抛出异常;支持泛型的返回值;需要借助FutureTask类,比如获取反回结果
Future接口可以对具体Runnable,Callable任务的执行结果进行取消,查询是否完成,获取结果等;FuturTask是Funture接口的唯一的实现类;FuturTask同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1.创建一个Callable的实现类
class MyThread1 implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int num = 0;
for (int i = 1; i <= 100; i++) {
num +=i;
}
return num;
}
}
public class ThreadText1 {
public static void main(String[] args) {
//3.创建Callable接口的实现类对象
MyThread1 myThread1 = new MyThread1();
//4.将此Callable接口的实现类对象传递到FutureTask构造器中,创建FutureTask对象
FutureTask futureTask1 = new FutureTask(myThread1);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中华,创建Thread对象,并调用start()
new Thread(futureTask1).start();
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写的方法call()的返回值
Object o = futureTask1.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
8.6.2 JDK新增线程创建方式二:使用线程池
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程。对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。
好处:提高响应速度(减少了创建新线程的时间);降低资源消耗(重复利用线程池中线程,不需要每次都创建);便于线程管理
corePoolSize(核心池的大小);maximumPoolSize(最大线程数);keepAliveTime(线程没有任务时最多保持多长时间后会终止)
线程池相关API:
JDK 5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command ):执行任务/命令,没有返回值,一般用来执行Runnable
<T>Future<T>submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
void shutdown:关闭连接池
Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池 Executors.newCachedThreadPool( ):创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i+" 偶数");
}
}
}
}
class NumberThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName()+":"+i+" 奇数");
}
}
}
}
//使用线程池创建线程完成任务
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//2.执行指定的线程操作。需要提供实现Runnable接口和Callable接口实现类的对象
executorService.execute(new NumberThread1());//适合适用于Runnable
executorService.execute(new NumberThread2());//适合适用于Runnable
//executorService.submit();//适合适用于Callable
//3.关闭线程池
executorService.shutdown();
}
}