今天我们来学学多线程;
首先我们要想清楚多线程的本质,为什么我们要引入多线程呢?
我们知道进程是我们平时运行的一个程序,它是拥有资源的基本单位;而线程是cpu调度资源的基本单位,它是用来完成任务的;如果没有多线程,一个进程只有一个线程来执行任务,那该进程一次只能做一件事情,无法并发和并行,例如我们所用的Word,如果它有自动保存的功能,那么岂不是说当自动保存的功能在运行时,我们就无法继续输入文字了,我们也没法做其他任何操作了;而进程之间又是隔离的,他们想要共享数据时很麻烦的事情,就好比让QQ音乐和word共享数据一样,所以我们也无法用多进程来协同完成工作,因此我们就必须引入多线程;
它可以完美利用cpu的多核,提高了应用程序的使用率,同时给用户更好的体验;
多线程
- 定义:
在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。是程序使用CPU的基本单位。 - 实现:
Java的程序其实是在JVM虚拟机里执行,而JVM虚拟机就好像一个应用程序,它就是一个进程,我们平时写的main方法就是它所启动的主线程;
而多线程的实现必须依赖与进程,因此我们需要调用系统功能去创建一个进程,但是Java是不能直接调用系统功能的;而它实现多线程的模式是调用C/C++写好的程序来实现多线程,由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。
上面我们说了那么多概念,现在我们看看具体Java实现多线程的类是什么; - Thread类
Java 提供了一个类 Thread 通过这个类就可以实现多线程;
我们将我们想要实现的多线程类继承于Thread类,并重写它的run()方法,这样就可以实现多线程了;
public class MyThread extends Thread {
//run() 是线程要执行的方法,方法里面的代码是让线程来执行的
@Override
public void run() {
//这里是多线程所要执行的代码,
//一般来说,一些耗时的操作,需要我们开启一个线程,在run()方法里面去操作
}
}
上面就是我们创建线程的方法,写一个类继承于Thread父类,然后重写它的run()方法,之后我们来看怎么开启这个线程;
MyThread th = new MyThread();
th.start();//开启线程
MyThread th2 = new MyThread();
th2.start();
先声明我们写好的线程类,获取到他的对象,之后调用start()方法,他就会启动线程,执行我们写在run()中的代码;
这里注意,我开启线程是调用start()方法,而不是th.run();如果是这样,它只是一个普通方法的调用,而不是开启线程;
我们还可以给我们创建的多个线程设置名字;
th1.setName("线程1");
th2.setName("线程2");
Thread thread = Thread.currentThread();
//上面这一句是返回对当前正在执行的线程对象的引用。
thread.setName("主线程");
除了上面的方法我们还可以用构造方法来设置线程的名字;
在我们写的线程类中给出传入线程名字的构造方法
public class MyThread extends Thread {
//设置线程名的构造方法
public MyThread(String name) {
super(name);
}
@Override
public void run() {
}
}
}
这样我们就可以直接将线程名当参数传入了;
MyThread th1 = new MyThread(“线程1”);
MyThread th2 = new MyThread(“线程2”);
- 线程执行
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,
线程只有得到 CPU时间片,也就是使用权,才可以执行指令。
那么java是怎么实现线程的调度呢?
线程有两种调度模型:
分时调度模型: 所有线程轮流使用 sun.plugin2.gluegen.runtime.CPU 的使用权;
// 平均分配每个线程占用 CPU 的时间片
抢占式调度模型: 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个;
//优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型,也就是说当优先级相同时,线程之间会互相抢夺时间片,谁抢到,谁先执行;
- 设置/获取线程的优先级
th1.setPriority(Thread.MAX_PRIORITY); 设置线程的优先级为最大
th2.setPriority(Thread.MIN_PRIORITY);设置线程的优先级为最小
线程的优先级默认为5,最大为10,最小为1;
th1.getPriority()获取线程th1的优先级,返回为int型整数,10
th2.getPriority() 获取线程th2的优先级,返回为int型整数,1 - 线程休眠
调用sleep()方法,让当前执行的线程沉睡指定毫秒值的时长;过后再继续执行;
thread.sleep(2000);让thread线程沉睡2000毫秒(2秒种); - 加入线程
调用join()方法,将线程设置为加入线程,效果为加入的线程先执行完再执行其他线程;
th1.start();
th1.join();//设置该线程为加入线程
例如有三个线程同时执行,将th1设置为加入线程,则先将th1完全执行完,再让th2和th3抢占执行;但是注意,在设置加入线程时,要先将该进程启动; - 礼让线程
调用vield()方法,将线程设置为礼让线程,表示先暂停当前线程的运行,不与别的线程抢夺时间片,让别的线程优先执行;
th1.vield();
该方法也是在线程启动后设置; - 守护线程
调用setDaemon()方法,将线程设为守护线程,表示该线程是某线程的守护线程,所守护的线程执行结束后,守护线程立即结束,无论它执行完毕与否;
th1.setDaemon();
该方法是在线程启动之前标识的,注意,如果所有线程都是守护线程,则Java虚拟机自动退出,即没有所要守护的线程;
- Runnable接口
多线程的实现方法除了Thread类还有一种方法是实现Runnable()接口,一样重写它的run()方法,然后可以分配该类的实例,在创建 Thread(线程)时作为一个参数来传递并启动;
开启线程的第二种方式
1.定义一个类实现Runnable接口
2.重写接口中的run()方法
3.创建线程对象,把Runnable接口的子类对象,作为参数传递进来
Thread(Runnable target) 分配新的 Thread 对象。
4.开启线程
这样说也不直观我们来做一个例子,来看看runnable是怎么实现线程的;
这里我们重写run()方法中用一个循环模仿线程的耗时操作;
public class MyRunable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//获取当前正在执行的线程对象
String name = Thread.currentThread().getName();
System.out.println(name + "==" + i);
}
}
}
上面就是我们声明一个MyRunable()方法实现了Runnable()接口,重写它的run()方法,我们用循环100次模仿线程的耗时操作;
MyRunable myRunable = new MyRunable();
Thread th = new Thread(myRunable);
th.setName("线程A");
th.start();
Thread th2 = new Thread(myRunable);
th2.setName("线程B");
th2.start();
实现时我们将写好的MyRunable()子类对象当作参数传入声明的Thread(线程)对象;然后开启线程;
以上的两种方法就是线程创建和开启,以及一些常规操作的方法;
线程安全问题
之前我们学习集合的各种方法的时候说起他们的优缺点总是会强调该方法是线程安全的,或者是线程不安全的;那么到底什么是线程安全?怎么样的线程算是安全的?我们又该怎么面对线程出现的安全问题呢?
下面我们就来分析一下线程的安全问题;
首先我们用一个案例来引出线程安全问题,
案例演示
需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
class WindowRunnale implements Runnable {
int piao = 100; //共享数据
@Override
public void run() {
while (true) {
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
WindowRunnale runnale = new WindowRunnale();
Thread th1 = new Thread(runnale);
Thread th2 = new Thread(runnale);
Thread th3 = new Thread(runnale);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
//开启线程
th1.start();
th2.start();
th3.start();
}
}
我们先写了Runnable接口的子类,然后main()方法里实现了子类,并写了三个窗口当作三个线程;让它们共同售卖100张票;
这时候我们来看看该程序的结果
这时我们发现结果出现了两个线程卖出相同票的结果,换出现了0票和负票的结果,显然这都是线程不安全所暴露出的问题;
那么我们就来分析一下这些问题出现的原因以及如何解决;
1.0票和负数票的出现原因;
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
}
这是由于线程的随机性所导致的,我们来看上面的代码,我们判断进入代码的条件是piao>0,即票数大于零,代表还有余票可买,那我们这样想,我们模拟网络延迟让线程沉睡了50毫秒;那我们想一下,当piao剩下最后一张,比如th1线程进来了,然后它睡了50毫秒,可在他还没执行到(piao–)这一步,票数还没有减一,这时另一个线程th2抢到了时间片,它一判断票数为1.还可以进来,所以它也进来了,向下执行代码,然后又对票数进行了自减操作,这时就可能会出现负数票和0票的情况;
这就是线程的随机性所导致的安全性问题;
2.相同票数出现的原因:
这是由于线程中命令的原子性所导致的;同样是这个代码,现在我们先不考虑随机性带来的线程安全问题,我们分析一下相同票数出现的原因;
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
}
这次我们的注意力不再它是否进入if()判断,而是要注意(piao–)这一句,原子性即该表达式是不可再分的,而显然piao–这一句是可以再分的,它会被分为三句,int i=piao;
piao = piao+1;int i = piao;我们先将原值记录,然后自减一次,然后又将新值赋回去;所以这一语句就不具备原子性,而不具备原子性就会出现线程安全问题;
我们要知道,piao作为一个共享数据,它是存放再主存里的;我们每一个线程都会对它操作,可是线程的存储区和主存是不在一起的,它有自己独立的内存,那我们要更改共享数据时,要先将数据从主存取出来,然后把他更改,然后再覆盖回主存,实现这一数据的更改;那这也是需要时间的呀,多次的循环就代表这我们将进行多次的共享值取出,更改,覆盖的操作,而多个线程抢到时间片的时间又是不确定的,倘若我们一个线程th1正在对piao数进行更改,但还没来得及覆盖回主存,这时主存里的piao数是没有改变的,这时又一个线程th3进来了,它去访问主存的共享数据,将他取出到自己的操作内存里,然后操作,那这时,这两个线程产生的数据就是一样的,因此,结果就会出现相同票数的情况;
我们上面说了线程安全性的原理和产生原因,我们会发现,这些安全性问题产生的条件基本有三点:
- 是否是多线程环境;
- 是否是多个线程共享一个数据的情况;
- 是否有多条语句操作共享数据;
当满足了以上这三个条件的时候,就会产生线程安全性的问题;
同时我们上面分析的随机性和原子性的问题,有一个很明显的共性就是当一个线程在执行操作或操作共享数据的时候,还没有完全操作完,就有另一个线程进入,因此导致出现了一系列的问题,那我们有没有一个方法,让一个线程执行任务代码的时候,先不要让别的线程进来,等它执行完后,再让别的线程进来执行?
很显然是有的,Java为我们提供了一种锁的机制,帮我们解决多线程的安全问题;
- synchronized锁
在学习这个关键字的开始我们先了解一下锁的概念;
Java内置锁:
每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
而synchronized关键字就给我们提供了这样一种机制;
我们给要实现多线程的代码加上锁,然后一个线程进入代码块时我们让他持有这个锁对象,后面想要进来的线程没有这个锁,就没法进来,当该线程执行完,出代码块时,它会自动释放这个锁,这时后面的线程就可以重新持有这个锁,进入代码块了;
public class WindowRunnale implements Runnable {
static int piao = 100; //共享数据
static Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代码块
synchronized (obj) {//锁对象:就是一个任意对象
//线程一旦进入同步代码块,就会持有锁的对象
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
}
//出了同步代码块 会释放锁对象
}
}
}
我们给上面的代码加了锁;同步代码块的锁对象可以是任意一个对象
synchronized (obj) 这当中的obj是我们定义的Object的子类对象;但实际上这个锁对象可以是任意对象;
synchronized关键字不止可以修饰同步代码块,也可以修饰同步方法,这个时候,同步方法的锁对象就不再是任意对象了,而是this,即线程本身对象;
public synchronized void maipiao() {
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
}
public synchronized void maipiao() 定义同步方法maipiao();
synchronized关键字修饰静态同步代码块时,他的锁对象是当前类的字节码文件对象;
注意:
锁是控制多个线程对共享资源进行访问的工具。
通常,锁提供了对共享资源的独占访问。
一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。
- ReentrantLock
除了synchronized方法的加锁,我们还可以自己定义一把锁,实现加锁和释放锁的操作;
Lock lock = new ReentrantLock();
这样我们就定义好了一把锁;如果它要实现和synchronized一样的效果,就需要手动调用lock.lock()和lock.unlock()方法,实现加锁和释放锁的操作;
public void run() {
while (true) {
lock.lock();//加锁
if (piao > 0) {
try {
Thread.sleep(50);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (piao--) + "张票");
}
lock.unlock();//释放锁
}
}
这个方法更加灵活,它可以自由的控制线程锁的范围;可是它需要我们人为的加锁和释放锁,因此会比较麻烦。
- 死锁现象:
即多个线程,互相持有对方的锁,而不释放,造成多个线程处于等待的现象;
这就好比中国人和美国人一起吃饭,中国人拿了美国人的刀,美国人拿了中国人的一支筷子,两个人都不能吃饭了;
比如同步代码块嵌套时,就可能发生死锁现象;
public class ObjectUtils {
//定义两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}
public class MyThread extends Thread {
boolean b;
public MyThread(boolean b) {
this.b = b;
}
@Override
public void run() {
if (b) {
//同步代码块嵌套
synchronized (ObjectUtils.objA) {
System.out.println("true 进来了objA");
synchronized (ObjectUtils.objB) {
System.out.println("true 进来了objB");
}
}
} else {
//同步代码块嵌套
synchronized (ObjectUtils.objB) {
System.out.println("false 进来了objB");
synchronized (ObjectUtils.objA) {
System.out.println("false 进来了objA");
}
}
}
}
}
public class MyTest {
public static void main(String[] args) {
MyThread th1 = new MyThread(true);
MyThread th2 = new MyThread(false);
th1.start();
th2.start();
}
}
应为同步代码块需要执行完代码块里的所有代码才会释放锁;因此,如果它当中有代码无法执行,他就会一直持有锁,而不会释放;就像上面的情况,两个线程都互相持有对方的锁而不释放,所以两个都无法继续往下执行;
- 线程的等待和唤醒
线程的等待和唤醒就是不同种类线程之间通信的重要前提;我们用一个例子来说明,比如就生产者和消费者的问题;
他们一个产生资源,一个消费资源,所以两者通信即实现资源的即产即消;
生产线程:如果没有资源资源我就生产,有了资源我就等待,通知消费线程来消费
消费线程:有了资源我就消费,没有资源我就等着,并通知生产线程生产
我们先定义了一个student类;
public class Student {
public String name;
public int age;
//提供一个标记
boolean flag;//false 代表没有资源 true 代表有资源
}
生产资源;
public class SetThread extends Thread {
Student student;
int i = 0;
public SetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while (true) {
synchronized (student){
//判断资源
if(student.flag){//有资源
try {
student.wait();//我就等着
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i % 2 == 0) {
//生产资源
student.name = "张三"; //张三
student.age = 23;//23
} else {
//生产资源
student.name = "李四";
student.age = 24;
}
//改标记
student.flag=true;
//通知
student.notifyAll();//唤醒以后,还要再次争抢时间片
i++;
}
}
}
}
消费资源;
public class GetThread extends Thread {
Student student;
public GetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while (true) {
synchronized (student) {
if (!student.flag) {//没有资源
try {
student.wait();//等着:wait()方法
//线程一旦等待 就要立马释放锁,等会被唤醒了,也就从这里醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(student.name + "===" + student.age);
//修改标记
student.flag = false;
student.notify();//通知
}
}
}
}
测试方法;
public class MyTest {
public static void main(String[] args) {
//生产线程:如果没有资源资源我就生产,有了资源我就等待,通知消费线程来消费
//消费线程:有了资源我就消费,没有资源我就等着,你通知生产线程生产
Student student = new Student();
SetThread th1 = new SetThread(student);
GetThread th2 = new GetThread(student);
th1.start();
th2.start();
}
}
通过上面的例子我们也学习了两个方法
1.wait()方法;
让线程暂停等待,等待其他线程唤醒后继续执行;
2.notify()/notifyAll()
唤醒等待的 单个/所有 线程,唤醒后正常执行;
在上面的例子里我们把学生对象当作了资源,flag是一个标记,判断当前有没有资源用来输出;代码里的注释也很详细,这里就不对其流程做过多赘述;请认真看代码中等待和唤醒的条件以及操作;
这样我们就实现了两个线程之间的通信和约束,两个线程必须等待彼此线程的唤醒才能继续执行;交互之后相互的等待和唤醒,互相推动执行;