什么是多线程?
什么是线程呢?了解线程之前得先知道什么是进程。
进程:操作系统进行资源分配的基本单位。当一个应用程序运行起来,那么会在内存中开辟使用空间,即创建了一个进程。
线程:操作系统中使用资源的最小单位,一个进程可以有多个线程。
进程就好像一个小工厂,总公司(操作系统)给他分配资源,小工厂的工人(线程)对这些资源进行使用。
多线程是Java的一大特性。顾名思义,多线程就是一个进程中有多个线程运行,这多个线程可以并发的执行。
多线程的好处
提高效率
一个工厂有多个工人的话,那么在好的管理模式(线程功能的分配)下效率自然而然提高了。
在操作系统(单核单线程)中,所有的线程并发执行,但在微观上,由于在同一时间CPU只能处理一条指令,所以它们是分开运行的,但是由于CPU的运行速度十分的快速,难以想象的快,CPU极其快速的轮换执行不同的指令,使得从宏观上,它们是同时执行的。
使用多线程,那么从宏观上,多个线程同时执行即提高了效率。
在Java中如何使用多线程
在Java中有两种方法可以实现多线程。
1.继承Thread类
class ThreadDemo extends Thread{
public void run(){
//通过Thread的静态方法currentThread()获取当前线程,并通过getName()获取线程的名字
System.out.println("我是继承Thread实现的线程:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
//通过Thread的静态方法currentThread()获取当前线程,并通过getName()获取线程的名字
System.out.println("这是主线程:"+Thread.currentThread().getName());
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
运行结果:
通过重写父类Thread的run方法,将此线程执行的任务写在run()中。然后通过他的start()(从父类Thread中继承的方法)运行线程,此线程执行的内容即为run()方法的内容。
在这里通过start()方法运行的线程,那么能否对象直接调用run()方法来开启线程呢?我在Main函数中直接调用对象的run()方法。
threadDemo.run();
运行结果:
显然:直接调用run()方法运行,还是在Main线程中执行的,并未开启新的线程。
通过对象直接调用run()方法并不是开启新的线程,而是普通的对象调用方法,即在当前线程中执行。
2.通过实现runable接口。
上面实现runable的方法是继承Thread类,如果我准备多线程执行的A类需要继承另外一个B类的话,那么A类就需要继承B类和Thread类,但是Java是单继承,不能继承多个类。李华:“这种需要继承多个类的情景应该会很多吧?Java难道没有解决这样的问题吗?”。当然有,java不能多继承,于是有另外一种机制,实现接口,Java可以实现多个接口。这时候第二种实现多线程的方法就就起了作用,实现runable接口。
System.out.println("这是主线程:"+Thread.currentThread().getName());
RunableDemo r = new RunableDemo();
r.start();//报错
class RunableDemo extends Father implements Runnable{
public void run(){
System.out.println("我是Runnable实现的线程:"+Thread.currentThread().getName());
}
}
当我们调用r.start()的时候报错了。。。。因为runable没有这个方法。
那么应该怎么启动呢?
Thread thread = new Thread(r);
thread.start();
通过new 一个Thread,将实现runable接口的对象传入构造函数即可。
在调用Thread对象的start()方法即可,这个时候start()方法会开启新线程并在其中执行RunableDemo 的run方法
运行结果:
如果线程中的内容比较简单,也可以直接只实现runable接口。
Runnable runnable = new Runnable() {
@Override
public void run() {
//线程执行内容
}
};
Thread t = new Thread(runnable);
t.start();
多线程的一个例子:多窗口卖票
需求:
为了提高卖票的速度,车站在原有一个卖票窗口的基础上,增加了2个卖票窗口,共3个卖票窗口。总共有100张票,每个人只能买一张票,请用程序模拟将票卖完。
public class Window extends Thread {
private static int num = 100;//因为总共100张票,所以静态共享。
private String name;
Window(String name) {
this.name = name;
}
public void run() {
while (num>0){
System.out.println("窗口"+this.name+"--------------卖出了第"+this.num+"张票");
num--;
}
}
}
public static void main(String[] args) {
//三个窗口
Window w1 = new Window("1");
Window w2 = new Window("2");
Window w3 = new Window("3");
//开启线程
w1.start();
w2.start();
w3.start();
}
运行结果:可以多运行几次,发现每次结果都不相同,这是正常的,因为线程随机的被CPU执行。
查看结果,发现竟然卖出了第0张票,而且两个窗口都卖出了。这显然是不正确的,因为根本没有第0张票。
多次运行发现在此次运行中,第15张票被卖出了多次,这显然也是不被允许的。
但在有些时候,又是正确卖出了所有票。
同步锁(synchronized )
为什么会出现上面的问题呢?
由于采用了多线程,所以每个线程会随机的被CPU执行,执行一段时间(非常短的时间)后,如果此线程未结束,那么CPU会保存现场,然后去执行另外一个线程。
比如在刚才的w1线程中,CPU执行到了如下语句,并执行完此语句,假设此时num=15,那么卖出了第15张票。注意,此时num–未执行。
此时CPU暂停w1线程的执行,转而执行w2,并执行到了w2的如下语句,并执行完此语句。此时num依然为15,那么第15张票就被卖出 两次。
同理,上面的其他问题也是这种原因。多个线程操作同一资源,不同步,而导致很可能发生错误。
那应该怎么解决此问题呢?
Java提供了一种机制,synchronized关键字,中文译为同步,也可以叫锁。
它的用法长这样。
synchronized (obj){//参数可以是obejct类型或者其子类
//同步的内容
}
大括号{}里面的内容就是需要同步的内容。当进入大括号 { 时,先拿到锁再进入,出大括号 } 释放锁。如果拿不到锁则等待不进入。
比如上面的卖票,将他改造一下。
public class Window extends Thread {
private static int num = 100;//因为总共100张票,所以静态共享。
private static Object obj = new Object();//新加的,静态共享
private String name;
Window(String name) {
this.name = name;
}
public void run() {
while (true) {
synchronized (obj) {//锁一定要是这多个线程共享的变量,新加的静态
if(num>0){
System.out.println("窗口" + this.name + "--------------卖出了第" + this.num + "张票");
num--;
}
else break;
}
}
}
}
当CPU执行到w1线程的如下位置时,卖出第num张票,此时未num–;w1线程拿到了obj锁,未释放。
如果此时CPU暂停w1线程,转而执行w2,w2走到了如下位置,想拿obj锁,但是由于锁在w1手上,于是等待,直到w1释放后,才有可能拿到锁,这样就避免了不同步而导致的错误。
运行结果:
完美!太长就不截图了
李华:“为什么不能直接在之前的基础上,直接用synchronized把代码包起来呢?之前是while(num>0)现在变为了while(true)”。李华应该想的是将之前的变成如下样子。
public void run() {
while (num > 0) {
synchronized (obj) {
System.out.println("窗口" + this.name + "--------------卖出了第" + this.num + "张票");
num--;
}
}
}
如果用如上代码,那么当w1拿到obj锁后,假设此时num=1,执行到如下语句
此时CPU暂停执行w1,转而执行w2,由于w2没有obj锁,则执行到如下位置等待
当w1执行完后,num–;num=0,释放锁。CPU转而执行w2,由于w2在如上位置等待,拿到锁后,卖票,卖出了第0张票。但是由于在w1执行完后,num=0,票已经买完,所以不应该卖票,发生错误。因此条件判断一定要放在锁里面判断,不然会发生错误。
未完待续…