多线程概述
什么是线程
- 线程是程序执行的一条路径, 一个进程中可以包含多条线程(启动任务管理器点进程,那些都是进程)
- 多线程并发执行可以提高程序的效率, 可以同时完成多项工作
多线程的应用场景
举个某管家的栗子,我们有空需给电脑清清垃圾,在清垃圾的时候你又想去杀杀毒,于是又去点杀毒了。你会发现,杀毒和清理垃圾都是同时在运行的。假设单线程,那么某管家只能先清理完垃圾后再进行杀毒
还有许多的下载软件都是开启多条线程下载的
还有服务器同时处理多个客户端请求。假设我们使用的百度是单线程的会怎样?那么全国人民都只能有一个人去访问百度,这个人搜索完在下一个人(和排队一样)
举了这么几个小例子可见多线程的重要性。
多线程原理简单概述
假设你的电脑上开着QQ,听着音乐,写着Java代码。在你看来他们是同时运行的,其实由于cpu的运行效率非常之高,所以他会在QQ上执行一点然后切换到音乐程序上执行一点,通过这样来回不断的切换,使我们感觉他是在同时运行多个程序。简单的说就是在某一时刻(非常非常小)cpu只会执行一个程序。想象一下,加入你开着百来个QQ(或更多),CPU可能就忙不过来了,就出现了卡顿。
所以多线程表面上看是多线程,底层还是在某一时刻只执行的是一个事
多线程并行和并发的区别
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
举例:
比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。
如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。
Java程序运行原理
Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。
Q:JVM的启动是多线程的吗?
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的
class Demo {
@Override
protected void finalize() throws Throwable {
System.out.println("垃圾被清理啦!");
}
}
public class Test {
public static void main(String[] args) {
for(int i = 0 ; i < 50000 ; i++) {
new Demo();
}
for(int i = 0 ; i < 50000 ; i++) {
System.out.println("我是主线程的执行代码");
}
}
}
左图为部分截图,可以看出来,在执行主线程的同时还执行了垃圾回收线程,垃圾回收线程和主线程随机切换执行。
多线程程序的实现
实现方式一:继承Thread
- 定义类继承Thread
- 重写run方法
- 把新线程要做的事写在run方法中
- 创建线程对象
- 开启新线程, 内部会自动执行run方法
class MyThread extends Thread {//1,继承Thread
@Override
public void run() {//2,重写run方法
for(int i = 0 ; i < 50000 ; i++) {//3,将要执行的代码写在run方法中
System.out.println("OOO");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();//4,创建Thread类的子类对象
mt.start();//5,开启线程
for(int i = 0 ; i < 50000 ; i++) {
System.out.println("XXXXXXXXXXXXX");
}
}
}
左图为两处结果的拼图,可以看出来,线程启动成功了,两处代码交替执行了。
实现方式二:实现Runnable接口
- 定义类实现Runnable接口
- 实现run方法
- 把新线程要做的事写在run方法中
- 创建自定义的Runnable的子类对象
- 创建Thread对象, 传入Runnable
- 调用start()开启新线程, 内部会自动调用Runnable的run()方法
class MyRunnable implements Runnable {//1,定义一个类实现Runnable
@Override
public void run() {//2,重写run方法
for(int i = 0 ; i < 50000 ; i++) {//3,将要执行的代码写在run方法中
System.out.println("OOO");
}
}
}
public class Test {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();//4,创建Runnable的子类对象
Thread t = new Thread(mr);//5,将其当作参数传递给Thread的构造函数
t.start();//6,开启线程
for(int i = 0 ; i < 50000 ; i++) {
System.out.println("XXXXXXXXXXXXX");
}
}
}
左图还是为程序结果的两处截图的拼图(截图一处图片篇幅会过大)。从图像上看,该线程也启动成功了。
这两处的实现方式,你会发现我们都是通过start()方法来启动线程的。
查看API的解释
public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
假设你这样写
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.run();
for(int i = 0 ; i < 50000 ; i++) {
System.out.println("XXXXXXXXXXXXX");
}
}
那么这样你并没有开启多线程,你只是调用了MyThread的run方法而已,run方法执行完毕后重新回到main函数进行执行下面代码。
简单的说,run好像在起跑线上的运动员,start就是发令枪,只有听到发令枪 “砰” 运动员才可跑出去(调用start在调用run),但是未发枪也是可以跑出去(直接调用run),但是这样的成绩是无效的(未开启线程)。还有上面说了多次启动一个线程是非法的,就像运动员已经跑出去,还把他拉回来吗。线程结束就相当于比赛比完了,比赛只有一次,所以start也不能重新启动。
两种方式的区别
继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法
实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法
继承Thread比较好理解的,就是重写了run方法,start()时直接找该重写的run方法。
而下面的实现Runnable,我们来简单分析下源码(JDK1.8),看下run如何被调用的
public
class Thread implements Runnable {
private Runnable target;
private volatile char name[];
private ThreadGroup group;
//拿我们之前写的MyRunnable举例,这里我们把MyRunnable对象传递进来
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// g - 线程组,target - 任务,name - 线程名称, stackSize - 栈大小
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null);
}
//最终我们将MyRunnable对象传递到了这里
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name.toCharArray();
//此处省略部分源码
this.group = g;
//此处省略部分源码
this.target = target;//这里将我们传递进来的MyRunnable对象(mr)赋值给了成员变量
//此处省略部分源码
}
//这里调用了我们的start(),会调用我们的run方法。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
@Override//这里由于是我们自己定义的类实现Runnable接口,并没覆盖该方法.而继承Thread则覆盖了该方法
public void run() {//所以start就会调用这里的run,判断target是否为空。
if (target != null) {//上面我们将MyRunnable对象传递进来并赋值给了target,所以不为空
target.run();//这里调用的run就是我们自己写的MyRunnable对象里面的run方法
}
}
}
你可以有疑问,这个start方法是如何调用run的呢?
这里小菜能力不足,这里看源码也是看不出什么所以然来的。因为这是底层虚拟机帮我们完成调用的
继承Thread
好处是:可以直接使用Thread类中的方法,代码简单
弊端是:如果已经有了父类,就不能用这种方法
实现Runnable接口
好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的
弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂
可以看出来,两种方式正好进行了互补
匿名内部类实现线程的两种方式
public static void main(String[] args) {
//第一种继承方式
new Thread() {//1,继承Thread类
@Override //2,重写run方法
public void run() {
//TODO 3,将要执行的代码写在run方法中
}
}.start(); //4,开启线程
//第二种实现接口方式
new Thread(new Runnable() {//1,将Runnable的子类对象传递给Thread的构造方法
@Override //2,重写run方法
public void run() {
// TODO 3,将要执行的代码写在run方法中
}
}).start(); //4,开启线程
}
获取和设置线程的名字
获取名字
通过getName()方法获取线程对象的名字
设置名字
通过构造函数可以传入String类型的名字
通过setName(String)方法可以设置线程对象的名字
public static void main(String[] args) {
//第一种通过构造传入
new Thread("我是一个小小小线程") {
@Override
public void run() {
System.out.println(getName() + "---运行啦");
}
}.start();
//第二种setName(String)方法设置
Thread t = new Thread() {
@Override
public void run() {
System.out.println(getName() + "---运行啦");
}
};
t.setName("我是一个大大大线程");
t.start();
}
/*
* outPut:
* 我是一个小小小线程---运行啦
* 我是一个大大大线程---运行啦
*/
这里的输出看起来好像没有多线程,因为前面演示了多线程,下面的就方便起见,就输出了一句话,由于cpu执行速度非常快,自然很快就能执行完。你可以多运行几次试试,可能会有不同的结果。
获取当前线程的对象
public static Thread currentThread()
我们为什么需要获取当前对象?
前面讲了实现多线程的两种方式,继承Thread是可以直接使用该类的方法,但是实现Runnable接口的就会很痛苦,因为它不能直接调用Thread里面的方法。所以注意他这个方法是静态的,是可以直接类名.调用。
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//System.out.println(getName()); 编译错误,无getName方法
System.out.println("我是" + Thread.currentThread().getName());
}
}).start();
Thread.currentThread().setName("我是主线程");//获取主函数线程的引用,并改名字
System.out.println(Thread.currentThread().getName());//获取主函数线程的引用,并获取名字
}
/*
* outPut:
* 我是主线程
* 我是Thread-0
*/
这里就挺好的,因为开启了多线程,主线程中代码是在后面却先执行了。
休眠线程
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
前面这个参数为毫秒,后面这个为纳秒(1秒=1000000000纳秒)。由于win下对前面的那个方法支持性不好,就演示前面那个方法。
InterruptedException
当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常。
有时候,一种方法可能希望测试当前线程是否已被中断,如果已被中断,则立即抛出此异常。
if (Thread.interrupted()) // 测试当前线程是否已经中断
throw new InterruptedException();
public static void main(String[] args) {
new Thread() {
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName() + "...aaaaaaaaaaa");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() {
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName() + "...bbb");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
可以看出来,两个线程进行了交互执行。当执行到第一个线程(Thread-0)的sleep()时,该线程就睡了,于是cpu便把执行权给了第二个线程(Thread-1),还是当执行到sleep()时,第二个线程便睡了。于是cpu便又切回到第一个线程,发现它睡醒了,便开始执行循环后,又遇到了sleep(),重复以往操作,于是两个线程就交替输出了。所以如果两个线程你看不出什么效果时,你可以让他小睡一下,这样效果就很明显了。
其中代码中的sleep()是会抛异常的,然而这里是异常只能捕获,不能抛出去,因为父类的run()方法没有声明抛出异常,所以这里必须捕获。
守护线程
public final void setDaemon(boolean on)
设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出。
简单举个例子解释一下什么是守护线程:象棋大家应该都玩过,象棋做的是保帅(非守护线程)的动作,当帅死亡时,那些车马相士(守护线程)就相当于都直接死亡了。
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
for(int i = 0;i < 2 ; i++) {
System.out.println(getName() + "--aaaaaaaa");
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for(int i = 0;i < 50 ; i++) {
System.out.println(getName() + "--bbb");
}
}
};
t2.setDaemon(true);//设置为守护线程
t1.start();
t2.start();
}
这里将t2设置为守护线程,所以当这些主线程,t1线程这些非守护线程都运行完毕时,t2便不会在执行了,那么这里看样子t1的循环两次都执行完毕了,不是t1线程完毕后t2会自动退出吗,但是为什么t2线程还是输出了若干次语句?
因为这里有个时间缓冲。用上面的白话说,老帅死了,但是其车马相士 自杀还是有一个时间段的问题,就是时间缓冲。
加入线程
//当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
public final void join() throws InterruptedException
//可以等待指定的毫秒之后当前线程继续
public final void join(long millis) throws InterruptedException
public static void main(String[] args) {
final Thread t1 = new Thread() {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println(getName() + "...aaaaa");
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
if (i == 1) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName() + "...bbb");
}
}
};
t1.start();
t2.start();
}
该结果是非常明显的,当Thread-1执行执行完一次循环后便让Thread-0线程加入了,等Thread-0中的方法全部执行完毕后Thread-1才继续执行。join()方法简单的说就是插队,要等插队的这个人所有事情都办完后面人才可继续。相当于把两条线程又合并到一条了。
还有代码中的t1是必须使用final修饰的(JDK1.8不用,默认)。因为t1相当于是方法内部的局部变量,而t2中的new Thread()相当于内部类,匿名内部类在使用它所在方法中的局部变量时,该变量必须使用final修饰。
礼让线程
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。相当于让出CPU。
但是yield支持的特别不好,很多时候你是看不出效果的。理论上会让出CPU,但是达不到该效果。了解即可
class MyThread extends Thread {
public void run() {
for(int i = 1; i <= 1000; i++) {
if(i % 10 == 0) {
Thread.yield(); //让出CPU
}
System.out.println(getName() + "..." + i);
}
}
}
public class Test {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
我选了两处效果明显的截了图(这两幅图我拼接了,而不是46后面直接58)。看第一例,打印完159后便是160了,由于160符合让出CPU条件所以便换了一条线程执行。但是总体你会发现效果还是不明显的,因为很多符合条件的还是没有让出CPU。
线程的优先级
public final static int MIN_PRIORITY = 1; //最小优先级
public final static int NORM_PRIORITY = 5; //默认优先级
public final static int MAX_PRIORITY = 10; //最大优先级
//设置优先级,这里我们可以传入上述的字段,或者传入一数字[1,10]
public final void setPriority(int newPriority) {}
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(getName() + "...aaaaaaaa" );
}
}
};
Thread t2 = new Thread(){
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println(getName() + "...bbb" );
}
}
};
t1.setPriority(1); //设置最小的线程优先级
t2.setPriority(Thread.MAX_PRIORITY); //设置最大的线程优先级
t1.start();
t2.start();
}
这里结果就不截图了,你会发现,bbb大部分输出都是在前面的,而a输出留在了后面。
同步代码块
什么情况下需要同步
当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.
我们来看下并发会出现的异常:
class Printer {
public void print1() {
System.out.print("X");
System.out.print("X");
System.out.print("X");
System.out.print("X");
System.out.print("\r\n");
}
public void print2() {
System.out.print("O");
System.out.print("O");
System.out.print("O");
System.out.print("O");
System.out.print("\r\n");
}
}
public class Test {
public static void main(String[] args) {
final Printer p = new Printer();
new Thread() {
public void run() {
while(true) {
p.print1();
}
}
}.start();
new Thread() {
public void run() {
while(true) {
p.print2();
}
}
}.start();
}
}
看到了吧,当两个线程跑的时候,这里print1()方法还未都输出完,CPU的执行权就被另外一个线程抢去了。所以那里会输出三个X后就接着输出O,然后当CPU的执行权又回来时,便接下去运行。
当我们不希望CPU去切换时,我们就需要加同步。
同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
class Printer {
private Object obj = new Object();
public void print1() {
synchronized(obj) { //同步代码块,锁机制,锁对象可以是任意的
System.out.print("X");
//...
System.out.print("\r\n");
}
}
public void print2() {
synchronized(obj) {
System.out.print("O");
//...
System.out.print("\r\n");
}
}
}
这样子,就不会出现上面的并发问题了。锁对象可以任意但是你要保证需要的同步的代码块的锁必须为同一把,上面的代码就是print1里面的锁和print2里面的锁必须一致,否则还是会出现并发问题。
同步方法
使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的
class Printer {
public void print1() {
synchronized(Printer.class) { //同步代码块,锁机制,锁对象可以是任意的
System.out.print("X");
//...
System.out.print("\r\n");
}
}
/*
* 非静态同步函数的锁是:this
* 静态的同步函数的锁是:字节码对象(这里是Printer.class)
*/
public static synchronized void print2() {
System.out.print("O");
//...
System.out.print("\r\n");
}
}
这里运行后你会发现这样子也是同步,因为下面的为静态方法,而静态方法的同步函数的锁为字节码对象,字节码对象(class对象)肯定是唯一的。如果你将上面的同步代码块内的锁改为其他,那么将会出现并发的一些错误。非静态方法的锁为this,也就是说 一个对象就对应着这么一个他自己的方法。
死锁
多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁。
有这么两个人,他们手上都只有一根筷子。但是两个人都不肯让步,不把筷子给对方这样子就出现了死锁的现象,就这样僵持着。
public class Test {
private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
new Thread("小明") {
public void run() {
while(true) {
synchronized(s1) {
System.out.println(getName() + "...获取" + s1 + "等待" + s2);
synchronized(s2) {
System.out.println(getName() + "...拿到" + s2 + "开吃");
}
}
}
}
}.start();
new Thread("小黑") {
public void run() {
while(true) {
synchronized(s2) {
System.out.println(getName() + "...获取" + s2 + "等待" + s1);
synchronized(s1) {
System.out.println(getName() + "...拿到" + s1 + "开吃");
}
}
}
}
}.start();
}
}
一开始,小明还是很幸运的,接连拿到左筷子和右筷子,但是到后面小明手上是左筷子,而小黑终于抢到了右筷子,这样子双方都等待着对方给筷子,程序也不会停止运行,就这样僵持着,直到把系统资源耗尽。那就都结束了。
经典卖票问题练习
需求:铁路售票,一共100张,通过四个窗口卖完.
1.使用继承Thread
class Ticket extends Thread {
private static int ticket = 100; //使用继承这里的票必须共享static
public Ticket(String name) {
super(name);
}
private void sale() {//这里不能使用同步方法,因为他会使用this作为锁
synchronized(Ticket.class) {
if(ticket > 0) {
System.out.println(getName() + "...这是第" + ticket-- + "号票");
}
}
}
public void run() {
while(ticket > 0) {
sale();
}
}
}
public class Test {
public static void main(String[] args) {
new Ticket("窗口一").start();
new Ticket("窗口二").start();
new Ticket("窗口三").start();
new Ticket("窗口四").start();
}
}
2.实现Runnable接口
class Ticket implements Runnable {
private int ticket = 100;
private synchronized void sale() {//这里可以使用同步方法,因为只有一个Ticket对象
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + "...这是第" + ticket-- + "号票");
}
}
public void run() {
while(ticket > 0) {
sale();
}
}
}
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "窗口一").start();
new Thread(ticket, "窗口二").start();
new Thread(ticket, "窗口三").start();
new Thread(ticket, "窗口四").start();
}
}