Java多线程一直是面试中的必考点,满打满算学习Java也有一年的时间了。从开始入门的C语言到现在的JavaEE,过程中学了非常多的东西,但是很多基础由于开始理解的不够透彻。最近想放慢一些脚步,将自己基础扎实后再去攻坚三大框架,所以总结内容基本上都是JavaSE,数据结构算法之类的内容。
线程与进程
这两个概念很容易理解,我们可以认为线程为进程的子集,一个进程可以包含多个线程,这些线程独立工作,执行着不同的任务。在内存方面,每个进程拥有自己的内存空间,而所有线程则拥有同一片内存空间。
Java中多线程内存模型
我们在聊多线程具体内容前,有必要总结一下内存模型。由于Java程序运行与虚拟机之上,所以我们可以通过计算机的类比来理解。当一条线程运行时,其过程应该是从内存获取共享变量副本,将该副本存于CPU内的高速缓存(cache)之中 。CPU执行完成后再将数据写回主存中。
关于Java内存模型中的一些规则。
- 线程内的代码能够按先后顺序执行,这被称为程序次序规则。
- 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
- 前一个对
volatile
的写操作在后一个volatile
的读操作之前,也叫volatile
变量规则。 - 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
- 一个线程的所有操作都会在线程终止之前,线程终止规则。
- 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
- 可传递性
该图即为线程的内存模型,正因为该模型,出现了缓存一致性问题,关于这点,不赘述。在Volatile关键词的总结中有详细说明。
Java中多线程
Java中已经为我们提供了类和接口来供我们实现多线程处理。我们实现一个多线程可以通过继承Thread类和继承Runnable接口。而在大多数情况下,我们都通过继承Runnable接口来实现Run方法。
public class ThreadTest implements Runnable{
//复写方法
@Override
public void run() {
//在这里写需要执行的多线程代码
// TODO Auto-generated method stub
doSomething();
}
public static void main(String[] args) {
//创建本类对象
ThreadTest t = new ThreadTest();
//在这里,新建一个线程,并将实例的对象传参
Thread t1 = new Thread(t);
//启动线程
t1.start();
}
}
上述内容就是创建一个多线程的过程。
下面我们来看一个对于线程的各个方法,首先先看一下一个线程运行时的状态。
我们知道,CPU每个Core在同一时间只能运行同一条线程,那么有必要了解下线程的状态。
- 初始化状态 当线程被创建,并未进行对CPU执行权的竞争
- 就绪状态 当线程被创建,并调用start方法启动线程,此时,该线程开始竞争CPU执行权,一旦获取将会执行
- 运行状态 获取到CPU执行权后运行状态
- 阻塞状态 因为某种原因放弃了CPU执行权,等待某个条件后重新进入Runnable状态去竞争执行权
- 销毁线程 线程生命周期结束,被销毁
public static void main(String[] args) {
ThreadTest t = new ThreadTest();
//此时为新建状态
Thread t1 = new Thread(t);
//进入准备状态,开始竞争执行权
t1.start();
//此时线程进入阻塞状态,等休眠定时结束后,重新竞争执行权
t1.sleep(1000);
}
虽然在代码中并没有体现出运行状态,但是,实际上Run方法已经在start内部被执行,所以我们可以将start方法看着同时就绪并执行,但是这两步并不一定同时发生,调用方法同时该线程就参与了CPU执行权的竞争,但是何时执行不能确定。当同一时间有多个线程都在就绪状态时,这些线程都有概率获取到执行权,而概率实际上是有所不同的,这就涉及到线程优先级的问题。这方面内容留待之后的两篇文章中进行详细的总结。
wait / notify方法
wait方法能够让线程进入阻塞状态,而notify方法能够让唤醒进入阻塞状态的线程。而这两个方法一般都会配合synchronized 关键字使用,一般会写入到synchronized 代码块中。因为在synchronized 代码块中,执行wait的当前线程必然已经获得了锁,此时调用wait方法会放弃CPU的执行权,进入阻塞状态,知道另一个线程使用notify唤醒后再去竞争CPU执行权。
而notify/notifyAll方法在执行的一瞬间,能够唤醒所有的线程。但需要注意的是,这时并不一定会立即释放锁,具体何时释放,要看代码块的编写方式。所以我们尽量在使用notify后将当前线程退出临界条件。
而notify方法和notifyAll方法区别在于,前者调用后只会唤醒多个阻塞线程中的一个线程,至于唤醒哪一个,这将有CPU进行管理,而notifyAll方法将唤醒所有线程。
我们通过一个非常著名的模型来理解这两个方法
生产者——消费者模型
和我们生活中的场景完全相同,生产者负责生产商品,消费者负责购买商品。当库存不足时,消费者将无法进行购买,而库存满后,生产者将停止生产。我们用编程思维来理解这个问题,我们将库存视为共享变量,生产者和消费者分别看作两条不同的线程,这两条线程将对该变量进行增减操作,当消费者连续购买直到库存为零时,这时消费者需要调用wait方法,停止购买,同时调用notify,来启动生产者的线程,开始生产商品,而生产者生产到一定的产量后,库存满,调用wait方法停止生产,并调用notify方法来告知消费者购买。
下面是该模型的代码实现。
import java.io.IOException;
public class WaitAndNotify
{
public static void main(String[] args) throws IOException
{
Person person = new Person();
new Thread(new Consumer("消费者一", person)).start();
new Thread(new Consumer("消费者二", person)).start();
new Thread(new Consumer("消费者三", person)).start();
new Thread(new Producer("生产者一", person)).start();
new Thread(new Producer("生产者一", person)).start();
new Thread(new Producer("生产者一", person)).start();
}
}
class Producer implements Runnable
{
//将生产者消费者都归位person类
private Person person;
private String producerName;
public Producer(String producerName, Person person)
{
this.producerName = producerName;
this.person = person;
}
@Override
public void run()
{
//将所有内容封装到run方法中
while (true)
{
try
{
person.produce();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable
{
private Person person;
private String consumerName;
public Consumer(String consumerName, Person person)
{
this.consumerName = consumerName;
this.person = person;
}
@Override
public void run()
{
try
{
person.consume();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
class Person
{
//库存初始
private int foodNum = 0;
//监视器对象
private Object synObj = new Object();
//最大库存
private final int MAX_NUM = 5;
public void produce() throws InterruptedException
{
synchronized (synObj)
{ //当到达库存时,停止生产
while (foodNum == 5)
{
System.out.println("box is full,size = " + foodNum);
synObj.wait();
}
foodNum++;
System.out.println("produce success foodNum = " + foodNum);
//唤醒消费者线程
synObj.notifyAll();
}
}
//同理
public void consume() throws InterruptedException
{
synchronized (synObj)
{
while (foodNum == 0)
{
System.out.println("box is empty,size = " + foodNum);
synObj.wait();
}
foodNum--;
System.out.println("consume success foodNum = " + foodNum);
synObj.notifyAll();
}
}
}