Java多线程
一、Java多线程编程
1. 进程与线程
在java语言里面最大的特点是支持多线程的开发(也是为数不多支持多线程的编程语言),所以在好着呢哥哥java技术的学习里面,如果不能够对多线程的概念有一个全面并且细致的了解,则在日后进行一些项目的设计的过程之中尤其是并发访问设计的 过程之中就会出现严重的技术缺陷。
如果要理解线程,首先需要了解进程的概念,在传统的DOS系统的时代,其本身有一个特征:如果你电脑上出现了一个病毒,那么所有的程序将无法执行,因为传统的DOS采用的是单进程处理,单进程处理的最达特点:在同一时间段只允许一个程序在执行。
在windows的时代就开启了多进程的设计,于是就表示在一个时间段上可以同时运行多个程序,并且这些程序将进行资源的轮流抢占。因此在同一个时间段上会有多个程序依次执行,但是在同一个时间点上只会有一个进程执行。多核CPU,由于可以处理的CPU多了,那么即便有再多的进程出现,也可以比单核CPU处理的速度有所提升。
线程是在进程基础上划分的更小的程序单元,线程是在进程基础上创建并且使用的,所以线程依赖于进程的支持。但是线程的启动速度要比进程快许多,所以当使用多线程进行并发处理的时候,其执行的性能要高于进程。
Java是多线程的编程语言,所以Java在进行并发访问处理的时候可以得到更高的处理性能
2. Java多线程的实现
如果要想在Java之中实现多线程的定义,那么就需要有一个专门的线程主体类进行线程的执行任务的定义,而这个主体类定义是有要求的,必须实现特定的接口或者继承特定的父类才可以完成。
继承Thread类实现多线程
Java里面提供有一个java.lang.Thread的程序类,那么一个类只要继承了此类就表示这个类为线程的主体类,但是并不是说这个类就可以直接实现多线程处理了,因为还需要重写Thread类中提供的一个run()方法,而这个方法就属于线程的主方法。
范例:多线程主体类
class MyThread extends Thread { // 线程的主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() { //线程的主体方法
for (int x = 0; x < 10; x ++) {
System.out.println(this.title + "运行:x = " + x);
}
}
}
多线程要执行的功能都应该在run()方法中进行定义。需要说明的是:在正常情况下如果想要使用一个类中的方法,那么肯定要产生实例化对象,而后去调佣类中提供的方法,但是run()方法是不能够直接调用的, 因为这里面牵扯到系统的资源调度问题,因此想要启动多线程必须使用start()方法完成。
public class ThreadDemo {
public static void main(String[] args) {
new MyThread("线程A").start();
new MyThread("线程B").start();
new MyThread("线程C").start();
}
}
通过此时的调用你可以发现,虽然调用了start()方法,但是最终执行的是run()方法,并且所有的线程对象都是交替执行的
疑问?
为什么多线程的启动不直接使用run()方法而必须使用Thread中的start()方法呢?
如果要想清楚这个问题,最好的做法是查看start()方法的实现操作 ,可以直接通过源代码观察。
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) {
}
}
}
private native void start0(); //只定义了方法名,没有实现
发现在start()方法里面会抛出一个“IllegalThreadStateException”异常对象,但是整个的程序并没有使用throws或者是明确的 try … catch 处理,因为该类异常一定是RuntimeException的子类,每一个线程类的对象只允许启动一次,如果重复启动则就抛出次异常。例如下面的代码就会抛出异常:
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread("线程A");
mt.start();
mt.start(); //重复执行
}
}
Exception in thread “main” java.lang.IllegalThreadStateExceptionnative :java考虑到对于不同层次开发者的需求,所以支持有本地的操作系统函数调用,这项技术,被称为JNI(java的本地接口)技术,但是在开发中,不推荐这样使用,利用这项技术,可以使用一些操作系统底层提供的函数进行一些特殊的处理,在Thread类里面提供的start0()就表示需要依靠操作系统实现
任何情况下,只要定义了多线程,多线程的启动永远只有一种方案:Thread 类中的 start()方法
3. 基于Runnable接口实现多线程
虽然可以通过Thread类的继承来实现多线程的定义,但是在Java程序里面对于继承永远都是存在有单继承局限的,所以在Java里面又提供有第二种多线程的主体定义结构形式:实现java.lang.Runnable接口,此接口定义如下:
@FunctionalInterface // 从JDK1.8引入了Lambda表达式之后就变为了函数式接口
public interface Runnable {
public void run();
}
范例:通过Runnable实现多线程主体类
class MyThread implements Runnable { // 线程的主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() { //线程的主体方法
for (int x = 0; x < 10; x ++) {
System.out.println(this.title + "运行:x = " + x);
}
}
}
但是此时有不再继承Thread父类了,那么对于此时的MyThread类中也就不在支持start()方法了。可是不使用start()方法是无法使用多线程启动的,那么这个时候就需要去观察一下Thread类所在的类提供的构造方法了
- 构造方法:public Thread(Runnable target);
范例:启动多线程
public class ThreadDemo {
public static void main(String[] args) {
Thread threadA = new Thread(new MyThread("线程A"));
Thread threadB = new Thread(new MyThread("线程B"));
Thread threadC = new Thread(new MyThread("线程C"));
threadA.start();
threadB.start();
threadC.start();
}
}
这个时候的多线程里面可以发现,由于只是实现了Runnable接口对象,所以此时线程主体类上就不再有单继承的局限了,这样的设计才是一个标准的设计
Runnable接口使用了函数式接口定义,所以也可以直接利用Lambad表达式进行线程的实现
范例:利用Lambda实现多线程定义
public class ThreadDemo {
public static void main(String[] args) {
for (int x = 0 ; x < 3 ; x ++) {
String title = "线程对象" + x;
//Lambda 表达式
Runnable run = () -> {
for (int y = 0 ; y < 10 ; y ++) {
System.out.println(title + "运行:y = " + y);
}
};
new Thread(run).start();
}
}
public class ThreadDemo {
public static void main(String[] args) {
for (int x = 0 ; x < 3 ; x ++) {
String title = "线程对象" + x;
//Lambda 表达式
new Thread(() -> {
for (int y = 0 ; y < 10 ; y ++) {
System.out.println(title + "运行:y = " + y);
}
}).start();
}
}
}
在以后的开发之中对于多线程的实现,优先考虑的就是Runnable接口实现,并且永恒都是通过Thread类对象启动多线程
4、Thread和Runnable关系
多线程的实现已经有了两种做法:Thread类,Runnable接口,从结构本身来说,使用Runnable是最方便的,即避免了单继承的局限,也可以更好的使功能得到扩充
从结构上观察Thread和Runnable的练习,打开Thread类的定义:
public class Thread extends Object implements Runnable{}
发现在Thread类也是Runnable接口的子类,那么其实际上继承Thread类的时候重写的还是Runnable的方法。
class MyThread implements Runnable { // 线程的主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() { //线程的主体方法
for (int x = 0; x < 10; x ++) {
System.out.println(this.title + "运行:x = " + x);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Thread threadA = new Thread(new MyThread("线程A"));
Thread threadB = new Thread(new MyThread("线程B"));
Thread threadC = new Thread(new MyThread("线程C"));
threadA.start();
threadB.start();
threadC.start();
}
}
多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理
在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类被重写过的run()方法
多线程开发的本质实质上是在多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的
范例:利用卖票程序来实现多个线程的资源并发访问
class MyThread implements Runnable { // 线程的主体类
private Integer ticket = 5;
@Override
public void run() { //线程的主体方法
for (int x = 0; x < 100; x ++) {
if(ticket > 0 ) {
System.out.println("卖票:ticket = " + this.ticket --);
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start(); //第一个线程启动
new Thread(mt).start(); //第一个线程启动
new Thread(mt).start(); //第一个线程启动
}
}
5、Callable实现多线程
从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点,当线程执行完毕之后无法获取一个返回值。从JDK1.5之后提出一个新的线程实现接口:java.util.concurrent.Callable接口。观察接口定义:
@FunctionalInterface
public interface Callable<V> {
public V call() throws Exception;
}
可以发现Callable定义的时候回可以设置一个泛型,此泛型的类型就是返回数据的类型,这样好处是可以避免向下转型所带来的安全隐患
范例:使用Callable实现
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10 ; i ++) {
System.out.println("********** 线程执行、 i = " + i);
}
return "线程执行完毕";
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<>(new MyThread());
new Thread(task).start();
System.out.println("【线程返回数据】" + task.get());
}
}
面试题:请解释Runnable与Callable的区别?
- Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的
- java.lang.Runnable接口之中只提供有一个run()方法,并且没有返回值
- java.util.concurrent.Callable接口提供有一个call()方法,可以有返回值
6、线程运行状态
对于多线程的开发而言,编写程序的过程之中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但是并不意味着你调用了start()方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态
- 任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候实际上若干和线程都将进入到一种就绪状态,现在并没有执行
- 进入到就绪状态之后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run方法),但是所有的线程不可能一直执行下去,中间需要产生一些暂停状态,例如:某个线程执行一段时间之后就需要让出资源,而后这个进行将进入到阻塞状态,随后重新回归就绪状态
- 当run()方法执行完毕之后,实际上该县城的主要任务也就执行完毕了,那么此时就可以直接进入到停止状态
二、线程的命名和取得
多线程的主要操作方法都在Thread类中定义了
多线程的运行状态是不确定的,那么在程序的开发之中为了可以获取到一些需要使用到线程就只能够依靠线程的名字来进行操作。所以线程的名字就是一个至关重要的概念,这样在Thread类之中就提供有限额和才能够名称的处理
- 构造方法:public Thread(Runnable target , String name)
- 设置名字:public final void setName(String name)
- 取得名字:public final void getName()
对于线程对象的获得是不可能只依靠一个this来完成的,因为线程的状态不可控,但是有一点是明确的,所有的线程对象一定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供有获取当前线程的方法
- 获取当前线程:public static Thread currentThread()
范例:观察线程的命名操作
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
new Thread(myThread, "线程A").start(); //设置了线程名字
new Thread(myThread).start(); //未设置线程名字
new Thread(myThread, "线程B").start(); //设置了线程名字
}
}
当开发者为线程设置名字的还回家欧就是用设置的名字,而如果没有设置名字,则会自动命名,这种自动命名的属性命名是依靠了static属性完成的
范例:观察一个程序
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
new Thread(myThread, "线程A").start(); //设置了线程名字
myThread.run();
}
}
通过此时的代码可以发现当使用“myThread.run()”直接在主方法之中调用线程类对象中的run()方法所获得的线程对象的名字为“main”,所以可以的出结论:主方法也是一个线程。
所有的线程都是在进程上划分,那么进程在哪里?
每当使用java命令执行程序的时候就表示启动了一个JVM的进程,一台电脑上可以启动若干个JVM的进程,每个JVM进程都会有各自的线程
在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程处理
范例:子线程处理
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("1、执行操作任务一");
new Thread( ()-> { //子线程负责统计
int temp = 0;
for (int x = 0 ; x < Integer.MAX_VALUE; x++){
temp += x;
}
}).start();
System.out.println("2、执行操作任务二");
System.out.println("n、执行操作任务N");
}
}
主线程负责整体流程,而子线程负责处理耗时操作
1、线程的休眠
如果说现在希望某一个线程可以暂缓执行,那么久可以使用休眠的处理,在Thread类中定义的休眠方法如下:
- 休眠:public static void sleep(long millis) throws INterruptedException
- 休眠:public static void sleep(long millis , int nanos) throws InterruptedException
在进行休眠的时候有可能产生中断异常"InterruptedException",中断异常属于Exception的子类,所以该异常必须进行处理
范例:观察休眠处理
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("1、执行操作任务一");
new Thread( ()-> { //子线程负责统计
for (int x = 0 ; x < 100; x++){
System.out.println(Thread.currentThread().getName() + "、 x = " + x);
try {
Thread.sleep(1000); //暂缓执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程对象").start();
}
}
休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果你有多个线程,那么休眠也是有先后顺序的
范例:产生多线程对象进行休眠处理
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 5; i++) {
new Thread( ()-> { //子线程负责统计
for (int x = 0 ; x < 100; x++){
System.out.println(Thread.currentThread().getName() + "、 x = " + x);
try {
Thread.sleep(1000); //暂缓执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程对象" + i).start();
}
}
}
此时将产生五个线程对象,并且这五个线程对象执行的方法体是相同的。此时从程序的执行的感觉上来讲,好像是若干个线程一起休眠,而后一起唤醒,但实际上是有差别的
2、线程中断
在之前发现线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其他线程完成的,在Thread类里面提供有这种中断执行的处理方法
- 判断线程是否被打断:public boolean isInterrupted()
- 中断线程执行:public void interrupt()
范例:观察线程的中断处理操作
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("**** 72小时的疯狂,我需要睡觉补充精力");
try {
Thread.sleep(1000); //预计休眠10s
} catch (InterruptedException e) {
System.out.println("胆敢阻止我睡觉,直接疯狂你");
}
System.out.println("**** 睡足了,又可以继续祸害别人了");
});
thread.start(); //开始睡
if (!thread.isInterrupted()){ //该线程中断了吗
System.out.println("我偷偷的打扰一下你的睡眠");
thread.interrupt(); //中断执行
}
}
所有正在执行的线程都是可以被中断的,中断进程必须进行异常的处理
3、线程的强制执行
所谓的线程的强制执行指的是当满足于某些条件之后,某一个线程对象可以一直独占资源,直到该线程的执行结束
范例:观察一个没有强制执行的程序
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread mainThread = Thread.currentThread(); //获取主线程
Thread thread = new Thread(() -> {
for (int i = 0; i < 100 ; i ++) {
if (i > 3) { //霸道线程出现
try {
mainThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
} //霸道线程先执行
}
System.out.println(Thread.currentThread().getName() + "执行、x = " + i);
}
},"玩耍的线程");
thread.start();
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
System.out.println("【霸道的main线程】 number = " + i);
}
}
}
在进行线程强制执行的时候一定要获取强制执行线程对象之后才可以执行join()调用
4、线程的礼让
线程的礼让指的是先将资源让出去别的线程先执行。线程的礼让可以使用Thread中提供的方法:
- 礼让:pubilc static void yield()
范例:使用礼让操作
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100 ; i ++) {
if ( i % 3 == 0) {
Thread.yield(); //线程礼让
System.out.println("### 玩耍的线程礼让执行 ###");
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} //霸道线程先执行
System.out.println(Thread.currentThread().getName() + "执行、x = " + i);
}
},"玩耍的线程");
thread.start();
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
System.out.println("【霸道的main线程】 number = " + i);
}
}
礼让执行的时候每用一次调用yield()方法都只会礼让一次当前的资源
5、线程的优先级
从理论上来讲线程的优先级越高越有可能先执行(越有可能先抢占到资源)。在Thread类里面针对于优先级有如下的两个处理方法:
- 设置优先级:public final void setPriority(int new Priority)
- 获取优先级:public final void getPriority()
在进行优先级定义的时候都是通过int型的数字来完成的,而对于此数字的选择在Thread类里面就定义有三个常量
- 最高优先级:public static final int MAX_PRIORITY
- 中等优先级:public static final int NORM_PRIORITY
- 最低优先级:public static final int MIN_PRIORITY
范例:观察优先级
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable run = () -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行、");
}
};
Thread threadA = new Thread(run , "线程对象A");
Thread threadB = new Thread(run , "线程对象B");
Thread threadC = new Thread(run , "线程对象C");
threadA.setPriority(Thread.NORM_PRIORITY);
threadB.setPriority(Thread.MIN_PRIORITY);
threadC.setPriority(Thread.MAX_PRIORITY);
threadA.start();
threadB.start();
threadC.start();
}
}
主方法是一个主线程,那么主线程的优先级呢?
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(new Thread().getPriority());
System.out.println(Thread.currentThread().getPriority());
}
} 主线程是中等优先级,而默认创建的线程也是中等优先级
三、线程的同步与死锁
在多线程的处理之中,可以利用Runnable描述多个线程操作的资源,而Thread描述每一个线程对象,于是党多个线程访问同一资源的时候,如果处理不当就会产生数据的错误操作
1、同步问题的引出
下面编写一个简单的卖票程序,将创建若干个线程对象实现卖票的处理操作
范例:实现卖票的操作
class MyThread implements Runnable {
private Integer ticket = 10;
@Override
public void run() {
while (true) {
if (ticket == null) {
System.out.println(Thread.currentThread().getName() + "卖票.ticked = " + ticket --);
} else {
System.out.println("***** 票已经卖完了 *****");
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
new Thread(thread , "票贩子A").start();
new Thread(thread , "票贩子B").start();
new Thread(thread , "票贩子C").start();
}
}
此时的程序将创建三个线程对象,并且这三个线程对象将进行5张票的出售。此时的程序在进行卖票处理的时候并没有任何的问题(这是假象)
class MyThread implements Runnable {
private Integer ticket = 10;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖票.ticked = " + ticket --);
} else {
System.out.println("***** 票已经卖完了 *****");
break;
}
}
}
}
这个时候追加了延迟问题就暴露出来了,而实际上这个问题一直都在
2、线程同步处理
经过分析之后已经可以确认同步问题所产生的主要原因了,那么下面就需要进行同步问题的解决了,但是解决问题的关键是锁,指的是党某一个线程执行操作的时候,其他线程在外面等待。
如果要想在程序之中实现这把锁的功能,就可以使用synchronized关键字来实现。利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作里面的代码只允许一个线程执行。
①、利用同步代码块
synchronized(同步对象){
同步代码操作
}
范例:利用同步代码块解决数据同步访问问题
import java.util.concurrent.ExecutionException;
class MyThread implements Runnable {
private Integer ticket = 10;
@Override
public void run() {
while (true) {
synchronized (this) { //每次只允许一个线程进行访问
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖票.ticked = " + ticket --);
} else {
System.out.println("***** 票已经卖完了 *****");
break;
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
new Thread(thread , "票贩子A").start();
new Thread(thread , "票贩子B").start();
new Thread(thread , "票贩子C").start();
}
}
加入同步处理之后,程序的整体的性能下降了,同步实际上回造成性能的降低
②、利用同步方法解决:只需要在方法定义上使用synchronized关键字即可
import java.util.concurrent.ExecutionException;
class MyThread implements Runnable {
private Integer ticket = 10;
public synchronized boolean sale(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖票.ticked = " + ticket --);
return true;
} else {
System.out.println("***** 票已经卖完了 *****");
return false;
}
}
@Override
public void run() {
while (this.sale()) {
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
new Thread(thread , "票贩子A").start();
new Thread(thread , "票贩子B").start();
new Thread(thread , "票贩子C").start();
}
}
在日后学习java类库的时候会发现,系统中许多的类上使用的同步处理采用的都是同步方法
注意:同步会造成性能下降
3、线程死锁
死锁是在进行多想爱你成同步的处理之中有可能产生的一种问题,所谓的死锁指的是若干个线程彼此互相等待的状态
通过一个简单的代码观察一下锁的表现形式,但是对于此代码不多为重点
范例:死锁的展示
class XiaoWu {
public synchronized void say(Qiang qiang){
System.out.println("小巫:给我十块钱!");
qiang.get();
}
public synchronized void get(){
System.out.println("小巫:得到十块钱,去找妹妹玩!");
}
}
class Qiang {
public synchronized void say(XiaoWu xiaoWu){
System.out.println("强哥:先让我见妹妹,然后给钱!");
xiaoWu.get();
}
public synchronized void get(){
System.out.println("强哥:终于见到妹妹了!");
}
}
public class DeadLock implements Runnable{
private XiaoWu xiaoWu = new XiaoWu();
private Qiang qiang = new Qiang();
@Override
public void run() {
xiaoWu.say(qiang);
}
public DeadLock(){
new Thread(this).start();
qiang.say(xiaoWu);
}
public static void main(String[] args) {
new DeadLock();
}
}
现在死锁造成的主要原因是因为批次都在互相等待着,等待着对方先让出资源。死锁实际上是开发中出现的不确定的状态,有的时候代码如果处理不当则会不定期出现死锁,这是属于正常开发中的调式问题。
若干个线程访问统一资源时一定要进行同步处理,而过多的同步会造成死锁。
四、综合案例:“生产者与消费者”模型
在多线程的开发过程之中最为著名的案例就是生产者与消费者操作,该操作的主要流程如下:
- 生产者负责信息内容的生产
- 每当消费者生产完成一项完整的信息之后,消费者要从这里取走信息
- 如果生产者没有生产,则消费者要等待它消费完成。如果消费者还没有对信息进行消费,则生产者应该等待消费消费完成后再进行成产
1、程序的基本实现
可以将生产者与消费者定义为两个独立的线程类对象, 但是对于现在生产的数据,可以使用如下的组成:
- 数据一:title = 人生小屋、content = 宇宙第一帅
- 数据二:title = 沈梦琳、content = 宇宙第一美
既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就需要一个数据的保存集中点, 那么可以单独定义一个Message类实现数据的保存
范例:程序的基本结构
import java.util.concurrent.ExecutionException;
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i ++){
if (i % 2 == 0) {
this.msg.setTitle("人生小屋");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.msg.setContent("宇宙第一帅");
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
msg.setTitle("沈梦琳");
msg.setContent("宇宙第一美,常态保持");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.msg.getTitle() + " - " + this.msg.getContent());
}
}
}
class Message{
private String title;
private String content;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Message message = new Message();
new Thread(new Producer(message)).start(); //启动生产者线程
new Thread(new Consumer(message)).start(); //启动消费者线程
}
}
通过整个代码的执行会发现此时有两个主要问题:
- 问题一:数据不同步了
- 问题而:生产一个取走一个,但是发现有了重复生产和重复取出问题
2、解决数据同步
如果要解决问题,首先解决的就是数据同步的处理问题,如果要想解决数据同步最简单的做法是使用synchronized关键字。同步代码块或同步方法,于是这个时候对于同步的处理就可以直接在Message类中完成
范例:解决同步操作
import java.util.concurrent.ExecutionException;
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i ++){
if (i % 2 == 0) {
this.msg.set("人生小屋" , "宇宙第一帅");
} else {
this.msg.set("沈梦琳" , "宇宙第一美,常态保持");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.msg.get());
}
}
}
class Message{
private String title;
private String content;
public synchronized void set(String title , String content) {
this.title = title;
this.content = content;
}
public synchronized String get(){
return this.title + " - " +this.content;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Message message = new Message();
new Thread(new Producer(message)).start(); //启动生产者线程
new Thread(new Consumer(message)).start(); //启动消费者线程
}
}
在进行同步处理的时候肯定要有一个同步的处理对象,那么此时肯定要将同步操作交由Message类处理时最合适的。这个时候发现数据已经可以正常的保持一致了,但是对于重复操作的问题依然存在。
3、线程的等待与唤醒
如果说现在要想解决生产者与消费者的问题,那么最好的解决方案就是使用等待唤醒机制。 等待与唤醒的机制主要依靠的是Object类中提供的方法处理的
- 等待机制:
- 死等:public final void wait() throws InterruptedException
- 设置等待时间:public final void wait(long timeout) throws InterruptedException
- 设置等待时间:public final void wait(long timeout , int nanos) throws InterruptedException
- 唤醒第一个等待线程 :public final void notify()
- 唤醒全部的等待线程:public final void notifyAll()
如果此时有若干个等待线程的话,那么notify()表示的是唤醒第一个等待的,而其他的线程继续等待,而notifyAll()则是全部唤醒,哪个优先级高,哪个就可能被先唤醒
import java.util.concurrent.ExecutionException;
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i ++){
if (i % 2 == 0) {
this.msg.set("人生小屋" , "宇宙第一帅");
} else {
this.msg.set("沈梦琳" , "宇宙第一美,常态保持");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.msg.get());
}
}
}
class Message{
private String title;
private String content;
private boolean flag = true; //表示生产或消费的形式
//flag = true ,允许生产,但是不允许消费
//flag = false , 允许消费,不允许生产
public synchronized void set(String title , String content){
if (!this.flag) {
try {
super.wait(); //正在生产,先让其等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.title = title;
this.content = content;
this.flag = false; //已经生产过了
super.notify(); //唤醒等待的线程
}
public synchronized String get(){
if (flag) { //还未生产,需要等待
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return this.title + " - " +this.content;
} finally { //不管如何都要执行
this.flag = true; //继续生产
super.notify(); //唤醒等待线程
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Message message = new Message();
new Thread(new Producer(message)).start(); //启动生产者线程
new Thread(new Consumer(message)).start(); //启动消费者线程
}
}
这种处理形式就是在进行多线程开发过程之中最原始的处理方案,整个的等待、同步、唤醒机制都有开发者自行通过源代码进行控制
五、多线程深入
1、优雅的停止线程
在多线程操作之中如果要启动多线程肯定使用的是Thread类中的start()方法。而如果对于多线程需要进行停止处理,Thread类原本提供有stop()方法,但是对于这些方法从JDK1.2版本开始就已经将其废除了,而且一直到现在也不在进行使用了,除了stop()方法,还有其他的一些方法也被禁用了
- 停止多线程:public void stop()
- 销毁多线程:public void destroy()
- 挂起多线程:public final void suspend()、暂停执行
- 恢复挂起的线程执行:public final void resume()
之所以废除,主要原因是因为这些方法可能会导致线程的死锁。因此想要实现线程的停止需要通过一种有柔和的方法来进行
范例:实现线程柔和的停止
public class ThreadDemo {
private static boolean flag = true;
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(() -> {
long num = 0;
while (flag){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在运行、num = " + num);
}
} , "执行线程").start();
Thread.sleep(200); //运行200毫秒
flag = false;
}
}
万一现在有其他的线程去控制这个flag的内容,那么这个时候对于线程的停止也不是立刻停止的,而是执行过程中判断flag
2、守护线程
在java中可以进行守护线程的定义,如果现在主线程的程序或者其他线程还在执行的时候,守护线程就会一直存在,并且运行在后台状态
在Thread类中提供有如下的守护线程的操作方法:
- 设置为守护线程:public final void setDaemon(boolean on)
- 判断是否为守护线程:public final boolean isDeamon()
范例:使用守护线程
public class ThreadDemo {
private static boolean flag = true;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread userThread = new Thread(() -> {
long num = 0;
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在运行、i = " + i);
}
} , "用户线程"); //完成核心的业务
Thread deamon = new Thread(() -> {
long num = 0;
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在运行、i = " + i);
}
} , "守护线程"); //完成核心的业务
deamon.setDaemon(true); //设置为守护线程
userThread.start();
deamon.start();
}
}
守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也就消失了,在JVM之中,最大的守护线程就是GC线程。
程序执行中CG线程会一直存在,如果程序执行完毕,GC线程也将消失。
3、volatile关键字
在多线程的定义之宗,volatile关键字主要是属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。这样的话在一些书上就将其错误的理解为同步属性了
- 获取变量原有的数据内容副本
- 利用副本为变量进行数学计算
- 将计算后的变量,保存到原始空间之中
而如果一个属性追加了volatile关键字,表示的就是使用副本,而是直接操原始变量,相当于节约了:拷贝副本、重新保存的步骤
class MyThread implements Runnable {
private volatile int ticket = 5; //直接内存操作
@Override
public void run() {
while (this.ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖票处理ticket = " + ticket --);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt , "票贩子A").start();
new Thread(mt , "票贩子B").start();
new Thread(mt , "票贩子C").start();
}
}
面试题:请解释volatile与synchronized的区别?
- volatile主要在属性上使用,而synchronized是在代码块与方法上使用的
- volatile无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,而synchronzied是实现同步的
六、多线程案例分析
1、案例分析一
1、设计4个线程对象,两个线程执行减操作,两个线程执行加操作
class Resource{ //定义一个操作的资源
private int num = 0; //这个要进行加减操作的数据
private boolean flag = true; //加减的切换
//flag = true 表示可以进行加法操作,但是无法进行减法操作
//flag = false 表示可以进行减法操作,但是无法进行加法操作
public synchronized void add() throws InterruptedException{
if (flag == false) { //现在需要执行的是加法操作,减法操作需要等待
super.wait();
}
Thread.sleep(100);
this.num ++;
System.out.println("加法操作 - " + Thread.currentThread().getName() + " num = " + this.num);
this.flag = false; //加法操作执行完毕,需要执行减法操作
super.notifyAll(); //唤醒全部等待线程
}
public synchronized void sub() throws InterruptedException{
if (flag == true) {
super.wait();
}
Thread.sleep(100);
this.num --;
System.out.println("减法操作 - " + Thread.currentThread().getName() + " num = " + this.num);
this.flag = true; //减法操作执行完毕,需要执行加法操作
super.notifyAll(); //唤醒全部等待线程
}
}
class AddThread implements Runnable {
private Resource resource;
public AddThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int x = 0;x < 50;x ++) {
try {
this.resource.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class SubThread implements Runnable {
private Resource resource;
public SubThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int x = 0;x < 50;x ++) {
try {
this.resource.sub();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Resource resource = new Resource();
SubThread subThread = new SubThread(resource);
AddThread addThread = new AddThread(resource);
new Thread(addThread , "加法线程 - A").start();
new Thread(addThread , "加法线程 - B").start();
new Thread(subThread , "减法线程 - X").start();
new Thread(subThread , "减法线程 - Y").start();
}
}
这一题目是一个经典的多线程的开发操作,这一个程序里面一定要考虑的核心本质在于:加一个,减一个,整体的计算结果应该只在0、-1、1之间循环出现,这才是合理的。
2、案例分析二
设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑,则搬运工要等待新电脑产出;如果生产出的电脑没有搬走,则需要等待电脑搬走之后再生产,并统计出生产的电脑数量。
在本程序之中实现的就是一个标准的的生产者与消费者的处理模型。
实现具体的程序代码
class Computer{
private static Integer count = 0;
private String name ;
private double price ;
public Computer(String name , double price){
this.name = name;
this.price = price;
count ++;
}
public String toString(){
return "第" + count + "台电脑" + "电脑名字," + name + "、价值," + price;
}
}
class Resource{
private Computer computer;
public synchronized void make() throws InterruptedException {
if (computer != null){
super.wait();
}
Thread.sleep(100);
computer = new Computer("Dell电脑", 1.1);
System.out.println("生产电脑" + computer);
super.notifyAll();
}
public synchronized void get() throws InterruptedException {
if (computer == null){
super.wait();
}
Thread.sleep(10);
System.out.println("取走电脑" + computer);
computer = null;
super.notifyAll();
}
}
class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 50; i ++){
try {
this.resource.make();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 50; i ++){
try {
this.resource.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(new Producer(resource)).start();
new Thread(new Consumer(resource)).start();
}
}
3、案例分析三
实现一个竞拍抢答程序:要求设置三个抢答者(三个线程),而后同时发出抢答指令,抢答成功者给出成功提示,未抢答成功者给出失败提示。
对于这个多线程的操作由于里面需要牵扯到数据的返回问题,那么现在最好使用的方案是Callable是一种处理形式
class MyThread implements Callable<String>{
private boolean flag = false; //抢答成功
@Override
public String call() throws Exception {
synchronized (this) {
if (flag == false){
flag = true;
return Thread.currentThread().getName() + "抢答成功!";
} else {
return Thread.currentThread().getName() + "抢答失败!";
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<String> taskA = new FutureTask<String>(myThread);
FutureTask<String> taskB = new FutureTask<String>(myThread);
FutureTask<String> taskC= new FutureTask<String>(myThread);
new Thread(taskA , "竞争者A").start();
new Thread(taskB , "竞争者B").start();
new Thread(taskC , "竞争者C").start();
System.out.println(taskA.get());
System.out.println(taskB.get());
System.out.println(taskC.get());
}
}