基本知识回顾
线程是比进程更小的能独立运行的基本单位,它是进程的一部分,一个进程可以拥有多个线程,但至少要有一个线程,即主执行线程(Java 的 main 方法)。我们既可以编写单线程 应用,也可以编写多线程应用。 一个进程中的多个线程可以并发(同时)执行,在一些执行时间长、需要等待的任务上(例 如:文件读写和网络传输等),多线程就比较有用了。 怎么理解多线程呢?来两个例子:
- 进程就是一个工厂,一个线程就是工厂中的一条生产线,一个工厂至少有一条生产 线,只有一条生产线就是单线程应用,拥有多条生产线就是多线程应用。多条生产线可 以同时运行。
- 我们使用迅雷可以同时下载多个视频,迅雷就是进程,多个下载任务就是线程,这 几个线程可以同时运行去下载视频。
多线程可以共享内存、充分利用 CPU,通过提高资源(内存和 CPU)使用率从而提高程序 的执行效率。CPU 使用抢占式调度模式在多个线程间进行着随机的高速的切换。对于 CPU 的一个核而言,某个时刻,只能执行一个线程,而 CPU 在多个线程间的切换速度相对我们 的感觉要快很多,看上去就像是多个线程或任务在同时运行。
Java实现多线程的方法
Java 天生就支持多线程并提供了两种编程方式,一个是继承 Thread 类,一个是实现 Runnable 接口,接下来咱们通过两个案例快速复习回顾一下。
继承 Thread 类
package com.tntxia.test.multithread;
public class ThreadFor1 extends Thread{
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(this.getName()+":"+i);
}
}
}
上述代码自定义了一个类去继承 Thread 类,并重写了 run 方法,在该方法内实现具体业务功能,这里用一个 for 循环模拟一下
package com.tntxia.test.multithread;
public class ThreadFor2 extends Thread {
public void run() {
for (int i = 51; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
}
}
}
上述代码又自定义了一个类去继承 Thread 类,并重写了 run 方法,在该方法内实现另一个业务功能,这里仍用一个 for 循环模拟一下
package com.tntxia.test.multithread;
public class TestThreadFor {
public static void main(String[] args) {
ThreadFor1 tf1=new ThreadFor1();
tf1.setName("线程 A");
ThreadFor2 tf2=new ThreadFor2();
tf2.setName("线程 B");
tf1.start();
tf2.start();
}
}
上述代码创建了两个线程对象并分别启动,运行效果如下图所示,我们能够清晰观察到,CPU在两个线程之间快速随机切换,也就是我们平时说的在同时运行。
实现 Runnable 接口
package com.tntxia.test.multithread;
public class RunnableFor1 implements Runnable{
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
上述代码自定义一个类去实现 Runnable 接口,并实现了 run 方法,在该方法内实现具体业务功能,这里用一个 for 循环模拟一下。
package com.tntxia.test.multithread;
public class RunnableFor2 implements Runnable {
public void run() {
for (int i = 51; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
上述代码又自定义一个类去实现 Runnable 接口,并实现了 run 方法,在该方法内实现另一个业务功能,这里仍用一个 for 循环模拟一下。
package com.tntxia.test.multithread;
public class TestRunnableFor {
public static void main(String[] args) {
Thread t1=new Thread(new RunnableFor1());
t1.setName("线程 A");
Thread t2=new Thread(new RunnableFor2());
t2.setName("线程 B");
t1.start();
t2.start();
}
}
上述代码创建两个线程对象并分别启动,运行效果如下图所示,我们能够清晰观察到,CPU在两个线程之间快速随机切换,也就是我们平时说的在同时运行。
线程安全
产生线程安全问题的原因
在进行多线程编程时,要注意线程安全问题,我们先通过一个案例了解一下什么是线程
安全问题。该案例模拟用两个售票窗口同时卖火车票,具体代码如下所示:
package com.tntxia.test.multithread;
public class SaleWindow implements Runnable {
private int id = 10; //表示 10 张火车票 这是共享资源
//卖 10 张火车票
public void run() {
for (int i = 0; i < 10; i++) {
if (id > 0) {
System.out.println(Thread.currentThread().getName() + "卖了编号为" + id + "的火车票");
id--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
上述代码定义了一个类去实现 Runnable 接口,该类中有一个属性 id,表示 10 张火车票,并在 run 方法中通过一个 for 循环销售火车票,为了让效果明显一些,中间用 sleep 方法停顿 半秒钟。
package com.tntxia.test.multithread;
public class TestSaleWindow {
public static void main(String[] args) {
SaleWindow sw=new SaleWindow();
Thread t1=new Thread(sw);
Thread t2=new Thread(sw);
t1.setName("窗口 A");
t2.setName("窗口 B");
t1.start();
t2.start();
}
}
上述代码创建两个线程对象模拟两个售票窗口同时卖票,运行结果如下图所示:
我们看到,10 张火车票都卖出去了,但是出现了重复售票,这就是线程安全问题造成的。这 10 张火车票是共享资源,也就是说任何窗口都可以进行操作和销售,问题在于窗口 A 把某一张火车票卖出去之后,窗口 B 并不知道,因为这是两个线程,所以窗口 B 也可能会 再卖出去一张相同的火车票。
多个线程操作的是同一个共享资源,但是线程之间是彼此独立、互相隔绝的,因此就会 出现数据(共享资源)不能同步更新的情况,这就是线程安全问题。
解决线程安全问题
Java 中提供了一个同步机制(锁)来解决线程安全问题,即让操作共享数据的代码在某一时间段,只被一个线程执行(锁住),在执行过程中,其他线程不可以参与进来,这样共享数 据就能同步了。简单来说,就是给某些代码加把锁。
锁是什么?又从哪儿来呢?锁的专业名称叫监视器 monitor,其实 Java 为每个对象都自 动内置了一个锁(监视器 monitor),当某个线程执行到某代码块时就会自动得到这个对象 的锁,那么其他线程就无法执行该代码块了,一直要等到之前那个线程停止(释放锁)。需要 特别注意的是:多个线程必须使用同一把锁(对象)。
Java 的同步机制提供了两种实现方式:
1.同步代码块:即给代码块上锁,变成同步代码块
2.同步方法:即给方法上锁,变成同步方法
接下来我们分别用这两种方式解决卖火车票案例的线程安全问题,其实这两种方式本质 上差不多,都是通过 synchronized 关键字来实现的
同步代码块
package com.tntxia.test.multithread;
public class SaleWindow1 implements Runnable {
private int id = 10; //表示 10 张火车票 这是共享资源
//卖 10 张火车票
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (this) {
if (id > 0) {
System.out.println(Thread.currentThread().getName() + "卖了编号为" + id + "的火车票");
id--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
同步代码块的语法是:synchronized(锁){...业务代码...}。
package com.tntxia.test.multithread;
public class TestSaleWindow1 {
public static void main(String[] args) {
SaleWindow1 sw=new SaleWindow1();
Thread t1=new Thread(sw);
Thread t2=new Thread(sw);
t1.setName("窗口 A");
t2.setName("窗口 B");
t1.start();
t2.start();
}
}
我们同样创建两个线程对象模拟两个售票窗口同时卖票,这次运行结果如下图所示:
我们看到 10 张火车票都卖出去了,这次没有问题,我们不关心这 10 张票都是哪个窗口卖出去的,我们关心的是没有重复卖票。
同步方法
package com.tntxia.test.multithread;
public class SaleWindow2 implements Runnable {
private int id = 10; //表示 10 张火车票 这是共享资源
public synchronized void saleOne() {
if (id > 0) {
System.out.println(Thread.currentThread().getName() + "卖了编号为" + id + "的火车票");
id--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//卖 10 张火车票
public void run() {
for (int i = 0; i < 10; i++) {
this.saleOne();
}
}
}
第二种方式是把原来同步代码块中的代码抽取出来放到一个方法中,然后给这个方法加上synchronized 关键字修饰,锁住的代码是一样的,因此本质上和第一种方式没什么区别。
package com.tntxia.test.multithread;
public class TestSaleWindow2 {
public static void main(String[] args) {
SaleWindow2 sw=new SaleWindow2();
Thread t1=new Thread(sw);
Thread t2=new Thread(sw);
t1.setName("窗口 A");
t2.setName("窗口 B");
t1.start();
t2.start();
}
}
我们同样还是创建两个线程对象模拟两个售票窗口同时卖票,这次运行结果如下图所示:
Java API 中的线程安全问题
我们平时在使用 Java API 进行编程时,经常遇到说哪个类是线程安全的,哪个类是不保
证线程安全的,例如:StringBuffer / StringBuilder 和 Vector / ArrayList ,谁是线程安全的? 谁不是线程安全的?我们查一下它们的源码便可知晓。
这两个类在使用时是保证线程安全的;而 StringBuilder 和 ArrayList 类中的方法都是普通方法,
没有使用 synchronized 关键字进行修饰,所以证明这两个类在使用时不保证线程安全。线程 安全和性能之间不可兼得,保证线程安全就会损失性能,保证性能就不能满足线程安全。
线程间通信
多个线程并发执行时, 在默认情况下 CPU 是随机性的在线程之间进行切换的,但是有时候我们希望它们能有规律的执行, 那么,多线程之间就需要一些协调通信来改变或控制 CPU 的随机性。Java 提供了等待唤醒机制来解决这个问题,具体来说就是多个线程依靠一个同步 锁,然后借助于 wait()和 notify()方法就可以实现线程间的协调通信。
同步锁相当于中间人的作用,多个线程必须用同一个同步锁(认识同一个中间人),只有 同一个锁上的被等待的线程,才可以被持有该锁的另一个线程唤醒,使用不同锁的线程之间 不能相互唤醒,也就无法协调通信。
Java 在 Object 类中提供了一些方法可以用来实现线程间的协调通信,我们一起来了解 一下:
- public final void wait(); 让当前线程释放锁
- public final native void wait(long timeout); 让当前线程释放锁,并等待 xx 毫秒
- public final native void notify(); 唤醒持有同一锁的某个线程
- public final native void notifyAll(); 唤醒持有同一锁的所有线程
需要注意的是:在调用 wait 和 notify 方法时,当前线程必须已经持有锁,然后才可以 调用,否则将会抛出 IllegalMonitorStateException 异常。接下来咱们通过两个案例来演示一 下具体如何编程实现线程间通信。
案例 1: 一个线程输出 10 次 1,一个线程输出 10 次 2,要求交替输出“1 2 1 2 1 2...”或“2 1 2 1 2 1...“
package com.tntxia.test.multithread;
public class MyLock {
// 锁
public static Object o=new Object();
}
为了保证两个线程使用的一定是同一个锁,我们创建一个对象作为静态属性放到一个类中,这个对象就用来充当锁。
package com.tntxia.test.multithread;
public class ThreadForNum1 extends Thread {
public void run() {
for (int i = 0; i < 11; i++) {
synchronized (MyLock.o) {
System.out.println("1");
MyLock.o.notify(); //唤醒另一个线程
try {
MyLock.o.wait(); //让自己休眠并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
该线程输出十次 1,使用 MyLock.o 作为锁,每输出一个 1 就唤醒另一个线程,然后自己休眠并释放锁。
package com.tntxia.test.multithread;
public class ThreadForNum2 extends Thread{
public void run() {
for (int i = 0; i < 10; i++) { synchronized (MyLock.o) {
System.out.println("2");
MyLock.o.notify(); //唤醒另一个线程
try {
MyLock.o.wait(); //让自己休眠并释放锁
} catch (InterruptedException e) {
}
}
}
}
}
该线程输出十次 2,也使用 MyLock.o 作为锁,每输出一个 2 就唤醒另一个线程,然后自己休眠并释放锁。
package com.tntxia.test.multithread;
public class TestNum {
public static void main(String[] args) {
new ThreadForNum1().start();
new ThreadForNum2().start();
}
}
我们创建两个线程对象分别运行,效果如下图所示:
案例 2:生产者消费者模式
该模式在现实生活中很常见,在项目开发中也广泛应用,它是线程间通信的经典应用。 生产者是一堆线程,消费者是另一堆线程,内存缓冲区可以使用 List 集合存储数据。该模式 的关键之处是如何处理多线程之间的协调通信,内存缓冲区为空的时候,消费者必须等待, 而内存缓冲区满的时候,生产者必须等待,其他时候可以是个动态平衡。
下面的案例模拟实现农夫采摘水果放到筐里,小孩从筐里拿水果吃,农夫是一个线程, 小孩是一个线程,水果筐放满了,农夫停;水果筐空了,小孩停。
package com.tntxia.test.multithread;
import java.util.ArrayList;
public class Kuang {
//这个集合就是水果筐 假设最多存 10 个水果
public static ArrayList<String> kuang=new ArrayList<String>();
}
上述代码定义一个静态集合作为内存缓冲区用来存储数据,同时这个集合也可以作为锁去被多个线程使用。
package com.tntxia.test.multithread;
public class Farmer extends Thread {
public void run() { while (true) { synchronized (Kuang.kuang) {
//1.筐放满了就让农夫休息
if (Kuang.kuang.size() == 10) {
try {
Kuang.kuang.wait();
} catch (InterruptedException e) {
}
}
//2.往筐里放水果
Kuang.kuang.add("apple");
System.out.println("农夫放了一个水果,目前筐里有" + Kuang.kuang.size() + "个水果");
//3.唤醒小孩继续吃
Kuang.kuang.notify();
}
//4.模拟控制速度
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
上述代码就是农夫线程,不断的往集合(筐)里放水果,当筐满了就停,同时释放锁。
package com.tntxia.test.multithread;
public class Child extends Thread {
public void run() {
while (true) {
synchronized (Kuang.kuang) {
//1.筐里没水果了就让小孩休息
if (Kuang.kuang.size() == 0) {
try {
Kuang.kuang.wait();
} catch (InterruptedException e) {
}
}
//2.小孩吃水果
Kuang.kuang.remove("apple");
System.out.println("小孩吃了一个水果,目前筐里有" + Kuang.kuang.size() + "个水果");
//3.唤醒农夫继续放水果
Kuang.kuang.notify();
}
//4.模拟控制速度
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代码是小孩线程,不断的从集合(筐)里拿水果吃,当筐空了就停,同时释放锁。
package com.tntxia.test.multithread;
public class TestFarmerChild {
public static void main(String[] args) {
new Farmer().start();
new Child().start();
}
}
我们创建两个线程同时运行,可以通过双方线程里的 sleep 方法模拟控制速度,当农夫往框里放水果的速度快于小孩吃水果的速度,运行效果如下:
当小孩吃水果的速度快于农夫往框里放水管的速度时,运行效果如下图所示: