前言:继续复习java基础知识,网上也看了不少,当看到多线程这边的时候发现荒废了一年基本都忘光了,这篇文章就把多线程的一些基础知识重新过一边,就当是复习了。
在讲解多线程之前先简要说说线程与进程的概念:
那为什么需要多线程呢?
本次主要讲解多线程的创建、线程状态、线程的同步和线程的死锁。
java要实现多线程有三种方式:继承Thread类、实现Runnable接口、实现Callable接口,下面分别举例说明:
package com.multiThreading.learn;
/**
* Created on 2020/2/26
* Package com.multiThreading.learn
*
* @author dsy
*/
public class MyThread extends Thread { //继承Thread类并重写run()方法
private String name ;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
for (int i =0 ;i<5;i++){
try {
sleep(1000); //使当前正在执行的线程进入休眠状态(暂时停止执行)以免当前线程强制占用CPU资源,单位为毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行了线程"+this.name);
}
}
}
测试类:
public class ThreadTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()); //打印当前正在执行的线程名称
MyThread thread = new MyThread("A");
MyThread thread1 = new MyThread("B");
thread.start(); //通过调用Thread类的start()方法启动线程
thread1.start();
//thread1.start(); 一个线程start()方法只能调用一次,否则会抛出java.lang.IllegalThreadStateException异常
}
}
运行结果:
main
执行了线程B
执行了线程A
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程A
执行了线程B
再次执行:
main
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程A
执行了线程B
说明:由于main()方法就是一个主线程,所以在调用Thread.currentThread().getName()方法时打印出了main。当MyThread类两个对象调用start()方法时就启动了两个线程(并没有开始运行),虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度后就会执行run()方法。仔细看这两次的打印结果,我们发现这两个线程执行的顺序不太一样,这并不是我们能控制的,是由操作系统来控制的。
package com.multiThreading.learn;
import static java.lang.Thread.sleep;
/**
* Created on 2020/2/26
* Package com.multiThreading.learn
*
* @author dsy
*/
public class RunnableThread implements Runnable { //实现Runnable接口并重写run()方法
private String name ;
public RunnableThread(String name){
this.name = name;
}
@Override
public void run() {
for (int i =0 ;i<5;i++){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行了线程"+this.name);
}
}
}
测试类:
public class ThreadTest {
public static void main(String[] args) {
RunnableThread thread = new RunnableThread("A"); //创建RunnableThread 实例
RunnableThread thread1 = new RunnableThread("B");
new Thread(thread).start(); //利用RunnableThread 实例thread来创建Thread实例并调用Thread类的start()方法来启动线程
new Thread(thread1).start();
}
}
打印输出:
执行了线程A
执行了线程B
执行了线程A
执行了线程B
执行了线程B
执行了线程A
执行了线程A
执行了线程B
执行了线程B
执行了线程A
在Runnable接口里面只有一个方法就是run()方法
继承Thread类与实现Runnable接口来实现多线程的区别:
下面介绍第三种方法
Callable接口与Runnable接口类似,唯一的区别就是使用Runnable接口没有返回值,而实现Callable接口可以有返回值(通过Future类的get()方法)
关系图如下(参考李兴华老师的讲解)
示例:使用Callable接口实现多线程并获得返回值
package com.multiThreading.learn;
import java.util.concurrent.Callable;
/**
* Created on 2020/2/26
* Package com.multiThreading.learn
*
* @author dsy
*/
public class MyThread implements Callable<String> { //实现Callable接口
@Override
public String call() throws Exception {
return "Hello world !";
}
}
测试类:
package com.multiThreading.learn;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Created on 2020/2/26
* Package com.multiThreading.learn
*
* @author dsy
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<String> task = new FutureTask<String>(myThread);
Thread thread = new Thread(task);
thread.start();
System.out.println(task.get()); //使用get()方法获得线程中的返回值
}
}
打印输出:
Hello world !
注意:上面哪一种方式实现多线程,线程的启动都是通过Thread.start()方法实现的。
一个线程主要包括5种状态:新建状态、可运行状态、运行状态、阻塞状态、终止状态,如下图所示:
现在我们先用一个例子来理解什么是同步:
现在我们假设要实现一个卖票的程序,有三个售票窗口要出售共10张车票:
package com.multiThreading.learn;
/**
* Created on 2020/2/27
* Package com.multiThreading.learn
*
* @author dsy
*/
public class SaleTicketsThread implements Runnable {
private Integer total = 10; //共十张车票
@Override
public void run() {
for (int i=0;i<20 ;i++){
if (total>0){
try {
Thread.sleep(100); //模拟线程的网络延迟,让问题暴露的更快
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"成功售出一张票,还剩"+--total+"张");
}
}
}
}
测试类:
package com.multiThreading.learn;
/**
* Created on 2020/2/27
* Package com.multiThreading.learn
*
* @author dsy
*/
public class SaleTicketsTest {
public static void main(String[] args) {
SaleTicketsThread saleTicketsThread = new SaleTicketsThread();
Thread thread1 = new Thread(saleTicketsThread,"窗口A");
Thread thread2 = new Thread(saleTicketsThread,"窗口B");
Thread thread3 = new Thread(saleTicketsThread,"窗口C");
thread1.start();
thread2.start();
thread3.start();
}
}
打印输出:
窗口C成功售出一张票,还剩9张
窗口B成功售出一张票,还剩8张
窗口A成功售出一张票,还剩7张
窗口C成功售出一张票,还剩6张
窗口A成功售出一张票,还剩5张
窗口B成功售出一张票,还剩4张
窗口C成功售出一张票,还剩3张
窗口B成功售出一张票,还剩2张
窗口A成功售出一张票,还剩2张
窗口C成功售出一张票,还剩1张
窗口A成功售出一张票,还剩0张
窗口B成功售出一张票,还剩-1张
窗口C成功售出一张票,还剩-2张
观察上述输出结果,发现票数出现了负数,用下面这张图来解释上面发生的情况:
当服务器中还剩最后一张票的时候,此时total = 1,其中某一个售票线程在判断时通过,但此时并没有立刻修改票数,而是进入延迟状态,随后(时间很短很短可能只有几毫秒的时间)另外两个线程也立刻进行票数的判断并都进入延迟状态,随后三个线程都修改票数导致total出现-2的情况。这就出现了不同步操作,也就是数据的访问是不安全的操作。
解决办法:synchronized关键字
synchronized关键字可以实现“锁”的功能,还是以上述卖票的例子来说,当一个售票线程进入时,将售票操作进行上锁,其他两个售票线程过来想要进行售票操作,发现该操作被锁上了,只能在外面进行等待了,只有主动上锁的那个售票线程操作完成后释放锁,其他售票进程才能进入。
代码如下:
package com.multiThreading.learn;
/**
* Created on 2020/2/27
* Package com.multiThreading.learn
*
* @author dsy
*/
public class SaleTicketsThread implements Runnable {
private Integer total = 10;
@Override
public void run() {
for (int i=0;i<20 ;i++){
synchronized (this){ //将下面的售票操作进行上锁
if (total>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"成功售出一张票,还剩"+--total+"张");
}
}
}
}
}
测试代码不变
打印输出:
窗口A成功售出一张票,还剩9张
窗口C成功售出一张票,还剩8张
窗口B成功售出一张票,还剩7张
窗口C成功售出一张票,还剩6张
窗口C成功售出一张票,还剩5张
窗口A成功售出一张票,还剩4张
窗口A成功售出一张票,还剩3张
窗口A成功售出一张票,还剩2张
窗口A成功售出一张票,还剩1张
窗口A成功售出一张票,还剩0张
从打印输出发现此时已经实现了数据的同步,但是步虽然可以保证数据的完整性,但是其执行速度明显较慢。
线程同步的本质是一个线程等待另一个线程执行完毕后再去继续执行,但是如果所有线程彼此之间都在等待着,此时虽然实现了同步,但是彼此之间都在等待无法继续往下执行,就出现了死锁状态。举个例子来理解:
package com.multiThreading.learn;
/**
* Created on 2020/2/27
* Package com.multiThreading.learn
*
* @author dsy
*/
public class DeadLockDemo {
private static final Object resource1 = new Object();//资源 1
private static final Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread().getName() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread().getName() + "get resource2");
}
}
}, "线程 1 ").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread().getName() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread().getName() + "get resource1");
}
}
}, "线程 2 ").start();
}
}
打印输出:
分析:我们看到此时线程1获得了资源1的锁,然后调用sleep(1000)方法让线程1休眠1s,为的是让线程二获得了资源2锁。等到两个线程休眠都结束了后,线程1在等待资源2,线程2在等待资源1,程序还在运行并没有结束,两者陷入了无限等待的僵局就产生了死锁。符合产生死锁的四个必要条件:
死锁的避免:
我们只要破坏产生死锁的四个必要条件之一就可以避免死锁:
在上述死锁案例中,我们修改线程2代码如下,即可避免死锁:
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread().getName() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread().getName() + "get resource2");
}
}
}, "线程 2 ").start();
打印输出:
线程 1 get resource1
线程 1 waiting get resource2
线程 1 get resource2
线程 2 get resource1
线程 2 waiting get resource2
线程 2 get resource2
分析:线程1获得resource1的锁后进入睡眠状态,随后线程2启动想要获得resource1的锁,但是已经被线程1拿走了,所以只能等待。线程1休眠完成后获得resource2的锁,随后线程1的生命周期结束并释放对resource1与resource2的占用,线程2就可以获取到了,这样就避免了死锁。